-
-
Notifications
You must be signed in to change notification settings - Fork 526
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a29e794
commit 786c470
Showing
4 changed files
with
309 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.