evcc-io/meter/fritz/smarthome/smarthome.go
2026-05-08 09:20:51 +02:00

236 lines
5.2 KiB
Go

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
UID string // unitUid resolved from /devices
unitG util.Cacheable[Unit]
}
// NewConnection creates a new REST API connection
func NewConnection(uri, ain, user, password string, unit int) (*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,
Unit: unit,
}
log := util.NewLogger("fritzsmarthome").Redact(password)
conn := &Connection{
Helper: request.NewHelper(log),
Settings: settings,
}
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()
}, 2*time.Second)
return conn, nil
}
// 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
}
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 {
return "", fmt.Errorf("invalid unit %d, got %v", unit, d.UnitUids)
}
return d.UnitUids[unit-1], nil
}
return "", fmt.Errorf("ain not found: %s", c.AIN)
}
// getUnit fetches unit data from REST API
func (c *Connection) getUnit() (Unit, error) {
sid, err := c.GetSessionID(c.Helper)
if err != nil {
return Unit{}, err
}
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 " + sid,
}, request.AcceptJSON)
var unit Unit
if err := c.DoJSON(req, &unit); err != nil {
return Unit{}, err
}
if !unit.IsConnected {
return unit, api.ErrNotAvailable
}
return unit, nil
}
// 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 {
sid, err := c.GetSessionID(c.Helper)
if 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 " + 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
}