From 1fbe26a6ca9110dcb0ea10feb5aa56b664c048f3 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 3 May 2026 10:10:47 +0200 Subject: [PATCH] Fritz!: optional unit parameter for multi-unit smarthome devices (BC) (#29570) --- charger/fritzdect.go | 6 +- meter/fritz/smarthome/smarthome.go | 87 +++++++++------------ meter/fritz/smarthome/types.go | 13 +-- meter/fritz/types.go | 1 + meter/fritzdect.go | 2 +- templates/definition/charger/fritzdect.yaml | 10 +++ templates/definition/meter/fritzdect.yaml | 10 +++ templates/definition/meter/fritzgrid.yaml | 10 +++ 8 files changed, 80 insertions(+), 59 deletions(-) diff --git a/charger/fritzdect.go b/charger/fritzdect.go index 7bef93c02..f49462bc1 100644 --- a/charger/fritzdect.go +++ b/charger/fritzdect.go @@ -38,17 +38,17 @@ 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, cc.Firmware82) + return NewFritzDECT(cc.embed, cc.URI, cc.AIN, cc.User, cc.Password, cc.StandbyPower, cc.Firmware82, cc.Unit) } // NewFritzDECT creates a new connection with standbypower for charger -func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower float64, firmware82 bool) (*FritzDECT, error) { +func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower float64, firmware82 bool, unit int) (*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) + conn, err = smarthome.NewConnection(uri, ain, user, password, unit) } else { conn, err = aha.NewConnection(uri, ain, user, password) } diff --git a/meter/fritz/smarthome/smarthome.go b/meter/fritz/smarthome/smarthome.go index e65647192..23a75ac32 100644 --- a/meter/fritz/smarthome/smarthome.go +++ b/meter/fritz/smarthome/smarthome.go @@ -21,12 +21,12 @@ import ( type Connection struct { *request.Helper *fritz.Settings - UID string // device UID (AIN with space) + UID string // unitUid resolved from /devices unitG util.Cacheable[Unit] } // NewConnection creates a new REST API connection -func NewConnection(uri, ain, user, password string) (*Connection, error) { +func NewConnection(uri, ain, user, password string, unit int) (*Connection, error) { if uri == "" { uri = "https://fritz.box" } @@ -40,6 +40,7 @@ func NewConnection(uri, ain, user, password string) (*Connection, error) { AIN: ain, User: user, Password: password, + Unit: unit, } log := util.NewLogger("fritzsmarthome").Redact(password) @@ -47,11 +48,16 @@ func NewConnection(uri, ain, user, password string) (*Connection, error) { conn := &Connection{ Helper: request.NewHelper(log), Settings: settings, - UID: ainToUID(ain), } conn.Client.Transport = request.NewTripper(log, transport.Insecure()) + uid, err := conn.resolveUnitUID(unit) + if err != nil { + return nil, err + } + conn.UID = uid + // cache unit data for 2 seconds to avoid excessive API calls conn.unitG = util.ResettableCached(func() (Unit, error) { return conn.getUnit() @@ -60,15 +66,35 @@ func NewConnection(uri, ain, user, password string) (*Connection, error) { 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:] +// resolveUnitUID looks up the device by AIN and returns its unitUid at the given index +func (c *Connection) resolveUnitUID(unit int) (string, error) { + sid, err := c.GetSessionID(c.Helper) + if err != nil { + return "", err } - return ain + + uri := fmt.Sprintf("%s/api/v0/smarthome/overview/devices", c.URI) + + req, _ := request.New("GET", uri, nil, map[string]string{ + "Authorization": "AVM-SID " + sid, + }, request.AcceptJSON) + + var devices []Device + if err := c.DoJSON(req, &devices); err != nil { + return "", err + } + + for _, d := range devices { + if d.AIN != c.AIN { + continue + } + if len(d.UnitUids) < unit+1 { + return "", fmt.Errorf("invalid unit %d, got %v", unit, d.UnitUids) + } + return d.UnitUids[unit], nil + } + + return "", fmt.Errorf("ain not found: %s", c.AIN) } // getUnit fetches unit data from REST API @@ -78,7 +104,6 @@ func (c *Connection) getUnit() (Unit, error) { 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{ @@ -87,8 +112,7 @@ func (c *Connection) getUnit() (Unit, error) { var unit Unit if err := c.DoJSON(req, &unit); err != nil { - // Fall back to getting all units and finding ours - return c.findUnit() + return Unit{}, err } if !unit.IsConnected { @@ -98,41 +122,6 @@ func (c *Connection) getUnit() (Unit, error) { return unit, nil } -// findUnit searches for our unit in the list of all units -func (c *Connection) findUnit() (Unit, error) { - sid, err := c.GetSessionID(c.Helper) - if err != nil { - return Unit{}, err - } - - uri := fmt.Sprintf("%s/api/v0/smarthome/overview/units", c.URI) - - req, err := request.New("GET", uri, nil, map[string]string{ - "Authorization": "AVM-SID " + 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() diff --git a/meter/fritz/smarthome/types.go b/meter/fritz/smarthome/types.go index ecf901d45..d75599013 100644 --- a/meter/fritz/smarthome/types.go +++ b/meter/fritz/smarthome/types.go @@ -2,12 +2,13 @@ package smarthome // Device represents a smarthome device from the /devices endpoint type Device struct { - UID string `json:"UID"` - AIN string `json:"ain"` - Name string `json:"name"` - ProductName string `json:"productName"` - ProductCategory string `json:"productCategory"` - IsConnected bool `json:"isConnected"` + UID string `json:"UID"` + AIN string `json:"ain"` + Name string `json:"name"` + ProductName string `json:"productName"` + ProductCategory string `json:"productCategory"` + IsConnected bool `json:"isConnected"` + UnitUids []string `json:"unitUids"` } // Unit represents a smarthome unit with its interfaces diff --git a/meter/fritz/types.go b/meter/fritz/types.go index 219970fde..991ee38a5 100644 --- a/meter/fritz/types.go +++ b/meter/fritz/types.go @@ -21,6 +21,7 @@ const SessionTimeout = 15 * time.Minute type Settings struct { URI, AIN, User, Password string Firmware82 bool // use new REST API (FritzOS 8.2+) + Unit int // unit index for multi-unit devices (REST API only) mu sync.Mutex sid string diff --git a/meter/fritzdect.go b/meter/fritzdect.go index f6e5a2627..caa281e1f 100644 --- a/meter/fritzdect.go +++ b/meter/fritzdect.go @@ -29,7 +29,7 @@ func NewFritzDECTFromConfig(other map[string]any) (api.Meter, error) { // 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 smarthome.NewConnection(cc.URI, cc.AIN, cc.User, cc.Password, cc.Unit) } 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 3d66840ac..bbba61ff6 100644 --- a/templates/definition/charger/fritzdect.yaml +++ b/templates/definition/charger/fritzdect.yaml @@ -35,6 +35,15 @@ params: 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 + - name: unit + advanced: true + type: int + description: + de: Einheit + en: Unit + help: + de: Index der Einheit für Geräte mit mehreren Einheiten (nur REST-API) + en: Unit index for multi-unit devices (REST API only) - preset: switchsocket render: | type: fritzdect @@ -43,4 +52,5 @@ render: | password: {{ .password }} ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) firmware82: {{ .firmware82 }} + unit: {{ .unit }} {{ include "switchsocket" . }} diff --git a/templates/definition/meter/fritzdect.yaml b/templates/definition/meter/fritzdect.yaml index aae5f2470..c4ca34a97 100644 --- a/templates/definition/meter/fritzdect.yaml +++ b/templates/definition/meter/fritzdect.yaml @@ -37,6 +37,15 @@ params: 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 + - name: unit + advanced: true + type: int + description: + de: Einheit + en: Unit + help: + de: Index der Einheit für Geräte mit mehreren Einheiten (nur REST-API) + en: Unit index for multi-unit devices (REST API only) render: | type: fritzdect uri: {{ .uri }} @@ -44,3 +53,4 @@ render: | password: {{ .password }} ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) firmware82: {{ .firmware82 }} + unit: {{ .unit }} diff --git a/templates/definition/meter/fritzgrid.yaml b/templates/definition/meter/fritzgrid.yaml index ab0c2f790..125ea2cbd 100644 --- a/templates/definition/meter/fritzgrid.yaml +++ b/templates/definition/meter/fritzgrid.yaml @@ -15,6 +15,15 @@ params: - name: ain required: true service: fritz/devices?uri={uri}&user={user}&password={password} + - name: unit + advanced: true + type: int + description: + de: Einheit + en: Unit + help: + de: Index der Einheit für Geräte mit mehreren Einheiten + en: Unit index for multi-unit devices render: | type: fritzdect uri: {{ .uri }} @@ -22,3 +31,4 @@ render: | password: {{ .password }} ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) firmware82: true + unit: {{ .unit }}