diff --git a/charger/homeassistant-switch.go b/charger/homeassistant-switch.go index 254a8fe5f..48e79936a 100644 --- a/charger/homeassistant-switch.go +++ b/charger/homeassistant-switch.go @@ -20,26 +20,24 @@ func init() { } func NewHomeAssistantSwitchFromConfig(other map[string]any) (api.Charger, error) { - cc := struct { + var cc struct { embed `mapstructure:",squash"` - URI_ string `mapstructure:"uri"` // TODO deprecated + URI string Token_ string `mapstructure:"token"` // TODO deprecated - Home string + Home string // TODO deprecated Enable string Power string StandbyPower float64 - }{ - Home: "Home", } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - return NewHomeAssistantSwitch(cc.embed, cc.Home, cc.Enable, cc.Power, cc.StandbyPower) + return NewHomeAssistantSwitch(cc.embed, cc.URI, cc.Home, cc.Enable, cc.Power, cc.StandbyPower) } -func NewHomeAssistantSwitch(embed embed, home, enable, power string, standbypower float64) (api.Charger, error) { +func NewHomeAssistantSwitch(embed embed, uri, home, enable, power string, standbypower float64) (api.Charger, error) { if enable == "" { return nil, errors.New("missing enable switch entity") } @@ -50,7 +48,8 @@ func NewHomeAssistantSwitch(embed embed, home, enable, power string, standbypowe } log := util.NewLogger("ha-switch") - conn, err := homeassistant.NewConnection(log, home) + + conn, err := homeassistant.NewConnection(log, uri, home) if err != nil { return nil, err } diff --git a/charger/homeassistant.go b/charger/homeassistant.go index 32fe32655..e1ae53e35 100644 --- a/charger/homeassistant.go +++ b/charger/homeassistant.go @@ -27,10 +27,10 @@ func init() { // NewHomeAssistantFromConfig creates a HomeAssistant charger from generic config func NewHomeAssistantFromConfig(other map[string]any) (api.Charger, error) { - cc := struct { - URI_ string `mapstructure:"uri"` // TODO deprecated - Token_ string `mapstructure:"token"` // TODO deprecated - Home string + var cc struct { + URI string + Token_ string `mapstructure:"token"` // TODO deprecated + Home string // TODO deprecated Status string // required - sensor for charge status Enabled string // required - sensor for enabled state Enable string // required - switch/input_boolean for enable/disable @@ -39,8 +39,6 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Charger, error) { Energy string // optional - energy sensor Currents []string // optional - current sensors for L1, L2, L3 Voltages []string // optional - voltage sensors for L1, L2, L3 - }{ - Home: "Home", } if err := util.DecodeOther(other, &cc); err != nil { @@ -58,7 +56,8 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Charger, error) { } log := util.NewLogger("ha-charger") - conn, err := homeassistant.NewConnection(log, cc.Home) + + conn, err := homeassistant.NewConnection(log, cc.URI, cc.Home) if err != nil { return nil, err } diff --git a/meter/homeassistant.go b/meter/homeassistant.go index ccf44447b..4334c5503 100644 --- a/meter/homeassistant.go +++ b/meter/homeassistant.go @@ -24,9 +24,9 @@ func init() { // NewHomeAssistantFromConfig creates a HomeAssistant meter from generic config func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) { cc := struct { - URI_ string `mapstructure:"uri"` // TODO deprecated + URI string Token_ string `mapstructure:"token"` // TODO deprecated - Home string + Home string // TODO deprecated, backward compatibility (v0.210.x) Power string Energy string Currents []string @@ -41,7 +41,6 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) { batterySocLimits `mapstructure:",squash"` batteryPowerLimits `mapstructure:",squash"` }{ - Home: "Home", batterySocLimits: batterySocLimits{ MinSoc: 20, MaxSoc: 95, @@ -57,7 +56,8 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) { } log := util.NewLogger("ha-meter") - conn, err := homeassistant.NewConnection(log, cc.Home) + + conn, err := homeassistant.NewConnection(log, cc.URI, cc.Home) if err != nil { return nil, err } diff --git a/templates/definition/charger/homeassistant-switch.yaml b/templates/definition/charger/homeassistant-switch.yaml index e9d8c2e35..90fe8ce0e 100644 --- a/templates/definition/charger/homeassistant-switch.yaml +++ b/templates/definition/charger/homeassistant-switch.yaml @@ -4,41 +4,44 @@ products: group: switchsockets requirements: evcc: ["skiptest"] + description: + en: Home Assistant instances in your network will be auto-discovered and suitable entities (e.g. `switch.*`, `sensor.*`) will be suggested. + de: Home Assistant Instanzen in deinem Netzwerk werden automatisch erkannt und passende Entitäten (z.B. `switch.*`, `sensor.*`) werden vorgeschlagen. auth: type: homeassistant - params: [home] + params: [uri] params: - name: uri - deprecated: true + description: + de: Home Assistant URI + en: Home Assistant URI + example: http://homeassistant.local:8123 + help: + en: " " # overwrite default + de: " " # overwrite default + service: homeassistant/instances + required: true - name: token deprecated: true - name: home - description: - de: Home Assistant Instanz - en: Home Assistant Instance - help: - en: Can be found in Home Assistant UI under Settings -> System -> General -> Name. Used to identify your instance via mDNS. - de: Kann in der Home Assistant UI unter Einstellungen -> System -> Allgemein -> Name gefunden werden. Wird verwendet, um die Instanz via mDNS zu identifizieren. - example: Home - service: homeassistant/homes - required: true + deprecated: true - name: switch description: de: Entity ID des schaltbaren Geräts en: Entity ID of the switch device - service: homeassistant/homes/{home}/entities?domain=switch + service: homeassistant/entities?uri={uri}&domain=switch example: switch.smartsocket required: true - name: power description: de: Entity ID für Leistungsmessung en: Entity ID for power measurement - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: sensor.smartsocket_power - preset: switchsocket render: | type: homeassistant-switch - home: {{ .home }} + uri: {{ .uri }} enable: {{ .switch }} power: {{ .power }} {{ include "switchsocket" . }} diff --git a/templates/definition/meter/homeassistant.yaml b/templates/definition/meter/homeassistant.yaml index 6bd787112..25884c7b0 100644 --- a/templates/definition/meter/homeassistant.yaml +++ b/templates/definition/meter/homeassistant.yaml @@ -4,34 +4,34 @@ products: group: generic requirements: description: - en: Requires a running Home Assistant instance with suitable sensor entities. All values are Home Assistant entity IDs (e.g. `sensor.*`) - de: Erfordert eine laufende Home Assistant Instanz mit passenden Sensor-Entitäten. Alle Werte sind Home Assistant Entity IDs (z.B. `sensor.*`) + en: Home Assistant instances in your network will be auto-discovered and suitable entities (e.g. `sensor.*`) will be suggested. + de: Home Assistant Instanzen in deinem Netzwerk werden automatisch erkannt und passende Entitäten (z.B. `sensor.*`) werden vorgeschlagen. auth: type: homeassistant - params: [home] + params: [uri] params: - name: usage choice: ["grid", "pv", "battery", "aux", "charge"] - name: uri - deprecated: true + description: + de: Home Assistant URI + en: Home Assistant URI + example: http://homeassistant.local:8123 + help: + en: " " # overwrite default + de: " " # overwrite default + service: homeassistant/instances + required: true - name: token deprecated: true - name: home - description: - de: Home Assistant Instanz - en: Home Assistant Instance - help: - en: Can be found in Home Assistant UI under Settings -> System -> General -> Name. Used to identify your instance via mDNS. - de: Kann in der Home Assistant UI unter Einstellungen -> System -> Allgemein -> Name gefunden werden. Wird verwendet, um die Instanz via mDNS zu identifizieren. - example: Home - service: homeassistant/homes - required: true + deprecated: true - name: power description: de: Leistungsentität en: Power Entity required: true - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.house_power" help: en: Entity ID for instantaneous power measurement in watts. The entity must provide numeric values only (e.g., "1234", not "1234 W"). @@ -40,7 +40,7 @@ params: description: de: Energieentität en: Energy Entity - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.house_energy" advanced: true help: @@ -50,7 +50,7 @@ params: description: de: L1 Stromentität en: L1 Current Entity - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.house_current_l1" advanced: true help: @@ -60,7 +60,7 @@ params: description: de: L2 Stromentität en: L2 Current Entity - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.house_current_l2" advanced: true help: @@ -70,7 +70,7 @@ params: description: de: L3 Stromentität en: L3 Current Entity - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.house_current_l3" advanced: true help: @@ -80,7 +80,7 @@ params: description: de: L1 Spannungsentität en: L1 Voltage Entity - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.house_voltage_l1" advanced: true help: @@ -90,7 +90,7 @@ params: description: de: L2 Spannungsentität en: L2 Voltage Entity - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.house_voltage_l2" advanced: true help: @@ -100,7 +100,7 @@ params: description: de: L3 Spannungsentität en: L3 Voltage Entity - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.house_voltage_l3" advanced: true help: @@ -110,7 +110,7 @@ params: description: de: Batterieladestand en: Battery State of Charge - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.battery_soc" advanced: true help: @@ -124,7 +124,7 @@ params: - name: maxacpower render: | type: homeassistant - home: {{ .home }} + uri: {{ .uri }} power: {{ .power }} energy: {{ .energy }} currents: diff --git a/templates/definition/vehicle/homeassistant.yaml b/templates/definition/vehicle/homeassistant.yaml index 177e37476..2356acb06 100644 --- a/templates/definition/vehicle/homeassistant.yaml +++ b/templates/definition/vehicle/homeassistant.yaml @@ -4,93 +4,93 @@ products: group: generic requirements: description: - en: Requires a running Home Assistant instance with suitable vehicle entities and services. All values are Home Assistant entity IDs (e.g. `sensor.*`, `binary_sensor.*`, `number.*`, `script.*`) - de: Erfordert eine laufende Home Assistant Instanz mit passenden Fahrzeug-Entities und Services. Alle Werte sind Home Assistant Entity IDs (z.B. `sensor.*`, `binary_sensor.*`, `number.*`, `script.*`) + en: Home Assistant instances in your network will be auto-discovered and suitable vehicle entities and services (e.g. `sensor.*`) will be suggested. + de: Home Assistant Instanzen in deinem Netzwerk werden automatisch erkannt und passende Entitäten und Services (z.B. `sensor.*`) werden vorgeschlagen. auth: type: homeassistant - params: [home] + params: [uri] params: - preset: vehicle-common - name: uri - deprecated: true + description: + de: Home Assistant URI + en: Home Assistant URI + example: http://homeassistant.local:8123 + help: + en: " " # overwrite default + de: " " # overwrite default + service: homeassistant/instances + required: true - name: token deprecated: true - name: home - description: - de: Home Assistant Instanz - en: Home Assistant Instance - help: - en: Can be found in Home Assistant UI under Settings -> System -> General -> Name. Used to identify your instance via mDNS. - de: Kann in der Home Assistant UI unter Einstellungen -> System -> Allgemein -> Name gefunden werden. Wird verwendet, um die Instanz via mDNS zu identifizieren. - example: Home - service: homeassistant/homes - required: true + deprecated: true - name: soc description: de: Ladezustand [%] en: State of charge [%] - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.vehicle_soc" required: true - name: range description: de: Restreichweite [km] en: Remaining range [km] - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.vehicle_range" - name: status description: de: Ladestatus en: Charging status - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.vehicle_charging" - name: limitSoc description: de: Ziel-Ladezustand [%] en: Target state of charge [%] - service: homeassistant/homes/{home}/entities?domain=number,input_number + service: homeassistant/entities?uri={uri}&domain=number,input_number example: "number.vehicle_target_state_of_charge" - name: odometer description: de: Kilometerstand [km] en: Odometer [km] - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.vehicle_odometer" - name: climater description: de: Klimatisierung aktiv en: Climatisation active - service: homeassistant/homes/{home}/entities?domain=binary_sensor + service: homeassistant/entities?uri={uri}&domain=binary_sensor example: "binary_sensor.vehicle_climater" - name: finishTime description: de: Ladeende (ISO8601 oder Unix) en: Finish time (ISO8601 or Unix) - service: homeassistant/homes/{home}/entities?domain=sensor + service: homeassistant/entities?uri={uri}&domain=sensor example: "sensor.vehicle_finish_time" - name: start_charging description: de: Service zum Laden starten en: Service to start charging - service: homeassistant/homes/{home}/entities?domain=script + service: homeassistant/entities?uri={uri}&domain=script example: "script.vehicle_start_charge" - name: stop_charging description: de: Service zum Laden stoppen en: Service to stop charging - service: homeassistant/homes/{home}/entities?domain=script + service: homeassistant/entities?uri={uri}&domain=script example: "script.vehicle_stop_charge" - name: wakeup description: de: Service zum Aufwecken en: Service to wake up vehicle - service: homeassistant/homes/{home}/entities?domain=script + service: homeassistant/entities?uri={uri}&domain=script example: "script.vehicle_wakeup" - name: setMaxCurrent description: de: Ladestromstärke setzen [A] en: Set charging current [A] - service: homeassistant/homes/{home}/entities?domain=number,input_number + service: homeassistant/entities?uri={uri}&domain=number,input_number example: "number.vehicle_charging_current" - preset: vehicle-features - name: streaming @@ -99,7 +99,7 @@ render: | type: homeassistant {{ include "vehicle-common" . }} {{ include "vehicle-features" . }} - home: {{ .home }} + uri: {{ .uri }} sensors: soc: {{ .soc }} range: {{ .range }} diff --git a/util/homeassistant/connection.go b/util/homeassistant/connection.go index a5a2304d7..0c7079623 100644 --- a/util/homeassistant/connection.go +++ b/util/homeassistant/connection.go @@ -22,10 +22,21 @@ type Connection struct { } // NewConnection creates a new Home Assistant connection -func NewConnection(log *util.Logger, home string) (*Connection, error) { +func NewConnection(log *util.Logger, uri, home string) (*Connection, error) { + if home != "" { + log.WARN.Printf("using deprecated 'home' parameter '%s', please use 'uri' instead", home) + } + + if uri == "" && home == "" { + return nil, errors.New("missing either uri or home") + } + c := &Connection{ - Helper: request.NewHelper(log), - instance: &proxyInstance{home: home}, + Helper: request.NewHelper(log), + instance: &proxyInstance{ + home: home, + uri: uri, + }, } // Set up authentication headers diff --git a/util/homeassistant/instance.go b/util/homeassistant/instance.go index 936bc78e3..6ea15eb8f 100644 --- a/util/homeassistant/instance.go +++ b/util/homeassistant/instance.go @@ -17,7 +17,8 @@ func (inst *proxyInstance) URI() string { inst.mu.Lock() defer inst.mu.Unlock() - if inst.uri == "" { + if inst.uri == "" && inst.home != "" { + // Try to resolve home name to URI (backward compatibility) inst.uri = instanceUriByName(inst.home) if inst.uri == "" { @@ -31,14 +32,17 @@ func (inst *proxyInstance) URI() string { func (inst *proxyInstance) Token() (*oauth2.Token, error) { uri := inst.URI() if uri == "" { - return nil, fmt.Errorf("unknown instance: %s", inst.home) + if inst.home != "" { + return nil, fmt.Errorf("unknown instance: %s", inst.home) + } + return nil, fmt.Errorf("no URI configured") } inst.mu.Lock() defer inst.mu.Unlock() if inst.TokenSource == nil { - ts, err := NewHomeAssistant(inst.home, uri) + ts, err := NewHomeAssistant(uri) if err != nil { return nil, err } diff --git a/util/homeassistant/oauth2.go b/util/homeassistant/oauth2.go index 25eb01322..1f9570284 100644 --- a/util/homeassistant/oauth2.go +++ b/util/homeassistant/oauth2.go @@ -3,6 +3,8 @@ package homeassistant import ( "context" "fmt" + "net" + "net/url" "github.com/evcc-io/evcc/plugin/auth" "github.com/evcc-io/evcc/server/network" @@ -19,22 +21,27 @@ func init() { func NewHomeAssistantFromConfig(other map[string]any) (oauth2.TokenSource, error) { var cc struct { - Home string + URI string + Home string // TODO remove deprecated } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - uri := instanceUriByName(cc.Home) - if uri == "" { - return nil, fmt.Errorf("unknown instance: %s", cc.Home) + uri := cc.URI + + if uri == "" && cc.Home != "" { + uri = instanceUriByName(cc.Home) + if uri == "" { + return nil, fmt.Errorf("unknown instance: %s", cc.Home) + } } - return NewHomeAssistant(cc.Home, uri) + return NewHomeAssistant(uri) } -func NewHomeAssistant(home, uri string) (oauth2.TokenSource, error) { +func NewHomeAssistant(uri string) (oauth2.TokenSource, error) { extUrl := network.Config().ExternalURL() redirectUri := extUrl + network.CallbackPath @@ -50,5 +57,16 @@ func NewHomeAssistant(home, uri string) (oauth2.TokenSource, error) { }, } - return auth.NewOAuth(ctx, "HomeAssistant", home, &oc) + // validate url + u, err := url.Parse(uri) + if err != nil { + return nil, err + } + + host := u.Host + if h, _, err := net.SplitHostPort(u.Host); err == nil { + host = h + } + + return auth.NewOAuth(ctx, "HomeAssistant", host, &oc) } diff --git a/util/homeassistant/service.go b/util/homeassistant/service.go index faba40256..0c944085c 100644 --- a/util/homeassistant/service.go +++ b/util/homeassistant/service.go @@ -2,6 +2,7 @@ package homeassistant import ( "encoding/json" + "errors" "maps" "net/http" "slices" @@ -16,28 +17,27 @@ var log = util.NewLogger("homeassistant") func init() { mux := http.NewServeMux() - mux.HandleFunc("GET /homes", getHomes) - mux.HandleFunc("GET /homes/{home}/entities", getEntities) + mux.HandleFunc("GET /instances", getInstances) + mux.HandleFunc("GET /entities", getEntities) service.Register("homeassistant", mux) } -func getHomes(w http.ResponseWriter, req *http.Request) { +func getInstances(w http.ResponseWriter, req *http.Request) { mu.Lock() defer mu.Unlock() - jsonWrite(w, slices.Sorted(maps.Keys(instances))) + jsonWrite(w, slices.Sorted(maps.Values(instances))) } func getEntities(w http.ResponseWriter, req *http.Request) { - home := req.PathValue("home") - - if instanceUriByName(home) == "" { - w.WriteHeader(http.StatusBadRequest) + uri := req.URL.Query().Get("uri") + if uri == "" { + jsonError(w, http.StatusBadRequest, errors.New("missing uri")) return } - conn, _ := NewConnection(log, home) + conn, _ := NewConnection(log, uri, "") res, err := conn.GetStates() if err != nil { jsonError(w, http.StatusBadRequest, err) diff --git a/vehicle/homeassistant.go b/vehicle/homeassistant.go index a3e2eb4b5..9b484e223 100644 --- a/vehicle/homeassistant.go +++ b/vehicle/homeassistant.go @@ -23,11 +23,11 @@ func init() { // Constructor from YAML config func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error) { - cc := struct { + var cc struct { embed `mapstructure:",squash"` - URI_ string `mapstructure:"uri"` // TODO deprecated + URI string Token_ string `mapstructure:"token"` // TODO deprecated - Home string + Home string // TODO deprecated Sensors struct { Soc string // required Range string // optional @@ -43,8 +43,6 @@ func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error Wakeup string // script.* optional SetMaxCurrent string // number.* or input_number.* optional } - }{ - Home: "Home", } if err := util.DecodeOther(other, &cc); err != nil { @@ -56,7 +54,8 @@ func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error } log := util.NewLogger("ha-vehicle") - conn, err := homeassistant.NewConnection(log, cc.Home) + + conn, err := homeassistant.NewConnection(log, cc.URI, cc.Home) if err != nil { return nil, err }