Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions templates/definition/vehicle/ioBroker.bmw.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
template: ioBroker.bmw
deprecated: true
products:
- description:
generic: ioBroker.bmw
Expand Down
32 changes: 32 additions & 0 deletions templates/definition/vehicle/iobroker.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{{ define "iobroker-get" }}
{{ .name }}:
source: http
uri: {{ .uri }}/rest-api/v1/state/{{ .state }}/plain?extraPlain=true
headers:
- accept: text/plain;charset=UTF-8
cache: {{ .cache }}
{{- if .jq }}
quote: true
jq: {{ .jq }}
{{- end }}
{{- if .user }}
auth:
type: basic
user: {{ .user }}
password: {{ .password }}
{{- end }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The template enables Basic authentication when .user is set, even if .password is absent. This contradicts the template's documented behavior that no password means no authentication and sends requests with an empty password instead of unauthenticated requests.

Triggers: When a configuration supplies user without password.

Suggested fix: Gate the auth blocks on .password (or on both .user and .password) so missing credentials do not enable Basic authentication.

{{- end }}

{{ define "iobroker-set" }}
{{ .name }}:
source: http
uri: {{ .uri }}/rest-api/v1/state/{{ .state }}?value={{ .value }}
headers:
- accept: text/plain;charset=UTF-8
{{- if .user }}
auth:
type: basic
user: {{ .user }}
password: {{ .password }}
{{- end }}
{{- end }}
175 changes: 175 additions & 0 deletions templates/definition/vehicle/iobroker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
template: iobroker
products:
- brand: ioBroker
group: generic
requirements:
evcc: ["skiptest"]
description:
de: |
Auf der verwendeten ioBroker Instanz muss der `rest-api` adapter (nicht `simple-api`) als Extension eines Web-Adapters installiert sein.
Authentifiziert werden kann nur mittels `basic` authentication. Wird kein Passwort angegeben, so wird keine Authentifizierung durchgeführt.
Bei der Angabe der Datenpunkte ist darauf zu achten, dass diese korrekt URL-Encoded sind.
So wird aus `bmw.0.WBA8E9G50GM091234#status` `bmw.0.WBA8E9G50GM091234%23status`
en: |
The `rest-api` adapter (not `simple-api`) must be installed as extension to a web adapter.
Authentication can only be performed using `basic` authentication. If no password is provided, no authentication will be performed.
When specifying the datapoints, ensure that they are correctly URL-encoded.
For example, `bmw.0.WBA8E9G50GM091234#status` becomes `bmw.0.WBA8E9G50GM091234%23status`
params:
- preset: vehicle-common
- name: uri
required: true
description:
en: The full uri of the host to query
Comment thread
andig marked this conversation as resolved.
de: Der vollständige URI der ioBroker Instanz
Comment thread
andig marked this conversation as resolved.
example: https://iobroker.homelab.lan:8082
- name: cache
default: 15m
- name: user
- name: password
- name: soc
description:
de: Ladestand
en: State of charge
unit: "%"
type: string
required: true
example: bmw.0.WBA8E9G50GM091234.status.chargingLevelHv
- name: range
description:
de: Restreichweite
en: Remaining range
unit: km
type: string
- name: status
description:
de: Ladestatus
en: Charging status
type: string
help:
en: Datapoint for charge status (A=disconnected, B=connected, C=charging)
de: Datenpunkt für Ladestatus (A=getrennt, B=verbunden, C=laden)
- name: statusA
description:
de: Werte für Status A
en: Values for status A
example: not_plugged, disconnected
advanced: true
help:
en: Comma-separated list of datapoint values meaning disconnected
de: Komma-getrennte Liste von Datenpunkt-Werten für getrennt
- name: statusB
description:
de: Werte für Status B
en: Values for status B
example: CONNECTED, charging_stopped
advanced: true
help:
en: Comma-separated list of datapoint values meaning connected
de: Komma-getrennte Liste von Datenpunkt-Werten für verbunden
- name: statusC
description:
de: Werte für Status C
en: Values for status C
example: CHARGINGACTIVE, instant_charging
advanced: true
help:
en: Comma-separated list of datapoint values meaning charging
de: Komma-getrennte Liste von Datenpunkt-Werten für laden
- name: limitSoc
description:
de: Ladeziel
en: Charge limit
unit: "%"
type: string
advanced: true
- name: odometer
description:
de: Kilometerstand
en: Odometer
unit: km
type: string
advanced: true
- name: climater
description:
de: Vorklimatisierung
en: Preconditioning
type: string
advanced: true
- name: finishTime
description:
de: Geschätztes Ladeende
en: Estimated finish time
type: string
advanced: true
help:
en: Datapoint holding a timestamp, duration or remaining seconds
de: Datenpunkt mit Zeitstempel, Dauer oder verbleibenden Sekunden
- name: chargeEnable
description:
de: Laden starten/stoppen
en: Start/stop charging
type: string
advanced: true
help:
en: Writable datapoint, requires charging status. Only useful with the [docs.evcc.io](https://docs.evcc.io/en/docs/devices/chargers#vehicle-api-only-charger).
de: Beschreibbarer Datenpunkt, benötigt Ladestatus. Nur sinnvoll mit der [docs.evcc.io](https://docs.evcc.io/docs/devices/chargers#vehicle-api-only-charger).
- name: setMaxCurrent
description:
de: Ladestromstärke setzen
en: Set charging current
unit: A
type: string
advanced: true
help:
en: Writable datapoint, requires charging status
de: Beschreibbarer Datenpunkt, benötigt Ladestatus
- name: wakeup
description:
de: Fahrzeug aufwecken
en: Wake up vehicle
type: string
advanced: true
help:
en: Writable datapoint that wakes up the vehicle
de: Beschreibbarer Datenpunkt zum Aufwecken des Fahrzeugs
- preset: vehicle-features
render: |
type: custom
{{- include "common" . }}
{{- include "iobroker-get" (merge (dict "name" "soc" "state" .soc) .) }}
{{- if .range }}
{{- include "iobroker-get" (merge (dict "name" "range" "state" .range) .) }}
{{- end }}
{{- if .status }}
{{- $a := .statusA | nospace | splitList "," | compact }}
{{- $b := .statusB | nospace | splitList "," | compact }}
{{- $c := .statusC | nospace | splitList "," | compact }}
{{- $jq := "" }}
{{- if or $a $b $c }}
{{- $jq = printf "'. as $s | if %s | index($s) then \"C\" elif %s | index($s) then \"B\" elif %s | index($s) then \"A\" else $s end'" (toJson $c) (toJson $b) (toJson $a) }}
{{- end }}
{{- include "iobroker-get" (merge (dict "name" "status" "state" .status "jq" $jq) .) }}
{{- end }}
{{- if .limitSoc }}
{{- include "iobroker-get" (merge (dict "name" "limitsoc" "state" .limitSoc) .) }}
{{- end }}
{{- if .odometer }}
{{- include "iobroker-get" (merge (dict "name" "odometer" "state" .odometer) .) }}
{{- end }}
{{- if .climater }}
{{- include "iobroker-get" (merge (dict "name" "climater" "state" .climater) .) }}
{{- end }}
{{- if .finishTime }}
{{- include "iobroker-get" (merge (dict "name" "finishtime" "state" .finishTime) .) }}
{{- end }}
{{- if .chargeEnable }}
{{- include "iobroker-set" (merge (dict "name" "chargeenable" "state" .chargeEnable "value" "${chargeenable}") .) }}
{{- end }}
{{- if .setMaxCurrent }}
{{- include "iobroker-set" (merge (dict "name" "maxcurrent" "state" .setMaxCurrent "value" "${maxcurrent}") .) }}
{{- end }}
{{- if .wakeup }}
{{- include "iobroker-set" (merge (dict "name" "wakeup" "state" .wakeup "value" "${wakeup}") .) }}
{{- end }}
{{ include "features" . }}
64 changes: 64 additions & 0 deletions vehicle/iobroker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package vehicle

import (
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/evcc-io/evcc/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestIobrokerTemplate verifies soc reading and the statusA/B/C value mapping
func TestIobrokerTemplate(t *testing.T) {
// datapoint id is the value it reports
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, ok := strings.CutSuffix(strings.TrimPrefix(r.URL.Path, "/rest-api/v1/state/"), "/plain")
if !ok {
http.NotFound(w, r)
return
}
w.Write([]byte(id))
}))
defer srv.Close()

for _, tc := range []struct {
status string
res api.ChargeStatus
err bool
}{
{"CHARGINGACTIVE", api.StatusC, false},
{"plugged", api.StatusB, false},
{"A", api.StatusA, false},
{"unknown", api.StatusNone, true},
} {
t.Run(tc.status, func(t *testing.T) {
v, err := NewFromConfig(t.Context(), "template", map[string]any{
"template": "iobroker",
"uri": srv.URL,
"soc": "42",
"status": tc.status,
"statusB": "CONNECTED, plugged",
"statusC": "CHARGINGACTIVE",
})
require.NoError(t, err)

soc, err := v.Soc()
require.NoError(t, err)
assert.Equal(t, 42.0, soc)

cs, ok := api.Cap[api.ChargeState](v)
require.True(t, ok)

s, err := cs.Status()
if tc.err {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.res, s)
})
}
}
Loading