Skip to content

Commit

Permalink
Add Bosch bpts5 (#3029)
Browse files Browse the repository at this point in the history
  • Loading branch information
waldtraut1981 committed Apr 3, 2022
1 parent a29e794 commit 786c470
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 0 deletions.
145 changes: 145 additions & 0 deletions meter/bosch/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package bosch

import (
"fmt"
"net/http"
"net/http/cookiejar"
"strconv"
"strings"
"sync"
"time"

"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
)

type API struct {
*request.Helper
uri string
status StatusResponse
login LoginResponse
updated time.Time
cache time.Duration
}

var Instances = new(sync.Map)

func NewLocal(log *util.Logger, uri string, cache time.Duration) *API {
api := &API{
Helper: request.NewHelper(log),
uri: util.DefaultScheme(strings.TrimSuffix(uri, "/"), "http"),
cache: cache,
}

// ignore the self signed certificate
api.Client.Transport = request.NewTripper(log, transport.Insecure())
// create cookie jar to save login tokens
api.Client.Jar, _ = cookiejar.New(nil)

return api
}

func (c *API) Login() (err error) {
req, err := request.New(http.MethodGet, c.uri, nil, nil)
if err != nil {
return err
}

body, err := c.DoBody(req)
if err != nil {
return err
}

return c.extractWuiSidFromBody(string(body))
}

func (c *API) Status() (StatusResponse, error) {
var err error
if time.Since(c.updated) > c.cache {
if err = c.updateValues(); err == nil {
c.updated = time.Now()
}
}
return c.status, err
}

func (c *API) extractWuiSidFromBody(body string) error {
index := strings.Index(body, "WUI_SID=")

if index < 0 || len(body) < index+9+15 {
c.login.wuSid = ""
return fmt.Errorf("error while extracting wui sid. body was= %s", body)
}

c.login.wuSid = body[index+9 : index+9+15]

return nil
}

func (c *API) updateValues() error {
data := "action=get.hyb.overview&flow=1"

uri := c.uri + "/cgi-bin/ipcclient.fcgi?" + c.login.wuSid
req, err := request.New(http.MethodPost, uri, strings.NewReader(data), map[string]string{
"Content-Type": "text/plain",
})

if err != nil {
return err
}

body, err := c.DoBody(req)
if err != nil {
return err
}

return c.extractValues(string(body))
}

func (c *API) extractValues(body string) error {
if strings.Contains(body, "session invalid") {
return c.Login()
}

values := strings.Split(body, "|")

if len(values) < 14 {
return fmt.Errorf("extractValues: response has not enough values")
}

soc, err := strconv.Atoi(values[3])
if err == nil {
c.status.CurrentBatterySoc = float64(soc)
c.status.SellToGrid, err = parseWattValue(values[11])
}

if err == nil {
c.status.BuyFromGrid, err = parseWattValue(values[14])
}

if err == nil {
c.status.PvPower, err = parseWattValue(values[2])
}

if err == nil {
c.status.BatteryChargePower, err = parseWattValue(values[10])
}

if err == nil {
c.status.BatteryDischargePower, err = parseWattValue(values[13])
}

return err
}

func parseWattValue(inputString string) (float64, error) {
if len(strings.TrimSpace(inputString)) == 0 || strings.Contains(inputString, "nbsp;") {
return 0.0, nil
}

num := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(inputString, "kW", " "), "von", " "))
res, err := strconv.ParseFloat(num, 64)

return res * 1000.0, err
}
14 changes: 14 additions & 0 deletions meter/bosch/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package bosch

type LoginResponse struct {
wuSid string
}

type StatusResponse struct {
CurrentBatterySoc float64
SellToGrid float64
BuyFromGrid float64
PvPower float64
BatteryChargePower float64
BatteryDischargePower float64
}
115 changes: 115 additions & 0 deletions meter/bosch_bpts5_hybrid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package meter

// LICENSE

// Bosch is the Bosch BPT-S 5 Hybrid meter

// 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 (
"errors"
"strings"
"time"

"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/meter/bosch"
"github.com/evcc-io/evcc/util"
)

// Example config:
// meters:
// - name: bosch_grid
// type: bosch-bpt
// uri: http://192.168.178.22
// usage: grid
// - name: bosch_pv
// type: bosch-bpt
// uri: http://192.168.178.22
// usage: pv
// - name: bosch_battery
// type: bosch-bpt
// uri: http://192.168.178.22
// usage: battery

type BoschBpts5Hybrid struct {
api *bosch.API
usage string
}

func init() {
registry.Add("bosch-bpt", NewBoschBpts5HybridFromConfig)
}

//go:generate go run ../cmd/tools/decorate.go -f decorateBoschBpts5Hybrid -b api.Meter -t "api.Battery,SoC,func() (float64, error)"

// NewBoschBpts5HybridFromConfig creates a Bosch BPT-S 5 Hybrid Meter from generic config
func NewBoschBpts5HybridFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
URI string
Usage string
Cache time.Duration
}{}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

if cc.Usage == "" {
return nil, errors.New("missing usage")
}

return NewBoschBpts5Hybrid(cc.URI, cc.Usage, cc.Cache)
}

// NewBoschBpts5Hybrid creates a Bosch BPT-S 5 Hybrid Meter
func NewBoschBpts5Hybrid(uri, usage string, cache time.Duration) (api.Meter, error) {
log := util.NewLogger("bosch-bpt")

instance, exists := bosch.Instances.LoadOrStore(uri, bosch.NewLocal(log, uri, cache))
if !exists {
if err := instance.(*bosch.API).Login(); err != nil {
return nil, err
}
}

m := &BoschBpts5Hybrid{
api: instance.(*bosch.API),
usage: strings.ToLower(usage),
}

// decorate api.BatterySoC
var batterySoC func() (float64, error)
if usage == "battery" {
batterySoC = m.batterySoC
}

return decorateBoschBpts5Hybrid(m, batterySoC), nil
}

// CurrentPower implements the api.Meter interface
func (m *BoschBpts5Hybrid) CurrentPower() (float64, error) {
status, err := m.api.Status()

switch m.usage {
case "grid":
return status.BuyFromGrid - status.SellToGrid, err
case "pv":
return status.PvPower, err
case "battery":
return status.BatteryDischargePower - status.BatteryChargePower, err
default:
return 0, err
}
}

// batterySoC implements the api.Battery interface
func (m *BoschBpts5Hybrid) batterySoC() (float64, error) {
status, err := m.api.Status()
return status.CurrentBatterySoc, err
}
35 changes: 35 additions & 0 deletions meter/bosch_bpts5_hybrid_decorators.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 786c470

Please sign in to comment.