Refactor configuration handling (#424)
This commit is contained in:
parent
9090514a9e
commit
7683f6637f
11 changed files with 79 additions and 153 deletions
|
|
@ -447,8 +447,7 @@ EVCC provides a REST and MQTT APIs.
|
|||
|
||||
### REST API
|
||||
|
||||
- `/api/config`: EVCC static configuration
|
||||
- `/api/state`: EVCC dynamic state
|
||||
- `/api/state`: EVCC state (static configuration and dynamic state)
|
||||
- `/api/mode`: global charge mode (writable)
|
||||
- `/api/targetsoc`: global target SoC (writable)
|
||||
- `/api/loadpoints/<id>/mode`: loadpoint charge mode (writable)
|
||||
|
|
|
|||
|
|
@ -176,14 +176,14 @@ loadpoints:
|
|||
</div>
|
||||
|
||||
<loadpoint v-for="(loadpoint,id) in state.loadpoints"
|
||||
v-bind:id="id" :state="loadpoint" :pv="state.gridMeter" :multi="multi">
|
||||
v-bind:id="id" :key="id" :state="loadpoint" :pv="state.gridConfigured" :multi="multi">
|
||||
</loadpoint>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="site-details-template">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.gridMeter">
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.gridConfigured">
|
||||
<div class="mb-2 value" v-if="state.gridPower > 0">
|
||||
Bezug <i class="text-primary fas fa-arrow-down"></i>
|
||||
</div>
|
||||
|
|
@ -194,7 +194,7 @@ loadpoints:
|
|||
{{fmt(state.gridPower)}} <small class="text-muted">{{fmtUnit(state.gridPower)}}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.pvMeter">
|
||||
<div class="col-6 col-md-3 mt-3" v-if="state.pvConfigured">
|
||||
<div class="mb-2 value">
|
||||
Erzeugung <i class="fas fa-sun" v-bind:class="{'text-primary':state.pvPower<0,'text-muted':state.pvPower>=0}"></i>
|
||||
</div>
|
||||
|
|
@ -202,7 +202,7 @@ loadpoints:
|
|||
{{fmt(state.pvPower)}} <small class="text-muted">{{fmtUnit(state.pvPower)}}W</small>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="d-md-block col-6 col-md-3 mt-3" v-bind:class="{'d-none':!state.batterySoC}" v-if="state.batteryMeter">
|
||||
<div class="d-md-block col-6 col-md-3 mt-3" v-bind:class="{'d-none':!state.batterySoC}" v-if="state.batteryConfigured">
|
||||
<div class="mb-2 value">Batterie
|
||||
<i class="text-primary fas" v-bind:class="{'fa-arrow-down':state.batteryPower<0, 'fa-arrow-up':state.batteryPower>0}"></i>
|
||||
</div>
|
||||
|
|
@ -407,23 +407,22 @@ loadpoints:
|
|||
<div class="col-md-4"></div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Netzzähler:
|
||||
<span class="text-primary" v-if="state.gridMeter">✓</span>
|
||||
<span class="text-primary" v-if="state.gridConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
PV Zähler:
|
||||
<span class="text-primary" v-if="state.pvMeter">✓</span>
|
||||
<span class="text-primary" v-if="state.pvConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Batteriezähler:
|
||||
<span class="text-primary" v-if="state.batteryMeter">✓</span>
|
||||
<span class="text-primary" v-if="state.batteryConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(loadpoint,id) in state.loadpoints" v-bind:loadpoint="loadpoint" :id="'loadpoint-'+id">
|
||||
|
||||
<div class="row mt-4 border-bottom">
|
||||
<div class="col-12">
|
||||
<p class="h1">{{loadpoint.title||"Ladepunkt"}}</p>
|
||||
|
|
@ -434,7 +433,7 @@ loadpoints:
|
|||
<div class="col-md-4"></div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
Ladezähler:
|
||||
<span class="text-primary" v-if="loadpoint.chargeMeter">✓</span>
|
||||
<span class="text-primary" v-if="loadpoint.chargeConfigured">✓</span>
|
||||
<span class="text-primary" v-else>—</span>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 py-3">
|
||||
|
|
|
|||
|
|
@ -82,38 +82,36 @@ let formatter = {
|
|||
// State
|
||||
//
|
||||
|
||||
function setProperty(obj, props, value) {
|
||||
const prop = props.shift()
|
||||
if (!obj[prop]) {
|
||||
Vue.set(obj, prop, {})
|
||||
}
|
||||
|
||||
if (!props.length) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
obj[prop] = { ...obj[prop], ...value }
|
||||
} else {
|
||||
obj[prop] = value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setProperty(obj[prop], props, value)
|
||||
}
|
||||
|
||||
let store = {
|
||||
state: {
|
||||
availableVersion: null,
|
||||
loadpoints: [],
|
||||
loadpoints: [], // ensure array type
|
||||
},
|
||||
update: function(msg) {
|
||||
let target = this.state;
|
||||
if (msg.loadpoint !== undefined) {
|
||||
while (this.state.loadpoints.length <= msg.loadpoint) {
|
||||
this.state.loadpoints.push({});
|
||||
}
|
||||
target = this.state.loadpoints[msg.loadpoint];
|
||||
}
|
||||
|
||||
Object.keys(msg).forEach(function (k) {
|
||||
if (typeof toasts[k] === "function") {
|
||||
toasts[k]({message: msg[k]})
|
||||
} else {
|
||||
Vue.set(target, k, msg[k]);
|
||||
setProperty(store.state, k.split('.'), msg[k])
|
||||
}
|
||||
});
|
||||
},
|
||||
init: function() {
|
||||
axios.get("config").then(function(msg) {
|
||||
for (let i=0; i<msg.data.loadpoints.length; i++) {
|
||||
let data = Object.assign(msg.data.loadpoints[i], { loadpoint: i });
|
||||
this.update(data);
|
||||
}
|
||||
|
||||
delete msg.data.loadpoints;
|
||||
this.update(msg.data);
|
||||
}.bind(this)).catch(toasts.error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -178,9 +176,32 @@ const app = new Vue({
|
|||
data: {
|
||||
compact: false,
|
||||
},
|
||||
methods: {
|
||||
connect: function() {
|
||||
const protocol = loc.protocol == "https:" ? "wss:" : "ws:";
|
||||
const uri = protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + loc.pathname + "ws";
|
||||
const ws = new WebSocket(uri), self = this;
|
||||
ws.onerror = function(evt) {
|
||||
ws.close();
|
||||
};
|
||||
ws.onclose = function(evt) {
|
||||
window.setTimeout(self.connect, 1000);
|
||||
};
|
||||
ws.onmessage = function(evt) {
|
||||
try {
|
||||
var msg = JSON.parse(evt.data);
|
||||
store.update(msg);
|
||||
}
|
||||
catch (e) {
|
||||
toasts.error(e, evt.data)
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.compact = urlParams.get("compact");
|
||||
this.connect(); // websocket listener
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -265,33 +286,9 @@ Vue.component('site', {
|
|||
mixins: [formatter],
|
||||
computed: {
|
||||
multi: function() {
|
||||
console.log(this.state);
|
||||
return this.state.loadpoints.length > 1 || app.compact;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
connect: function() {
|
||||
const protocol = loc.protocol == "https:" ? "wss:" : "ws:";
|
||||
const uri = protocol + "//" + loc.hostname + (loc.port ? ":" + loc.port : "") + loc.pathname + "ws";
|
||||
const ws = new WebSocket(uri), self = this;
|
||||
ws.onerror = function(evt) {
|
||||
ws.close();
|
||||
};
|
||||
ws.onclose = function(evt) {
|
||||
window.setTimeout(self.connect, 1000);
|
||||
};
|
||||
ws.onmessage = function(evt) {
|
||||
try {
|
||||
var msg = JSON.parse(evt.data);
|
||||
store.update(msg);
|
||||
}
|
||||
catch (e) {
|
||||
toasts.error(e, evt.data)
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
created: function() {
|
||||
this.connect();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -424,5 +421,3 @@ Vue.component("soc", {
|
|||
}
|
||||
},
|
||||
});
|
||||
|
||||
store.init();
|
||||
|
|
|
|||
|
|
@ -352,6 +352,7 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
|
|||
lp.publish("mode", lp.Mode)
|
||||
lp.publish("targetSoC", lp.SoC.Target)
|
||||
lp.publish("minSoC", lp.SoC.Min)
|
||||
lp.publish("socLevels", lp.SoC.Levels)
|
||||
lp.Unlock()
|
||||
|
||||
// prepare charger status
|
||||
|
|
|
|||
57
core/site.go
57
core/site.go
|
|
@ -101,63 +101,6 @@ func NewSite() *Site {
|
|||
return lp
|
||||
}
|
||||
|
||||
// SiteConfiguration contains the global site configuration
|
||||
type SiteConfiguration struct {
|
||||
Title string `json:"title"`
|
||||
GridMeter bool `json:"gridMeter"`
|
||||
PVMeter bool `json:"pvMeter"`
|
||||
BatteryMeter bool `json:"batteryMeter"`
|
||||
LoadPoints []LoadpointConfiguration `json:"loadpoints"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
MaxCurrent int64 `json:"maxCurrent"`
|
||||
ChargeMeter bool `json:"chargeMeter"`
|
||||
SoC bool `json:"soc"`
|
||||
SoCCapacity int64 `json:"socCapacity"`
|
||||
SoCTitle string `json:"socTitle"`
|
||||
SoCLevels []int `json:"socLevels"`
|
||||
TargetSoC int `json:"targetSoC"`
|
||||
}
|
||||
|
||||
// Configuration returns meter configuration
|
||||
func (site *Site) Configuration() SiteConfiguration {
|
||||
c := SiteConfiguration{
|
||||
Title: site.Title,
|
||||
GridMeter: site.gridMeter != nil,
|
||||
PVMeter: site.pvMeter != nil,
|
||||
BatteryMeter: site.batteryMeter != nil,
|
||||
}
|
||||
|
||||
for _, lp := range site.loadpoints {
|
||||
lpc := LoadpointConfiguration{
|
||||
Mode: string(lp.GetMode()),
|
||||
Title: lp.Name(),
|
||||
Phases: lp.Phases,
|
||||
MinCurrent: lp.MinCurrent,
|
||||
MaxCurrent: lp.MaxCurrent,
|
||||
ChargeMeter: lp.HasChargeMeter(),
|
||||
}
|
||||
|
||||
if lp.vehicle != nil {
|
||||
lpc.SoC = true
|
||||
lpc.SoCCapacity = lp.vehicle.Capacity()
|
||||
lpc.SoCTitle = lp.vehicle.Title()
|
||||
lpc.SoCLevels = lp.SoC.Levels
|
||||
lpc.TargetSoC = lp.GetTargetSoC()
|
||||
}
|
||||
|
||||
c.LoadPoints = append(c.LoadPoints, lpc)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func meterCapabilities(name string, meter interface{}) string {
|
||||
_, power := meter.(api.Meter)
|
||||
_, energy := meter.(api.MeterEnergy)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import "github.com/andig/evcc/api"
|
|||
// SiteAPI is the external site API
|
||||
type SiteAPI interface {
|
||||
Healthy() bool
|
||||
Configuration() SiteConfiguration
|
||||
LoadPoints() []LoadPointAPI
|
||||
LoadPointSettingsAPI
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ type SEMP struct {
|
|||
|
||||
// site is the minimal interface for accessing site methods
|
||||
type site interface {
|
||||
Configuration() core.SiteConfiguration
|
||||
LoadPoints() []core.LoadPointAPI
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ func indexHandler(site core.SiteAPI, useLocal bool) http.HandlerFunc {
|
|||
"Version": Version,
|
||||
"Commit": Commit,
|
||||
"Configured": len(site.LoadPoints()),
|
||||
"Tag": time.Now().Unix(),
|
||||
}); err != nil {
|
||||
log.ERROR.Println("httpd: failed to render main page:", err.Error())
|
||||
}
|
||||
|
|
@ -102,14 +101,6 @@ func HealthHandler(site core.SiteAPI) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// ConfigHandler returns current charge mode
|
||||
func ConfigHandler(site core.SiteAPI) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
res := site.Configuration()
|
||||
jsonResponse(w, r, res)
|
||||
}
|
||||
}
|
||||
|
||||
// TemplatesHandler returns current charge mode
|
||||
func TemplatesHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -259,9 +250,8 @@ type HTTPd struct {
|
|||
func NewHTTPd(url string, site core.SiteAPI, hub *SocketHub, cache *util.Cache) *HTTPd {
|
||||
var routes = map[string]route{
|
||||
"health": {[]string{"GET"}, "/health", HealthHandler(site)},
|
||||
"config": {[]string{"GET"}, "/config", ConfigHandler(site)},
|
||||
"templates": {[]string{"GET"}, "/config/templates/{class:[a-z]+}", TemplatesHandler()},
|
||||
"state": {[]string{"GET"}, "/state", StateHandler(cache)},
|
||||
"templates": {[]string{"GET"}, "/config/templates/{class:[a-z]+}", TemplatesHandler()},
|
||||
"getmode": {[]string{"GET"}, "/mode", CurrentChargeModeHandler(site)},
|
||||
"setmode": {[]string{"POST", "OPTIONS"}, "/mode/{mode:[a-z]+}", ChargeModeHandler(site)},
|
||||
"gettargetsoc": {[]string{"GET"}, "/targetsoc", CurrentTargetSoCHandler(site)},
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func (m *MQTT) encode(v interface{}) string {
|
|||
case fmt.Stringer, string:
|
||||
s = fmt.Sprintf("%s", val)
|
||||
case float64:
|
||||
s = fmt.Sprintf("%.4g", val)
|
||||
s = fmt.Sprintf("%.5g", val)
|
||||
default:
|
||||
s = fmt.Sprintf("%v", val)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,9 +94,8 @@ func encode(v interface{}) (string, error) {
|
|||
// must be before stringer to convert to seconds instead of string
|
||||
s = fmt.Sprintf("%d", int64(val.Seconds()))
|
||||
case float64:
|
||||
s = fmt.Sprintf("%.3f", val)
|
||||
s = fmt.Sprintf("%.5g", val)
|
||||
default:
|
||||
// s = fmt.Sprintf("%v", val)
|
||||
if b, err := json.Marshal(v); err == nil {
|
||||
s = string(b)
|
||||
}
|
||||
|
|
@ -104,25 +103,20 @@ func encode(v interface{}) (string, error) {
|
|||
return s, nil
|
||||
}
|
||||
|
||||
func kv(i util.Param) string {
|
||||
val, err := encode(i.Val)
|
||||
func kv(p util.Param) string {
|
||||
val, err := encode(p.Val)
|
||||
if err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return "\"" + i.Key + "\":" + val
|
||||
}
|
||||
|
||||
// paramToJSON converts util.Param to JSON as expected by UI
|
||||
func paramToJSON(p util.Param) string {
|
||||
var msg strings.Builder
|
||||
|
||||
msg.WriteString("{")
|
||||
msg.WriteString("\"")
|
||||
if p.LoadPoint != nil {
|
||||
msg.WriteString(fmt.Sprintf("\"loadpoint\":%d,", *p.LoadPoint))
|
||||
msg.WriteString(fmt.Sprintf("loadpoints.%d.", *p.LoadPoint))
|
||||
}
|
||||
msg.WriteString(kv(p))
|
||||
msg.WriteString("}")
|
||||
msg.WriteString(p.Key)
|
||||
msg.WriteString("\":")
|
||||
msg.WriteString(val)
|
||||
|
||||
return msg.String()
|
||||
}
|
||||
|
|
@ -130,19 +124,26 @@ func paramToJSON(p util.Param) string {
|
|||
func (h *SocketHub) welcome(client *SocketClient, params []util.Param) {
|
||||
h.clients[client] = true
|
||||
|
||||
var msg strings.Builder
|
||||
msg.WriteString("{")
|
||||
for _, p := range params {
|
||||
msg := paramToJSON(p)
|
||||
select {
|
||||
case client.send <- []byte(msg):
|
||||
default:
|
||||
close(client.send)
|
||||
if msg.Len() > 1 {
|
||||
msg.WriteString(",")
|
||||
}
|
||||
msg.WriteString(kv(p))
|
||||
}
|
||||
msg.WriteString("}")
|
||||
|
||||
select {
|
||||
case client.send <- []byte(msg.String()):
|
||||
default:
|
||||
close(client.send)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SocketHub) broadcast(p util.Param) {
|
||||
if len(h.clients) > 0 {
|
||||
msg := paramToJSON(p)
|
||||
msg := "{" + kv(p) + "}"
|
||||
|
||||
for client := range h.clients {
|
||||
select {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ func TestEncode(t *testing.T) {
|
|||
in, out interface{}
|
||||
}{
|
||||
{int64(1), "1"},
|
||||
{float64(1.2345), "1.234"},
|
||||
{float64(1.23456), "1.2346"},
|
||||
{"1.2345", "\"1.2345\""},
|
||||
{time.Duration(time.Hour), "3600"},
|
||||
{"minpv", "\"minpv\""},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue