From 2f106579e78e38c2f88e3c031c50dc942255bfaa Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 19 Apr 2026 13:55:54 +0200 Subject: [PATCH] Add Fritz smarthome REST API support (FritzOS 8.2+) (#29013) --- charger/fritzdect.go | 65 ++--- .../fritzdect.go => fritz/aha/aha.go} | 108 +++----- meter/fritz/aha/types.go | 15 + meter/fritz/api.go | 16 ++ meter/fritz/smarthome/smarthome.go | 258 ++++++++++++++++++ meter/fritz/smarthome/types.go | 58 ++++ meter/fritz/types.go | 71 +++++ meter/fritzdect.go | 14 +- templates/definition/charger/fritzdect.yaml | 10 + templates/definition/meter/fritzdect.yaml | 10 + templates/definition/meter/fritzgrid.yaml | 1 + 11 files changed, 516 insertions(+), 110 deletions(-) rename meter/{fritzdect/fritzdect.go => fritz/aha/aha.go} (54%) create mode 100644 meter/fritz/aha/types.go create mode 100644 meter/fritz/api.go create mode 100644 meter/fritz/smarthome/smarthome.go create mode 100644 meter/fritz/smarthome/types.go create mode 100644 meter/fritz/types.go diff --git a/charger/fritzdect.go b/charger/fritzdect.go index 1b68bbc2c..7bef93c02 100644 --- a/charger/fritzdect.go +++ b/charger/fritzdect.go @@ -1,20 +1,20 @@ package charger import ( - "errors" - "strconv" - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/meter/fritzdect" + "github.com/evcc-io/evcc/meter/fritz" + "github.com/evcc-io/evcc/meter/fritz/aha" + "github.com/evcc-io/evcc/meter/fritz/smarthome" "github.com/evcc-io/evcc/util" ) // FRITZ! FritzBox AHA interface specifications: // https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf +// https://fritz.support/resources/SmarthomeRestApiFRITZOS82.html (REST API for FritzOS 8.2+) // FritzDECT charger implementation type FritzDECT struct { - conn *fritzdect.Connection + conn fritz.Switch *switchSocket } @@ -25,9 +25,9 @@ func init() { // NewFritzDECTFromConfig creates a fritzdect charger from generic config func NewFritzDECTFromConfig(other map[string]any) (api.Charger, error) { var cc struct { - embed `mapstructure:",squash"` - fritzdect.Settings `mapstructure:",squash"` - StandbyPower float64 + embed `mapstructure:",squash"` + fritz.Settings `mapstructure:",squash"` + StandbyPower float64 } if err := util.DecodeOther(other, &cc); err != nil { @@ -38,12 +38,20 @@ func NewFritzDECTFromConfig(other map[string]any) (api.Charger, error) { return nil, api.ErrMissingCredentials } - return NewFritzDECT(cc.embed, cc.URI, cc.AIN, cc.User, cc.Password, cc.StandbyPower) + return NewFritzDECT(cc.embed, cc.URI, cc.AIN, cc.User, cc.Password, cc.StandbyPower, cc.Firmware82) } // NewFritzDECT creates a new connection with standbypower for charger -func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower float64) (*FritzDECT, error) { - conn, err := fritzdect.NewConnection(uri, ain, user, password) +func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower float64, firmware82 bool) (*FritzDECT, error) { + var conn fritz.Switch + var err error + + // Use new REST API if firmware82 is set, otherwise use legacy LUA API + if firmware82 { + conn, err = smarthome.NewConnection(uri, ain, user, password) + } else { + conn, err = aha.NewConnection(uri, ain, user, password) + } if err != nil { return nil, err } @@ -59,18 +67,13 @@ func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower flo // Status implements the api.Charger interface func (c *FritzDECT) Status() (api.ChargeStatus, error) { - resp, err := c.conn.ExecCmd("getswitchpresent") + present, err := c.conn.SwitchPresent() if err != nil { return api.StatusNone, err } - present, err := strconv.ParseBool(resp) - if err == nil && !present { - err = api.ErrNotAvailable - } - - if err != nil { - return api.StatusNone, err + if !present { + return api.StatusNone, api.ErrNotAvailable } return c.switchSocket.Status() @@ -78,33 +81,15 @@ func (c *FritzDECT) Status() (api.ChargeStatus, error) { // Enabled implements the api.Charger interface func (c *FritzDECT) Enabled() (bool, error) { - resp, err := c.conn.ExecCmd("getswitchstate") - if err != nil { - return false, err - } - - return strconv.ParseBool(resp) + return c.conn.SwitchState() } // Enable implements the api.Charger interface func (c *FritzDECT) Enable(enable bool) error { - cmd := "setswitchoff" if enable { - cmd = "setswitchon" + return c.conn.SwitchOn() } - - // on 0/1 - DECT Switch state off/on (empty if unknown or error) - resp, err := c.conn.ExecCmd(cmd) - if err != nil { - return err - } - - on, err := strconv.ParseBool(resp) - if err == nil && enable != on { - err = errors.New("switch failed") - } - - return err + return c.conn.SwitchOff() } var _ api.MeterEnergy = (*FritzDECT)(nil) diff --git a/meter/fritzdect/fritzdect.go b/meter/fritz/aha/aha.go similarity index 54% rename from meter/fritzdect/fritzdect.go rename to meter/fritz/aha/aha.go index ef28e850d..e98542f47 100644 --- a/meter/fritzdect/fritzdect.go +++ b/meter/fritz/aha/aha.go @@ -1,9 +1,6 @@ -package fritzdect +package aha import ( - "crypto/md5" - "encoding/hex" - "encoding/xml" "errors" "fmt" "net/url" @@ -12,44 +9,24 @@ import ( "time" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/fritz" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" - "golang.org/x/text/encoding/unicode" ) // FRITZ! FritzBox AHA interface and authentication specifications: // https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf // https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID.pdf -// FritzDECT settings -type Settings struct { - URI, AIN, User, Password string -} - // FritzDECT connection type Connection struct { *request.Helper - *Settings + *fritz.Settings SID string updated time.Time } -// https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_english_2021-05-03.pdf -const sessionTimeout = 15 * time.Minute - -// Devicestats structures getbasicdevicesstats command response (AHA-HTTP-Interface) -type Devicestats struct { - XMLName xml.Name `xml:"devicestats"` - Energy Energy `xml:"energy"` -} - -// Energy structures getbasicdevicesstats command energy response (AHA-HTTP-Interface) -type Energy struct { - XMLName xml.Name `xml:"energy"` - Values []string `xml:"stats"` -} - // NewConnection creates FritzDECT connection func NewConnection(uri, ain, user, password string) (*Connection, error) { if uri == "" { @@ -60,7 +37,7 @@ func NewConnection(uri, ain, user, password string) (*Connection, error) { return nil, errors.New("missing ain") } - settings := &Settings{ + settings := &fritz.Settings{ URI: strings.TrimRight(uri, "/"), AIN: ain, User: user, @@ -82,11 +59,13 @@ func NewConnection(uri, ain, user, password string) (*Connection, error) { // ExecCmd execautes an FritzDECT AHA-HTTP-Interface command func (c *Connection) ExecCmd(function string) (string, error) { // refresh Fritzbox session id - if time.Since(c.updated) >= sessionTimeout { - if err := c.getSessionID(); err != nil { + if time.Since(c.updated) >= fritz.SessionTimeout { + sid, err := c.GetSessionID(c.Helper) + if err != nil { return "", err } // update session timestamp + c.SID = sid c.updated = time.Now() } @@ -123,7 +102,7 @@ func (c *Connection) CurrentPower() (float64, error) { var _ api.MeterEnergy = (*Connection)(nil) -// CurrentPower implements the api.MeterEnergy interface +// TotalEnergy implements the api.MeterEnergy interface func (c *Connection) TotalEnergy() (float64, error) { // Energy value in Wh (total switch energy, refresh approximately every 2 minutes) resp, err := c.ExecCmd("getswitchenergy") @@ -136,53 +115,48 @@ func (c *Connection) TotalEnergy() (float64, error) { return energy / 1000, err // Wh ==> KWh } -// Fritzbox helpers (credits to https://github.com/rsdk/ahago) +// SwitchPresent checks if the device is connected +func (c *Connection) SwitchPresent() (bool, error) { + resp, err := c.ExecCmd("getswitchpresent") + if err != nil { + return false, err + } + return strconv.ParseBool(resp) +} -// getSessionID fetches a session-id based on the username and password in the connection struct -func (c *Connection) getSessionID() error { - uri := fmt.Sprintf("%s/login_sid.lua", c.URI) - body, err := c.GetBody(uri) +// SwitchState returns the current switch state +func (c *Connection) SwitchState() (bool, error) { + resp, err := c.ExecCmd("getswitchstate") + if err != nil { + return false, err + } + return strconv.ParseBool(resp) +} + +// SwitchOn turns the switch on +func (c *Connection) SwitchOn() error { + resp, err := c.ExecCmd("setswitchon") if err != nil { return err } - var v struct { - SID string - Challenge string - BlockTime string + on, err := strconv.ParseBool(resp) + if err == nil && !on { + err = errors.New("switch on failed") } - - 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": {c.User}, - "response": {challresp}, - } - - if body, err = c.GetBody(uri + "?" + params.Encode()); err == nil { - err = xml.Unmarshal(body, &v) - if v.SID == "0000000000000000" { - return errors.New("invalid user or password") - } - c.SID = v.SID - } - } - } - return err } -// createChallengeResponse creates the Fritzbox challenge response string -func createChallengeResponse(challenge, pass string) (string, error) { - encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() - utf16le, err := encoder.String(challenge + "-" + pass) +// SwitchOff turns the switch off +func (c *Connection) SwitchOff() error { + resp, err := c.ExecCmd("setswitchoff") if err != nil { - return "", err + return err } - hash := md5.Sum([]byte(utf16le)) - md5hash := hex.EncodeToString(hash[:]) - - return challenge + "-" + md5hash, nil + off, err := strconv.ParseBool(resp) + if err == nil && off { + err = errors.New("switch off failed") + } + return err } diff --git a/meter/fritz/aha/types.go b/meter/fritz/aha/types.go new file mode 100644 index 000000000..0af22c3d6 --- /dev/null +++ b/meter/fritz/aha/types.go @@ -0,0 +1,15 @@ +package aha + +import "encoding/xml" + +// Devicestats structures getbasicdevicesstats command response (AHA-HTTP-Interface) +type Devicestats struct { + XMLName xml.Name `xml:"devicestats"` + Energy Energy `xml:"energy"` +} + +// Energy structures getbasicdevicesstats command energy response (AHA-HTTP-Interface) +type Energy struct { + XMLName xml.Name `xml:"energy"` + Values []string `xml:"stats"` +} diff --git a/meter/fritz/api.go b/meter/fritz/api.go new file mode 100644 index 000000000..371f241ac --- /dev/null +++ b/meter/fritz/api.go @@ -0,0 +1,16 @@ +package fritz + +// Meter defines the interface for Fritz connections (both legacy LUA and REST) +type Meter interface { + CurrentPower() (float64, error) + TotalEnergy() (float64, error) +} + +// Switch extends Meter with switch control capabilities +type Switch interface { + Meter + SwitchPresent() (bool, error) + SwitchState() (bool, error) + SwitchOn() error + SwitchOff() error +} diff --git a/meter/fritz/smarthome/smarthome.go b/meter/fritz/smarthome/smarthome.go new file mode 100644 index 000000000..624af3daf --- /dev/null +++ b/meter/fritz/smarthome/smarthome.go @@ -0,0 +1,258 @@ +package smarthome + +import ( + "errors" + "fmt" + "net/url" + "strings" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/fritz" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" +) + +// FRITZ! Smarthome REST API (FritzOS 8.2+) +// https://fritz.support/resources/SmarthomeRestApiFRITZOS82.html + +// Connection implements the new REST API for Fritz smarthome devices +type Connection struct { + *request.Helper + *fritz.Settings + SID string + UID string // device UID (AIN with space) + updated time.Time + unitG util.Cacheable[Unit] +} + +// NewConnection creates a new REST API connection +func NewConnection(uri, ain, user, password string) (*Connection, error) { + if uri == "" { + uri = "https://fritz.box" + } + + if ain == "" { + return nil, errors.New("missing ain") + } + + settings := &fritz.Settings{ + URI: strings.TrimRight(uri, "/"), + AIN: ain, + User: user, + Password: password, + } + + log := util.NewLogger("fritzsmarthome").Redact(password) + + conn := &Connection{ + Helper: request.NewHelper(log), + Settings: settings, + UID: ainToUID(ain), + } + + conn.Client.Transport = request.NewTripper(log, transport.Insecure()) + + // cache unit data for 2 seconds to avoid excessive API calls + conn.unitG = util.ResettableCached(func() (Unit, error) { + return conn.getUnit() + }, 2*time.Second) + + return conn, nil +} + +// ainToUID converts AIN format to UID format by adding space +// AIN: "116300015376" -> UID: "11630 0015376" +func ainToUID(ain string) string { + // Remove any existing spaces first + ain = strings.ReplaceAll(ain, " ", "") + if len(ain) >= 5 { + return ain[:5] + " " + ain[5:] + } + return ain +} + +// getUnit fetches unit data from REST API +func (c *Connection) getUnit() (Unit, error) { + if err := c.refreshSession(); err != nil { + return Unit{}, err + } + + // Try to get the specific unit first + uri := fmt.Sprintf("%s/api/v0/smarthome/overview/units/%s", c.URI, url.PathEscape(c.UID)) + + req, _ := request.New("GET", uri, nil, map[string]string{ + "Authorization": "AVM-SID " + c.SID, + }, request.AcceptJSON) + + var unit Unit + if err := c.DoJSON(req, &unit); err != nil { + // Fall back to getting all units and finding ours + return c.findUnit() + } + + if !unit.IsConnected { + return unit, api.ErrNotAvailable + } + + return unit, nil +} + +// findUnit searches for our unit in the list of all units +func (c *Connection) findUnit() (Unit, error) { + uri := fmt.Sprintf("%s/api/v0/smarthome/overview/units", c.URI) + + req, err := request.New("GET", uri, nil, map[string]string{ + "Authorization": "AVM-SID " + c.SID, + }, request.AcceptJSON) + if err != nil { + return Unit{}, err + } + + var units []Unit + if err := c.DoJSON(req, &units); err != nil { + return Unit{}, err + } + + // Search for matching unit by UID or AIN + for _, unit := range units { + unitAIN := strings.ReplaceAll(unit.UID, " ", "") + if unit.UID == c.UID || unitAIN == c.AIN { + if !unit.IsConnected { + return unit, api.ErrNotAvailable + } + return unit, nil + } + } + + return Unit{}, fmt.Errorf("unit not found: %s", c.AIN) +} + +// CurrentPower implements the api.Meter interface +func (c *Connection) CurrentPower() (float64, error) { + unit, err := c.unitG.Get() + if err != nil { + return 0, err + } + + if unit.Statistics != nil && len(unit.Statistics.Powers) > 0 { + if stats := unit.Statistics.Powers[0].Values; len(stats) > 0 { + return (float64)(stats[0]) / 1000, nil + } + } + + return 0, api.ErrNotAvailable +} + +var _ api.MeterEnergy = (*Connection)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (c *Connection) TotalEnergy() (float64, error) { + unit, err := c.unitG.Get() + if err != nil { + return 0, err + } + + if unit.Statistics != nil && len(unit.Statistics.Energies) > 0 { + if stats := unit.Statistics.Energies[len(unit.Statistics.Energies)-1].Values; len(stats) > 0 { + return (float64)(stats[0]) / 1000, nil + } + } + + return 0, api.ErrNotAvailable +} + +// SwitchPresent checks if the device is connected +func (c *Connection) SwitchPresent() (bool, error) { + unit, err := c.unitG.Get() + if err != nil { + if errors.Is(err, api.ErrNotAvailable) { + return false, nil + } + return false, err + } + return unit.IsConnected, nil +} + +// SwitchState returns the current switch state +func (c *Connection) SwitchState() (bool, error) { + unit, err := c.unitG.Get() + if err != nil { + return false, err + } + + if unit.Interfaces.OnOffInterface == nil { + return false, errors.New("device has no switch") + } + + return unit.Interfaces.OnOffInterface.State == "on", nil +} + +// SwitchOn turns the switch on +func (c *Connection) SwitchOn() error { + return c.setSwitch(true) +} + +// SwitchOff turns the switch off +func (c *Connection) SwitchOff() error { + return c.setSwitch(false) +} + +// setSwitch sets the switch state via REST API +func (c *Connection) setSwitch(on bool) error { + if err := c.refreshSession(); err != nil { + return err + } + + state := "off" + if on { + state = "on" + } + + uri := fmt.Sprintf("%s/api/v0/smarthome/overview/units/%s", c.URI, url.PathEscape(c.UID)) + + data := map[string]any{ + "onOffInterface": map[string]string{ + "state": state, + }, + } + + req, _ := request.New("PUT", uri, request.MarshalJSON(data), map[string]string{ + "Authorization": "AVM-SID " + c.SID, + }, request.JSONEncoding) + + var unit Unit + if err := c.DoJSON(req, &unit); err != nil { + return err + } + + // Reset cache after state change + c.unitG.Reset() + + // Verify state was changed + if unit.Interfaces.OnOffInterface != nil { + actualState := unit.Interfaces.OnOffInterface.State == "on" + if actualState != on { + return errors.New("switch state change failed") + } + } + + return nil +} + +// refreshSession ensures we have a valid session ID +func (c *Connection) refreshSession() error { + // refresh Fritzbox session id + if time.Since(c.updated) >= fritz.SessionTimeout { + sid, err := c.GetSessionID(c.Helper) + if err != nil { + return err + } + // update session timestamp + c.SID = sid + c.updated = time.Now() + } + + return nil +} diff --git a/meter/fritz/smarthome/types.go b/meter/fritz/smarthome/types.go new file mode 100644 index 000000000..9b74a48cc --- /dev/null +++ b/meter/fritz/smarthome/types.go @@ -0,0 +1,58 @@ +package smarthome + +// Unit represents a smarthome unit with its interfaces +type Unit struct { + GroupUID string `json:"groupUid,omitempty"` + UID string `json:"UID,omitempty"` + DeviceUID string `json:"deviceUid"` + UnitType string `json:"unitType"` + IsConnected bool `json:"isConnected"` + Statistics *Statistics `json:"statistics,omitempty"` + Interfaces *Interfaces `json:"interfaces,omitempty"` +} + +type Interfaces struct { + MultimeterInterface *MultimeterInterface `json:"multimeterInterface,omitempty"` + OnOffInterface *OnOffInterface `json:"onOffInterface,omitempty"` + TemperatureInterface *TemperatureInterface `json:"temperatureInterface,omitempty"` +} + +type Statistics struct { + Temperatures []ElementFloat `json:"temperatures,omitempty"` + Powers []Element `json:"powers,omitempty"` + Voltages []Element `json:"voltages,omitempty"` + Energies []Element `json:"energies,omitempty"` +} + +type ElementFloat struct { + Interval int64 `json:"interval"` + StasticsState string `json:"statisticsState"` + Period string `json:"period"` + Values []float64 `json:"values,omitempty"` +} + +type Element struct { + Interval int64 `json:"interval"` + StasticsState string `json:"statisticsState"` + Period string `json:"period"` + Values []int64 `json:"values,omitempty"` +} + +// MultimeterInterface contains power/energy measurements +type MultimeterInterface struct { + State string `json:"state"` + Power float64 `json:"power"` // W + Voltage float64 `json:"voltage"` // V + Current float64 `json:"current"` // A + Energy float64 `json:"energy"` // Wh +} + +// OnOffInterface contains switch state +type OnOffInterface struct { + State string `json:"state"` // "on" or "off" +} + +type TemperatureInterface struct { + State string `json:"state"` // "on" or "off" + Celsius float64 `json:"celsius"` +} diff --git a/meter/fritz/types.go b/meter/fritz/types.go new file mode 100644 index 000000000..7b5599517 --- /dev/null +++ b/meter/fritz/types.go @@ -0,0 +1,71 @@ +package fritz + +import ( + "crypto/md5" + "encoding/hex" + "encoding/xml" + "errors" + "fmt" + "net/url" + "time" + + "github.com/evcc-io/evcc/util/request" + "golang.org/x/text/encoding/unicode" +) + +// https://fritz.com/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_english_2021-05-03.pdf +const SessionTimeout = 15 * time.Minute + +// FritzDECT settings +type Settings struct { + URI, AIN, User, Password string + Firmware82 bool // use new REST API (FritzOS 8.2+) +} + +// Fritzbox helpers (credits to https://github.com/rsdk/ahago) + +// getSessionID fetches a session-id based on the username and password in the connection struct +func (s Settings) GetSessionID(c *request.Helper) (string, error) { + uri := fmt.Sprintf("%s/login_sid.lua", s.URI) + body, err := c.GetBody(uri) + if err != nil { + return "", err + } + + var v struct { + SID string + Challenge string + } + + if err = xml.Unmarshal(body, &v); err == nil && v.SID == "0000000000000000" { + var challresp string + if challresp, err = s.createChallengeResponse(v.Challenge); err == nil { + params := url.Values{ + "username": {s.User}, + "response": {challresp}, + } + + if body, err = c.GetBody(uri + "?" + params.Encode()); err == nil { + if err = xml.Unmarshal(body, &v); err == nil && v.SID == "0000000000000000" { + return "", errors.New("invalid user or password") + } + } + } + } + + return v.SID, err +} + +// createChallengeResponse creates the Fritzbox challenge response string +func (s Settings) createChallengeResponse(challenge string) (string, error) { + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + utf16le, err := encoder.String(challenge + "-" + s.Password) + if err != nil { + return "", err + } + + hash := md5.Sum([]byte(utf16le)) + md5hash := hex.EncodeToString(hash[:]) + + return challenge + "-" + md5hash, nil +} diff --git a/meter/fritzdect.go b/meter/fritzdect.go index 1a3f97d6c..f6e5a2627 100644 --- a/meter/fritzdect.go +++ b/meter/fritzdect.go @@ -2,12 +2,15 @@ package meter import ( "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/meter/fritzdect" + "github.com/evcc-io/evcc/meter/fritz" + "github.com/evcc-io/evcc/meter/fritz/aha" + "github.com/evcc-io/evcc/meter/fritz/smarthome" "github.com/evcc-io/evcc/util" ) // AVM FritzBox AHA interface specifications: // https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf +// https://fritz.support/resources/SmarthomeRestApiFRITZOS82.html (REST API for FritzOS 8.2+) func init() { registry.Add("fritzdect", NewFritzDECTFromConfig) @@ -15,7 +18,7 @@ func init() { // NewFritzDECTFromConfig creates a fritzdect meter from generic config func NewFritzDECTFromConfig(other map[string]any) (api.Meter, error) { - var cc fritzdect.Settings + var cc fritz.Settings if err := util.DecodeOther(other, &cc); err != nil { return nil, err } @@ -24,5 +27,10 @@ func NewFritzDECTFromConfig(other map[string]any) (api.Meter, error) { return nil, api.ErrMissingCredentials } - return fritzdect.NewConnection(cc.URI, cc.AIN, cc.User, cc.Password) + // Use new REST API if firmware82 is set, otherwise use legacy LUA API + if cc.Firmware82 { + return smarthome.NewConnection(cc.URI, cc.AIN, cc.User, cc.Password) + } + + return aha.NewConnection(cc.URI, cc.AIN, cc.User, cc.Password) } diff --git a/templates/definition/charger/fritzdect.yaml b/templates/definition/charger/fritzdect.yaml index 8099513bf..922f4d127 100644 --- a/templates/definition/charger/fritzdect.yaml +++ b/templates/definition/charger/fritzdect.yaml @@ -25,6 +25,15 @@ params: required: true - name: ain required: true + - name: firmware82 + advanced: true + type: bool + description: + de: Neue REST-API verwenden (FritzOS 8.2+) + en: Use new REST API (FritzOS 8.2+) + help: + de: Verwende die neue REST-API für FritzOS ab Version 8.2 + en: Use the new REST API for FritzOS version 8.2 and later - preset: switchsocket render: | type: fritzdect @@ -32,4 +41,5 @@ render: | user: {{ .user }} password: {{ .password }} ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) + firmware82: {{ .firmware82 }} {{ include "switchsocket" . }} diff --git a/templates/definition/meter/fritzdect.yaml b/templates/definition/meter/fritzdect.yaml index 98dbb6934..caab3a1b8 100644 --- a/templates/definition/meter/fritzdect.yaml +++ b/templates/definition/meter/fritzdect.yaml @@ -27,9 +27,19 @@ params: required: true - name: ain required: true + - name: firmware82 + advanced: true + type: bool + description: + de: Neue REST-API verwenden (FritzOS 8.2+) + en: Use new REST API (FritzOS 8.2+) + help: + de: Verwende die neue REST-API für FritzOS ab Version 8.2 + en: Use the new REST API for FritzOS version 8.2 and later render: | type: fritzdect uri: {{ .uri }} user: {{ .user }} password: {{ .password }} ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) + firmware82: {{ .firmware82 }} diff --git a/templates/definition/meter/fritzgrid.yaml b/templates/definition/meter/fritzgrid.yaml index 0d11df4d1..f67125749 100644 --- a/templates/definition/meter/fritzgrid.yaml +++ b/templates/definition/meter/fritzgrid.yaml @@ -20,3 +20,4 @@ render: | user: {{ .user }} password: {{ .password }} ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) + firmware82: true