Repository: evcc-io/evcc
Pull request #5902 (2023-01-25 07:49:49)

Remember and restore energy flow details opened state
Remember last open/close state (local storage) of energy flow details and restore it on page load.

![Bildschirm­foto 2023-01-25 um 08 48 02](https://user-images.githubusercontent.com/152287/214508015-fb000552-b289-4e91-bc73-e5c0d3496e19.png)


andig: I'm not a big fan of this as the details can be confusing for novice users. Maybe closed by default but remember "open" state?
andig: I also remember there was a request to show >1 loadpoint concurrently. This PR would be sort of diametrical? 
naltatis: Ok, then I'll switch to "remember last state". It's also more universal and also works on smaller screens.

Patch:
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index a7fc58ed..7a263659 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -128,6 +128,7 @@ import Visualization from "./Visualization.vue";
 import EnergyflowEntry from "./EnergyflowEntry.vue";
 import formatter from "../../mixins/formatter";
 import AnimatedNumber from "../AnimatedNumber.vue";
+import settings from "../../settings";
 
 export default {
 	name: "Energyflow",
@@ -189,6 +190,10 @@ export default {
 	},
 	mounted() {
 		window.addEventListener("resize", this.updateHeight);
+		// height must be calculated in case of initially open details
+		if (settings.energyflowDetails) {
+			setTimeout(this.toggleDetails, 50);
+		}
 	},
 	unmounted() {
 		window.removeEventListener("resize", this.updateHeight);
@@ -200,6 +205,7 @@ export default {
 		toggleDetails: function () {
 			this.updateHeight();
 			this.detailsOpen = !this.detailsOpen;
+			settings.energyflowDetails = this.detailsOpen;
 		},
 		updateHeight: function () {
 			this.detailsCompleteHeight = this.$refs.detailsInner.offsetHeight;
diff --git a/assets/js/settings.js b/assets/js/settings.js
index db655b16..195676bf 100644
--- a/assets/js/settings.js
+++ b/assets/js/settings.js
@@ -3,6 +3,7 @@ import { reactive, watch } from "vue";
 const SETTINGS_LOCALE = "settings_locale";
 const SETTINGS_THEME = "settings_theme";
 const SETTINGS_UNIT = "settings_unit";
+const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details";
 
 function read(key) {
   return window.localStorage[key];
@@ -22,15 +23,27 @@ function save(key) {
   };
 }
 
+function readBool(key) {
+  return read(key) === "true";
+}
+
+function saveBool(key) {
+  return (value) => {
+    save(key)(value ? "true" : "false");
+  };
+}
+
 const settings = reactive({
   telemetry: null,
   locale: read(SETTINGS_LOCALE),
   theme: read(SETTINGS_THEME),
   unit: read(SETTINGS_UNIT),
+  energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS),
 });
 
 watch(() => settings.locale, save(SETTINGS_LOCALE));
 watch(() => settings.theme, save(SETTINGS_THEME));
 watch(() => settings.unit, save(SETTINGS_UNIT));
+watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS));
 
 export default settings;
diff --git a/charger/vestel.go b/charger/vestel.go
index 7ad33c10..af970cde 100644
--- a/charger/vestel.go
+++ b/charger/vestel.go
@@ -93,18 +93,13 @@ func NewVestel(uri string, id uint8) (*Vestel, error) {
 		current: 6,
 	}
 
-	// 5min failsafe timeout
-	if _, err := wb.conn.WriteSingleRegister(vestelRegFailsafeTimeout, 5*60); err != nil {
-		return nil, fmt.Errorf("could not set failsafe timeout: %v", err)
-	}
-
 	go wb.heartbeat()
 
 	return wb, nil
 }
 
 func (wb *Vestel) heartbeat() {
-	for range time.NewTicker(time.Minute).C {
+	for range time.NewTicker(time.Second * 3).C {
 		if _, err := wb.conn.WriteSingleRegister(vestelRegAlive, 1); err != nil {
 			wb.log.ERROR.Println("heartbeat:", err)
 		}
diff --git a/cmd/demo.yaml b/cmd/demo.yaml
index 362951fa..e32909db 100644
--- a/cmd/demo.yaml
+++ b/cmd/demo.yaml
@@ -150,7 +150,7 @@ vehicles:
       vm: shared
       script: |
         var lp = state.loadpoints[0];
-        if (lp.chargepower > 0) lp.vehicleSoc++; else lp.vehicleSoc--;
+        if (lp.chargepower > 0) lp.vehicleSoc+=0.1; else lp.vehicleSoc-=0.1;
         if (lp.vehicleSoc < 15) lp.vehicleSoc = 80;
         if (lp.vehicleSoc > 80) lp.vehicleSoc = 15;
         lp.vehicleSoc;
diff --git a/meter/openwb.go b/meter/openwb.go
index 121e1618..d923d266 100644
--- a/meter/openwb.go
+++ b/meter/openwb.go
@@ -101,7 +101,11 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Meter, error) {
 			return nil, errors.New("battery not available")
 		}
 
-		power = floatG(fmt.Sprintf("%s/housebattery/%s", cc.Topic, openwb.PowerTopic))
+		inner := floatG(fmt.Sprintf("%s/housebattery/%s", cc.Topic, openwb.PowerTopic))
+		power = func() (float64, error) {
+			f, err := inner()
+			return -f, err
+		}
 		soc = floatG(fmt.Sprintf("%s/housebattery/%s", cc.Topic, openwb.SocTopic))
 		capacity = cc.capacity.Decorator()
 
diff --git a/push/pushover.go b/push/pushover.go
index 619a6693..e8e570dc 100644
--- a/push/pushover.go
+++ b/push/pushover.go
@@ -2,6 +2,7 @@ package push
 
 import (
 	"errors"
+	"strings"
 
 	"github.com/evcc-io/evcc/util"
 	"github.com/gregdel/pushover"
@@ -15,6 +16,7 @@ func init() {
 type PushOver struct {
 	log        *util.Logger
 	app        *pushover.Pushover
+	device     string
 	recipients []string
 }
 
@@ -23,6 +25,7 @@ func NewPushOverFromConfig(other map[string]interface{}) (Messenger, error) {
 	var cc struct {
 		App        string
 		Recipients []string
+		Devices    []string
 		Events     map[string]EventTemplate
 	}
 
@@ -37,6 +40,7 @@ func NewPushOverFromConfig(other map[string]interface{}) (Messenger, error) {
 	m := &PushOver{
 		log:        util.NewLogger("pushover"),
 		app:        pushover.New(cc.App),
+		device:     strings.Join(cc.Devices, ","),
 		recipients: cc.Recipients,
 	}
 
@@ -46,6 +50,7 @@ func NewPushOverFromConfig(other map[string]interface{}) (Messenger, error) {
 // Send sends to all receivers
 func (m *PushOver) Send(title, msg string) {
 	message := pushover.NewMessageWithTitle(msg, title)
+	message.DeviceName = m.device
 
 	for _, id := range m.recipients {
 		go func(id string) {
diff --git a/tariff/awattar.go b/tariff/awattar.go
index f1c16ca6..592b08c7 100644
--- a/tariff/awattar.go
+++ b/tariff/awattar.go
@@ -10,6 +10,7 @@ import (
 	"github.com/evcc-io/evcc/tariff/awattar"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
+	"golang.org/x/exp/slices"
 )
 
 type Awattar struct {
@@ -103,5 +104,5 @@ func (t *Awattar) Unit() string {
 func (t *Awattar) Rates() (api.Rates, error) {
 	t.mux.Lock()
 	defer t.mux.Unlock()
-	return append([]api.Rate{}, t.data...), outdatedError(t.updated, time.Hour)
+	return slices.Clone(t.data), outdatedError(t.updated, time.Hour)
 }
diff --git a/tariff/fixed/day.go b/tariff/fixed/day.go
index 27d56733..bbee9961 100644
--- a/tariff/fixed/day.go
+++ b/tariff/fixed/day.go
@@ -78,7 +78,14 @@ func ParseDays(s string) ([]Day, error) {
 			return nil, fmt.Errorf("invalid day range: %s", segment)
 		}
 
-		from, err := ParseDay(fromto[0])
+		fromToFrom := fromto[0]
+
+		// single empty segment
+		if len(fromto) == 1 && strings.TrimSpace(fromToFrom) == "" {
+			return slices.Clone(Week), nil
+		}
+
+		from, err := ParseDay(fromToFrom)
 		if err != nil {
 			return nil, err
 		}
diff --git a/tariff/fixed/timerange.go b/tariff/fixed/timerange.go
index ec91c2db..310c11a4 100644
--- a/tariff/fixed/timerange.go
+++ b/tariff/fixed/timerange.go
@@ -75,6 +75,10 @@ func ParseTimeRange(s string) (TimeRange, error) {
 		return TimeRange{}, err
 	}
 
+	if tom := to.Minutes(); tom != 0 && from.Minutes() >= tom {
+		return TimeRange{}, fmt.Errorf("invalid time range: %s, <from> must be before <to>", s)
+	}
+
 	return TimeRange{from, to}, nil
 }
 
diff --git a/tariff/gruenstromindex.go b/tariff/gruenstromindex.go
index 85530768..556b5be6 100644
--- a/tariff/gruenstromindex.go
+++ b/tariff/gruenstromindex.go
@@ -9,6 +9,7 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
+	"golang.org/x/exp/slices"
 )
 
 type GrünStromIndex struct {
@@ -132,5 +133,5 @@ func (t *GrünStromIndex) Unit() string {
 func (t *GrünStromIndex) Rates() (api.Rates, error) {
 	t.mux.Lock()
 	defer t.mux.Unlock()
-	return append(api.Rates{}, t.data...), outdatedError(t.updated, time.Hour)
+	return slices.Clone(t.data), outdatedError(t.updated, time.Hour)
 }
diff --git a/tariff/octopus.go b/tariff/octopus.go
index 5c280b8e..b25588c9 100644
--- a/tariff/octopus.go
+++ b/tariff/octopus.go
@@ -2,13 +2,14 @@ package tariff
 
 import (
 	"errors"
-	"github.com/evcc-io/evcc/tariff/octopus"
 	"sync"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/tariff/octopus"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
+	"golang.org/x/exp/slices"
 )
 
 type Octopus struct {
@@ -99,5 +100,5 @@ func (t *Octopus) Unit() string {
 func (t *Octopus) Rates() (api.Rates, error) {
 	t.mux.Lock()
 	defer t.mux.Unlock()
-	return append([]api.Rate{}, t.data...), outdatedError(t.updated, time.Hour)
+	return slices.Clone(t.data), outdatedError(t.updated, time.Hour)
 }
diff --git a/tariff/tibber.go b/tariff/tibber.go
index 1ab21de5..9a41d846 100644
--- a/tariff/tibber.go
+++ b/tariff/tibber.go
@@ -11,6 +11,7 @@ import (
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/shurcooL/graphql"
+	"golang.org/x/exp/slices"
 )
 
 type Tibber struct {
@@ -144,5 +145,5 @@ func (t *Tibber) Unit() string {
 func (t *Tibber) Rates() (api.Rates, error) {
 	t.mux.Lock()
 	defer t.mux.Unlock()
-	return append([]api.Rate{}, t.data...), outdatedError(t.updated, time.Hour)
+	return slices.Clone(t.data), outdatedError(t.updated, time.Hour)
 }


Test patch:
diff --git a/tariff/fixed/day_test.go b/tariff/fixed/day_test.go
index 9c63438a..d98aa28c 100644
--- a/tariff/fixed/day_test.go
+++ b/tariff/fixed/day_test.go
@@ -35,10 +35,17 @@ func TestParseDays(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, []Day{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday}, d)
 
+	_, err = ParseDays(" ")
+	assert.NoError(t, err)
+	assert.Equal(t, []Day{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday}, d)
+
 	d, err = ParseDays("1, 3-7")
 	assert.NoError(t, err)
 	assert.Equal(t, []Day{Monday, Wednesday, Thursday, Friday, Saturday, Sunday}, d)
 
+	_, err = ParseDays("-")
+	assert.EqualError(t, err, "invalid day: ")
+
 	_, err = ParseDays("-1")
 	assert.EqualError(t, err, "invalid day: ")
 
diff --git a/tariff/fixed/timerange_test.go b/tariff/fixed/timerange_test.go
index 9af73121..a7a59892 100644
--- a/tariff/fixed/timerange_test.go
+++ b/tariff/fixed/timerange_test.go
@@ -11,11 +11,24 @@ func TestParseTimeRange(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, []TimeRange{{HourMin{7, 0}, HourMin{12, 30}}}, d, "hour:min range")
 
-	d, err = ParseTimeRanges("8-10")
+	d, err = ParseTimeRanges(" 8-10 ")
 	assert.NoError(t, err)
 	assert.Equal(t, []TimeRange{{HourMin{8, 0}, HourMin{10, 0}}}, d, "hour range")
 
-	d, err = ParseTimeRanges("8-10,20-22:30")
+	_, err = ParseTimeRanges("-1-10")
+	assert.EqualError(t, err, "invalid time: ")
+
+	_, err = ParseTimeRanges("8-25")
+	assert.EqualError(t, err, "invalid time: 25", "<to> after midnight")
+
+	d, err = ParseTimeRanges("8-0")
+	assert.NoError(t, err)
+	assert.Equal(t, []TimeRange{{HourMin{8, 0}, HourMin{0, 0}}}, d, "hour till midnight (0)")
+
+	_, err = ParseTimeRanges("10-8")
+	assert.EqualError(t, err, "invalid time range: 10-8, <from> must be before <to>")
+
+	d, err = ParseTimeRanges("8-10, 20-22:30")
 	assert.NoError(t, err)
 	assert.Equal(t, []TimeRange{
 		{HourMin{8, 0}, HourMin{10, 0}},