Fritz!: optional unit parameter for multi-unit smarthome devices (BC) (#29570)

This commit is contained in:
andig 2026-05-03 10:10:47 +02:00 • committed by GitHub
parent c96dbed78e
commit 1fbe26a6ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 80 additions and 59 deletions

View file

@ -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)
}

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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" . }}

View file

@ -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 }}

View file

@ -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 }}