Home Assistant: Enable insecure option for self-signed certificates in oauth2 flow (#30648)

Co-authored-by: andig <cpuidle@gmail.com>
Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
Markus 2026-06-18 16:37:04 +02:00 • committed by GitHub
parent d7a294f06e
commit e5b9a014c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 336 additions and 115 deletions

View file

@ -52,7 +52,7 @@
<div v-if="authRequired">
<PropertyEntry
v-for="param in authParams"
v-for="param in authNormalParams"
:id="`${deviceType}Param${param.Name}`"
:key="param.Name"
v-bind="param"
@ -60,6 +60,19 @@
:service-values="serviceValues[param.Name]"
:currency="currency"
/>
<PropertyCollapsible v-if="authAdvancedParams.length">
<template #advanced>
<PropertyEntry
v-for="param in authAdvancedParams"
:id="`${deviceType}Param${param.Name}`"
:key="param.Name"
v-bind="param"
v-model="values[param.Name]"
:service-values="serviceValues[param.Name]"
:currency="currency"
/>
</template>
</PropertyCollapsible>
<div v-if="auth.code">
<hr class="my-5" />
@ -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 ?? [];

View file

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

View file

@ -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<ProviderLoginResponse>(url, {
validateStatus: (code) => [200, 400].includes(code),
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"]);
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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