diff --git a/cmd/charger.go b/cmd/charger.go index 9a8594be2..0a86ef50a 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -24,7 +24,10 @@ func runCharger(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit) // load config - conf := loadConfigFile(cfgFile) + conf, err := loadConfigFile(cfgFile) + if err != nil { + log.FATAL.Fatal(err) + } // setup mqtt if conf.Mqtt.Broker != "" { diff --git a/cmd/config.go b/cmd/config.go index a6ca22710..560051cb6 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -21,6 +21,7 @@ type config struct { Levels map[string]string Interval time.Duration Mqtt mqttConfig + Javascript map[string]interface{} Influx server.InfluxConfig HEMS typedConfig Messaging messagingConfig diff --git a/cmd/demo.go b/cmd/demo.go new file mode 100644 index 000000000..3e42b404b --- /dev/null +++ b/cmd/demo.go @@ -0,0 +1,140 @@ +package cmd + +import ( + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +const demoYaml = ` +log: info + +javascript: +- vm: shared + script: | + state = { + residualpower: 200, + pvpower: -3000, + batterypower: 200, + gridpower: -2000, + chargepower: 0, + maxcurrent: 0, + battery: 63, // car + }; + function get() { + console.log("state:", JSON.stringify(state)); + } + function set() { + console.log(param+":", val); + console.log("state:", JSON.stringify(state)); + } + +meters: +- name: grid + type: default + power: + type: js + vm: shared + script: state.gridpower = state.pvpower + state.chargepower + state.residualpower + state.batterypower; + +- name: pv + type: default + power: + type: js + vm: shared + script: state.pvpower += 100*Math.random(); + +- name: battery + type: default + power: + type: js + vm: shared + script: state.batterypower; + soc: + type: js + vm: shared + script: "30" +- name: charge + type: default + power: + type: js + vm: shared + script: state.chargepower; + soc: + type: js + vm: shared + script: "30" + +chargers: +- name: demo + type: default + enable: + type: js + vm: shared + script: | + set(); + state.enabled = val; + if (state.enabled) state.chargepower = state.maxcurrent * 230; else state.chargepower = 0; + enabled: + type: js + vm: shared + script: | + state.enabled; + status: + type: js + vm: shared + script: | + if (state.enabled) "C"; else "B"; + maxcurrent: + type: js + vm: shared + script: | + set(); + state.maxcurrent = val; + if (state.enabled) state.chargepower = state.maxcurrent * 230; + +vehicles: +- name: demo + title: e-Golf + type: default + charge: + type: js + vm: shared + script: | + if (state.chargepower > 0) state.battery++; else state.battery--; + if (state.battery < 15) state.battery = 15; + if (state.battery > 100) state.battery = 100; + state.battery; + cache: 1s + +site: + title: Demo + meters: + grid: grid + pv: pv + battery: battery + +loadpoints: +- title: Carport + charger: demo + meters: + charge: charge + vehicle: demo +` + +func demoConfig() (conf config) { + demo := map[string]interface{}{} + + if err := yaml.Unmarshal([]byte(demoYaml), &demo); err != nil { + log.FATAL.Fatalf("failed decoding demo config: %+v", err) + } + + for k, v := range demo { + viper.Set(k, v) + } + + if err := viper.UnmarshalExact(&conf); err != nil { + log.FATAL.Fatalf("failed loading demo config: %v", err) + } + + return conf +} diff --git a/cmd/dump.go b/cmd/dump.go index 0ec76bc31..45c481fa3 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -26,7 +26,10 @@ func runDump(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit) // load config - conf := loadConfigFile(cfgFile) + conf, err := loadConfigFile(cfgFile) + if err != nil { + log.FATAL.Fatal(err) + } // setup mqtt if conf.Mqtt.Broker != "" { diff --git a/cmd/meter.go b/cmd/meter.go index d8d7f2a5b..73c67d5eb 100644 --- a/cmd/meter.go +++ b/cmd/meter.go @@ -24,7 +24,10 @@ func runMeter(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit) // load config - conf := loadConfigFile(cfgFile) + conf, err := loadConfigFile(cfgFile) + if err != nil { + log.FATAL.Fatal(err) + } // setup mqtt if conf.Mqtt.Broker != "" { diff --git a/cmd/root.go b/cmd/root.go index 9c3a74fb3..1d0ce7867 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -142,7 +142,12 @@ func run(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit) // load config and re-configure logging after reading config file - conf := loadConfigFile(cfgFile) + conf, err := loadConfigFile(cfgFile) + if err != nil { + log.ERROR.Println("missing evcc config - switching into demo mode") + conf = demoConfig() + } + util.LogLevel(viper.GetString("log"), viper.GetStringMapString("levels")) uri := viper.GetString("uri") @@ -153,6 +158,9 @@ func run(cmd *cobra.Command, args []string) { configureMQTT(conf.Mqtt) } + // setup javascript VMs + configureJavascript(conf.Javascript) + // start broadcasting values tee := &Tee{} diff --git a/cmd/setup.go b/cmd/setup.go index 9060fbbfd..834264324 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -9,6 +9,7 @@ import ( "github.com/andig/evcc/core" "github.com/andig/evcc/hems" + "github.com/andig/evcc/provider/javascript" "github.com/andig/evcc/provider/mqtt" "github.com/andig/evcc/push" "github.com/andig/evcc/server" @@ -57,6 +58,13 @@ func configureMQTT(conf mqttConfig) { } } +// setup javascript +func configureJavascript(conf map[string]interface{}) { + if err := javascript.Configure(conf); err != nil { + log.FATAL.Fatalf("failed configuring javascript: %v", err) + } +} + // setup HEMS func configureHEMS(conf typedConfig, site *core.Site, cache *util.Cache, httpd *server.HTTPd) hems.HEMS { hems, err := hems.NewFromConfig(conf.Type, conf.Other, site, cache, httpd) @@ -131,15 +139,15 @@ func configureLoadPoints(conf config, cp *ConfigProvider) (loadPoints []*core.Lo return loadPoints, nil } -func loadConfigFile(cfgFile string) (conf config) { +func loadConfigFile(cfgFile string) (conf config, err error) { if cfgFile != "" { log.INFO.Println("using config file", cfgFile) if err := viper.UnmarshalExact(&conf); err != nil { log.FATAL.Fatalf("failed parsing config file %s: %v", cfgFile, err) } } else { - log.FATAL.Fatal("missing evcc config") + err = errors.New("missing evcc config") } - return conf + return conf, err } diff --git a/cmd/vehicle.go b/cmd/vehicle.go index 5c07f6da4..d9d00b90a 100644 --- a/cmd/vehicle.go +++ b/cmd/vehicle.go @@ -24,7 +24,10 @@ func runVehicle(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit) // load config - conf := loadConfigFile(cfgFile) + conf, err := loadConfigFile(cfgFile) + if err != nil { + log.FATAL.Fatal(err) + } // setup mqtt if conf.Mqtt.Broker != "" { diff --git a/go.mod b/go.mod index 7825ad6b2..0fcbf05d8 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c github.com/volkszaehler/mbmd v0.0.0-20201205173745-5106cb0b334e golang.org/x/net v0.0.0-20200904194848-62affa334b73 + golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 // indirect gopkg.in/ini.v1 v1.62.0 gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 diff --git a/go.sum b/go.sum index c1cb64e3f..6a5803c11 100644 --- a/go.sum +++ b/go.sum @@ -435,6 +435,8 @@ golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIA golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 h1:joucsQqXmyBVxViHCPFjG3hx8JzIFSaym3l3MM/Jsdg= +golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/provider/javascript.go b/provider/javascript.go index b877733a8..4426a2dc9 100644 --- a/provider/javascript.go +++ b/provider/javascript.go @@ -1,6 +1,9 @@ package provider import ( + "strings" + + "github.com/andig/evcc/provider/javascript" "github.com/andig/evcc/util" "github.com/robertkrimen/otto" ) @@ -19,6 +22,7 @@ func init() { // NewJavascriptProviderFromConfig creates a HTTP provider func NewJavascriptProviderFromConfig(other map[string]interface{}) (IntProvider, error) { cc := struct { + VM string Script string }{} @@ -28,9 +32,11 @@ func NewJavascriptProviderFromConfig(other map[string]interface{}) (IntProvider, log := util.NewLogger("js") + vm := javascript.RegisteredVM(strings.ToLower(cc.VM)) + p := &Javascript{ log: log, - vm: otto.New(), + vm: vm, script: cc.Script, } @@ -85,10 +91,21 @@ func (p *Javascript) BoolGetter() func() (bool, error) { } } +func (p *Javascript) setParam(param string, val interface{}) error { + err := p.vm.Set(param, val) + if err == nil { + err = p.vm.Set("param", param) + } + if err == nil { + err = p.vm.Set("val", val) + } + return err +} + // IntSetter sends int request func (p *Javascript) IntSetter(param string) func(int64) error { return func(val int64) error { - err := p.vm.Set(param, val) + err := p.setParam(param, val) if err == nil { _, err = p.vm.Eval(p.script) } @@ -99,7 +116,7 @@ func (p *Javascript) IntSetter(param string) func(int64) error { // StringSetter sends string request func (p *Javascript) StringSetter(param string) func(string) error { return func(val string) error { - err := p.vm.Set(param, val) + err := p.setParam(param, val) if err == nil { _, err = p.vm.Eval(p.script) } @@ -110,7 +127,7 @@ func (p *Javascript) StringSetter(param string) func(string) error { // BoolSetter sends bool request func (p *Javascript) BoolSetter(param string) func(bool) error { return func(val bool) error { - err := p.vm.Set(param, val) + err := p.setParam(param, val) if err == nil { _, err = p.vm.Eval(p.script) } diff --git a/provider/javascript/init.go b/provider/javascript/init.go new file mode 100644 index 000000000..0c644d9e1 --- /dev/null +++ b/provider/javascript/init.go @@ -0,0 +1,56 @@ +package javascript + +import ( + "github.com/andig/evcc/util" + "github.com/robertkrimen/otto" +) + +// Configure initializes JS VMs +func Configure(other map[string]interface{}) error { + cc := []struct { + VM string + Script string + }{} + + if err := util.DecodeOther(other, &cc); err != nil { + return err + } + + // init all VMs that require it + for _, conf := range cc { + if conf.Script == "" { + continue + } + + if _, ok := registry[conf.VM]; !ok { + vm := otto.New() + + _, err := vm.Run(conf.Script) + if err != nil { + return err + } + + registry[conf.VM] = vm + } + } + + return nil +} + +var registry = make(map[string]*otto.Otto) + +// RegisteredVM returns a JS VM. If name is not empty, it will return a shared instance. +func RegisteredVM(name string) *otto.Otto { + vm, ok := registry[name] + + // create new VM + if !ok { + vm = otto.New() + + if name != "" { + registry[name] = vm + } + } + + return vm +}