@@ -330,6 +343,12 @@ export default defineComponent({
const { params = [] } = this.template?.Auth ?? {};
return this.templateParams.filter((p) => params.includes(p.Name));
},
+ authNormalParams() {
+ return this.authParams.filter((p: TemplateParam) => !p.Advanced && !p.Deprecated);
+ },
+ authAdvancedParams() {
+ return this.authParams.filter((p: TemplateParam) => p.Advanced || p.Deprecated);
+ },
normalParams() {
return this.templateParams.filter((p) => !p.Advanced && !p.Deprecated);
},
@@ -429,7 +448,13 @@ export default defineComponent({
return this.template?.Auth && !this.auth.ok;
},
authValuesMissing() {
- return this.template?.Auth && Object.values(this.authValues).some((value) => !value);
+ const authParamNames: string[] = this.template?.Auth?.params ?? [];
+ return (
+ authParamNames.length > 0 &&
+ this.templateParams
+ .filter((p: TemplateParam) => authParamNames.includes(p.Name) && p.Required)
+ .some((p: TemplateParam) => !this.values[p.Name])
+ );
},
authValues() {
const params = this.template?.Auth?.params ?? [];
diff --git a/assets/js/components/Config/DeviceModal/index.ts b/assets/js/components/Config/DeviceModal/index.ts
index bf2cd6edb..3f4e16a4e 100644
--- a/assets/js/components/Config/DeviceModal/index.ts
+++ b/assets/js/components/Config/DeviceModal/index.ts
@@ -28,6 +28,7 @@ export type TemplateParam = {
Advanced: boolean;
Deprecated: boolean;
Default?: string | number | boolean;
+ Type?: string;
Choice?: string[];
Service?: string;
Usages?: TemplateParamUsage[];
@@ -89,11 +90,13 @@ export function handleError(e: any, msg: string) {
export function applyDefaultsFromTemplate(template: Template | null, values: DeviceValues) {
const params = template?.Params || [];
- params
- .filter((p) => p.Default && !values[p.Name])
- .forEach((p) => {
+ params.forEach((p) => {
+ if (p.Default && !values[p.Name]) {
values[p.Name] = p.Default;
- });
+ } else if (p.Type === "Bool" && values[p.Name] === undefined) {
+ values[p.Name] = false; // initialize
+ }
+ });
}
export function customChargerName(type: ConfigType, isHeating: boolean) {
diff --git a/assets/js/components/Config/utils/authProvider.ts b/assets/js/components/Config/utils/authProvider.ts
index e542cfba6..60030864f 100644
--- a/assets/js/components/Config/utils/authProvider.ts
+++ b/assets/js/components/Config/utils/authProvider.ts
@@ -30,7 +30,12 @@ export const prepareAuthLogin = async (state: AuthState, providerId: string) =>
state.loading = true;
state.error = null;
- const url = `providerauth/login?id=${encodeURIComponent(providerId)}`;
+ let url = `providerauth/login?id=${encodeURIComponent(providerId)}`;
+ // restore the config modal stack on callback
+ const returnTo = window.location.hash.split("?")[1];
+ if (returnTo) {
+ url += `&return=${encodeURIComponent(returnTo)}`;
+ }
const { status, data } = await baseApi.get
(url, {
validateStatus: (code) => [200, 400].includes(code),
});
diff --git a/charger/homeassistant-switch.go b/charger/homeassistant-switch.go
index f0af4c75b..acc9e6dcc 100644
--- a/charger/homeassistant-switch.go
+++ b/charger/homeassistant-switch.go
@@ -21,23 +21,21 @@ func init() {
func NewHomeAssistantSwitchFromConfig(other map[string]any) (api.Charger, error) {
var cc struct {
- embed `mapstructure:",squash"`
- URI string
- Token_ string `mapstructure:"token"` // TODO deprecated
- Home_ string `mapstructure:"home"` // TODO deprecated
- Enable string
- Power string
- StandbyPower float64
+ embed `mapstructure:",squash"`
+ homeassistant.Config `mapstructure:",squash"`
+ Enable string
+ Power string
+ StandbyPower float64
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
- return NewHomeAssistantSwitch(cc.embed, cc.URI, cc.Home_, cc.Enable, cc.Power, cc.StandbyPower)
+ return NewHomeAssistantSwitch(cc.embed, cc.URI, cc.Home_, cc.Insecure, cc.Enable, cc.Power, cc.StandbyPower)
}
-func NewHomeAssistantSwitch(embed embed, uri, home, enable, power string, standbypower float64) (api.Charger, error) {
+func NewHomeAssistantSwitch(embed embed, uri, home string, insecure bool, enable, power string, standbypower float64) (api.Charger, error) {
if enable == "" {
return nil, errors.New("missing enable switch entity")
}
@@ -49,7 +47,7 @@ func NewHomeAssistantSwitch(embed embed, uri, home, enable, power string, standb
log := util.NewLogger("ha-switch")
- conn, err := homeassistant.NewConnection(log, uri, home)
+ conn, err := homeassistant.NewConnection(log, uri, home, insecure)
if err != nil {
return nil, err
}
diff --git a/charger/homeassistant.go b/charger/homeassistant.go
index 9da621a4f..c61b359ff 100644
--- a/charger/homeassistant.go
+++ b/charger/homeassistant.go
@@ -28,18 +28,16 @@ func init() {
// NewHomeAssistantFromConfig creates a HomeAssistant charger from generic config
func NewHomeAssistantFromConfig(other map[string]any) (api.Charger, error) {
var cc struct {
- URI string
- Token_ string `mapstructure:"token"` // TODO deprecated
- Home_ string `mapstructure:"home"` // 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
- MaxCurrent string // required - number entity for setting max current
- Power string // optional - power sensor
- Energy string // optional - energy sensor
- Currents []string // optional - current sensors for L1, L2, L3
- Voltages []string // optional - voltage sensors for L1, L2, L3
- Phases string // optional - select entity for 1p/3p phase switching
+ homeassistant.Config `mapstructure:",squash"`
+ Status string // required - sensor for charge status
+ Enabled string // required - sensor for enabled state
+ Enable string // required - switch/input_boolean for enable/disable
+ MaxCurrent string // required - number entity for setting max current
+ Power string // optional - power sensor
+ Energy string // optional - energy sensor
+ Currents []string // optional - current sensors for L1, L2, L3
+ Voltages []string // optional - voltage sensors for L1, L2, L3
+ Phases string // optional - select entity for 1p/3p phase switching
}
if err := util.DecodeOther(other, &cc); err != nil {
@@ -61,7 +59,7 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Charger, error) {
log := util.NewLogger("ha-charger")
- conn, err := homeassistant.NewConnection(log, cc.URI, cc.Home_)
+ conn, err := cc.Config.NewConnection(log)
if err != nil {
return nil, err
}
diff --git a/messenger/homeassistant.go b/messenger/homeassistant.go
index fd3a6f219..d8599ad39 100644
--- a/messenger/homeassistant.go
+++ b/messenger/homeassistant.go
@@ -25,9 +25,9 @@ type HomeAssistant struct {
// NewHomeAssistantFromConfig creates a new Home Assistant messenger
func NewHomeAssistantFromConfig(other map[string]any) (api.Messenger, error) {
var cc struct {
- URI string
- Notify string
- Data map[string]any
+ homeassistant.Config `mapstructure:",squash"`
+ Notify string
+ Data map[string]any
}
if err := util.DecodeOther(other, &cc); err != nil {
@@ -40,7 +40,7 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Messenger, error) {
log := util.NewLogger("homeassistant")
- conn, err := homeassistant.NewConnection(log, cc.URI, "")
+ conn, err := cc.Config.NewConnection(log)
if err != nil {
return nil, err
}
diff --git a/meter/homeassistant.go b/meter/homeassistant.go
index 8f42dad89..2f638240e 100644
--- a/meter/homeassistant.go
+++ b/meter/homeassistant.go
@@ -18,15 +18,13 @@ func init() {
// NewHomeAssistantFromConfig creates a HomeAssistant meter from generic config
func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) {
cc := struct {
- URI string
- Token_ string `mapstructure:"token"` // TODO deprecated
- Home_ string `mapstructure:"home"` // TODO deprecated
- Power string
- Energy string
- Currents []string
- Voltages []string
- Powers []string
- Soc string
+ homeassistant.Config `mapstructure:",squash"`
+ Power string
+ Energy string
+ Currents []string
+ Voltages []string
+ Powers []string
+ Soc string
// pv
pvMaxACPower `mapstructure:",squash"`
@@ -57,7 +55,7 @@ func NewHomeAssistantFromConfig(other map[string]any) (api.Meter, error) {
log := util.NewLogger("ha-meter")
- conn, err := homeassistant.NewConnection(log, cc.URI, cc.Home_)
+ conn, err := cc.Config.NewConnection(log)
if err != nil {
return nil, err
}
diff --git a/plugin/auth/demo.go b/plugin/auth/demo.go
index deaa7cb7a..66c719c27 100644
--- a/plugin/auth/demo.go
+++ b/plugin/auth/demo.go
@@ -34,6 +34,7 @@ func NewDemoFromConfig(_ context.Context, other map[string]any) (oauth2.TokenSou
Method string
RedirectUri string
Secret string
+ Scope string // advanced auth param, used by e2e tests
}
if err := util.DecodeOther(other, &cc); err != nil {
diff --git a/server/providerauth/handler.go b/server/providerauth/handler.go
index 888eb8e05..15336b98c 100644
--- a/server/providerauth/handler.go
+++ b/server/providerauth/handler.go
@@ -44,10 +44,15 @@ type Handler struct {
log *util.Logger
secret []byte
providers map[string]api.AuthProvider
- states map[string]string
+ states map[string]stateEntry
updateC chan string
}
+type stateEntry struct {
+ id string
+ returnTo string // config modal query to restore on callback
+}
+
// TODO get status from update channel
func (a *Handler) run(paramC chan<- util.Param) {
for range a.updateC {
@@ -97,7 +102,7 @@ func (a *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
// Generate a new state and store the provider
state := NewState()
encryptedState := state.Encrypt(a.secret)
- a.states[encryptedState] = id
+ a.states[encryptedState] = stateEntry{id: id, returnTo: r.URL.Query().Get("return")}
// Schedule cleanup for stale state entries after state becomes invalid
time.AfterFunc(stateValidity, func() {
@@ -176,11 +181,12 @@ func (a *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
defer a.mu.Unlock()
// Find the corresponding provider
- id, ok := a.states[encryptedState]
+ entry, ok := a.states[encryptedState]
if !ok {
a.redirectToError(w, r, "no provider found for state")
return
}
+ id := entry.id
provider, ok := a.providers[id]
if !ok {
@@ -198,5 +204,11 @@ func (a *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
return
}
- http.Redirect(w, r, "/#/config?callbackCompleted="+url.QueryEscape(id), http.StatusFound)
+ // restore the config modal stack alongside the completion marker
+ query := "callbackCompleted=" + url.QueryEscape(id)
+ if entry.returnTo != "" {
+ query = entry.returnTo + "&" + query
+ }
+
+ http.Redirect(w, r, "/#/config?"+query, http.StatusFound)
}
diff --git a/server/providerauth/providerauth.go b/server/providerauth/providerauth.go
index d3239a41e..7dc22b435 100644
--- a/server/providerauth/providerauth.go
+++ b/server/providerauth/providerauth.go
@@ -27,7 +27,7 @@ func init() {
log: util.NewLogger("providerauth"),
secret: secret[:],
providers: make(map[string]api.AuthProvider),
- states: make(map[string]string),
+ states: make(map[string]stateEntry),
updateC: make(chan string, 1),
}
}
diff --git a/templates/definition/charger/homeassistant-switch.yaml b/templates/definition/charger/homeassistant-switch.yaml
index f626107c1..c77edce01 100644
--- a/templates/definition/charger/homeassistant-switch.yaml
+++ b/templates/definition/charger/homeassistant-switch.yaml
@@ -11,7 +11,7 @@ requirements:
de: Home Assistant Instanzen in deinem Netzwerk werden automatisch erkannt und passende Entitäten (z.B. `switch.*`, `sensor.*`) werden vorgeschlagen.
auth:
type: homeassistant
- params: [uri]
+ params: [uri, insecure]
params:
- name: uri
description:
@@ -27,24 +27,26 @@ params:
deprecated: true
- name: home
deprecated: true
+ - name: insecure
- name: switch
description:
de: Entity ID des schaltbaren Geräts
en: Entity ID of the switch device
- service: homeassistant/entities?uri={uri}&domain=switch
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=switch
example: switch.smartsocket
required: true
- name: power
description:
de: Entity ID für Leistungsmessung
en: Entity ID for power measurement
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: sensor.smartsocket_power
- preset: switchsocket
render: |
type: homeassistant-switch
uri: {{ .uri }}
home: {{ .home }} # deprecated
+ insecure: {{ .insecure }}
enable: {{ .switch }}
power: {{ .power }}
{{ include "switchsocket" . }}
diff --git a/templates/definition/charger/homeassistant.yaml b/templates/definition/charger/homeassistant.yaml
index 6b6b1ce60..79d1b0318 100644
--- a/templates/definition/charger/homeassistant.yaml
+++ b/templates/definition/charger/homeassistant.yaml
@@ -11,7 +11,7 @@ requirements:
de: Home Assistant Instanzen in deinem Netzwerk werden automatisch erkannt und passende Wallbox-Entitäten und Services (z.B. `sensor.*`, `switch.*`, `number.*`) werden vorgeschlagen.
auth:
type: homeassistant
- params: [uri]
+ params: [uri, insecure]
params:
- name: uri
description:
@@ -23,11 +23,12 @@ params:
de: " " # overwrite default
service: homeassistant/instances
required: true
+ - name: insecure
- name: status
description:
de: Ladestatus-Sensor
en: Charging status sensor
- service: homeassistant/entities?uri={uri}&domain=sensor,binary_sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor,binary_sensor
example: "sensor.charger_status"
required: true
help:
@@ -37,7 +38,7 @@ params:
description:
de: Aktivierungsstatus-Sensor
en: Enabled status sensor
- service: homeassistant/entities?uri={uri}&domain=sensor,binary_sensor,switch
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor,binary_sensor,switch
example: "binary_sensor.charger_enabled"
required: true
help:
@@ -47,7 +48,7 @@ params:
description:
de: Aktivierungsschalter
en: Enable switch
- service: homeassistant/entities?uri={uri}&domain=switch,input_boolean
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=switch,input_boolean
example: "switch.charger_enable"
required: true
help:
@@ -57,7 +58,7 @@ params:
description:
de: Maximale Stromstärke-Entität [A]
en: Maximum current entity [A]
- service: homeassistant/entities?uri={uri}&domain=number,input_number
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=number,input_number
example: "number.charger_max_current"
required: true
help:
@@ -67,7 +68,7 @@ params:
description:
de: Leistungsentität
en: Power entity
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.charger_power"
advanced: true
help:
@@ -77,7 +78,7 @@ params:
description:
de: Energieentität
en: Energy entity
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.charger_energy"
advanced: true
help:
@@ -87,7 +88,7 @@ params:
description:
de: L1 Stromentität
en: L1 current entity
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.charger_current_l1"
advanced: true
help:
@@ -97,7 +98,7 @@ params:
description:
de: L2 Stromentität
en: L2 current entity
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.charger_current_l2"
advanced: true
help:
@@ -107,7 +108,7 @@ params:
description:
de: L3 Stromentität
en: L3 current entity
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.charger_current_l3"
advanced: true
help:
@@ -117,7 +118,7 @@ params:
description:
de: L1 Spannungsentität
en: L1 voltage entity
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.charger_voltage_l1"
advanced: true
help:
@@ -127,7 +128,7 @@ params:
description:
de: L2 Spannungsentität
en: L2 voltage entity
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.charger_voltage_l2"
advanced: true
help:
@@ -137,7 +138,7 @@ params:
description:
de: L3 Spannungsentität
en: L3 voltage entity
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.charger_voltage_l3"
advanced: true
help:
@@ -152,7 +153,7 @@ params:
description:
de: Phasenumschaltungs-Entität
en: Phase switching entity
- service: homeassistant/entities?uri={uri}&domain=select,input_select
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=select,input_select
example: "select.charger_phases"
advanced: true
help:
@@ -162,6 +163,7 @@ params:
render: |
type: homeassistant
uri: {{ .uri }}
+ insecure: {{ .insecure }}
status: {{ .status }}
enabled: {{ .enabled }}
enable: {{ .enable }}
diff --git a/templates/definition/messenger/homeassistant.yaml b/templates/definition/messenger/homeassistant.yaml
index 2394a32be..e5bd49ced 100644
--- a/templates/definition/messenger/homeassistant.yaml
+++ b/templates/definition/messenger/homeassistant.yaml
@@ -3,7 +3,7 @@ products:
- brand: Home Assistant
auth:
type: homeassistant
- params: [uri]
+ params: [uri, insecure]
params:
- name: uri
required: true
@@ -27,7 +27,8 @@ params:
en: >
Home Assistant notify service in the form `domain.service` (e.g. `notify.mobile_app_android`).
Leave empty to create a persistent notification in the HA UI.
- service: homeassistant/services?uri={uri}&domain=notify
+ service: homeassistant/services?uri={uri}&insecure={insecure}&domain=notify
+ - name: insecure
- name: critical
type: bool
default: false
@@ -56,6 +57,7 @@ params:
render: |
type: homeassistant
uri: {{ .uri }}
+ insecure: {{ .insecure }}
{{- if .notify }}
notify: {{ .notify }}
{{- end }}
diff --git a/templates/definition/meter/homeassistant.yaml b/templates/definition/meter/homeassistant.yaml
index f62309f2b..61166af50 100644
--- a/templates/definition/meter/homeassistant.yaml
+++ b/templates/definition/meter/homeassistant.yaml
@@ -8,7 +8,7 @@ requirements:
de: Home Assistant Instanzen in deinem Netzwerk werden automatisch erkannt und passende Entitäten (z.B. `sensor.*`) werden vorgeschlagen.
auth:
type: homeassistant
- params: [uri]
+ params: [uri, insecure]
params:
- name: usage
choice: ["grid", "pv", "battery", "aux", "charge"]
@@ -31,7 +31,7 @@ params:
de: Leistungsentität
en: Power Entity
required: true
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&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/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&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/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&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/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&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/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&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/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&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/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&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/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.house_voltage_l3"
advanced: true
help:
@@ -111,7 +111,7 @@ params:
description:
de: Batterieladestand
en: Battery State of Charge
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.battery_soc"
advanced: true
help:
@@ -122,7 +122,7 @@ params:
description:
de: Normalbetrieb Skript
en: Normal Mode Script
- service: homeassistant/entities?uri={uri}&domain=script
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=script
example: "script.battery_normal"
advanced: true
help:
@@ -133,7 +133,7 @@ params:
description:
de: Halten Skript
en: Hold Mode Script
- service: homeassistant/entities?uri={uri}&domain=script
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=script
example: "script.battery_hold"
advanced: true
help:
@@ -144,18 +144,20 @@ params:
description:
de: Netzladung Skript
en: Grid Charge Mode Script
- service: homeassistant/entities?uri={uri}&domain=script
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=script
example: "script.battery_grid_charge"
advanced: true
help:
en: Optional script to grid-charge the home battery.
de: Optionales Skript um die Heimbatterie aus dem Netz zu laden.
+ - name: insecure
- name: maxacpower
- preset: battery-params
render: |
type: homeassistant
uri: {{ .uri }}
home: {{ .home }} # deprecated
+ insecure: {{ .insecure }}
power: {{ .power }}
energy: {{ .energy }}
currents:
diff --git a/templates/definition/vehicle/homeassistant.yaml b/templates/definition/vehicle/homeassistant.yaml
index 06ee35609..2e8f2000d 100644
--- a/templates/definition/vehicle/homeassistant.yaml
+++ b/templates/definition/vehicle/homeassistant.yaml
@@ -8,7 +8,7 @@ requirements:
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: [uri]
+ params: [uri, insecure]
params:
- preset: vehicle-common
- name: uri
@@ -25,11 +25,12 @@ params:
deprecated: true
- name: home
deprecated: true
+ - name: insecure
- name: soc
description:
de: Ladezustand [%]
en: State of charge [%]
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.vehicle_soc"
required: true
help:
@@ -39,7 +40,7 @@ params:
description:
de: Restreichweite [km]
en: Remaining range [km]
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.vehicle_range"
help:
en: Entity ID for the vehicle's remaining range in kilometers
@@ -48,7 +49,7 @@ params:
description:
de: Ladestatus
en: Charging status
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.vehicle_charging"
help:
en: Entity ID for charging status (A=disconnected, B=connected, C=charging)
@@ -57,7 +58,7 @@ params:
description:
de: Ziel-Ladezustand [%]
en: Target state of charge [%]
- service: homeassistant/entities?uri={uri}&domain=number,input_number
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=number,input_number
example: "number.vehicle_target_state_of_charge"
help:
en: Entity ID for the vehicle's target state of charge in percent (`number` or `input_number` entity)
@@ -66,7 +67,7 @@ params:
description:
de: Kilometerstand [km]
en: Odometer [km]
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.vehicle_odometer"
help:
en: Entity ID for the vehicle's odometer reading in kilometers
@@ -75,7 +76,7 @@ params:
description:
de: Klimatisierung aktiv
en: Climatisation active
- service: homeassistant/entities?uri={uri}&domain=binary_sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=binary_sensor
example: "binary_sensor.vehicle_climater"
help:
en: Entity ID for the vehicle's climatisation state (`binary_sensor` with `on`/`off` state)
@@ -84,7 +85,7 @@ params:
description:
de: Ladeende
en: Finish time
- service: homeassistant/entities?uri={uri}&domain=sensor
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=sensor
example: "sensor.vehicle_finish_time"
help:
en: Entity ID for the estimated charging finish time (ISO8601 or Unix timestamp)
@@ -93,7 +94,7 @@ params:
description:
de: Service zum Laden starten
en: Service to start charging
- service: homeassistant/entities?uri={uri}&domain=script,switch
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=script,switch
example: "script.vehicle_start_charge; switch.vehicle_charger"
help:
en: Entity ID for a script or a switch that starts charging the vehicle. Only useful with the [docs.evcc.io](https://docs.evcc.io/en/docs/devices/chargers#vehicle-api-only-charger). If a switch is provided, Service to stop charging must be left empty.
@@ -102,7 +103,7 @@ params:
description:
de: Service zum Laden stoppen
en: Service to stop charging
- service: homeassistant/entities?uri={uri}&domain=script
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=script
example: "script.vehicle_stop_charge"
help:
en: Entity ID for a script that stops charging the vehicle. Only useful with the [docs.evcc.io](https://docs.evcc.io/en/docs/devices/chargers#vehicle-api-only-charger).
@@ -111,7 +112,7 @@ params:
description:
de: Service zum Aufwecken
en: Service to wake up vehicle
- service: homeassistant/entities?uri={uri}&domain=script,button
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=script,button
example: "script.vehicle_wakeup; button.wakeup_vehicle"
help:
en: Entity ID for a script or a button that wakes up the vehicle
@@ -120,7 +121,7 @@ params:
description:
de: Ladestromstärke setzen [A]
en: Set charging current [A]
- service: homeassistant/entities?uri={uri}&domain=number,input_number
+ service: homeassistant/entities?uri={uri}&insecure={insecure}&domain=number,input_number
example: "number.vehicle_charging_current"
help:
en: Entity ID for setting the maximum charging current in amperes (`number` or `input_number` entity)
@@ -134,6 +135,7 @@ render: |
{{ include "vehicle-features" . }}
uri: {{ .uri }}
home: {{ .home }} # deprecated
+ insecure: {{ .insecure }}
sensors:
soc: {{ .soc }}
range: {{ .range }}
diff --git a/tests/backup-restore.spec.ts b/tests/backup-restore.spec.ts
index 8d5615f3b..f11f57516 100644
--- a/tests/backup-restore.spec.ts
+++ b/tests/backup-restore.spec.ts
@@ -153,8 +153,8 @@ test.describe("backup and restore", async () => {
const backupConfirmModal = page.getByTestId("backup-restore-confirm-modal");
await expectModalVisible(backupConfirmModal);
await backupConfirmModal.getByRole("button", { name: "Download backup" }).click();
- await expectModalHidden(backupConfirmModal);
const download = await downloadPromise;
+ await expectModalHidden(backupConfirmModal);
await expectModalVisible(backupModal);
await backupModal.locator(".btn-close").click();
await expectModalHidden(backupModal);
diff --git a/tests/config-bool-service-demo.tpl.yaml b/tests/config-bool-service-demo.tpl.yaml
new file mode 100644
index 000000000..12d60af6c
--- /dev/null
+++ b/tests/config-bool-service-demo.tpl.yaml
@@ -0,0 +1,17 @@
+template: bool-service-demo
+group: generic
+products:
+ - description:
+ generic: Bool Service Meter
+params:
+ - name: usage
+ choice: ["grid"]
+ - name: insecure
+ type: bool
+ advanced: true
+ - name: entity
+ description:
+ generic: Entity
+ service: demo/insecure?insecure={insecure}
+render: |
+ type: custom
diff --git a/tests/config-bool-service.spec.ts b/tests/config-bool-service.spec.ts
new file mode 100644
index 000000000..710e9289e
--- /dev/null
+++ b/tests/config-bool-service.spec.ts
@@ -0,0 +1,42 @@
+import { test, expect } from "@playwright/test";
+import { start, stop, baseUrl } from "./evcc";
+import { expectModalVisible, getDatalistOptions } from "./utils";
+
+test.use({ baseURL: baseUrl() });
+
+const templateFlags = [
+ "--disable-auth",
+ "--template-type",
+ "meter",
+ "--template",
+ "tests/config-bool-service-demo.tpl.yaml",
+];
+
+test.beforeAll(async () => {
+ await start(undefined, undefined, templateFlags);
+});
+
+test.afterAll(async () => {
+ await stop();
+});
+
+test.describe("config bool service param", async () => {
+ test("unset bool defaults to false and fills service placeholder", async ({ page }) => {
+ // service call fires with insecure=false even though the bool was never touched
+ const servicePromise = page.waitForRequest(
+ /\/config\/service\/demo\/insecure\?insecure=false$/
+ );
+
+ await page.goto("/#/config");
+ await page.getByRole("button", { name: "Add grid meter" }).click();
+ const meterModal = page.getByTestId("meter-modal");
+ await expectModalVisible(meterModal);
+ await meterModal.getByLabel("Manufacturer").selectOption("Bool Service Meter");
+
+ await servicePromise;
+
+ // entity datalist is populated from the echoed flag
+ const entity = meterModal.getByLabel("Entity");
+ expect(await getDatalistOptions(entity)).toEqual(["insecure=false"]);
+ });
+});
diff --git a/tests/config-device-auth-demo.tpl.yaml b/tests/config-device-auth-demo.tpl.yaml
index b33aa1fba..f05f70fbc 100644
--- a/tests/config-device-auth-demo.tpl.yaml
+++ b/tests/config-device-auth-demo.tpl.yaml
@@ -6,7 +6,7 @@ products:
en: Auth Demo Meter
auth:
type: demo
- params: ["server", "method", "redirectUri", "secret"]
+ params: ["server", "method", "redirectUri", "secret", "scope"]
params:
- name: usage
choice: ["grid"]
@@ -30,6 +30,11 @@ params:
generic: Secret
required: true
mask: true
+ - name: scope
+ description:
+ generic: Scope
+ advanced: true
+ default: "read"
- name: power
description:
generic: Power
diff --git a/tests/config-device-auth.spec.ts b/tests/config-device-auth.spec.ts
index d5b1b7267..02d824f8c 100644
--- a/tests/config-device-auth.spec.ts
+++ b/tests/config-device-auth.spec.ts
@@ -79,8 +79,7 @@ test.describe("config device auth", async () => {
await expect(successBanner).toContainText("Authorization Successful");
await expect(successBanner).toContainText("Demo Auth is now connected and ready to use");
- // After successful auth, reopen the meter modal to continue configuration
- await page.getByRole("button", { name: "Add grid meter" }).click();
+ // modal reopens automatically after auth; continue configuration
await expectModalVisible(meterModal);
await meterModal.getByLabel("Manufacturer").selectOption("Auth Demo Meter");
@@ -135,6 +134,58 @@ test.describe("config device auth", async () => {
await expect(meterModal.getByRole("button", { name: "Validate & save" })).not.toBeVisible();
});
+ test("redirect auth restores the open modal on return", async ({ page }) => {
+ await page.goto("/#/config");
+
+ // open the grid meter modal and prepare auth
+ await page.getByRole("button", { name: "Add grid meter" }).click();
+ const meterModal = page.getByTestId("meter-modal");
+ await expectModalVisible(meterModal);
+ await meterModal.getByLabel("Manufacturer").selectOption("Auth Demo Meter");
+ await meterModal.getByLabel("Server").fill(simulatorUrl());
+ await meterModal.getByLabel("Redirect URI").fill(getRedirectUri(page.url()));
+ await meterModal.getByLabel("Authentication Method").selectOption("redirect");
+ await meterModal.getByLabel("Secret").fill(secret);
+ await meterModal.getByRole("button", { name: "Prepare connection" }).click();
+
+ // jump to provider in same page
+ const loginLink = meterModal.getByRole("link", { name: "Connect to localhost" });
+ await expect(loginLink).toBeVisible();
+ await loginLink.evaluate((el) => el.removeAttribute("target"));
+ await loginLink.click();
+
+ await page.waitForLoadState("networkidle");
+ const loginButton = page.getByRole("button", { name: "Login Successfully" });
+ await expect(loginButton).toBeVisible();
+ await loginButton.evaluate((btn: HTMLButtonElement) => btn.click());
+
+ // callback restores the modal query alongside the completion marker
+ await page.waitForURL(/.*\/#\/config.*callbackCompleted.*/);
+ expect(page.url()).toContain("meter[type:grid]");
+
+ // modal reopens automatically and success banner is shown
+ await expectModalVisible(meterModal);
+ await expect(page.getByTestId("auth-success-banner")).toBeVisible();
+ });
+
+ test("advanced auth field is hidden until expanded", async ({ page }) => {
+ await page.goto("/#/config");
+
+ await page.getByRole("button", { name: "Add grid meter" }).click();
+ const meterModal = page.getByTestId("meter-modal");
+ await expectModalVisible(meterModal);
+ await meterModal.getByLabel("Manufacturer").selectOption("Auth Demo Meter");
+
+ // normal auth fields are shown, advanced "Scope" is collapsed
+ await expect(meterModal.getByLabel("Server")).toBeVisible();
+ await expect(meterModal.getByLabel("Scope")).not.toBeVisible();
+
+ // expand advanced section manually
+ await meterModal.getByRole("button", { name: "Show advanced settings" }).click();
+ await expect(meterModal.getByLabel("Scope")).toBeVisible();
+ await expect(meterModal.getByLabel("Scope")).toHaveValue("read");
+ });
+
test("create grid meter with device-code auth", async ({ page }) => {
await page.goto("/#/config");
diff --git a/util/homeassistant/config.go b/util/homeassistant/config.go
new file mode 100644
index 000000000..77d7bcb98
--- /dev/null
+++ b/util/homeassistant/config.go
@@ -0,0 +1,17 @@
+package homeassistant
+
+import "github.com/evcc-io/evcc/util"
+
+// Config holds the common Home Assistant instance connection parameters.
+// It is embedded into device configs via mapstructure squash.
+type Config struct {
+ URI string
+ Token_ string `mapstructure:"token"` // TODO deprecated
+ Home_ string `mapstructure:"home"` // TODO deprecated
+ Insecure bool // optional - allow self-signed certificates
+}
+
+// NewConnection creates a Home Assistant connection from the config
+func (c Config) NewConnection(log *util.Logger) (*Connection, error) {
+ return NewConnection(log, c.URI, c.Home_, c.Insecure)
+}
diff --git a/util/homeassistant/connection.go b/util/homeassistant/connection.go
index 4098cf005..3b0973eb7 100644
--- a/util/homeassistant/connection.go
+++ b/util/homeassistant/connection.go
@@ -12,6 +12,7 @@ import (
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
+ "github.com/evcc-io/evcc/util/transport"
"github.com/samber/lo"
"golang.org/x/oauth2"
)
@@ -23,7 +24,7 @@ type Connection struct {
}
// NewConnection creates a new Home Assistant connection
-func NewConnection(log *util.Logger, uri, home string) (*Connection, error) {
+func NewConnection(log *util.Logger, uri, home string, insecure bool) (*Connection, error) {
if home != "" {
log.WARN.Printf("using deprecated 'home' parameter '%s', please use 'uri' instead", home)
}
@@ -35,11 +36,17 @@ func NewConnection(log *util.Logger, uri, home string) (*Connection, error) {
c := &Connection{
Helper: request.NewHelper(log),
instance: &proxyInstance{
- home: home,
- uri: util.DefaultScheme(strings.TrimSuffix(uri, "/"), "http"),
+ home: home,
+ uri: util.DefaultScheme(strings.TrimSuffix(uri, "/"), "http"),
+ insecure: insecure,
},
}
+ // override the transport to accept self-signed certificates
+ if insecure {
+ c.Client.Transport = request.NewTripper(log, transport.Insecure())
+ }
+
// Set up authentication headers
c.Client.Transport = &oauth2.Transport{
Base: c.Client.Transport,
diff --git a/util/homeassistant/instance.go b/util/homeassistant/instance.go
index 6ea15eb8f..c28c0ad87 100644
--- a/util/homeassistant/instance.go
+++ b/util/homeassistant/instance.go
@@ -10,6 +10,7 @@ import (
type proxyInstance struct {
mu sync.Mutex
home, uri string
+ insecure bool
oauth2.TokenSource
}
@@ -42,7 +43,7 @@ func (inst *proxyInstance) Token() (*oauth2.Token, error) {
defer inst.mu.Unlock()
if inst.TokenSource == nil {
- ts, err := NewHomeAssistant(uri)
+ ts, err := NewHomeAssistant(uri, inst.insecure)
if err != nil {
return nil, err
}
diff --git a/util/homeassistant/oauth2.go b/util/homeassistant/oauth2.go
index 241369f53..f021eb493 100644
--- a/util/homeassistant/oauth2.go
+++ b/util/homeassistant/oauth2.go
@@ -4,12 +4,14 @@ import (
"context"
"fmt"
"net"
+ "net/http"
"net/url"
"strings"
"github.com/evcc-io/evcc/plugin/auth"
"github.com/evcc-io/evcc/server/network"
"github.com/evcc-io/evcc/util"
+ "github.com/evcc-io/evcc/util/transport"
"golang.org/x/oauth2"
)
@@ -21,8 +23,9 @@ func init() {
func NewHomeAssistantFromConfig(other map[string]any) (oauth2.TokenSource, error) {
var cc struct {
- URI string
- Home string // TODO remove deprecated
+ URI string
+ Home string // TODO remove deprecated
+ Insecure bool
}
if err := util.DecodeOther(other, &cc); err != nil {
@@ -38,10 +41,10 @@ func NewHomeAssistantFromConfig(other map[string]any) (oauth2.TokenSource, error
}
}
- return NewHomeAssistant(uri)
+ return NewHomeAssistant(uri, cc.Insecure)
}
-func NewHomeAssistant(uri string) (oauth2.TokenSource, error) {
+func NewHomeAssistant(uri string, insecure bool) (oauth2.TokenSource, error) {
uri = strings.TrimRight(uri, "/") // normalize
extUrl := network.Config().ExternalURL()
@@ -76,5 +79,11 @@ func NewHomeAssistant(uri string) (oauth2.TokenSource, error) {
log := util.NewLogger("homeassistant")
ctx := util.WithLogger(context.Background(), log)
+ if insecure {
+ log.WARN.Println("insecure mode enabled - TLS certificate verification is disabled, use only for trusted local/self-signed instances")
+ httpClient := &http.Client{Transport: transport.Insecure()}
+ ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
+ }
+
return auth.NewOAuth(ctx, "HomeAssistant", host, &oc)
}
diff --git a/util/homeassistant/service.go b/util/homeassistant/service.go
index 72f0d4837..c93f879f7 100644
--- a/util/homeassistant/service.go
+++ b/util/homeassistant/service.go
@@ -6,6 +6,7 @@ import (
"maps"
"net/http"
"slices"
+ "strconv"
"strings"
"github.com/evcc-io/evcc/server/service"
@@ -35,7 +36,14 @@ func connectionFromRequest(req *http.Request) (*Connection, error) {
if uri == "" {
return nil, errors.New("missing uri")
}
- return NewConnection(log, uri, "")
+ var insecure bool
+ if s := req.URL.Query().Get("insecure"); s != "" {
+ var err error
+ if insecure, err = strconv.ParseBool(s); err != nil {
+ return nil, err
+ }
+ }
+ return NewConnection(log, uri, "", insecure)
}
// domainsFromRequest parses the comma-separated "domain" query parameter.
diff --git a/util/service/demo.go b/util/service/demo.go
index 7f4fe3dea..a0a593a71 100644
--- a/util/service/demo.go
+++ b/util/service/demo.go
@@ -13,6 +13,7 @@ func init() {
mux.HandleFunc("GET /country", getCountry)
mux.HandleFunc("GET /{country}/city", getCity)
mux.HandleFunc("GET /modbus", getModbus)
+ mux.HandleFunc("GET /insecure", getInsecure)
service.Register("demo", mux)
}
@@ -39,6 +40,10 @@ func getCity(w http.ResponseWriter, req *http.Request) {
json.NewEncoder(w).Encode(cities)
}
+func getInsecure(w http.ResponseWriter, req *http.Request) {
+ json.NewEncoder(w).Encode([]string{"insecure=" + req.URL.Query().Get("insecure")})
+}
+
func getModbus(w http.ResponseWriter, req *http.Request) {
// Verify that either uri or device is provided (mimics modbus connection params)
uri := req.URL.Query().Get("uri")
diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml
index f29ea1599..f89602f06 100644
--- a/util/templates/defaults.yaml
+++ b/util/templates/defaults.yaml
@@ -353,6 +353,12 @@ params:
help:
en: Integrated device. No charging sessions
de: Fest angeschlossenes Gerät. Keine Ladevorgänge
+ - name: insecure
+ type: bool
+ advanced: true
+ description:
+ en: Accept self-signed certificates
+ de: Selbstsignierte Zertifikate akzeptieren
- name: defaultmode
type: int
advanced: true
diff --git a/util/templates/render_testing.go b/util/templates/render_testing.go
index 8f49d4845..e43452a6d 100644
--- a/util/templates/render_testing.go
+++ b/util/templates/render_testing.go
@@ -38,7 +38,8 @@ func test(t *testing.T, tmpl Template, values map[string]any, cb func(values map
cb(values)
}
-func testAuth(other map[string]any) error {
+func testAuth(tmpl Template) error {
+ other := tmpl.Auth
if len(other) == 0 {
return nil
}
@@ -54,7 +55,11 @@ func testAuth(other map[string]any) error {
params := make(map[string]any)
for _, p := range cc.Params {
- params[p] = "foo"
+ if _, param := tmpl.ParamByName(p); param.Type == TypeBool {
+ params[p] = true
+ } else {
+ params[p] = "foo"
+ }
}
_, err := auth.NewFromConfig(context.TODO(), cc.Type, params)
@@ -94,7 +99,7 @@ func TestClass(t *testing.T, class Class, instantiate func(t *testing.T, values
values["host"] = "localhost"
// test auth configuration
- if err := testAuth(tmpl.Auth); err != nil {
+ if err := testAuth(tmpl); err != nil {
t.Error("authorization:", err)
}
diff --git a/vehicle/homeassistant.go b/vehicle/homeassistant.go
index 4c05caf8e..b2946e52f 100644
--- a/vehicle/homeassistant.go
+++ b/vehicle/homeassistant.go
@@ -26,11 +26,9 @@ func init() {
// Constructor from YAML config
func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error) {
var cc struct {
- embed `mapstructure:",squash"`
- URI string
- Token_ string `mapstructure:"token"` // TODO deprecated
- Home_ string `mapstructure:"home"` // TODO deprecated
- Sensors struct {
+ embed `mapstructure:",squash"`
+ homeassistant.Config `mapstructure:",squash"`
+ Sensors struct {
Soc string // required
Range string // optional
Status string // optional
@@ -57,7 +55,7 @@ func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error
log := util.NewLogger("ha-vehicle")
- conn, err := homeassistant.NewConnection(log, cc.URI, cc.Home_)
+ conn, err := cc.Config.NewConnection(log)
if err != nil {
return nil, err
}