Add live community data & telemetry opt-in UI (#5001)
This commit is contained in:
parent
5868036461
commit
09eab09415
25 changed files with 579 additions and 193 deletions
|
|
@ -308,3 +308,21 @@ small {
|
|||
--bs-table-color: var(--evcc-default-text);
|
||||
--bs-table-border-color: var(--evcc-gray);
|
||||
}
|
||||
.nav-tabs .nav-link {
|
||||
color: var(--evcc-gray) !important;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: var(--evcc-default-text) !important;
|
||||
background-color: var(--evcc-box) !important;
|
||||
border-bottom-color: var(--evcc-box);
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
border-color: var(--bs-primary);
|
||||
background-color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.dark .form-switch .form-check-input:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2328293e'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ modalUpdateStatusStart = "Aktualisierung gestartet: "
|
|||
modalUpdateStatusFailed = "Aktualisierung nicht möglich: "
|
||||
|
||||
[footer.savings]
|
||||
tabTitle = "Meine Daten"
|
||||
footerShort = "{percent}% Sonne"
|
||||
footerLong = "{percent}% Sonnenenergie"
|
||||
modalTitle = "Auswertung Ladeenergie"
|
||||
since = "Seit {since}."
|
||||
since = "seit {since}"
|
||||
percentTitle = "Sonnenenergie"
|
||||
percentSelf = "{self} kWh Sonne"
|
||||
percentGrid = "{grid} kWh Netz"
|
||||
|
|
@ -40,8 +41,28 @@ savingsTitle = "Ersparnis"
|
|||
savingsComparedToGrid = "gegenüber Netzbezug"
|
||||
savingsTotalEnergy = "{total} kWh geladen"
|
||||
|
||||
[footer.community]
|
||||
tabTitle = "Live-Community"
|
||||
power = "Ladeleistung"
|
||||
powerSub1 = "{activeClients} von {totalClients} Nutzern"
|
||||
powerSub2 = "laden mit evcc"
|
||||
greenShare = "Sonnenanteil"
|
||||
greenShareSub1 = "der Leistung kommt"
|
||||
greenShareSub2 = "aus PV & Hausakku"
|
||||
greenEnergy = "Sonnenenergie"
|
||||
greenEnergySub1 = "seit Oktober 2022"
|
||||
greenEnergySub2 = "über evcc geladen"
|
||||
|
||||
[footer.telemetry]
|
||||
optIn = "Ich möchte meine Daten auch beisteuern."
|
||||
optInMoreDetails = "Mehr Details gibts {0}."
|
||||
optInMoreDetailsLink = "hier"
|
||||
optInSponsorship = "Sponsoring erforderlich."
|
||||
|
||||
[footer.sponsor]
|
||||
thanks = "Danke für deine Unterstützung, {sponsor}! Das hilft uns bei der Weiterentwicklung."
|
||||
titleSponsor = "Du bist Unterstützer"
|
||||
titleNoSponsor = "Unterstütze uns"
|
||||
thanks = "Danke für dein Sponsoring, {sponsor}! Das hilft uns bei der Weiterentwicklung."
|
||||
confetti = "Lust auf Konfetti?"
|
||||
supportUs = "Unsere Mission: Sonne tanken zum Standard machen. Hilf uns und unterstütze evcc finanziell."
|
||||
sticker = "...oder evcc Sticker?"
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ modalUpdateStatusStart = "Update started: "
|
|||
modalUpdateStatusFailed = "Update failed: "
|
||||
|
||||
[footer.savings]
|
||||
tabTitle = "My data"
|
||||
footerShort = "{percent}% solar"
|
||||
footerLong = "{percent}% solar energy"
|
||||
modalTitle = "Charge Energy Overview"
|
||||
since = "Since {since}."
|
||||
since = "since {since}"
|
||||
percentTitle = "Solar Energy"
|
||||
percentSelf = "{self} kWh solar"
|
||||
percentGrid = "{grid} kWh grid"
|
||||
|
|
@ -40,7 +41,27 @@ savingsTitle = "Savings"
|
|||
savingsComparedToGrid = "compared to grid"
|
||||
savingsTotalEnergy = "{total} kWh charged"
|
||||
|
||||
[footer.community]
|
||||
tabTitle = "Live community"
|
||||
power = "Charging power"
|
||||
powerSub1 = "{activeClients} of {totalClients} participants"
|
||||
powerSub2 = "are charging right now"
|
||||
greenShare = "Solar share"
|
||||
greenShareSub1 = "power is covered by"
|
||||
greenShareSub2 = "PV & battery storage"
|
||||
greenEnergy = "Solar energy"
|
||||
greenEnergySub1 = "charged with evcc"
|
||||
greenEnergySub2 = "since October 2022"
|
||||
|
||||
[footer.telemetry]
|
||||
optIn = "I want to contribute my data as well."
|
||||
optInMoreDetails = "More details are available {0}."
|
||||
optInMoreDetailsLink = "here"
|
||||
optInSponsorship = "Sponsoring required."
|
||||
|
||||
[footer.sponsor]
|
||||
titleSponsor = "You are a supporter"
|
||||
titleNoSponsor = "Support us"
|
||||
thanks = "Thanks for your support, {sponsor}! It helps us with the further development."
|
||||
confetti = "Ready for confetti?"
|
||||
supportUs = "Our mission: Make solar charging the standard. Help us and support evcc financially."
|
||||
|
|
|
|||
|
|
@ -26,13 +26,14 @@ modalUpdateStatusStart = "Naujinimas prasidėjo: "
|
|||
modalUpdateStatusFailed = "Naujinimas nepavyko: "
|
||||
|
||||
[footer.savings]
|
||||
tabTitle = "Mano duomenys"
|
||||
footerShort = "{percent}% saulės"
|
||||
footerLong = "{percent}% saulės energija"
|
||||
modalTitle = "Įkrovimo energijos apžvalga"
|
||||
since = "Nuo {since}."
|
||||
since = "nuo {since}"
|
||||
percentTitle = "Saulės energija"
|
||||
percentSelf = "{self} kWh saulės"
|
||||
percentGrid = "{grid} kWh tinklas"
|
||||
percentGrid = "{grid} kWh tinklo"
|
||||
priceTitle = "Energijos kaina"
|
||||
priceFeedIn = "{feedInPrice} eksporto"
|
||||
priceGrid = "{gridPrice} tinklo"
|
||||
|
|
@ -40,7 +41,27 @@ savingsTitle = "Sutaupyta"
|
|||
savingsComparedToGrid = "palyginus su tinklu"
|
||||
savingsTotalEnergy = "{total} kWh įkrauta"
|
||||
|
||||
[footer.community]
|
||||
tabTitle = "Bendruomenės duomenys"
|
||||
power = "Įkrovimo galia"
|
||||
powerSub1 = "{activeClients} iš {totalClients} dalyvių"
|
||||
powerSub2 = "įkrauna šiuo metu"
|
||||
greenShare = "Saulės dalis"
|
||||
greenShareSub1 = "galios tieka Saulė ir"
|
||||
greenShareSub2 = "energijos kaupikliai"
|
||||
greenEnergy = "Saulės energija"
|
||||
greenEnergySub1 = "įkrauta su evcc"
|
||||
greenEnergySub2 = "nuo 2022 Spalio"
|
||||
|
||||
[footer.telemetry]
|
||||
optIn = "Noriu prisidėti savo duomenimis."
|
||||
optInMoreDetails = "Daugiau informacijos rasite {0}."
|
||||
optInMoreDetailsLink = "čia"
|
||||
optInSponsorship = "(Gali tik rėmėjai.)"
|
||||
|
||||
[footer.sponsor]
|
||||
titleSponsor = "Esate rėmėjais"
|
||||
titleNoSponsor = "Paremkite mus"
|
||||
thanks = "Ačiū, kad mus remiate {sponsor}! Taip prisidedate prie projekto vystymo."
|
||||
confetti = "Norite konfeti?"
|
||||
supportUs = "Mūsų misija: Siekiame, kad įkrovimas saulės energija taptų standartu. Padėkite mums ir paremkite evcc finansiškai."
|
||||
|
|
|
|||
21
assets/js/communityApi.js
Normal file
21
assets/js/communityApi.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import axios from "axios";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: "https://api.evcc.io/v1/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// global error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const url = error.config.baseURL + error.config.url;
|
||||
const message = `${error.message}: API request failed ${url}`;
|
||||
window.app.error({ message });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
|
@ -11,18 +11,19 @@ export default {
|
|||
to: { type: Number },
|
||||
format: { type: Function },
|
||||
noAnimation: { type: Boolean },
|
||||
duration: { type: Number, default: DURATION },
|
||||
},
|
||||
data() {
|
||||
return { duration: 0 };
|
||||
return { activeDuration: 0 };
|
||||
},
|
||||
computed: {
|
||||
animationDuration() {
|
||||
return this.noAnimation ? 0 : this.duration;
|
||||
return this.noAnimation ? 0 : this.activeDuration;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
to: function () {
|
||||
this.duration = DURATION;
|
||||
this.activeDuration = this.duration;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
106
assets/js/components/LiveCommunity.vue
Normal file
106
assets/js/components/LiveCommunity.vue
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="d-block d-lg-flex mb-2 justify-content-between">
|
||||
<SavingsTile
|
||||
class="text-accent2"
|
||||
icon="car"
|
||||
:title="$t('footer.community.power')"
|
||||
:value="chargePower"
|
||||
:valueFmt="numberAnimationFmt"
|
||||
:animationDuration="animationDuration"
|
||||
unit="kW"
|
||||
:sub1="$t('footer.community.powerSub1', { totalClients, activeClients })"
|
||||
:sub2="$t('footer.community.powerSub2')"
|
||||
/>
|
||||
|
||||
<SavingsTile
|
||||
class="text-accent1"
|
||||
icon="sun"
|
||||
:title="$t('footer.community.greenShare')"
|
||||
:value="greenShare"
|
||||
:valueFmt="numberAnimationFmt"
|
||||
:animationDuration="animationDuration"
|
||||
unit="%"
|
||||
:sub1="$t('footer.community.greenShareSub1')"
|
||||
:sub2="$t('footer.community.greenShareSub2')"
|
||||
/>
|
||||
|
||||
<SavingsTile
|
||||
class="text-accent3"
|
||||
icon="eco"
|
||||
:title="$t('footer.community.greenEnergy')"
|
||||
:value="greenEnergyMWh"
|
||||
:valueFmt="numberAnimationFmt"
|
||||
:animationDuration="animationDuration"
|
||||
unit="MWh"
|
||||
:sub1="$t('footer.community.greenEnergySub1')"
|
||||
:sub2="$t('footer.community.greenEnergySub2')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SavingsTile from "./SavingsTile.vue";
|
||||
|
||||
import formatter from "../mixins/formatter";
|
||||
import communityApi from "../communityApi";
|
||||
|
||||
export default {
|
||||
name: "LiveCommunity",
|
||||
components: { SavingsTile },
|
||||
mixins: [formatter],
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
refresh: null,
|
||||
result: {},
|
||||
animationDuration: 0.5,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
totalClients() {
|
||||
return this.result.totalClients;
|
||||
},
|
||||
activeClients() {
|
||||
return this.result.activeClients;
|
||||
},
|
||||
chargePower() {
|
||||
const { chargePower = 0 } = this.result;
|
||||
return chargePower / 1e3;
|
||||
},
|
||||
greenShare() {
|
||||
const { chargePower, greenPower } = this.result;
|
||||
if (!chargePower) {
|
||||
return 0;
|
||||
}
|
||||
return (100 / chargePower) * greenPower;
|
||||
},
|
||||
greenEnergyMWh() {
|
||||
const { greenEnergy = 0 } = this.result;
|
||||
return greenEnergy / 1e3;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.refresh = setInterval(this.update, 5000);
|
||||
await this.update();
|
||||
this.$nextTick(() => {
|
||||
this.animationDuration = 5;
|
||||
});
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval(this.refresh);
|
||||
},
|
||||
methods: {
|
||||
async update() {
|
||||
try {
|
||||
const response = await communityApi.get("total");
|
||||
this.result = response.data || {};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
numberAnimationFmt(number) {
|
||||
return this.fmtNumber(number, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -41,64 +41,98 @@
|
|||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{{
|
||||
$t("footer.savings.since", {
|
||||
since: fmtTimeAgo(secondsSinceStart()),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
:class="{ active: !communityView }"
|
||||
href="#"
|
||||
@click.prevent="showMyData"
|
||||
>
|
||||
{{ $t("footer.savings.tabTitle") }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
:class="{ active: communityView }"
|
||||
href="#"
|
||||
@click.prevent="showCommunity"
|
||||
>
|
||||
{{ $t("footer.community.tabTitle") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="d-block d-lg-flex mb-4">
|
||||
<SavingsTile
|
||||
class="text-accent1"
|
||||
icon="sun"
|
||||
:title="$t('footer.savings.percentTitle')"
|
||||
:value="percent"
|
||||
unit="%"
|
||||
:sub1="
|
||||
$t('footer.savings.percentSelf', {
|
||||
self: fmtKw(selfConsumptionCharged * 1000, true, false),
|
||||
})
|
||||
"
|
||||
:sub2="
|
||||
$t('footer.savings.percentGrid', {
|
||||
grid: fmtKw(gridCharged * 1000, true, false),
|
||||
})
|
||||
"
|
||||
/>
|
||||
<div v-if="!communityView" class="my-4">
|
||||
<div class="d-block d-lg-flex mb-2 justify-content-between">
|
||||
<SavingsTile
|
||||
class="text-accent1"
|
||||
icon="sun"
|
||||
:title="$t('footer.savings.percentTitle')"
|
||||
:value="percent"
|
||||
unit="%"
|
||||
:sub1="
|
||||
$t('footer.savings.percentSelf', {
|
||||
self: fmtKw(
|
||||
selfConsumptionCharged * 1000,
|
||||
true,
|
||||
false
|
||||
),
|
||||
})
|
||||
"
|
||||
:sub2="
|
||||
$t('footer.savings.percentGrid', {
|
||||
grid: fmtKw(gridCharged * 1000, true, false),
|
||||
})
|
||||
"
|
||||
/>
|
||||
|
||||
<SavingsTile
|
||||
class="text-accent2"
|
||||
icon="receivepayment"
|
||||
:title="$t('footer.savings.priceTitle')"
|
||||
:value="effectivePriceFormatted.value"
|
||||
:unit="effectivePriceFormatted.unit"
|
||||
:sub1="
|
||||
$t('footer.savings.priceFeedIn', {
|
||||
feedInPrice: fmtPricePerKWh(feedInPrice, currency),
|
||||
})
|
||||
"
|
||||
:sub2="
|
||||
$t('footer.savings.priceGrid', {
|
||||
gridPrice: fmtPricePerKWh(gridPrice, currency),
|
||||
})
|
||||
"
|
||||
/>
|
||||
<SavingsTile
|
||||
class="text-accent2"
|
||||
icon="receivepayment"
|
||||
:title="$t('footer.savings.priceTitle')"
|
||||
:value="effectivePriceFormatted.value"
|
||||
:unit="effectivePriceFormatted.unit"
|
||||
:sub1="
|
||||
$t('footer.savings.priceFeedIn', {
|
||||
feedInPrice: fmtPricePerKWh(feedInPrice, currency),
|
||||
})
|
||||
"
|
||||
:sub2="
|
||||
$t('footer.savings.priceGrid', {
|
||||
gridPrice: fmtPricePerKWh(gridPrice, currency),
|
||||
})
|
||||
"
|
||||
/>
|
||||
|
||||
<SavingsTile
|
||||
class="text-accent3"
|
||||
icon="coinjar"
|
||||
:title="$t('footer.savings.savingsTitle')"
|
||||
:value="fmtMoney(amount, currency)"
|
||||
:unit="fmtCurrencySymbol(currency)"
|
||||
:sub1="$t('footer.savings.savingsComparedToGrid')"
|
||||
:sub2="
|
||||
$t('footer.savings.savingsTotalEnergy', {
|
||||
total: fmtKw(totalCharged * 1000, true, false),
|
||||
})
|
||||
"
|
||||
/>
|
||||
<SavingsTile
|
||||
class="text-accent3"
|
||||
icon="coinjar"
|
||||
:title="$t('footer.savings.savingsTitle')"
|
||||
:value="fmtMoney(amount, currency)"
|
||||
:unit="fmtCurrencySymbol(currency)"
|
||||
:sub1="$t('footer.savings.savingsComparedToGrid')"
|
||||
:sub2="
|
||||
$t('footer.savings.savingsTotalEnergy', {
|
||||
total: fmtKw(totalCharged * 1000, true, false),
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<p class="my-3 lh-2">
|
||||
<small>
|
||||
{{
|
||||
$t("footer.savings.since", {
|
||||
since: fmtDayMonthYear(startDate),
|
||||
})
|
||||
}}
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="my-4">
|
||||
<LiveCommunity />
|
||||
<TelemetrySettings :sponsor="sponsor" />
|
||||
</div>
|
||||
|
||||
<Sponsor :sponsor="sponsor" />
|
||||
|
|
@ -114,10 +148,12 @@
|
|||
import formatter from "../mixins/formatter";
|
||||
import Sponsor from "./Sponsor.vue";
|
||||
import SavingsTile from "./SavingsTile.vue";
|
||||
import LiveCommunity from "./LiveCommunity.vue";
|
||||
import TelemetrySettings from "./TelemetrySettings.vue";
|
||||
|
||||
export default {
|
||||
name: "Savings",
|
||||
components: { Sponsor, SavingsTile },
|
||||
components: { Sponsor, SavingsTile, LiveCommunity, TelemetrySettings },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
selfConsumptionPercent: Number,
|
||||
|
|
@ -132,6 +168,9 @@ export default {
|
|||
feedInPrice: { type: Number },
|
||||
currency: String,
|
||||
},
|
||||
data() {
|
||||
return { communityView: false, telemetryEnabled: false };
|
||||
},
|
||||
computed: {
|
||||
percent() {
|
||||
return Math.round(this.selfConsumptionPercent) || 0;
|
||||
|
|
@ -142,35 +181,17 @@ export default {
|
|||
);
|
||||
return { value, unit };
|
||||
},
|
||||
startDate() {
|
||||
return new Date(this.since * 1000);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
secondsSinceStart() {
|
||||
return this.since * 1000 - Date.now();
|
||||
showCommunity() {
|
||||
this.communityView = true;
|
||||
},
|
||||
showMyData() {
|
||||
this.communityView = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.chart {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.chart-item--self {
|
||||
background-color: var(--evcc-self);
|
||||
}
|
||||
.chart-item--grid {
|
||||
background-color: var(--evcc-grid);
|
||||
}
|
||||
.chart-item--no-data {
|
||||
background-color: var(--bs-gray-medium);
|
||||
}
|
||||
|
||||
.chart-item {
|
||||
transition-property: width;
|
||||
transition-duration: var(--evcc-transition-medium);
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
.tile-icon {
|
||||
width: 70px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="flex-grow-1 d-flex mb-4 mb-lg-0 align-items-center align-items-lg-start">
|
||||
<div class="flex-shrink-1 d-flex mb-4 mb-lg-0 align-items-center align-items-lg-start">
|
||||
<shopicon-regular-sun v-if="icon === 'sun'" class="tile-icon"></shopicon-regular-sun>
|
||||
<shopicon-regular-coinjar
|
||||
v-if="icon === 'coinjar'"
|
||||
|
|
@ -9,17 +9,25 @@
|
|||
v-if="icon === 'receivepayment'"
|
||||
class="tile-icon"
|
||||
></shopicon-regular-receivepayment>
|
||||
<shopicon-regular-car3 v-if="icon === 'car'" class="tile-icon"></shopicon-regular-car3>
|
||||
<shopicon-regular-eco1 v-if="icon === 'eco'" class="tile-icon"></shopicon-regular-eco1>
|
||||
<div class="ms-3 d-flex flex-grow-1 d-lg-block align-items-center justify-content-between">
|
||||
<div>
|
||||
<p class="my-0 fw-bold">{{ title }}</p>
|
||||
<p class="my-0 fw-bold text-truncate">{{ title }}</p>
|
||||
<strong class="d-flex align-items-baseline lh-sm">
|
||||
<span class="fs-1">{{ value }}</span>
|
||||
<span class="fs-1">
|
||||
<AnimatedNumber
|
||||
v-if="valueFmt"
|
||||
:to="value"
|
||||
:format="valueFmt"
|
||||
:duration="animationDuration"
|
||||
/>
|
||||
<span v-else>{{ value }}</span>
|
||||
</span>
|
||||
<span class="ms-1 unit">{{ unit }}</span>
|
||||
</strong>
|
||||
</div>
|
||||
<small
|
||||
class="d-block mt-0 ms-3 ms-lg-0 text-end text-lg-start flex-grow-1 flex-shrink-1"
|
||||
>
|
||||
<small class="d-block mt-0 ms-3 ms-lg-0 text-end text-lg-start">
|
||||
{{ sub1 }} <br />
|
||||
{{ sub2 }}
|
||||
</small>
|
||||
|
|
@ -31,13 +39,19 @@
|
|||
import "@h2d2/shopicons/es/regular/coinjar";
|
||||
import "@h2d2/shopicons/es/regular/sun";
|
||||
import "@h2d2/shopicons/es/regular/receivepayment";
|
||||
import "@h2d2/shopicons/es/regular/car3";
|
||||
import "@h2d2/shopicons/es/regular/eco1";
|
||||
import AnimatedNumber from "./AnimatedNumber.vue";
|
||||
|
||||
export default {
|
||||
name: "SavingsTile",
|
||||
components: { AnimatedNumber },
|
||||
props: {
|
||||
title: String,
|
||||
icon: String,
|
||||
value: [String, Number],
|
||||
valueFmt: Function,
|
||||
animationDuration: Number,
|
||||
unit: String,
|
||||
sub1: String,
|
||||
sub2: String,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
<template>
|
||||
<div v-if="sponsor">
|
||||
<p class="mb-3">
|
||||
{{ $t("footer.sponsor.thanks", { sponsor }) }}
|
||||
<p class="fw-bold mb-1 d-flex">
|
||||
<shopicon-regular-heart
|
||||
class="title-icon text-primary d-inline-block me-1"
|
||||
></shopicon-regular-heart>
|
||||
{{ $t("footer.sponsor.titleSponsor") }}
|
||||
</p>
|
||||
<p class="mb-3">{{ $t("footer.sponsor.thanks", { sponsor }) }}</p>
|
||||
<div
|
||||
class="d-flex justify-content-center align-items-center flex-column flex-lg-row align-items-lg-baseline justify-content-lg-start"
|
||||
>
|
||||
|
|
@ -20,9 +24,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="mb-3">
|
||||
{{ $t("footer.sponsor.supportUs") }}
|
||||
<p class="fw-bold mb-1">
|
||||
{{ $t("footer.sponsor.titleNoSponsor") }}
|
||||
</p>
|
||||
<p class="mb-3">{{ $t("footer.sponsor.supportUs") }}</p>
|
||||
<div
|
||||
class="d-flex justify-content-center align-items-center flex-column flex-lg-row align-items-lg-baseline justify-content-lg-start"
|
||||
>
|
||||
|
|
@ -89,6 +94,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title-icon {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.confetti-button {
|
||||
/* prevent double-tap zoom */
|
||||
touch-action: none;
|
||||
|
|
|
|||
64
assets/js/components/TelemetrySettings.vue
Normal file
64
assets/js/components/TelemetrySettings.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="form-check form-switch my-3">
|
||||
<input
|
||||
id="telemetryEnabled"
|
||||
:checked="enabled"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
:disabled="!sponsor"
|
||||
@change="change"
|
||||
/>
|
||||
<label class="form-check-label" for="telemetryEnabled">
|
||||
{{ $t("footer.telemetry.optIn") }}
|
||||
<i18n-t v-if="sponsor" tag="span" keypath="footer.telemetry.optInMoreDetails">
|
||||
<a href="https://docs.evcc.io/docs/guides/setup/#telemetry--community-daten">
|
||||
{{ $t("footer.telemetry.optInMoreDetailsLink") }}
|
||||
</a>
|
||||
</i18n-t>
|
||||
<span v-else>{{ $t("footer.telemetry.optInSponsorship") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "../api";
|
||||
|
||||
export default {
|
||||
name: "TelemetrySettings",
|
||||
props: { sponsor: String },
|
||||
data() {
|
||||
return {
|
||||
enabled: null,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.update();
|
||||
},
|
||||
methods: {
|
||||
async change(e) {
|
||||
try {
|
||||
const response = await api.post(`settings/telemetry/${e.target.checked}`);
|
||||
this.enabled = response.data.result;
|
||||
} catch (err) {
|
||||
window.app.error({
|
||||
message: `Unable to update telemetry setting: ${err.message}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
try {
|
||||
const response = await api.get("settings/telemetry");
|
||||
this.enabled = response.data.result;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.form-check {
|
||||
min-height: inherit !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -33,6 +33,13 @@ export default {
|
|||
fmtKWh: function (val) {
|
||||
return this.fmtKw(val) + "h";
|
||||
},
|
||||
fmtNumber: function (number, decimals) {
|
||||
return new Intl.NumberFormat(this.$i18n.locale, {
|
||||
style: "decimal",
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(number);
|
||||
},
|
||||
fmtUnit: function (val) {
|
||||
return Math.abs(val) >= this.fmtLimit ? "k" : "";
|
||||
},
|
||||
|
|
@ -139,6 +146,13 @@ export default {
|
|||
year: "numeric",
|
||||
}).format(date);
|
||||
},
|
||||
fmtDayMonthYear: function (date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n.locale, {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(date);
|
||||
},
|
||||
fmtMoney: function (amout = 0, currency = "EUR") {
|
||||
return new Intl.NumberFormat(this.$i18n.locale, {
|
||||
style: "currency",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ network:
|
|||
port: 7070
|
||||
|
||||
log: debug
|
||||
levels:
|
||||
mercedes: trace
|
||||
plant: 683fd96cb7de8cf3080e1d79a12ffedd03d66d7c39be742350559817833ae6f7
|
||||
|
||||
interval: 3s
|
||||
|
||||
|
|
|
|||
|
|
@ -94,8 +94,14 @@ func configureEnvironment(cmd *cobra.Command, conf config) (err error) {
|
|||
}
|
||||
|
||||
// setup telemetry
|
||||
if err == nil {
|
||||
err = telemetry.Create(conf.Plant)
|
||||
}
|
||||
|
||||
// deprecated: enable telemetry if its in the config
|
||||
if err == nil && conf.Telemetry {
|
||||
err = telemetry.Create(sponsor.Token, conf.Plant)
|
||||
log.WARN.Println("Enabling telementry in config is deprecated. Use UI instead.")
|
||||
err = telemetry.Enable(true)
|
||||
}
|
||||
|
||||
// setup mqtt client listener
|
||||
|
|
|
|||
70
dist/assets/index.714c7ce4.js
vendored
70
dist/assets/index.714c7ce4.js
vendored
File diff suppressed because one or more lines are too long
6
dist/assets/index.c2a568b4.css
vendored
Normal file
6
dist/assets/index.c2a568b4.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
dist/assets/index.f831c606.css
vendored
6
dist/assets/index.f831c606.css
vendored
File diff suppressed because one or more lines are too long
70
dist/assets/index.fc0aa81e.js
vendored
Normal file
70
dist/assets/index.fc0aa81e.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
|
|
@ -20,8 +20,8 @@
|
|||
<meta name="theme-color" content="#020318" />
|
||||
|
||||
<title>evcc</title>
|
||||
<script type="module" crossorigin src="./assets/index.714c7ce4.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.f831c606.css">
|
||||
<script type="module" crossorigin src="./assets/index.fc0aa81e.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.c2a568b4.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -11,7 +11,7 @@
|
|||
"@popperjs/core": "^2.11.5",
|
||||
"@vitejs/plugin-vue": "^3.0.1",
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "^5.1.3",
|
||||
"bootstrap": "^5.2.2",
|
||||
"canvas-confetti": "^1.4.0",
|
||||
"smoothscroll-polyfill": "^0.4.4",
|
||||
"vue": "^3.2.29",
|
||||
|
|
@ -17098,9 +17098,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.0.tgz",
|
||||
"integrity": "sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.2.tgz",
|
||||
"integrity": "sha512-dEtzMTV71n6Fhmbg4fYJzQsw1N29hJKO1js5ackCgIpDcGid2ETMGC6zwSYw09v05Y+oRdQ9loC54zB1La3hHQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -17112,7 +17112,7 @@
|
|||
}
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.5"
|
||||
"@popperjs/core": "^2.11.6"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen": {
|
||||
|
|
@ -45052,9 +45052,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"bootstrap": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.0.tgz",
|
||||
"integrity": "sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.2.tgz",
|
||||
"integrity": "sha512-dEtzMTV71n6Fhmbg4fYJzQsw1N29hJKO1js5ackCgIpDcGid2ETMGC6zwSYw09v05Y+oRdQ9loC54zB1La3hHQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"boxen": {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"@popperjs/core": "^2.11.5",
|
||||
"@vitejs/plugin-vue": "^3.0.1",
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "^5.1.3",
|
||||
"bootstrap": "^5.2.2",
|
||||
"canvas-confetti": "^1.4.0",
|
||||
"smoothscroll-polyfill": "^0.4.4",
|
||||
"vue": "^3.2.29",
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ func SetJson(key string, val any) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func SetBool(key string, val bool) {
|
||||
SetString(key, strconv.FormatBool(val))
|
||||
}
|
||||
|
||||
func String(key string) (string, error) {
|
||||
idx := slices.IndexFunc(settings, func(s setting) bool {
|
||||
return s.Key == key
|
||||
|
|
@ -109,6 +113,14 @@ func Time(key string) (time.Time, error) {
|
|||
return time.Parse(time.RFC3339, s)
|
||||
}
|
||||
|
||||
func Bool(key string) (bool, error) {
|
||||
s, err := String(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strconv.ParseBool(s)
|
||||
}
|
||||
|
||||
func Json(key string, res any) error {
|
||||
s, err := String(key)
|
||||
if err == nil {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/telemetry"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
|
@ -98,6 +99,8 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
|
|||
"prioritysoc": {[]string{"POST", "OPTIONS"}, "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoC, site.GetPrioritySoC)},
|
||||
"residualpower": {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
|
||||
"sessions": {[]string{"GET"}, "/sessions", sessionHandler},
|
||||
"telemetry": {[]string{"GET"}, "/settings/telemetry", boolGetHandler(telemetry.Enabled)},
|
||||
"telemetry2": {[]string{"POST", "OPTIONS"}, "/settings/telemetry/{value:[a-z]+}", boolHandler(telemetry.Enable, telemetry.Enabled)},
|
||||
}
|
||||
|
||||
for _, r := range routes {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,34 @@ func intHandler(set func(int) error, get func() int) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// boolHandler updates bool-param api
|
||||
func boolHandler(set func(bool) error, get func() bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
val, err := strconv.ParseBool(vars["value"])
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = set(val)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotAcceptable, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonResult(w, get())
|
||||
}
|
||||
}
|
||||
|
||||
// boolGetHandler retrievs bool api values
|
||||
func boolGetHandler(get func() bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
jsonResult(w, get())
|
||||
}
|
||||
}
|
||||
|
||||
// stateHandler returns current charge mode
|
||||
func stateHandler(cache *util.Cache) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/evcc-io/evcc/server/db/settings"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/machine"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
|
|
@ -16,14 +17,21 @@ const api = "https://api.evcc.io"
|
|||
var instanceID string
|
||||
|
||||
func Enabled() bool {
|
||||
return instanceID != ""
|
||||
enabled, _ := settings.Bool("telemetry.enabled")
|
||||
return enabled && sponsor.IsAuthorized()
|
||||
}
|
||||
|
||||
func Create(token, machineID string) error {
|
||||
if token == "" {
|
||||
func Enable(enable bool) error {
|
||||
if enable && !sponsor.IsAuthorized() {
|
||||
return errors.New("telemetry requires sponsorship")
|
||||
}
|
||||
settings.SetBool("telemetry.enabled", enable)
|
||||
// TODO: remove once settings has central persistance mechanism
|
||||
err := settings.Persist()
|
||||
return err
|
||||
}
|
||||
|
||||
func Create(machineID string) error {
|
||||
if machineID == "" {
|
||||
var err error
|
||||
if machineID, err = machine.ProtectedID("evcc-api"); err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue