Repository: evcc-io/evcc
Pull request #2315 (2022-01-16 11:37:07)

Add Alphatec charger
Fix https://github.com/evcc-io/evcc/issues/2311


Patch:
diff --git a/charger/alphatec.go b/charger/alphatec.go
new file mode 100644
index 00000000..8f4cdafa
--- /dev/null
+++ b/charger/alphatec.go
@@ -0,0 +1,140 @@
+package charger
+
+// LICENSE
+
+// Copyright (c) 2019-2022 andig
+
+// This module is NOT covered by the MIT license. All rights reserved.
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import (
+	"encoding/binary"
+	"fmt"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+	"github.com/evcc-io/evcc/util/sponsor"
+)
+
+// https://github.com/evcc-io/evcc/discussions/1965
+
+// Alphatec charger implementation
+type Alphatec struct {
+	log  *util.Logger
+	conn *modbus.Connection
+}
+
+const (
+	alphatecRegStatus     = 0
+	alphatecRegEnable     = 4
+	alphatecRegAmpsConfig = 5
+)
+
+func init() {
+	registry.Add("alphatec", NewAlphatecFromConfig)
+}
+
+// NewAlphatecFromConfig creates a Alphatec charger from generic config
+func NewAlphatecFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := modbus.Settings{
+		ID: 1,
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	format := modbus.TcpFormat
+	if cc.RTU != nil && *cc.RTU {
+		format = modbus.RtuFormat
+	}
+
+	return NewAlphatec(cc.URI, cc.Device, cc.Comset, cc.Baudrate, format, cc.ID)
+}
+
+// NewAlphatec creates Alphatec charger
+func NewAlphatec(uri, device, comset string, baudrate int, format modbus.WireFormat, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, format, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
+	log := util.NewLogger("alpha")
+	conn.Logger(log.TRACE)
+
+	wb := &Alphatec{
+		log:  log,
+		conn: conn,
+	}
+
+	return wb, err
+}
+
+// Status implements the api.Charger interface
+func (wb *Alphatec) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadHoldingRegisters(alphatecRegStatus, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	var res api.ChargeStatus
+	switch b[0] {
+	case 1:
+		res = api.StatusA
+	case 2:
+		res = api.StatusB
+	case 3:
+		res = api.StatusC
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %d", b[0])
+	}
+
+	return res, nil
+}
+
+// Enabled implements the api.Charger interface
+func (wb *Alphatec) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(alphatecRegEnable, 1)
+	if err != nil {
+		return false, err
+	}
+
+	return binary.BigEndian.Uint16(b) > 0, nil
+}
+
+// Enable implements the api.Charger interface
+func (wb *Alphatec) Enable(enable bool) error {
+	b := make([]byte, 2)
+	if enable {
+		binary.BigEndian.PutUint16(b, 1)
+	}
+
+	_, err := wb.conn.WriteMultipleRegisters(alphatecRegEnable, 1, b)
+
+	return err
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *Alphatec) MaxCurrent(current int64) error {
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, uint16(current))
+
+	_, err := wb.conn.WriteMultipleRegisters(alphatecRegAmpsConfig, 1, b)
+
+	return err
+}
diff --git a/charger/pcelectric.go b/charger/pcelectric.go
index 1e1ff846..ccb49dfb 100644
--- a/charger/pcelectric.go
+++ b/charger/pcelectric.go
@@ -9,6 +9,7 @@ import (
 	"github.com/evcc-io/evcc/charger/pcelectric"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
+	"github.com/evcc-io/evcc/util/sponsor"
 )
 
 // PCElectric charger implementation
@@ -63,6 +64,10 @@ func NewPCElectric(uri string, slaveIndex int, meter string) (*PCElectric, error
 	log := util.NewLogger("pce")
 	uri = strings.TrimSuffix(strings.TrimRight(uri, "/"), "/servlet") + "/servlet/rest/chargebox"
 
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
 	wb := &PCElectric{
 		Helper:     request.NewHelper(log),
 		log:        log,
diff --git a/core/loadpoint.go b/core/loadpoint.go
index a01b40ed..8d6f6570 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -1408,7 +1408,7 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool, batteryBuffered bool)
 	// track if remote disabled is actually active
 	remoteDisabled := loadpoint.RemoteEnable
 
-	// reset detection if soc timer needs be deactived after evaluating the loading strategy
+	// reset detection if soc timer needs be deactivated after evaluating the loading strategy
 	lp.socTimer.MustValidateDemand()
 
 	// execute loading strategy
diff --git a/core/savings.go b/core/savings.go
index 398c4a10..469f927e 100644
--- a/core/savings.go
+++ b/core/savings.go
@@ -24,6 +24,7 @@ type Savings struct {
 	updated                        time.Time // Time of last charged value update
 	gridCharged                    float64   // Grid energy charged since startup (kWh)
 	gridCost                       float64   // Running total of charged grid energy cost (e.g. EUR)
+	gridSavedCost                  float64   // Running total of saved cost from self consumption (e.g. EUR)
 	selfConsumptionCharged         float64   // Self-produced energy charged since startup (kWh)
 	selfConsumptionCost            float64   // Running total of charged self-produced energy cost (e.g. EUR)
 	lastGridPrice, lastFeedInPrice float64   // Stores the last published grid price. Needed to detect price changes (Awattar, ..)
@@ -68,7 +69,7 @@ func (s *Savings) EffectivePrice() float64 {
 }
 
 func (s *Savings) SavingsAmount() float64 {
-	return s.selfConsumptionCharged * (s.currentGridPrice() - s.currentFeedInPrice())
+	return s.gridSavedCost
 }
 
 func (s *Savings) shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower float64) float64 {
@@ -140,6 +141,7 @@ func (s *Savings) Update(p publisher, gridPower, pvPower, batteryPower, chargePo
 
 	s.gridCharged += addedGrid
 	s.gridCost += addedGrid * gridPrice
+	s.gridSavedCost += addedSelfConsumption * (gridPrice - feedinPrice)
 	s.selfConsumptionCharged += addedSelfConsumption
 	s.selfConsumptionCost += addedSelfConsumption * feedinPrice
 
diff --git a/core/soc/estimator.go b/core/soc/estimator.go
index fe6ae5f4..55b0929c 100644
--- a/core/soc/estimator.go
+++ b/core/soc/estimator.go
@@ -121,8 +121,8 @@ func (s *Estimator) SoC(chargedEnergy float64) (float64, error) {
 				s.log.WARN.Printf("vehicle soc (charger): %v (ignored by estimator)", err)
 			}
 
-			s.vehicleSoc = f
 			fetchedSoC = &f
+			s.vehicleSoc = f
 		}
 	}
 
diff --git a/templates/definition/vehicle/evnotify.yaml b/templates/definition/vehicle/evnotify.yaml
index cea1b6fb..290f143a 100644
--- a/templates/definition/vehicle/evnotify.yaml
+++ b/templates/definition/vehicle/evnotify.yaml
@@ -18,7 +18,7 @@ render: |
   title: {{ .title }}
   {{- end }}
   capacity: {{ .capacity }}
-  charge:
+  soc:
     type: http
     uri: https://app.evnotify.de/soc?akey={{ .akey }}&token={{ .token }} # evNotify Server + AKEY
     method: GET
diff --git a/templates/definition/vehicle/smart.yaml b/templates/definition/vehicle/smart.yaml
new file mode 100644
index 00000000..a98f2dab
--- /dev/null
+++ b/templates/definition/vehicle/smart.yaml
@@ -0,0 +1,12 @@
+template: smart
+description: 
+  generic: Smart EQ
+params:
+- base: vehiclebase
+- base: vehicleidentify
+- name: capacity
+  default: 17.6
+render: |
+  type: smart
+  {{include "vehicle-base" .}}
+  {{include "vehicle-identify" .}}
diff --git a/templates/docs/vehicle/smart.yaml b/templates/docs/vehicle/smart.yaml
new file mode 100644
index 00000000..a69215cb
--- /dev/null
+++ b/templates/docs/vehicle/smart.yaml
@@ -0,0 +1,7 @@
+type: template
+template: smart
+description: Smart EQ
+user:
+password:
+vin: W... 
+capacity: 17.6
\ No newline at end of file


Test patch:
diff --git a/vehicle/template_test.go b/vehicle/template_test.go
index 474e8c5f..aaa2d37e 100644
--- a/vehicle/template_test.go
+++ b/vehicle/template_test.go
@@ -29,6 +29,7 @@ var acceptable = []string{
 	"missing access and/or refresh token, use `evcc token` to create", // Tesla
 	"login failed: Unauthorized: Authentication Failed",               // Nissan
 	"login failed: no auth code",                                      // Porsche
+	"login failed: unexpected status: 400",                            // Smart
 	"invalid_client:Client authentication failed (e.g., login failure, unknown client, no client authentication included or unsupported authentication method)",   // BMW, Mini
 	"login failed: oauth2: cannot fetch token: 400 Bad Request Response: {\"error\":\"invalid_request\",\"error_description\":\"Missing parameter, 'username'\"}", // Opel, DS, Citroen, PSA
 	"401: Unauthorized: Invalid credentials", // Volvo