Add Fritz smarthome REST API support (FritzOS 8.2+)#29013
Add Fritz smarthome REST API support (FritzOS 8.2+)#29013
Conversation
Add support for the new Fritz smarthome REST API introduced in FritzOS 8.2. The old homeautoswitch.lua API returns HTTP 500 errors for newer devices like the FRITZ!Smart Energy 250. Changes: - Add new REST API implementation in meter/fritzdect/rest.go - Define common API/SwitchAPI interfaces for both implementations - Add legacy parameter to templates for users who need the old API - Default to new REST API, use legacy: true for FritzOS < 8.2 Fixes #28906 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The new parseXML helper is defined in meter/fritzdect/fritzdect.go but only used from the REST implementation; consider either moving it next to its usage in rest.go or actually refactoring the legacy code paths to use it as well so the helper isn’t orphaned in an unrelated file.
- In RestConnection, SwitchPresent converts api.ErrNotAvailable into (false, nil) while CurrentPower/TotalEnergy propagate api.ErrNotAvailable; if this asymmetry is intentional for the charger.Status logic it may be worth making that explicit in code (e.g. comment) or aligning the behavior so callers don’t have to special‑case one method.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new parseXML helper is defined in meter/fritzdect/fritzdect.go but only used from the REST implementation; consider either moving it next to its usage in rest.go or actually refactoring the legacy code paths to use it as well so the helper isn’t orphaned in an unrelated file.
- In RestConnection, SwitchPresent converts api.ErrNotAvailable into (false, nil) while CurrentPower/TotalEnergy propagate api.ErrNotAvailable; if this asymmetry is intentional for the charger.Status logic it may be worth making that explicit in code (e.g. comment) or aligning the behavior so callers don’t have to special‑case one method.
## Individual Comments
### Comment 1
<location path="meter/fritzdect/rest.go" line_range="29-31" />
<code_context>
+ unitG util.Cacheable[Unit]
+}
+
+// Overview response from /smarthome/overview
+type Overview struct {
+ Units []Unit `json:"units"`
+}
+
</code_context>
<issue_to_address>
**suggestion:** The Overview type and its comment don’t match the actual usage of the REST endpoints.
`Overview` is documented as the response from `/smarthome/overview`, but the code only calls `/smarthome/units` and never uses `Overview`. Please either adapt `findUnit` (or related logic) to use `/smarthome/overview` with this type, or update/remove `Overview` and its comment so the types and documented endpoints match actual usage.
Suggested implementation:
```golang
// RestConnection implements the new REST API for Fritz smarthome devices
type RestConnection struct {
*request.Helper
*Settings
SID string
UID string // device UID (AIN with space)
updated time.Time
unitG util.Cacheable[Unit]
}
import (
```
```golang
unitG util.Cacheable[Unit]
}
// Overview response from /smarthome/overview
type Overview struct {
Units []Unit `json:"units"`
}
import (
"errors"
```
1. Delete the `Overview` type and its comment entirely from `rest.go` (the block shown in the second SEARCH section). The code snippet above is structured so you can remove that block by replacing it with nothing.
2. After removal, ensure there are no remaining references to `Overview` in this file or elsewhere in the package. If there are, either remove those references or reintroduce a correctly documented type that matches the actual endpoint being used.
</issue_to_address>
### Comment 2
<location path="meter/fritzdect/rest.go" line_range="104" />
<code_context>
+ return ain
+}
+
+// refreshSession ensures we have a valid session ID
+func (c *RestConnection) refreshSession() error {
+ if time.Since(c.updated) < sessionTimeout {
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting shared session handling and unit lookup into centralized helpers so this REST client reuses auth logic and hides availability branching behind a single code path.
You can simplify this implementation without changing behavior by factoring out the duplicated auth flow and centralizing unit retrieval / availability semantics.
### 1. Extract shared session/auth helper
`refreshSession` re-implements the same XML/Challenge logic as the legacy connection. You can move that into a shared helper and reuse it from both types to avoid divergence:
```go
// shared_auth.go
type SessionConfig struct {
URI string
User string
Password string
}
func RefreshSession(cfg SessionConfig, client *request.Helper, sid *string, updated *time.Time) error {
if time.Since(*updated) < sessionTimeout {
return nil
}
uri := fmt.Sprintf("%s/login_sid.lua", cfg.URI)
body, err := client.GetBody(uri)
if err != nil {
return err
}
var v struct {
SID string `xml:"SID"`
Challenge string `xml:"Challenge"`
}
if err = parseXML(body, &v); err != nil {
return err
}
if v.SID == "0000000000000000" {
challresp, err := createChallengeResponse(v.Challenge, cfg.Password)
if err != nil {
return err
}
params := url.Values{
"username": {cfg.User},
"response": {challresp},
}
body, err = client.GetBody(uri + "?" + params.Encode())
if err != nil {
return err
}
if err = parseXML(body, &v); err != nil {
return err
}
if v.SID == "0000000000000000" {
return errors.New("invalid user or password")
}
}
*sid = v.SID
*updated = time.Now()
return nil
}
```
Usage in `RestConnection`:
```go
func (c *RestConnection) refreshSession() error {
return RefreshSession(
SessionConfig{URI: c.URI, User: c.User, Password: c.Password},
c.Helper,
&c.SID,
&c.updated,
)
}
```
The legacy `Connection` can call the same helper, keeping the login behavior in one place.
### 2. Centralize unit retrieval (single code path)
Instead of exposing both `getUnit` and `findUnit` logic to callers and mixing fallback behavior, you can have a single helper that encapsulates the “try direct endpoint, then overview” behavior and availability semantics:
```go
// internal helper returning presence and error
func (c *RestConnection) fetchUnit() (Unit, bool /*present*/, error) {
if err := c.refreshSession(); err != nil {
return Unit{}, false, err
}
// 1) direct unit endpoint
uri := fmt.Sprintf("%s/smarthome/units/%s?sid=%s", c.URI, url.PathEscape(c.UID), c.SID)
var unit Unit
if err := c.GetJSON(uri, &unit); err == nil {
return unit, unit.IsConnected, nil
}
// 2) fallback to overview
uri = fmt.Sprintf("%s/smarthome/units?sid=%s", c.URI, c.SID)
var units []Unit
if err := c.GetJSON(uri, &units); err != nil {
return Unit{}, false, err
}
for _, u := range units {
unitAIN := strings.ReplaceAll(u.UID, " ", "")
if u.UID == c.UID || unitAIN == c.AIN {
return u, u.IsConnected, nil
}
}
return Unit{}, false, fmt.Errorf("unit not found: %s", c.AIN)
}
```
Then wire `unitG` to this single source of truth and move the `ErrNotAvailable` decision to one place:
```go
func NewRestConnection(...) (*RestConnection, error) {
// ...
conn.unitG = util.ResettableCached(func() (Unit, error) {
unit, present, err := conn.fetchUnit()
if err != nil {
return Unit{}, err
}
if !present {
return unit, api.ErrNotAvailable
}
return unit, nil
}, 2*time.Second)
// ...
}
```
With that, public methods don’t need to think about connectivity; they just use `unitG` and handle `api.ErrNotAvailable` if relevant.
### 3. Simplify public method semantics
Once `fetchUnit` and `unitG` encapsulate presence:
- `SwitchPresent` only converts `api.ErrNotAvailable` to `(false, nil)`:
```go
func (c *RestConnection) 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
}
```
- Other methods don’t need to reason about `IsConnected` at all; they just propagate `ErrNotAvailable` as-is:
```go
func (c *RestConnection) CurrentPower() (float64, error) {
unit, err := c.unitG.Get()
if err != nil {
return 0, err
}
if unit.MultimeterInterface == nil {
return 0, errors.New("device has no power meter")
}
if unit.MultimeterInterface.State != "valid" {
return 0, api.ErrNotAvailable
}
return float64(unit.MultimeterInterface.Power) / 1000, nil
}
```
This keeps functionality identical but reduces branching and spreads of auth and presence logic across the file.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- Rename meter/fritzdect -> meter/fritz - Rename fritzdect.go -> legacy.go (legacy LUA API implementation) - Rename API interface to Meter - Rename SwitchAPI interface to Switch - Update all imports and references Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
010555f to
643490e
Compare
|
Interesting approach and certainly better than #29009, but it completely breaks existing evcc installations with at least one FritzDect 200/210 (see log below) and it does not show the proper power values (see screenshot below). Suggestion: use the legacy API variant as default until FritzDect 200&210 have been migrated to use the new API and fix the parsing of the data provided by the new API.
|
Can do before merging. But first lets find out if this works at all. |
Needs a trace log to understand. Use |
|
Claude seems to use an API endpoint that does not exist or uses a wrong url. Output for fritz dect 250: Output for the fritz dect 210: |
|
When adding "/api/v0" to the uri in all cases besides the sid retrieval, the error changes: Curling the API via Maybe Claude uses the old authentication method instead of the curl output: |
|
@copilot fix the errors according to openapi spec in linked issue |
- Use /api/smarthome/ path prefix (not /smarthome/) per OpenAPI spec - Use Authorization: Bearer <SID> header instead of ?sid= query param - Remove unused Overview type - Change MultimeterInterface numeric fields from int to float64 - Remove mW->W conversion as REST API returns power in W directly Agent-Logs-Url: https://github.com/evcc-io/evcc/sessions/293df6e0-c747-4743-9468-4faa29b6d441 Co-authored-by: andig <184815+andig@users.noreply.github.com>
Fixed in f9f04b1. The REST API implementation had two issues per the OpenAPI spec:
Also cleaned up the unused Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@sidietz could you take another look? |
|
Using instead of results in working authentication and following error message: |
|
Could you pr your changes tageting this branch or create your own pr? |
|
When I have something working, I'll create a PR targeting this branch. It turned out, that the structs (like Unit) weren't correct / according to the openapi specification, either. |
|
Do we have any understanding which Fritz device (250?) comes with the new API only? Then we could enable there out of the box? |
|
It seems only the Fritz 250 (the one used as gridmeter) uses only the new API. So maybe we should indeed switch to the new API for the Fritz 250. As documentation you find the JSON response of and Fritz 250: |
|
That means we change |
- Rename `legacy` parameter to `firmware82` in Fritz smarthome - Invert logic: firmware82=true uses new REST API, false uses legacy AHA - Default firmware82=false for fritzdect (preserves existing behavior) - Default firmware82=true for fritzgrid (Smart Energy 250 requires FritzOS 8.2+) Addresses PR feedback: #29013 (comment) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Sounds reasonable. |
Co-authored-by: andig <cpuidle@gmail.com>
|
@andig I think some whitespaces in the .yaml file are messed up... With them fixed, it works: |



Summary
homeautoswitch.luaAPI returns HTTP 500 errors for newer devices like the FRITZ!Smart Energy 250legacyparameter to templates for users who need the old API (FritzOS < 8.2)Changes
APIandSwitchAPIinterfaces/smarthome/units/{UID}endpointSwitchAPImethods to legacy connection, addLegacysettingSwitchAPIinterfacelegacyadvanced parameter with FritzOS version infoTest plan
legacy: trueFixes #28906
🤖 Generated with Claude Code