Skip to content

Commit

Permalink
Add FritzDECT meter (evcc-io#1775)
Browse files Browse the repository at this point in the history
  • Loading branch information
thierolm authored Oct 25, 2021
1 parent 21960b7 commit 9969590
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 159 deletions.
167 changes: 23 additions & 144 deletions charger/fritzdect.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,23 @@
package charger

import (
"crypto/md5"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"

"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/charger/fritzdect"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"golang.org/x/text/encoding/unicode"
"github.com/evcc-io/evcc/util/fritzdect"
)

// AVM FritzBox AHA interface and authentification specifications:
// AVM FritzBox AHA interface specifications:
// https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf
// https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID.pdf

// FritzDECT charger implementation
type FritzDECT struct {
*request.Helper
uri, ain, user, password, sid string
standbypower float64
updated time.Time
fritzdect *fritzdect.Connection
standbypower float64
}

func init() {
Expand All @@ -41,85 +31,41 @@ func NewFritzDECTFromConfig(other map[string]interface{}) (api.Charger, error) {
AIN string
User string
Password string
SID string
StandbyPower float64
Updated time.Time
}{}

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

if cc.URI == "" {
cc.URI = "https://fritz.box"
}

if cc.AIN == "" {
return nil, errors.New("missing ain")
}

return NewFritzDECT(cc.URI, cc.AIN, cc.User, cc.Password, cc.SID, cc.StandbyPower, cc.Updated)
}

// NewFritzDECT creates FritzDECT charger
func NewFritzDECT(uri, ain, user, password, sid string, standbypower float64, updated time.Time) (*FritzDECT, error) {
log := util.NewLogger("fritzdect")

c := &FritzDECT{
Helper: request.NewHelper(log),
uri: strings.TrimRight(uri, "/"),
ain: ain,
user: user,
password: password,
standbypower: standbypower,
sid: sid,
}

c.Client.Transport = request.NewTripper(log, request.InsecureTransport())

return c, nil
return NewFritzDECT(cc.URI, cc.AIN, cc.User, cc.Password, cc.StandbyPower)
}

func (c *FritzDECT) execFritzDectCmd(function string) (string, error) {
// refresh Fritzbox session id
if time.Since(c.updated).Minutes() >= 10 {
err := c.getSessionID()
if err != nil {
return "", err
}
// update session timestamp
c.updated = time.Now()
// NewFritzDECT creates a new connection with standbypower for charger
func NewFritzDECT(uri, ain, user, password string, standbypower float64) (*FritzDECT, error) {
fritzdect, err := fritzdect.NewConnection(uri, ain, user, password)
if err != nil {
return nil, err
}

parameters := url.Values{
"sid": []string{c.sid},
"ain": []string{c.ain},
"switchcmd": []string{function},
fd := &FritzDECT{
fritzdect: fritzdect,
standbypower: standbypower,
}

uri := fmt.Sprintf("%s/webservices/homeautoswitch.lua", c.uri)
response, err := c.GetBody(uri + "?" + parameters.Encode())
return strings.TrimSpace(string(response)), err
return fd, nil
}

// Status implements the api.Charger interface
func (c *FritzDECT) Status() (api.ChargeStatus, error) {
// present 0/1 - DECT Switch connected to fritzbox (no/yes)
var present int64
resp, err := c.execFritzDectCmd("getswitchpresent")
resp, err := c.fritzdect.ExecCmd("getswitchpresent")
if err == nil {
present, err = strconv.ParseInt(resp, 10, 64)
}

// power value in 0,001 W (current switch power, refresh approximately every 2 minutes)
var power float64
if err == nil {
if resp, err = c.execFritzDectCmd("getswitchpower"); err == nil {
power, err = strconv.ParseFloat(resp, 64)
if err != nil {
return api.StatusNone, err
}
}

power = power / 1000 // mW ==> W
power, err := c.fritzdect.CurrentPower()

switch {
case present == 1 && power <= c.standbypower:
return api.StatusB, err
Expand All @@ -133,7 +79,7 @@ func (c *FritzDECT) Status() (api.ChargeStatus, error) {
// Enabled implements the api.Charger interface
func (c *FritzDECT) Enabled() (bool, error) {
// state 0/1 - DECT Switch state off/on (empty if unknown or error)
resp, err := c.execFritzDectCmd("getswitchstate")
resp, err := c.fritzdect.ExecCmd("getswitchstate")
if err != nil {
return false, err
}
Expand All @@ -155,7 +101,7 @@ func (c *FritzDECT) Enable(enable bool) error {
}

// state 0/1 - DECT Switch state off/on (empty if unknown or error)
resp, err := c.execFritzDectCmd(cmd)
resp, err := c.fritzdect.ExecCmd(cmd)

var state int64
if err == nil {
Expand Down Expand Up @@ -183,20 +129,7 @@ var _ api.Meter = (*FritzDECT)(nil)

// CurrentPower implements the api.Meter interface
func (c *FritzDECT) CurrentPower() (float64, error) {
// power value in 0,001 W (current switch power, refresh approximately every 2 minutes)
resp, err := c.execFritzDectCmd("getswitchpower")
if err != nil {
return 0, err
}

if resp == "inval" {
return 0, api.ErrNotAvailable
}

power, err := strconv.ParseFloat(resp, 64)

// ignore standby power
power = power / 1000 // mW ==> W
power, err := c.fritzdect.CurrentPower()
if power < c.standbypower {
power = 0
}
Expand All @@ -209,7 +142,7 @@ var _ api.ChargeRater = (*FritzDECT)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (c *FritzDECT) ChargedEnergy() (float64, error) {
// fetch basicdevicestats
resp, err := c.execFritzDectCmd("getbasicdevicestats")
resp, err := c.fritzdect.ExecCmd("getbasicdevicestats")
if err != nil {
return 0, err
}
Expand All @@ -229,57 +162,3 @@ func (c *FritzDECT) ChargedEnergy() (float64, error) {

return energy / 1000, err
}

// Fritzbox helpers (based on ideas of https://github.com/rsdk/ahago)

// getSessionID fetches a session-id based on the username and password in the connection struct
func (c *FritzDECT) getSessionID() error {
uri := fmt.Sprintf("%s/login_sid.lua", c.uri)
body, err := c.GetBody(uri)
if err != nil {
return err
}

v := struct {
SID string
Challenge string
BlockTime string
}{}

if err = xml.Unmarshal(body, &v); err == nil && v.SID == "0000000000000000" {
var challresp string
if challresp, err = createChallengeResponse(v.Challenge, c.password); err == nil {
params := url.Values{
"username": []string{c.user},
"response": []string{challresp},
}

if body, err = c.GetBody(uri + "?" + params.Encode()); err == nil {
err = xml.Unmarshal(body, &v)
if v.SID == "0000000000000000" {
return errors.New("invalid username (" + c.user + ") or password")
}
c.sid = v.SID
}
}
}

return err
}

// createChallengeResponse creates the Fritzbox challenge response string
func createChallengeResponse(challenge string, pass string) (string, error) {
encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
utf16le, err := encoder.String(challenge + "-" + pass)
if err != nil {
return "", err
}

hash := md5.New()
if _, err = hash.Write([]byte(utf16le)); err != nil {
return "", err
}

md5hash := hex.EncodeToString(hash.Sum(nil))
return challenge + "-" + md5hash, nil
}
15 changes: 0 additions & 15 deletions charger/fritzdect/types.go

This file was deleted.

23 changes: 23 additions & 0 deletions meter/fritzdect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package meter

import (
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/fritzdect"
)

// AVM FritzBox AHA interface specifications:
// https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf

func init() {
registry.Add("fritzdect", NewFritzDECTFromConfig)
}

// NewFritzDECTFromConfig creates a fritzdect meter from generic config
func NewFritzDECTFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := &fritzdect.Settings{}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return fritzdect.NewConnection(cc.URI, cc.AIN, cc.User, cc.Password)
}
Loading

0 comments on commit 9969590

Please sign in to comment.