Implement per-loadpoint mode and target soc setting (#240)
This commit is contained in:
parent
66aa4838a7
commit
880d39e07e
10 changed files with 578 additions and 316 deletions
10
api/api.go
10
api/api.go
|
|
@ -14,6 +14,11 @@ const (
|
|||
ModePV ChargeMode = "pv"
|
||||
)
|
||||
|
||||
// String implements Stringer
|
||||
func (c ChargeMode) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// ChargeStatus is the EV's charging status from A to F
|
||||
type ChargeStatus string
|
||||
|
||||
|
|
@ -27,6 +32,11 @@ const (
|
|||
StatusF ChargeStatus = "F" // Fzg. angeschlossen: ja Laden aktiv: nein - Fehler (Ausfall Wallbox)
|
||||
)
|
||||
|
||||
// String implements Stringer
|
||||
func (c ChargeStatus) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// Meter is able to provide current power in W
|
||||
type Meter interface {
|
||||
CurrentPower() (float64, error)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,13 @@
|
|||
.progress-bar-animated {
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.btn.caption {
|
||||
opacity: 1;
|
||||
}
|
||||
.btn.first {
|
||||
border-top-left-radius: .2rem;
|
||||
border-bottom-left-radius: .2rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -128,66 +135,52 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="mode-template">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="btn-group btn-group-toggle bg-white">
|
||||
<label class="btn btn-outline-primary shadow-none" v-bind:class="{active:mode=='off'}">
|
||||
<input type="radio" value="off" v-on:click="mode='off'">Stop
|
||||
</label>
|
||||
<label class="btn btn-outline-primary shadow-none" v-bind:class="{active:mode=='now'}">
|
||||
<input type="radio" value="now" v-on:click="mode='now'">
|
||||
<span class="d-inline d-sm-none">Sofort</span>
|
||||
<span class="d-none d-sm-inline">Sofortladen</span>
|
||||
</label>
|
||||
<label class="btn btn-outline-primary shadow-none" v-bind:class="{active:mode=='minpv'}" v-if="state.gridMeter">
|
||||
<input type="radio" value="minpv" v-on:click="mode='minpv'">
|
||||
<span class="d-inline d-sm-none">Min + PV</span>
|
||||
<span class="d-none d-sm-inline">Minimum + PV</span>
|
||||
</label>
|
||||
<label class="btn btn-outline-primary shadow-none" v-bind:class="{active:mode=='pv'}" v-if="state.gridMeter">
|
||||
<input type="radio" value="pv" v-on:click="mode='pv'">
|
||||
<span class="d-inline d-sm-none">PV</span>
|
||||
<span class="d-none d-sm-inline">Nur PV</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="site-template">
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="d-none d-md-flex col-12 col-md-4 mt-md-4 align-items-end">
|
||||
<p class="h1">{{state.title||"Home"}}</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-8 mt-md-4" v-if="multi">
|
||||
<site-details v-bind:state="state"></site-details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4 border-bottom">
|
||||
<div class="col-12 col-md-4">
|
||||
<p class="h1">{{state.title||"Home"}}</p>
|
||||
</div>
|
||||
<div class="d-none col-12 col-md-8 pt-1"
|
||||
v-bind:class="{'d-md-block': state.loadpoints.length>1}">
|
||||
<modeswitch></modeswitch>
|
||||
<div class="row d-none d-md-flex border-bottom"></div>
|
||||
|
||||
<div class="row" v-if="!multi">
|
||||
<div class="d-none d-md-inline-block col-md-4"></div>
|
||||
<div class="col-12 col-md-8">
|
||||
<site-details v-bind:state="state"></site-details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<loadpoint v-for="(loadpoint,id) in state.loadpoints"
|
||||
v-bind:id="id" :state="loadpoint" :pv="state.gridMeter" :multi="multi">
|
||||
</loadpoint>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="site-details-template">
|
||||
<div class="row">
|
||||
<div class="d-none d-md-block col-md-4">
|
||||
</div>
|
||||
<div class="col-6 col-md-2 mt-3" v-if="state.gridMeter">
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.gridMeter">
|
||||
<div class="mb-2" v-if="state.gridPower > 0">
|
||||
Bezug <i class="text-primary fas fa-arrow-down"></i>
|
||||
</div><div class="mb-2" v-else>
|
||||
</div>
|
||||
<div class="mb-2" v-else>
|
||||
Einspeisung <i class="text-primary fas fa-arrow-up"></i>
|
||||
</div>
|
||||
<h2>
|
||||
{{fmt(state.gridPower)}} <small class="text-muted">{{fmtUnit(state.gridPower)}}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 mt-3" v-if="state.pvMeter">
|
||||
<div class="mb-2">Erzeugung <i class="text-primary fas fas fa-sun" v-if="state.pvPower < 0"></i></div>
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.pvMeter">
|
||||
<div class="mb-2">Erzeugung <i class="fas fas fa-sun" v-bind:class="{'text-primary':state.pvPower<0,'text-muted':state.pvPower>=0}"></i></div>
|
||||
<h2>
|
||||
{{fmt(state.pvPower)}} <small class="text-muted">{{fmtUnit(state.pvPower)}}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-none col-md-2 mt-3" v-if="state.batteryMeter">
|
||||
<div class="d-none d-md-flex col-md-3 mt-3" v-if="state.batteryMeter">
|
||||
<div class="mb-2">Batterie <i class="text-primary fas"
|
||||
v-bind:class="{'fa-arrow-down':state.batteryPower<0, 'fa-arrow-up':state.batteryPower>0}"></i>
|
||||
</div>
|
||||
|
|
@ -195,71 +188,99 @@
|
|||
{{fmt(state.batteryPower)}} <small class="text-muted">{{fmtUnit(state.batteryPower)}}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-block col-12 text-center pt-4 pb-4 mt-3 mt-md-5 mb-0 bg-light"
|
||||
v-bind:class="{ 'd-md-none': state.loadpoints.length>1 }">
|
||||
<modeswitch></modeswitch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<loadpoint v-for="(loadpoint,id) in state.loadpoints" v-bind:state="loadpoint" :id="'loadpoint-'+id">
|
||||
</loadpoint>
|
||||
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="loadpoint-template">
|
||||
<div>
|
||||
<div class="row mt-3 mt-md-5 border-bottom">
|
||||
<div class="col-12">
|
||||
<p class="h1">{{state.title||"Ladepunkt"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 mt-3">
|
||||
<div class="mb-2">{{state.socTitle||"Fahrzeug"}}</div>
|
||||
<div class="progress" style="height: 24px; font-size:100%; margin-top:16px">
|
||||
<div class="progress-bar" role="progressbar" v-if="!state.soc"
|
||||
v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-primary': state.connected,
|
||||
'bg-light': !(state.connected || state.charging),
|
||||
'text-secondary': !(state.connected || state.charging),
|
||||
}"
|
||||
v-bind:style="{width: '100%'}">{{state.charging ? "laden" : (state.connected ? "verbunden" : "getrennt")}}</div>
|
||||
<div class="progress-bar" role="progressbar" v-else-if="state.socCharge > 9"
|
||||
v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-primary': !state.charging && state.connected,
|
||||
}"
|
||||
v-bind:style="{width: state.socCharge+'%'}">{{state.socCharge}}%</div>
|
||||
<div class="progress-bar" role="progressbar" v-else
|
||||
v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-primary': !state.charging && state.connected,
|
||||
}"
|
||||
v-bind:style="{width: (state.socCharge<0) ? 0 : state.socCharge+'%'}"></div>
|
||||
<div class="row" v-if="multi">
|
||||
<div class="col-12 col-md-4 d-md-flex mt-3 mt-md-5 align-items-end">
|
||||
<span class="h1 align-bottom">{{state.title||"Ladepunkt"}}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-8 d-none d-md-inline-block mt-3 mt-md-5">
|
||||
<loadpoint-details v-bind:state="state"></loadpoint-details>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-md-none">
|
||||
<div class="row mt-3 pb-3 bg-light">
|
||||
<div class="col-12 mt-3">
|
||||
<mode class="w-100" v-bind:mode="state.mode" :pv="pv" v-on:updated="targetMode"></mode>
|
||||
</div>
|
||||
<div class="col-12 mt-3" v-if="hasTargetSoC">
|
||||
<soc class="w-100" v-bind:soc="state.targetSoC" :levels="state.socLevels" v-on:updated="targetSoC"></soc>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2 mt-3">
|
||||
<div class="row d-none d-md-flex mt-5 py-3 pb-4 text-center bg-light" v-if="!multi">
|
||||
<div class="mt-3" v-bind:class="{'col-md-6':hasTargetSoC,'col-md-12':!hasTargetSoC}">
|
||||
<mode v-bind:mode="state.mode" :pv="pv" :caption="true" v-on:updated="targetMode"></mode>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3" v-if="hasTargetSoC">
|
||||
<soc v-bind:soc="state.targetSoC" :levels="state.socLevels" :caption="true" v-on:updated="targetSoC"></soc>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-md-none mt-2 pb-3 bg-light" v-if="!multi">
|
||||
<div class="col-12 mt-3">
|
||||
<mode class="w-100" v-bind:mode="state.mode" :pv="pv" v-on:updated="targetMode"></mode>
|
||||
</div>
|
||||
<div class="col-12 mt-3" v-if="hasTargetSoC">
|
||||
<soc class="w-100" v-bind:soc="state.targetSoC" :levels="state.socLevels" v-on:updated="targetSoC"></soc>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="!multi">
|
||||
<div class="col-12 col-md-4 d-none d-md-flex mt-3 mt-md-5 align-items-end">
|
||||
<span class="h1 align-bottom">{{state.title||"Ladepunkt"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row border-bottom d-none d-md-block"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 mt-3 mb-3 mb-md-0">
|
||||
<vehicle v-bind:state="state"></vehicle>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 d-none d-md-inline-block mt-3" v-if="multi">
|
||||
<div class="mb-2">Modus</div>
|
||||
<mode class="btn-group-sm" v-bind:mode="state.mode" :pv="pv" v-on:updated="targetMode"></mode>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 d-none d-md-inline-block mt-3" v-if="multi && hasTargetSoC">
|
||||
<div class="mb-2">Ladeziel</div>
|
||||
<soc class="btn-group-sm" v-bind:soc="state.targetSoC" :levels="state.socLevels" v-on:updated="targetSoC"></soc>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 d-none d-md-inline-block" v-if="!multi">
|
||||
<loadpoint-details v-bind:state="state"></loadpoint-details>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-md-none">
|
||||
<loadpoint-details v-bind:state="state"></loadpoint-details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="loadpoint-details-template">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2">Leistung</div>
|
||||
<h2>
|
||||
{{fmt(state.chargePower)}} <small class="text-muted">{{fmtUnit(state.chargePower)}}W</small>
|
||||
<!-- <span class="text-muted">/</span>
|
||||
{{fmt(state.chargeCurrent)}} <small class="text-muted">A</small> -->
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 mt-3">
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2">Geladen</div>
|
||||
<h2>
|
||||
{{fmt(state.chargedEnergy)}} <small class="text-muted">{{fmtUnit(state.chargedEnergy)}}Wh</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2 mt-3">
|
||||
<div class="col-6 col-md-3 mt-3">
|
||||
<div class="mb-2">Dauer</div>
|
||||
<h2>
|
||||
{{fmtShortDuration(state.chargeDuration)}} <small
|
||||
|
|
@ -267,7 +288,7 @@
|
|||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2 mt-3">
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.soc">
|
||||
<div class="mb-2">Restzeit</div>
|
||||
<h2>
|
||||
{{fmtShortDuration(state.chargeEstimate)}} <small
|
||||
|
|
@ -275,6 +296,67 @@
|
|||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="mode-template">
|
||||
<div class="btn-group btn-group-toggle bg-white shadow-none">
|
||||
<label class="btn btn-outline-primary disabled caption font-weight-bold" v-if="caption">
|
||||
Modus
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{active:mode=='off',first:!caption}">
|
||||
<input type="radio" value="off" v-on:click="targetMode('off')">Stop
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{active:mode=='now'}">
|
||||
<input type="radio" value="now" v-on:click="targetMode('now')">Sofort
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{active:mode=='minpv'}" v-if="pv">
|
||||
<input type="radio" value="minpv" v-on:click="targetMode('minpv')">
|
||||
<span class="d-inline d-sm-none">Min + PV</span>
|
||||
<span class="d-none d-sm-inline">Minimum + PV</span>
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-bind:class="{active:mode=='pv'}" v-if="pv">
|
||||
<input type="radio" value="pv" v-on:click="targetMode('pv')">
|
||||
<span class="d-inline d-sm-none">PV</span>
|
||||
<span class="d-none d-sm-inline">Nur PV</span>
|
||||
</label>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="soc-template">
|
||||
<div class="btn-group btn-group-toggle bg-white shadow-none">
|
||||
<label class="btn btn-outline-primary disabled caption font-weight-bold" v-if="caption">
|
||||
Ladeziel
|
||||
</label>
|
||||
<label class="btn btn-outline-primary" v-for="(level,id) in levelsOrDefault" v-bind:level="level" :id="id"
|
||||
:class="{active:soc==level,first:!caption&&id==0}">
|
||||
<input type="radio" v-bind:value="level" v-on:click="targetSoC(level)">{{level}}%
|
||||
</label>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="vehicle-template">
|
||||
<div>
|
||||
<div class="mb-2">{{state.socTitle||"Fahrzeug"}}</div>
|
||||
<div class="progress" style="height: 24px; font-size:100%; margin-top:16px">
|
||||
<div class="progress-bar" role="progressbar" v-if="!state.soc" v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-primary': state.connected,
|
||||
'bg-light': !(state.connected || state.charging),
|
||||
'text-secondary': !(state.connected || state.charging),
|
||||
}" v-bind:style="{width: '100%'}">{{state.charging ? "laden" : (state.connected ? "verbunden" : "getrennt")}}
|
||||
</div>
|
||||
<div class="progress-bar" role="progressbar" v-else-if="state.socCharge > 9" v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-primary': !state.charging && state.connected,
|
||||
}" v-bind:style="{width: state.socCharge+'%'}">{{state.socCharge}}%</div>
|
||||
<div class="progress-bar" role="progressbar" v-else v-bind:class="{
|
||||
'progress-bar-striped': state.charging,
|
||||
'progress-bar-animated': state.charging,
|
||||
'bg-primary': !state.charging && state.connected,
|
||||
}" v-bind:style="{width: (state.socCharge<0) ? 0 : state.socCharge+'%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
|
@ -366,10 +448,10 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">© 2020</small>
|
||||
<small class="text-muted"><<.Version>> <<if ne .Version .Commit>>(<<.Commit>>)<<end>></small>
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<small class="text-muted"><<.Version>> <<if ne .Version .Commit>>(<<.Commit>>)<<end>></small>
|
||||
<small class="text-muted">© 2020</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ Vue.component('version', {
|
|||
watch: {
|
||||
"state.availableVersion": function () {
|
||||
if (this.installed != "<<.Version>>" && // go template parsed?
|
||||
this.installed != "0.0.1-alpha" && // make used?
|
||||
this.state.availableVersion != this.installed) {
|
||||
$(this.$refs.bar).collapse("show");
|
||||
}
|
||||
|
|
@ -193,27 +194,6 @@ Vue.component('version', {
|
|||
}
|
||||
});
|
||||
|
||||
Vue.component('modeswitch', {
|
||||
template: '#mode-template',
|
||||
data: function() {
|
||||
return {
|
||||
state: store.state // global state
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
mode: {
|
||||
get: function() {
|
||||
return this.state.mode;
|
||||
},
|
||||
set: function(mode) {
|
||||
axios.post('mode/' + mode).then(function(response) {
|
||||
this.state.mode = response.data.mode;
|
||||
}.bind(this)).catch(toasts.error);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component("site", {
|
||||
template: "#site-template",
|
||||
mixins: [formatter],
|
||||
|
|
@ -222,6 +202,11 @@ Vue.component("site", {
|
|||
state: store.state // global state
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
multi: function() {
|
||||
return this.state.loadpoints.length > 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
connect: function() {
|
||||
const protocol = loc.protocol == "https:" ? "wss:" : "ws:";
|
||||
|
|
@ -249,15 +234,26 @@ Vue.component("site", {
|
|||
}
|
||||
});
|
||||
|
||||
Vue.component("site-details", {
|
||||
template: "#site-details-template",
|
||||
props: ["state"],
|
||||
mixins: [formatter]
|
||||
});
|
||||
|
||||
Vue.component("loadpoint", {
|
||||
template: "#loadpoint-template",
|
||||
props: { state: Object },
|
||||
props: ["state", "id", "pv", "multi"],
|
||||
mixins: [formatter],
|
||||
data: function() {
|
||||
return {
|
||||
tickerHandle: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasTargetSoC: function () {
|
||||
return this.state.socLevels != null && this.state.socLevels.length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"state.chargeDuration": function() {
|
||||
window.clearInterval(this.tickerHandle);
|
||||
|
|
@ -269,11 +265,65 @@ Vue.component("loadpoint", {
|
|||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
api: function (func) {
|
||||
return "lp/" + this.id + "/" + func;
|
||||
},
|
||||
targetMode: function (mode) {
|
||||
axios.post(this.api("mode") + "/" + mode).then(function (response) {
|
||||
this.state.mode = response.data.mode;
|
||||
}.bind(this)).catch(toasts.error);
|
||||
},
|
||||
targetSoC: function (soc) {
|
||||
axios.post(this.api("targetsoc") + "/" + soc).then(function (response) {
|
||||
this.state.targetSoC = response.data.targetSoC;
|
||||
}.bind(this)).catch(toasts.error);
|
||||
},
|
||||
},
|
||||
destroyed: function() {
|
||||
window.clearInterval(this.tickerHandle);
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component("loadpoint-details", {
|
||||
template: "#loadpoint-details-template",
|
||||
props: ["state"],
|
||||
mixins: [formatter]
|
||||
});
|
||||
|
||||
Vue.component("vehicle", {
|
||||
template: "#vehicle-template",
|
||||
props: ["state"]
|
||||
});
|
||||
|
||||
Vue.component("mode", {
|
||||
template: "#mode-template",
|
||||
props: ["mode", "pv", "caption"],
|
||||
methods: {
|
||||
targetMode: function (mode) {
|
||||
this.$emit("updated", mode)
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component("soc", {
|
||||
template: "#soc-template",
|
||||
props: ["soc", "caption", "levels"],
|
||||
computed: {
|
||||
levelsOrDefault: function() {
|
||||
if (this.levels == null || this.levels.length == 0) {
|
||||
return []; // disabled, or use 30, 50, 80, 100
|
||||
}
|
||||
return this.levels;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
targetSoC: function (mode) {
|
||||
this.$emit("updated", mode)
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
//
|
||||
// Routing
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
|
|
@ -38,9 +39,14 @@ type LoadPoint struct {
|
|||
bus evbus.Bus // event bus
|
||||
pushChan chan<- push.Event // notifications
|
||||
uiChan chan<- util.Param // client push messages
|
||||
lpChan chan<- *LoadPoint // update requests
|
||||
log *util.Logger
|
||||
|
||||
// exposed public configuration
|
||||
sync.Mutex // guard status
|
||||
Mode api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex
|
||||
targetSoC int `mapstructure:"targetSoC"` // Target SoC, guarded by mutex
|
||||
|
||||
Title string `mapstructure:"title"` // UI title
|
||||
Phases int64 `mapstructure:"phases"` // Phases- required for converting power and current
|
||||
ChargerRef string `mapstructure:"charger"` // Charger reference
|
||||
|
|
@ -49,7 +55,12 @@ type LoadPoint struct {
|
|||
ChargeMeterRef string `mapstructure:"charge"` // Charge meter reference
|
||||
}
|
||||
SoC struct {
|
||||
AlwaysUpdate bool `mapstructure:"alwaysUpdate"`
|
||||
AlwaysUpdate bool `mapstructure:"alwaysUpdate"`
|
||||
Levels []int `mapstructure:"levels"`
|
||||
}
|
||||
OnDisconnect struct {
|
||||
Mode api.ChargeMode `mapstructure:"mode"` // Charge mode to apply when car disconnected
|
||||
TargetSoC int `mapstructure:"targetSoC"` // Target SoC to apply when car disconnected
|
||||
}
|
||||
Enable, Disable ThresholdConfig
|
||||
|
||||
|
|
@ -69,6 +80,7 @@ type LoadPoint struct {
|
|||
connectedTime time.Time // Time when vehicle was connected
|
||||
pvTimer time.Time // PV enabled/disable timer
|
||||
|
||||
socCharge float64 // Vehicle SoC
|
||||
chargedEnergy float64 // Charged energy while connected
|
||||
deltaChargedEnergy float64 // Charged energy for single cycle
|
||||
chargeDuration time.Duration // Charge duration
|
||||
|
|
@ -79,6 +91,16 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
|
|||
lp := NewLoadPoint(log)
|
||||
util.DecodeOther(log, other, &lp)
|
||||
|
||||
// workaround mapstructure
|
||||
if lp.Mode == "0" {
|
||||
lp.Mode = api.ModeOff
|
||||
}
|
||||
|
||||
lp.targetSoC = 100
|
||||
if len(lp.SoC.Levels) > 0 {
|
||||
lp.targetSoC = lp.SoC.Levels[len(lp.SoC.Levels)-1]
|
||||
}
|
||||
|
||||
if lp.Meters.ChargeMeterRef != "" {
|
||||
lp.chargeMeter = cp.Meter(lp.Meters.ChargeMeterRef)
|
||||
}
|
||||
|
|
@ -116,6 +138,7 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
|
|||
log: log, // logger
|
||||
clock: clock, // mockable time
|
||||
bus: bus, // event bus
|
||||
Mode: api.ModeOff,
|
||||
Phases: 1,
|
||||
status: api.StatusNone,
|
||||
HandlerConfig: HandlerConfig{
|
||||
|
|
@ -129,6 +152,50 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
|
|||
return lp
|
||||
}
|
||||
|
||||
// GetMode returns loadpoint charge mode
|
||||
func (lp *LoadPoint) GetMode() api.ChargeMode {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
return lp.Mode
|
||||
}
|
||||
|
||||
// SetMode sets loadpoint charge mode
|
||||
func (lp *LoadPoint) SetMode(mode api.ChargeMode) {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
lp.log.INFO.Printf("set charge mode: %s", string(mode))
|
||||
|
||||
// apply immediately
|
||||
if lp.Mode != mode {
|
||||
lp.Mode = mode
|
||||
lp.publish("mode", mode)
|
||||
lp.lpChan <- lp // request loadpoint update
|
||||
}
|
||||
}
|
||||
|
||||
// GetTargetSoC returns loadpoint charge targetSoC
|
||||
func (lp *LoadPoint) GetTargetSoC() int {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
return lp.targetSoC
|
||||
}
|
||||
|
||||
// SetTargetSoC sets loadpoint charge targetSoC
|
||||
func (lp *LoadPoint) SetTargetSoC(targetSoC int) {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
lp.log.INFO.Println("set target soc:", targetSoC)
|
||||
|
||||
// apply immediately
|
||||
if lp.targetSoC != targetSoC {
|
||||
lp.targetSoC = targetSoC
|
||||
lp.publish("targetSoC", targetSoC)
|
||||
lp.lpChan <- lp // request loadpoint update
|
||||
}
|
||||
}
|
||||
|
||||
// configureChargerType ensures that chargeMeter, Rate and Timer can use charger capabilities
|
||||
func (lp *LoadPoint) configureChargerType(charger api.Charger) {
|
||||
// ensure charge meter exists
|
||||
|
|
@ -214,6 +281,14 @@ func (lp *LoadPoint) evVehicleDisconnectHandler() {
|
|||
lp.publish("connectedDuration", lp.clock.Since(lp.connectedTime))
|
||||
|
||||
lp.notify(evVehicleDisconnect)
|
||||
|
||||
// set default mode on disconnect
|
||||
if lp.OnDisconnect.Mode != "" {
|
||||
lp.SetMode(lp.OnDisconnect.Mode)
|
||||
}
|
||||
if lp.OnDisconnect.TargetSoC != 0 {
|
||||
lp.SetTargetSoC(lp.OnDisconnect.TargetSoC)
|
||||
}
|
||||
}
|
||||
|
||||
// evChargeCurrentHandler updates the dummy charge meter's charge power. This simplifies the main flow
|
||||
|
|
@ -248,14 +323,21 @@ func (lp *LoadPoint) Name() string {
|
|||
}
|
||||
|
||||
// Prepare loadpoint configuration by adding missing helper elements
|
||||
func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event) {
|
||||
lp.pushChan = pushChan
|
||||
func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event, lpChan chan<- *LoadPoint) {
|
||||
lp.uiChan = uiChan
|
||||
lp.pushChan = pushChan
|
||||
lp.lpChan = lpChan
|
||||
|
||||
// event handlers
|
||||
_ = lp.bus.Subscribe(evChargeStart, lp.evChargeStartHandler)
|
||||
_ = lp.bus.Subscribe(evChargeStop, lp.evChargeStopHandler)
|
||||
|
||||
// publish initial values
|
||||
lp.Lock()
|
||||
lp.publish("mode", lp.Mode)
|
||||
lp.publish("targetSoC", lp.targetSoC)
|
||||
lp.Unlock()
|
||||
|
||||
// prepare charger status
|
||||
lp.handler.Prepare()
|
||||
}
|
||||
|
|
@ -292,7 +374,7 @@ func (lp *LoadPoint) updateChargeStatus() error {
|
|||
}
|
||||
}
|
||||
|
||||
// changed to A - disconnected
|
||||
// changed to A - disconnected
|
||||
if status == api.StatusA {
|
||||
lp.bus.Publish(evVehicleDisconnect)
|
||||
}
|
||||
|
|
@ -474,8 +556,9 @@ func (lp *LoadPoint) publishSoC() {
|
|||
if lp.SoC.AlwaysUpdate || lp.connected() {
|
||||
f, err := lp.vehicle.ChargeState()
|
||||
if err == nil {
|
||||
lp.log.DEBUG.Printf("vehicle soc: %.0f%%", f)
|
||||
lp.publish("socCharge", f)
|
||||
lp.socCharge = f
|
||||
lp.log.DEBUG.Printf("vehicle soc: %.0f%%", lp.socCharge)
|
||||
lp.publish("socCharge", lp.socCharge)
|
||||
lp.publish("chargeEstimate", lp.remainingChargeDuration(f))
|
||||
return
|
||||
}
|
||||
|
|
@ -487,7 +570,10 @@ func (lp *LoadPoint) publishSoC() {
|
|||
}
|
||||
|
||||
// Update is the main control function. It reevaluates meters and charger state
|
||||
func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) {
|
||||
func (lp *LoadPoint) Update(sitePower float64) {
|
||||
mode := lp.GetMode()
|
||||
lp.publish("mode", string(mode))
|
||||
|
||||
// read and publish meters first
|
||||
lp.updateChargeMeter()
|
||||
|
||||
|
|
@ -522,11 +608,14 @@ func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) {
|
|||
var err error
|
||||
|
||||
// execute loading strategy
|
||||
switch mode {
|
||||
case api.ModeOff:
|
||||
switch {
|
||||
case lp.targetSoC > 0 && lp.vehicle != nil && lp.socCharge >= float64(lp.targetSoC):
|
||||
err = lp.handler.Ramp(0)
|
||||
|
||||
case mode == api.ModeOff:
|
||||
err = lp.handler.Ramp(0, true)
|
||||
|
||||
case api.ModeNow:
|
||||
case mode == api.ModeNow:
|
||||
// ensure that new connections happen at min current
|
||||
current := lp.MinCurrent
|
||||
if lp.connected() {
|
||||
|
|
@ -534,7 +623,7 @@ func (lp *LoadPoint) Update(mode api.ChargeMode, sitePower float64) {
|
|||
}
|
||||
err = lp.handler.Ramp(current, true)
|
||||
|
||||
case api.ModeMinPV, api.ModePV:
|
||||
case mode == api.ModeMinPV || mode == api.ModePV:
|
||||
targetCurrent := lp.maxCurrent(mode, sitePower)
|
||||
if !lp.connected() {
|
||||
// ensure minimum current when not connected
|
||||
|
|
|
|||
|
|
@ -160,7 +160,8 @@ func TestUpdate(t *testing.T) {
|
|||
tc.expect(handler)
|
||||
}
|
||||
|
||||
lp.Update(tc.mode, 0)
|
||||
lp.Mode = tc.mode
|
||||
lp.Update(0)
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
|
|
|
|||
90
core/site.go
90
core/site.go
|
|
@ -1,7 +1,6 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
|
|
@ -16,22 +15,21 @@ import (
|
|||
|
||||
// Updater abstracts the LoadPoint implementation for testing
|
||||
type Updater interface {
|
||||
Update(api.ChargeMode, float64)
|
||||
Update(float64)
|
||||
}
|
||||
|
||||
// Site is the main configuration container. A site can host multiple loadpoints.
|
||||
type Site struct {
|
||||
sync.Mutex // guard status
|
||||
triggerChan chan struct{} // API updates
|
||||
uiChan chan<- util.Param // client push messages
|
||||
log *util.Logger
|
||||
uiChan chan<- util.Param // client push messages
|
||||
lpUpdateChan chan *LoadPoint
|
||||
|
||||
log *util.Logger
|
||||
|
||||
// configuration
|
||||
Title string `mapstructure:"title"` // UI title
|
||||
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
|
||||
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
||||
Mode api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex
|
||||
Meters MetersConfig // Meter references
|
||||
Title string `mapstructure:"title"` // UI title
|
||||
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
|
||||
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
||||
Meters MetersConfig // Meter references
|
||||
|
||||
// meters
|
||||
gridMeter api.Meter // Grid usage meter
|
||||
|
|
@ -66,11 +64,6 @@ func NewSiteFromConfig(
|
|||
Voltage = site.Voltage
|
||||
site.loadpoints = loadpoints
|
||||
|
||||
// workaround mapstructure
|
||||
if site.Mode == "0" {
|
||||
site.Mode = api.ModeOff
|
||||
}
|
||||
|
||||
// configure meter from references
|
||||
// if site.Meters.PVMeterRef == "" && site.Meters.GridMeterRef == "" {
|
||||
// site.log.FATAL.Fatal("config: missing either pv or grid meter")
|
||||
|
|
@ -94,10 +87,8 @@ func NewSiteFromConfig(
|
|||
// NewSite creates a Site with sane defaults
|
||||
func NewSite() *Site {
|
||||
lp := &Site{
|
||||
log: util.NewLogger("core"),
|
||||
triggerChan: make(chan struct{}, 1),
|
||||
Mode: api.ModeOff,
|
||||
Voltage: 230, // V
|
||||
log: util.NewLogger("core"),
|
||||
Voltage: 230, // V
|
||||
}
|
||||
|
||||
return lp
|
||||
|
|
@ -106,7 +97,6 @@ func NewSite() *Site {
|
|||
// SiteConfiguration contains the global site configuration
|
||||
type SiteConfiguration struct {
|
||||
Title string `json:"title"`
|
||||
Mode string `json:"mode"`
|
||||
GridMeter bool `json:"gridMeter"`
|
||||
PVMeter bool `json:"pvMeter"`
|
||||
BatteryMeter bool `json:"batteryMeter"`
|
||||
|
|
@ -115,6 +105,7 @@ type SiteConfiguration struct {
|
|||
|
||||
// LoadpointConfiguration is the loadpoint feature structure
|
||||
type LoadpointConfiguration struct {
|
||||
Mode string `json:"mode"`
|
||||
Title string `json:"title"`
|
||||
Phases int64 `json:"phases"`
|
||||
MinCurrent int64 `json:"minCurrent"`
|
||||
|
|
@ -123,6 +114,7 @@ type LoadpointConfiguration struct {
|
|||
SoC bool `json:"soc"`
|
||||
SoCCapacity int64 `json:"socCapacity"`
|
||||
SoCTitle string `json:"socTitle"`
|
||||
SoCLevels []int `json:"socLevels"`
|
||||
}
|
||||
|
||||
func (lp *LoadPoint) hasChargeMeter() bool {
|
||||
|
|
@ -139,7 +131,6 @@ func (site *Site) LoadPoints() []*LoadPoint {
|
|||
func (site *Site) Configuration() SiteConfiguration {
|
||||
c := SiteConfiguration{
|
||||
Title: site.Title,
|
||||
Mode: string(site.GetMode()),
|
||||
GridMeter: site.gridMeter != nil,
|
||||
PVMeter: site.pvMeter != nil,
|
||||
BatteryMeter: site.batteryMeter != nil,
|
||||
|
|
@ -147,6 +138,7 @@ func (site *Site) Configuration() SiteConfiguration {
|
|||
|
||||
for _, lp := range site.loadpoints {
|
||||
lpc := LoadpointConfiguration{
|
||||
Mode: string(lp.GetMode()),
|
||||
Title: lp.Name(),
|
||||
Phases: lp.Phases,
|
||||
MinCurrent: lp.MinCurrent,
|
||||
|
|
@ -158,6 +150,7 @@ func (site *Site) Configuration() SiteConfiguration {
|
|||
lpc.SoC = true
|
||||
lpc.SoCCapacity = lp.vehicle.Capacity()
|
||||
lpc.SoCTitle = lp.vehicle.Title()
|
||||
lpc.SoCLevels = lp.SoC.Levels
|
||||
}
|
||||
|
||||
c.LoadPoints = append(c.LoadPoints, lpc)
|
||||
|
|
@ -169,7 +162,6 @@ func (site *Site) Configuration() SiteConfiguration {
|
|||
// DumpConfig site configuration
|
||||
func (site *Site) DumpConfig() {
|
||||
site.log.INFO.Println("site config:")
|
||||
site.log.INFO.Printf(" mode: %s", site.GetMode())
|
||||
site.log.INFO.Printf(" grid %s", presence[site.gridMeter != nil])
|
||||
site.log.INFO.Printf(" pv %s", presence[site.pvMeter != nil])
|
||||
site.log.INFO.Printf(" battery %s", presence[site.batteryMeter != nil])
|
||||
|
|
@ -192,36 +184,6 @@ func (site *Site) DumpConfig() {
|
|||
}
|
||||
}
|
||||
|
||||
// Update triggers loadpoint to run main control loop and push messages to UI socket
|
||||
func (site *Site) Update() {
|
||||
select {
|
||||
case site.triggerChan <- struct{}{}: // non-blocking send
|
||||
default:
|
||||
site.log.WARN.Printf("update blocked")
|
||||
}
|
||||
}
|
||||
|
||||
// GetMode returns loadpoint charge mode
|
||||
func (site *Site) GetMode() api.ChargeMode {
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
return site.Mode
|
||||
}
|
||||
|
||||
// SetMode sets loadpoint charge mode
|
||||
func (site *Site) SetMode(mode api.ChargeMode) {
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
|
||||
site.log.INFO.Printf("set charge mode: %s", string(mode))
|
||||
|
||||
// apply immediately
|
||||
if site.Mode != mode {
|
||||
site.Mode = mode
|
||||
site.Update()
|
||||
}
|
||||
}
|
||||
|
||||
// publish sends values to UI and databases
|
||||
func (site *Site) publish(key string, val interface{}) {
|
||||
// test helper
|
||||
|
|
@ -300,17 +262,16 @@ func (site *Site) sitePower() (float64, error) {
|
|||
|
||||
func (site *Site) update(lp Updater) {
|
||||
site.log.DEBUG.Println("----")
|
||||
mode := site.GetMode()
|
||||
site.publish("mode", string(mode))
|
||||
|
||||
if sitePower, err := site.sitePower(); err == nil {
|
||||
lp.Update(mode, sitePower)
|
||||
lp.Update(sitePower)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare attaches communication channels to site and loadpoints
|
||||
func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event) {
|
||||
site.uiChan = uiChan
|
||||
site.lpUpdateChan = make(chan *LoadPoint)
|
||||
|
||||
for id, lp := range site.loadpoints {
|
||||
lpUIChan := make(chan util.Param)
|
||||
|
|
@ -330,7 +291,7 @@ func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event)
|
|||
}
|
||||
}(id)
|
||||
|
||||
lp.Prepare(lpUIChan, lpPushChan)
|
||||
lp.Prepare(lpUIChan, lpPushChan, site.lpUpdateChan)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -346,23 +307,18 @@ func (site *Site) loopLoadpoints(next chan<- Updater) {
|
|||
// Run is the main control loop. It reacts to trigger events by
|
||||
// updating measurements and executing control logic.
|
||||
func (site *Site) Run(interval time.Duration) {
|
||||
// update ticker
|
||||
ticker := time.NewTicker(interval)
|
||||
site.triggerChan <- struct{}{} // start immediately
|
||||
|
||||
loadpointChan := make(chan Updater)
|
||||
go site.loopLoadpoints(loadpointChan)
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
site.update(<-loadpointChan) // start immediately
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
site.update(<-loadpointChan)
|
||||
case <-site.triggerChan:
|
||||
for range site.loadpoints {
|
||||
site.update(<-loadpointChan)
|
||||
}
|
||||
ticker.Stop()
|
||||
ticker = time.NewTicker(interval)
|
||||
case lp := <-site.lpUpdateChan:
|
||||
site.update(lp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,8 +231,17 @@ site:
|
|||
loadpoints:
|
||||
- title: Ladepunkt # ui display name
|
||||
vehicle: audi
|
||||
mode: pv
|
||||
soc:
|
||||
alwaysUpdate: false # set true to update vehicle soc even when disconnected
|
||||
levels: # target soc levels for UI
|
||||
- 30
|
||||
- 50
|
||||
- 80
|
||||
- 100
|
||||
onDisconnect:
|
||||
mode: pv # switch back to pv mode
|
||||
targetSoC: 100 # charge fully
|
||||
charger: wallbe # charger
|
||||
phases: 3 # ev phases (default 3)
|
||||
sensitivity: 1 # current raise/lower step size (default 10A)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
package mock
|
||||
|
||||
import (
|
||||
api "github.com/andig/evcc/api"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
|
@ -34,13 +33,13 @@ func (m *MockUpdater) EXPECT() *MockUpdaterMockRecorder {
|
|||
}
|
||||
|
||||
// Update mocks base method
|
||||
func (m *MockUpdater) Update(arg0 api.ChargeMode, arg1 float64) {
|
||||
func (m *MockUpdater) Update(arg0 float64) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Update", arg0, arg1)
|
||||
m.ctrl.Call(m, "Update", arg0)
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update
|
||||
func (mr *MockUpdaterMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call {
|
||||
func (mr *MockUpdaterMockRecorder) Update(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0, arg1)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0)
|
||||
}
|
||||
|
|
|
|||
211
server/assets.go
211
server/assets.go
|
|
@ -3561,115 +3561,128 @@ oJE6K0E084u3ea7ne0X73qsc2P2KOB+ngKecxITsMAgTVJfqMwAA//+21HeD5gEAAA==
|
|||
"/index.html": {
|
||||
name: "index.html",
|
||||
local: "../assets/index.html",
|
||||
size: 14298,
|
||||
size: 17523,
|
||||
modtime: 1566640112,
|
||||
compressed: `
|
||||
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
|
||||
`,
|
||||
},
|
||||
|
||||
"/js/app.js": {
|
||||
name: "app.js",
|
||||
local: "../assets/js/app.js",
|
||||
size: 7225,
|
||||
size: 8507,
|
||||
modtime: 1566640112,
|
||||
compressed: `
|
||||
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
|
||||
fwUAAP//HalhmTkcAAA=
|
||||
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
|
||||
`,
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
|
|
@ -33,6 +34,10 @@ type chargeModeJSON struct {
|
|||
Mode api.ChargeMode `json:"mode"`
|
||||
}
|
||||
|
||||
type targetSoCJSON struct {
|
||||
TargetSoC int `json:"targetSoC"`
|
||||
}
|
||||
|
||||
type route struct {
|
||||
Methods []string
|
||||
Pattern string
|
||||
|
|
@ -41,9 +46,16 @@ type route struct {
|
|||
|
||||
// site is the minimal interface for accessing site methods
|
||||
type site interface {
|
||||
Configuration() core.SiteConfiguration
|
||||
LoadPoints() []*core.LoadPoint
|
||||
}
|
||||
|
||||
// mode is the minimal interface for accessing loadpoint methods
|
||||
type loadpoint interface {
|
||||
GetMode() api.ChargeMode
|
||||
SetMode(api.ChargeMode)
|
||||
Configuration() core.SiteConfiguration
|
||||
GetTargetSoC() int
|
||||
SetTargetSoC(targetSoC int)
|
||||
}
|
||||
|
||||
// routeLogger traces matched routes including their executing time
|
||||
|
|
@ -128,15 +140,15 @@ func StateHandler(cache *util.Cache) http.HandlerFunc {
|
|||
}
|
||||
|
||||
// CurrentChargeModeHandler returns current charge mode
|
||||
func CurrentChargeModeHandler(site site) http.HandlerFunc {
|
||||
func CurrentChargeModeHandler(loadpoint loadpoint) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
res := chargeModeJSON{Mode: site.GetMode()}
|
||||
res := chargeModeJSON{Mode: loadpoint.GetMode()}
|
||||
jsonResponse(w, r, res)
|
||||
}
|
||||
}
|
||||
|
||||
// ChargeModeHandler updates charge mode
|
||||
func ChargeModeHandler(site site) http.HandlerFunc {
|
||||
func ChargeModeHandler(loadpoint loadpoint) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
|
|
@ -146,9 +158,37 @@ func ChargeModeHandler(site site) http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
site.SetMode(api.ChargeMode(mode))
|
||||
loadpoint.SetMode(api.ChargeMode(mode))
|
||||
|
||||
res := chargeModeJSON{Mode: site.GetMode()}
|
||||
res := chargeModeJSON{Mode: loadpoint.GetMode()}
|
||||
jsonResponse(w, r, res)
|
||||
}
|
||||
}
|
||||
|
||||
// CurrentTargetSoCHandler returns current target soc
|
||||
func CurrentTargetSoCHandler(loadpoint loadpoint) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
res := targetSoCJSON{TargetSoC: loadpoint.GetTargetSoC()}
|
||||
jsonResponse(w, r, res)
|
||||
}
|
||||
}
|
||||
|
||||
// TargetSoCHandler updates target soc
|
||||
func TargetSoCHandler(loadpoint loadpoint) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
socS, ok := vars["soc"]
|
||||
soc, err := strconv.ParseInt(socS, 10, 32)
|
||||
|
||||
if !ok || err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
loadpoint.SetTargetSoC(int(soc))
|
||||
|
||||
res := targetSoCJSON{TargetSoC: loadpoint.GetTargetSoC()}
|
||||
jsonResponse(w, r, res)
|
||||
}
|
||||
}
|
||||
|
|
@ -162,17 +202,15 @@ func SocketHandler(hub *SocketHub) http.HandlerFunc {
|
|||
|
||||
// NewHTTPd creates HTTP server with configured routes for loadpoint
|
||||
func NewHTTPd(url string, links []MenuConfig, site site, hub *SocketHub, cache *util.Cache) *http.Server {
|
||||
var routes = []route{{
|
||||
[]string{"GET"}, "/health", HealthHandler(),
|
||||
}, {
|
||||
[]string{"GET"}, "/config", ConfigHandler(site),
|
||||
}, {
|
||||
[]string{"GET"}, "/state", StateHandler(cache),
|
||||
}, {
|
||||
[]string{"GET"}, "/mode", CurrentChargeModeHandler(site),
|
||||
}, {
|
||||
[]string{"PUT", "POST", "OPTIONS"}, "/mode/{mode:[a-z]+}", ChargeModeHandler(site),
|
||||
}}
|
||||
var routes = map[string]route{
|
||||
"health": {[]string{"GET"}, "/health", HealthHandler()},
|
||||
"config": {[]string{"GET"}, "/config", ConfigHandler(site)},
|
||||
"state": {[]string{"GET"}, "/state", StateHandler(cache)},
|
||||
"getmode": {[]string{"GET"}, "/mode", CurrentChargeModeHandler(site.LoadPoints()[0])},
|
||||
"setmode": {[]string{"PUT", "POST", "OPTIONS"}, "/mode/{mode:[a-z]+}", ChargeModeHandler(site.LoadPoints()[0])},
|
||||
"gettargetsoc": {[]string{"GET"}, "/targetsoc", CurrentTargetSoCHandler(site.LoadPoints()[0])},
|
||||
"settargetsoc": {[]string{"PUT", "POST", "OPTIONS"}, "/targetsoc/{soc:[0-9]+}", TargetSoCHandler(site.LoadPoints()[0])},
|
||||
}
|
||||
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
|
||||
|
|
@ -199,11 +237,26 @@ func NewHTTPd(url string, links []MenuConfig, site site, hub *SocketHub, cache *
|
|||
}),
|
||||
))
|
||||
|
||||
// site api
|
||||
for _, r := range routes {
|
||||
api.
|
||||
Methods(r.Methods...).
|
||||
Path(r.Pattern).
|
||||
Handler(r.HandlerFunc) // routeLogger
|
||||
api.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
|
||||
}
|
||||
|
||||
// loadpoint api
|
||||
for id, lp := range site.LoadPoints() {
|
||||
subAPI := api.PathPrefix(fmt.Sprintf("/lp/%d", id)).Subrouter()
|
||||
|
||||
r := routes["getmode"]
|
||||
subAPI.Methods(r.Methods...).Path(r.Pattern).Handler(CurrentChargeModeHandler(lp))
|
||||
|
||||
r = routes["setmode"]
|
||||
subAPI.Methods(r.Methods...).Path(r.Pattern).Handler(ChargeModeHandler(lp))
|
||||
|
||||
r = routes["gettargetsoc"]
|
||||
subAPI.Methods(r.Methods...).Path(r.Pattern).Handler(CurrentTargetSoCHandler(lp))
|
||||
|
||||
r = routes["settargetsoc"]
|
||||
subAPI.Methods(r.Methods...).Path(r.Pattern).Handler(TargetSoCHandler(lp))
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue