├── CLAUDE.md ├── .cursorrules ├── tests ├── config-empty.evcc.yaml ├── fast.evcc.yaml ├── fatal-syntax.evcc.yaml ├── fatal-db.evcc.yaml ├── simulator │ ├── src │ │ └── main.ts │ ├── index.html │ └── vite.config.ts ├── password.sql ├── custom-css.css ├── issue.evcc.yaml ├── config-one-lp.evcc.yaml ├── plan-fixed-tariff.evcc.yaml ├── sponsor.evcc.yaml ├── config-circuit.evcc.yaml ├── config-grid-only.evcc.yaml ├── mqtt.ts └── sessions.evcc.yaml ├── templates ├── evcc.io │ └── .gitignore └── definition │ ├── embed.go │ ├── vehicle │ ├── zero.yaml │ ├── porsche.yaml │ ├── aiways.yaml │ ├── nissan.yaml │ ├── audi.yaml │ ├── carwings.yaml │ ├── jaguar-landrover.yaml │ ├── dacia.yaml │ ├── nissan-ariya.yaml │ ├── seat.yaml │ ├── volvo.yaml │ ├── polestar.yaml │ ├── smart-hello.yaml │ ├── skoda-enyaq.yaml │ ├── seat-cupra.yaml │ ├── mg.yaml │ ├── vw.yaml │ ├── ford.yaml │ ├── toyota.yaml │ └── offline.yaml │ ├── charger │ ├── evsewifi.yaml │ ├── pantabox.yaml │ ├── homewizard.yaml │ ├── openwb-pro.yaml │ ├── phoenix-ev-ser.yaml │ ├── porsche-pmcp.yaml │ ├── v2c.yaml │ ├── ocpp-orbis.yaml │ ├── ocpp-beny.yaml │ ├── ocpp-enplus.yaml │ ├── ocpp-sungrow.yaml │ ├── phoenix-em-eth.yaml │ ├── pulsares.yaml │ ├── smartwb.yaml │ ├── ocpp-alfen.yaml │ ├── ocpp-chargeamps.yaml │ ├── mystrom.yaml │ ├── weidmüller.yaml │ ├── eebus.yaml │ ├── tplink.yaml │ ├── ocpp-esolutions.yaml │ ├── porsche-pmcc.yaml │ ├── compleo-solo.yaml │ ├── smartevse.yaml │ ├── em2go-duo.yaml │ ├── sigenergy.yaml │ ├── hesotec.yaml │ ├── dadapower.yaml │ ├── kse.yaml │ ├── evse-din.yaml │ ├── scheider-evlink-v3.yaml │ ├── ocpp-huawei.yaml │ ├── sungrow.yaml │ ├── ocpp-autoaid.yaml │ ├── ocpp-entratek.yaml │ ├── ocpp-elecq.yaml │ ├── compleo-duo.yaml │ ├── amperfied-solar.yaml │ ├── abb.yaml │ ├── eprowallbox.yaml │ ├── versicharge.yaml │ ├── pulsatrix.yaml │ ├── amperfied.yaml │ ├── mennekes-hcc3.yaml │ ├── tinkerforge-warp3-smart.yaml │ ├── innogy-ebox.yaml │ ├── tapo.yaml │ ├── em2go-home.yaml │ ├── obo.yaml │ ├── ocpp-enercab.yaml │ ├── openevse.yaml │ ├── ocpp-evbox-elvi.yaml │ ├── phoenix-charx.yaml │ ├── abl-em4.yaml │ ├── hardybarth-salia.yaml │ ├── ocpp-zaptec.yaml │ ├── pcelectric-garo.yaml │ ├── ocpp-abl.yaml │ ├── ocpp-abb-tac.yaml │ ├── em2go.yaml │ ├── fronius-wattpilot.yaml │ ├── abl.yaml │ ├── etrel-duo.yaml │ ├── etrel.yaml │ ├── nrgkick-bluetooth.yaml │ ├── go-e.yaml │ ├── victron-evcs.yaml │ ├── ocpp-goe.yaml │ ├── nrgkick-connect.yaml │ ├── wallbe.yaml │ ├── victron.yaml │ ├── webasto-next.yaml │ ├── hardybarth-ecb1.yaml │ ├── delta.yaml │ ├── daheimladen-pro.yaml │ ├── wallbe-pre2019.yaml │ ├── daheimladen-mb.yaml │ └── pracht-alpha.yaml │ ├── meter │ ├── tplink.yaml │ ├── mystrom.yaml │ ├── homewizard-p1.yaml │ ├── homewizard-kwh.yaml │ ├── cfos.yaml │ ├── eastron-sdm72.yaml │ ├── tq-em.yaml │ ├── thor.yaml │ ├── tapo.yaml │ ├── sma-energymeter.yaml │ ├── ac-elwa-e.yaml │ ├── dzg.yaml │ ├── hoymiles-opendtu.yaml │ ├── bosch-bpt.yaml │ ├── apsystems-ez1.yaml │ ├── discovergy.yaml │ ├── ac-elwa-2.yaml │ ├── goodwe-wifi.yaml │ ├── eastron-sdm220_230.yaml │ ├── shelly-pro-3em.yaml │ ├── wago-879-30xx.yaml │ ├── fritzgrid.yaml │ ├── tibber-pulse.yaml │ ├── kostal-piko-legacy.yaml │ ├── solaranzeige-mqtt.yaml │ ├── volkszaehler-ws.yaml │ ├── inepro.yaml │ ├── iometer.yaml │ ├── solarmax-inverter-smt.yaml │ └── be-mpm3pm.yaml │ └── tariff │ ├── groupe-e.yaml │ ├── smartenergy.yaml │ ├── awattar.yaml │ ├── energinet.yaml │ ├── elering.yaml │ ├── pun.yaml │ ├── amber.yaml │ ├── allinpower.yaml │ └── gruenstromindex.yaml ├── .prettierignore ├── .github ├── copilot-instructions.md ├── issue_label_bot.yaml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── feature_request.md ├── workflows │ ├── schema.yml │ └── stale.yaml └── FUNDING.yml ├── env.d.ts ├── icon.png ├── assets ├── js │ ├── components │ │ ├── Site │ │ │ └── types.d.ts │ │ ├── Energyflow │ │ │ └── types.d.ts │ │ ├── MultiIcon │ │ │ └── index.ts │ │ ├── VehicleIcon │ │ │ ├── index.ts │ │ │ ├── Desktop.vue │ │ │ ├── Floorlamp.vue │ │ │ └── Climate.vue │ │ ├── Top │ │ │ ├── types.d.ts │ │ │ └── baseapi.ts │ │ ├── Config │ │ │ ├── defaultYaml │ │ │ │ ├── eebus.yaml │ │ │ │ ├── modbusproxy.yaml │ │ │ │ ├── hems.yaml │ │ │ │ ├── sgreadyBoost.yaml │ │ │ │ ├── tariffs.yaml │ │ │ │ └── heatpump.yaml │ │ │ ├── Markdown.vue │ │ │ ├── EebusModal.vue │ │ │ ├── ExperimentalBanner.vue │ │ │ ├── HemsModal.vue │ │ │ └── TariffsModal.vue │ │ ├── MaterialIcon │ │ │ ├── Add.vue │ │ │ ├── Dropdown.vue │ │ │ ├── Total.vue │ │ │ ├── Play.vue │ │ │ ├── DynamicPrice.vue │ │ │ ├── Shm.vue │ │ │ ├── Edit.vue │ │ │ ├── PlanEnd.vue │ │ │ └── PlanStart.vue │ │ ├── Helper │ │ │ └── IconSelectGroup.vue │ │ ├── Savings │ │ │ ├── types.d.ts │ │ │ ├── LiveCommunity.stories.ts │ │ │ └── communityApi.ts │ │ ├── Loadpoints │ │ │ ├── session.ts │ │ │ └── SettingsButton.vue │ │ ├── Forecast │ │ │ └── types.ts │ │ ├── Issue │ │ │ └── types.d.ts │ │ └── Footer │ │ │ └── Footer.vue │ ├── utils │ │ ├── deepClone.ts │ │ ├── deepEqual.ts │ │ ├── log.ts │ │ ├── native.ts │ │ ├── convertRates.ts │ │ └── fatal.ts │ ├── types │ │ ├── shopicons.d.ts │ │ └── vue.d.ts │ ├── featureflags.ts │ ├── experimental.js │ └── restart.ts ├── github │ ├── evcc-gopher.png │ └── screenshot.webp ├── public │ └── meta │ │ ├── favicon.ico │ │ ├── mstile-70x70.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── android-chrome-192x192-maskable.png │ │ ├── android-chrome-512x512-maskable.png │ │ └── browserconfig.xml └── font │ ├── Montserrat-Bold.woff2 │ └── Montserrat-Medium.woff2 ├── api ├── plugin.go ├── store │ └── types.go ├── reason.go ├── actionconfig_test.go ├── batterymode.go ├── feature.go ├── plans.go ├── proto │ ├── auth.proto │ ├── victron.proto │ └── vehicle.proto └── tariff.go ├── vehicle ├── vag │ ├── cariad │ │ └── const.go │ ├── challenge.go │ └── loginapps │ │ ├── token.go │ │ └── token_test.go ├── audi │ └── etron │ │ ├── types.go │ │ └── params.go ├── bluelink │ └── params.go ├── ford │ ├── types.go │ └── autonomic │ │ └── types.go ├── tesla │ ├── helper_test.go │ └── types.go ├── saic │ └── requests │ │ ├── api_config.go │ │ ├── macUtils.go │ │ └── hashUtils.go ├── seat │ ├── cupra │ │ └── params.go │ └── params.go ├── psa │ └── helper.go ├── smart │ └── hello │ │ └── const.go ├── vw │ ├── id │ │ └── params.go │ └── params.go ├── template.go ├── toyota │ └── provider.go ├── bmw │ └── types.go ├── tronity │ └── auth.go └── skoda │ └── params.go ├── charger ├── echarge │ └── types.go ├── warp │ └── const.go ├── nrgble.go ├── config.go ├── template.go ├── connectiq │ └── types.go ├── embed.go ├── config │ └── config.go ├── measurement │ ├── energy.go │ └── heating.go └── ocpp │ └── helper_test.go ├── cmd ├── detect │ ├── tasks │ │ └── const.go │ └── analyze.go ├── password.go ├── cache.go ├── settings.go ├── dump.tpl ├── error_test.go ├── discuss.tpl ├── openapi │ └── openapi.go ├── shutdown │ └── shutdown.go ├── sponsor.go └── demo.go ├── server ├── log.go ├── openapi.go ├── assets │ ├── assets_live.go │ └── assets.go ├── product.go ├── uds_windows.go ├── modbus │ └── readonlymode.go ├── db │ └── settings │ │ └── api.go ├── eebus │ ├── helper.go │ ├── eebus_test.go │ └── types.go ├── openapi_test.go ├── mcp │ └── tools.go └── updater │ └── run.go ├── util ├── templates │ ├── includes │ │ ├── vehicle-language.tpl │ │ ├── eebus.tpl │ │ ├── vehicle-base.tpl │ │ ├── tariff-base.tpl │ │ ├── vehicle-features.tpl │ │ ├── switchsocket.tpl │ │ └── mqtt.tpl │ ├── class.go │ ├── usage.go │ ├── paramtype.go │ ├── proxy.tpl │ └── merge_test.go ├── modbus │ └── mutex.go ├── test │ ├── ci.go │ └── errors.go ├── log_context.go ├── duration.go ├── locale │ ├── internal │ │ └── types.go │ └── locale_test.go ├── metering.go ├── env.go ├── token.go ├── cloud │ └── api.go ├── version.go ├── log_test.go ├── telemetry │ └── types.go ├── format_functions.go ├── transport │ ├── bearer.go │ └── basicauth.go ├── param_test.go ├── encode │ └── encode_test.go ├── logstash │ ├── levels.go │ └── element.go ├── oauth │ └── tokensource_test.go ├── config │ └── types.go └── request │ └── json.go ├── .prettierrc.js ├── core ├── keys │ └── auth.go ├── circuit │ └── config.go ├── loadpoint │ ├── error.go │ └── remote.go ├── planner │ └── sort.go ├── session │ └── format_test.go ├── soc │ └── helper.go ├── site │ └── vehicles.go ├── wrapper │ ├── chargemeter_test.go │ ├── chargemeter.go │ └── chargetimer_test.go ├── settings │ └── settings.go ├── metrics │ └── types.go ├── progress.go ├── progress_test.go └── coordinator │ └── dummy.go ├── i18n ├── .prettierrc └── et.json ├── jest.config.js ├── hems └── eebus │ └── types.go ├── plugin ├── auth │ ├── api.go │ ├── nop.go │ └── config.go ├── method.go ├── golang │ └── stdlib │ │ └── generate.go └── http_limit.go ├── packaging ├── scripts │ └── preremove.sh ├── patch │ └── asn1.diff └── init │ └── evcc.service ├── .vscode └── extensions.json ├── LICENSES ├── fonts.md ├── exclusions.md └── icons.md ├── tariff ├── elering │ └── types.go ├── smartenergy │ └── types.go ├── octopus │ └── graphql │ │ └── errors.go ├── proxy_error.go ├── energinet │ └── types.go ├── template.go └── types.go ├── .dockerignore ├── vitest.config.js ├── .storybook └── main.js ├── meter ├── bosch │ └── types.go ├── usage_pv.go ├── goodwe │ └── types.go ├── config.go ├── template.go ├── shelly │ └── types.go ├── tplink.go ├── obis │ └── obis.go ├── discovergy │ └── types.go ├── config │ └── config.go ├── measurement │ └── energy.go └── mystrom.go ├── .editorconfig └── tsconfig.json /CLAUDE.md: -------------------------------------------------------------------------------- 1 | AGENTS.md -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | AGENTS.md -------------------------------------------------------------------------------- /tests/config-empty.evcc.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/evcc.io/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tests/custom-css.css -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | ../AGENTS.md -------------------------------------------------------------------------------- /tests/fast.evcc.yaml: -------------------------------------------------------------------------------- 1 | interval: 0.1s 2 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tests/fatal-syntax.evcc.yaml: -------------------------------------------------------------------------------- 1 | s!ite: 2 | title: Hello World 3 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/icon.png -------------------------------------------------------------------------------- /assets/js/components/Site/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Grid { 2 | power?: number; 3 | } 4 | -------------------------------------------------------------------------------- /api/plugin.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Custom meter/charger/vehicle type 4 | const Custom = "custom" 5 | -------------------------------------------------------------------------------- /vehicle/vag/cariad/const.go: -------------------------------------------------------------------------------- 1 | package cariad 2 | 3 | const BaseURL = "https://emea.bff.cariad.digital" 4 | -------------------------------------------------------------------------------- /assets/github/evcc-gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/github/evcc-gopher.png -------------------------------------------------------------------------------- /assets/github/screenshot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/github/screenshot.webp -------------------------------------------------------------------------------- /assets/js/components/Energyflow/types.d.ts: -------------------------------------------------------------------------------- 1 | interface Pv { 2 | title?: string; 3 | power: number; 4 | } 5 | -------------------------------------------------------------------------------- /assets/public/meta/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/favicon.ico -------------------------------------------------------------------------------- /charger/echarge/types.go: -------------------------------------------------------------------------------- 1 | package echarge 2 | 3 | const ( 4 | ModeEco = "eco" 5 | ModeManual = "manual" 6 | ) 7 | -------------------------------------------------------------------------------- /cmd/detect/tasks/const.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import "time" 4 | 5 | const timeout = 200 * time.Millisecond 6 | -------------------------------------------------------------------------------- /vehicle/audi/etron/types.go: -------------------------------------------------------------------------------- 1 | package etron 2 | 3 | type Vehicle struct { 4 | VIN, Type, Nickname string 5 | } 6 | -------------------------------------------------------------------------------- /assets/font/Montserrat-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/font/Montserrat-Bold.woff2 -------------------------------------------------------------------------------- /assets/font/Montserrat-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/font/Montserrat-Medium.woff2 -------------------------------------------------------------------------------- /assets/js/components/MultiIcon/index.ts: -------------------------------------------------------------------------------- 1 | import MultiIcon from "./MultiIcon.vue"; 2 | 3 | export default MultiIcon; 4 | -------------------------------------------------------------------------------- /assets/public/meta/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/mstile-70x70.png -------------------------------------------------------------------------------- /server/log.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/evcc-io/evcc/util" 4 | 5 | var log = util.NewLogger("server") 6 | -------------------------------------------------------------------------------- /.github/issue_label_bot.yaml: -------------------------------------------------------------------------------- 1 | label-alias: 2 | bug: "bug" 3 | feature_request: "enhancement" 4 | question: "question" 5 | -------------------------------------------------------------------------------- /assets/js/components/VehicleIcon/index.ts: -------------------------------------------------------------------------------- 1 | import VehicleIcon from "./VehicleIcon.vue"; 2 | 3 | export default VehicleIcon; 4 | -------------------------------------------------------------------------------- /assets/js/utils/deepClone.ts: -------------------------------------------------------------------------------- 1 | export default function (obj: T): T { 2 | return JSON.parse(JSON.stringify(obj)); 3 | } 4 | -------------------------------------------------------------------------------- /assets/public/meta/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/favicon-16x16.png -------------------------------------------------------------------------------- /assets/public/meta/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/favicon-32x32.png -------------------------------------------------------------------------------- /assets/public/meta/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/mstile-144x144.png -------------------------------------------------------------------------------- /assets/public/meta/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/mstile-150x150.png -------------------------------------------------------------------------------- /assets/public/meta/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/mstile-310x150.png -------------------------------------------------------------------------------- /assets/public/meta/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/mstile-310x310.png -------------------------------------------------------------------------------- /tests/fatal-db.evcc.yaml: -------------------------------------------------------------------------------- 1 | site: 2 | title: Hello World 3 | 4 | database: 5 | type: sqliteInvalid 6 | dsn: /path/to/db 7 | -------------------------------------------------------------------------------- /util/templates/includes/vehicle-language.tpl: -------------------------------------------------------------------------------- 1 | {{ define "vehicle-language" }} 2 | language: {{ .language }} 3 | {{- end }} 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | printWidth: 100, 3 | trailingComma: "es5", 4 | plugins: ["prettier-plugin-sh"], 5 | }; 6 | -------------------------------------------------------------------------------- /assets/js/types/shopicons.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@h2d2/shopicons/es/regular/*"; 2 | declare module "@h2d2/shopicons/es/filled/*"; 3 | -------------------------------------------------------------------------------- /assets/public/meta/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/apple-touch-icon.png -------------------------------------------------------------------------------- /core/keys/auth.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | const ( 4 | AdminPassword = "adminPassword" 5 | JwtSecret = "jwtSecretKey" 6 | ) 7 | -------------------------------------------------------------------------------- /i18n/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "prettier-plugin-sort-json" 4 | ], 5 | "jsonRecursiveSort": true 6 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: "@vue/cli-plugin-unit-jest/presets/no-babel", 3 | testMatch: ["**/*.spec.js"], 4 | }; 5 | -------------------------------------------------------------------------------- /assets/public/meta/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/public/meta/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/android-chrome-512x512.png -------------------------------------------------------------------------------- /charger/warp/const.go: -------------------------------------------------------------------------------- 1 | package warp 2 | 3 | import "time" 4 | 5 | const ( 6 | RootTopic = "warp" 7 | Timeout = 30 * time.Second 8 | ) 9 | -------------------------------------------------------------------------------- /util/templates/includes/eebus.tpl: -------------------------------------------------------------------------------- 1 | {{ define "eebus" }} 2 | type: eebus 3 | ski: {{ .ski }} 4 | {{ if .ip }}ip: {{ .ip }}{{ end }} 5 | {{- end}} 6 | -------------------------------------------------------------------------------- /assets/js/utils/deepEqual.ts: -------------------------------------------------------------------------------- 1 | export default function (obj1: any, obj2: any): boolean { 2 | return JSON.stringify(obj1) === JSON.stringify(obj2); 3 | } 4 | -------------------------------------------------------------------------------- /assets/public/meta/android-chrome-192x192-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/android-chrome-192x192-maskable.png -------------------------------------------------------------------------------- /assets/public/meta/android-chrome-512x512-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/evcc/master/assets/public/meta/android-chrome-512x512-maskable.png -------------------------------------------------------------------------------- /i18n/et.json: -------------------------------------------------------------------------------- 1 | { 2 | "batterySettings": { 3 | "batteryLevel": "Akutase", 4 | "capacity": "{energy} / {total}", 5 | "control": "Akuhaldus" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /hems/eebus/types.go: -------------------------------------------------------------------------------- 1 | package eebus 2 | 3 | type status int 4 | 5 | const ( 6 | StatusUnlimited status = iota 7 | StatusLimited 8 | StatusFailsafe 9 | ) 10 | -------------------------------------------------------------------------------- /assets/js/components/Top/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Provider { 2 | title: string; 3 | authenticated: boolean; 4 | loginPath: string; 5 | logoutPath: string; 6 | } 7 | -------------------------------------------------------------------------------- /server/openapi.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | //go:generate go tool openapi openapi.yaml mcp/openapi.json 4 | //go:generate go tool openapi-mcp --doc mcp/openapi.md openapi.yaml 5 | -------------------------------------------------------------------------------- /plugin/auth/api.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Authorizer interface { 8 | Transport(base http.RoundTripper) http.RoundTripper 9 | } 10 | -------------------------------------------------------------------------------- /templates/definition/embed.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import "embed" 4 | 5 | //go:embed charger/*.yaml meter/*.yaml vehicle/*.yaml tariff/*.yaml 6 | var YamlTemplates embed.FS 7 | -------------------------------------------------------------------------------- /vehicle/bluelink/params.go: -------------------------------------------------------------------------------- 1 | package bluelink 2 | 3 | const ( 4 | KiaAppID = "a2b8469b-30a3-4361-8e13-6fceea8fbe74" 5 | HyundaiAppID = "014d2225-8495-4735-812d-2616334fd15d" 6 | ) 7 | -------------------------------------------------------------------------------- /vehicle/ford/types.go: -------------------------------------------------------------------------------- 1 | package ford 2 | 3 | type VehiclesResponse struct { 4 | UserVehicles struct { 5 | VehicleDetails []struct { 6 | VIN string 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packaging/scripts/preremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -d /run/systemd/system ] && [ "$1" = remove ]; then 5 | deb-systemd-invoke stop evcc.service > /dev/null || true 6 | fi 7 | -------------------------------------------------------------------------------- /util/modbus/mutex.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import "sync" 4 | 5 | var mu2 sync.Mutex 6 | 7 | func Lock() { 8 | mu2.Lock() 9 | } 10 | 11 | func Unlock() { 12 | mu2.Unlock() 13 | } 14 | -------------------------------------------------------------------------------- /plugin/method.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | //go:generate go tool enumer -type Method -text 4 | type Method int 5 | 6 | const ( 7 | _ Method = iota 8 | Energy 9 | Power 10 | Soc 11 | ) 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | labels: 8 | - "infrastructure" 9 | -------------------------------------------------------------------------------- /server/assets/assets_live.go: -------------------------------------------------------------------------------- 1 | //go:build !release 2 | 3 | package assets 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func init() { 10 | Web = os.DirFS("dist") 11 | I18n = os.DirFS("i18n") 12 | } 13 | -------------------------------------------------------------------------------- /templates/definition/vehicle/zero.yaml: -------------------------------------------------------------------------------- 1 | template: zero 2 | products: 3 | - brand: Zero Motorcycles 4 | params: 5 | - preset: vehicle-base 6 | render: | 7 | type: zero 8 | {{ include "vehicle-base" . }} 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "vitest.explorer", 5 | "golang.go", 6 | "esbenp.prettier-vscode", 7 | "yoavbls.pretty-ts-errors", 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /assets/js/utils/log.ts: -------------------------------------------------------------------------------- 1 | export const LOG_LEVELS = ["fatal", "error", "warn", "info", "debug", "trace"] as const; 2 | export const DEFAULT_LOG_LEVEL = "debug"; 3 | 4 | export type LogLevel = (typeof LOG_LEVELS)[number]; 5 | -------------------------------------------------------------------------------- /LICENSES/fonts.md: -------------------------------------------------------------------------------- 1 | # Font Licenses 2 | 3 | ## Montserrat Font 4 | 5 | - **Source**: https://github.com/JulietaUla/Montserrat 6 | - **License**: SIL Open Font License 1.1 7 | - **Files**: /assets/font/Montserrat-\*.woff2 8 | -------------------------------------------------------------------------------- /vehicle/tesla/helper_test.go: -------------------------------------------------------------------------------- 1 | package tesla 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestApiError(t *testing.T) { 10 | assert.Nil(t, apiError(nil)) 11 | } 12 | -------------------------------------------------------------------------------- /server/product.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | type product struct { 4 | Name string `json:"name"` 5 | Template string `json:"template"` 6 | Group string `json:"group,omitempty"` 7 | } 8 | 9 | type products []product 10 | -------------------------------------------------------------------------------- /templates/definition/charger/evsewifi.yaml: -------------------------------------------------------------------------------- 1 | template: evsewifi 2 | products: 3 | - description: 4 | generic: EVSE-WiFi 5 | params: 6 | - name: host 7 | render: | 8 | type: evsewifi 9 | uri: http://{{ .host }} 10 | -------------------------------------------------------------------------------- /templates/definition/vehicle/porsche.yaml: -------------------------------------------------------------------------------- 1 | template: porsche 2 | deprecated: true 3 | products: 4 | - brand: Porsche 5 | params: 6 | - preset: vehicle-base 7 | render: | 8 | type: porsche 9 | {{ include "vehicle-base" . }} 10 | -------------------------------------------------------------------------------- /vehicle/saic/requests/api_config.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | const ( 4 | CONTENT_ENCRYPTED = "1" 5 | PARAM_AUTHENTICATION = "Basic c3dvcmQ6c3dvcmRfc2VjcmV0" 6 | TENANT_ID = "459771" 7 | USER_TYPE = "app" 8 | ) 9 | -------------------------------------------------------------------------------- /tests/simulator/src/main.ts: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.min.css"; 2 | import "../../../assets/css/app.css"; 3 | import { createApp } from "vue"; 4 | import Simulator from "./Simulator.vue"; 5 | 6 | createApp(Simulator).mount("#app"); 7 | -------------------------------------------------------------------------------- /util/test/ci.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func SkipCI(t *testing.T) { 9 | t.Helper() 10 | 11 | if os.Getenv("CI") != "" { 12 | t.Skip("Skipping testing in CI environment") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/uds_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package server 4 | 5 | import "github.com/evcc-io/evcc/core/site" 6 | 7 | // HealthListener attaches listener to unix domain socket 8 | func HealthListener(_ site.API) { 9 | // nop 10 | } 11 | -------------------------------------------------------------------------------- /templates/definition/charger/pantabox.yaml: -------------------------------------------------------------------------------- 1 | template: pantabox 2 | products: 3 | - brand: INRO 4 | description: 5 | generic: Pantabox 6 | params: 7 | - name: host 8 | render: | 9 | type: pantabox 10 | uri: http://{{ .host }} 11 | -------------------------------------------------------------------------------- /util/log_context.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | var CtxLogger = struct{}{} 8 | 9 | func WithLogger(ctx context.Context, log *Logger) context.Context { 10 | return context.WithValue(ctx, CtxLogger, log) 11 | } 12 | -------------------------------------------------------------------------------- /api/store/types.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | // Provider creates a Persister for given string key 4 | type Provider func(string) Store 5 | 6 | // Store can load and store data 7 | type Store interface { 8 | Load(any) error 9 | Save(any) error 10 | } 11 | -------------------------------------------------------------------------------- /templates/definition/vehicle/aiways.yaml: -------------------------------------------------------------------------------- 1 | template: aiways 2 | products: 3 | - brand: Aiways 4 | params: 5 | - preset: vehicle-base 6 | - name: vin 7 | required: true 8 | render: | 9 | type: aiways 10 | {{ include "vehicle-base" . }} 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Need help? 4 | url: https://github.com/evcc-io/evcc/discussions/categories/need-help 5 | about: GitHub community discussions is a good place to ask questions. 6 | -------------------------------------------------------------------------------- /api/reason.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type Reason int 4 | 5 | //go:generate go tool enumer -type Reason -trimprefix Reason -transform=lower 6 | const ( 7 | ReasonUnknown Reason = iota 8 | ReasonWaitingForAuthorization 9 | ReasonDisconnectRequired 10 | ) 11 | -------------------------------------------------------------------------------- /templates/definition/vehicle/nissan.yaml: -------------------------------------------------------------------------------- 1 | template: nissan 2 | products: 3 | - brand: Nissan 4 | description: 5 | generic: Leaf 6 | params: 7 | - preset: vehicle-base 8 | render: | 9 | type: nissan 10 | {{ include "vehicle-base" . }} 11 | -------------------------------------------------------------------------------- /util/templates/class.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | type Class int 4 | 5 | //go:generate go tool enumer -type Class -transform=lower 6 | const ( 7 | _ Class = iota 8 | Charger 9 | Meter 10 | Vehicle 11 | Tariff 12 | Loadpoint 13 | Circuit 14 | ) 15 | -------------------------------------------------------------------------------- /util/templates/includes/vehicle-base.tpl: -------------------------------------------------------------------------------- 1 | {{ define "vehicle-base" }} 2 | user: {{ .user }} 3 | password: {{ .password }} 4 | vin: {{ .vin }} 5 | {{ template "vehicle-common" . }} 6 | {{- if .cache }} 7 | cache: {{ .cache }} 8 | {{- end }} 9 | {{- end }} 10 | -------------------------------------------------------------------------------- /templates/definition/charger/homewizard.yaml: -------------------------------------------------------------------------------- 1 | template: homewizard 2 | products: 3 | - brand: HomeWizard 4 | group: switchsockets 5 | params: 6 | - name: host 7 | - preset: switchsocket 8 | render: | 9 | type: homewizard 10 | uri: http://{{ .host }} 11 | -------------------------------------------------------------------------------- /cmd/password.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var passwordCmd = &cobra.Command{ 8 | Use: "password", 9 | Short: "Password administration", 10 | } 11 | 12 | func init() { 13 | rootCmd.AddCommand(passwordCmd) 14 | } 15 | -------------------------------------------------------------------------------- /tariff/elering/types.go: -------------------------------------------------------------------------------- 1 | package elering 2 | 3 | const URI = "https://dashboard.elering.ee/api" 4 | 5 | type NpsPrice struct { 6 | Success bool 7 | Data map[string][]Price 8 | } 9 | 10 | type Price struct { 11 | Timestamp int64 12 | Price float64 13 | } 14 | -------------------------------------------------------------------------------- /templates/definition/vehicle/audi.yaml: -------------------------------------------------------------------------------- 1 | template: audi 2 | covers: ["etron"] 3 | products: 4 | - brand: Audi 5 | params: 6 | - preset: vehicle-base 7 | - name: vin 8 | example: WAUZZZ... 9 | render: | 10 | type: etron 11 | {{ include "vehicle-base" . }} 12 | -------------------------------------------------------------------------------- /templates/definition/vehicle/carwings.yaml: -------------------------------------------------------------------------------- 1 | template: carwings 2 | products: 3 | - brand: Nissan 4 | description: 5 | generic: Leaf (pre 2019) 6 | params: 7 | - preset: vehicle-base 8 | render: | 9 | type: carwings 10 | {{ include "vehicle-base" . }} 11 | -------------------------------------------------------------------------------- /util/templates/includes/tariff-base.tpl: -------------------------------------------------------------------------------- 1 | {{ define "tariff-base" }} 2 | {{- if .charges }} 3 | charges: {{ .charges }} 4 | {{- end }} 5 | {{- if .tax }} 6 | tax: {{ .tax }} 7 | {{- end }} 8 | {{- if .formula }} 9 | formula: {{ .formula }} 10 | {{- end }} 11 | {{- end }} 12 | -------------------------------------------------------------------------------- /util/templates/usage.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | type Usage int 4 | 5 | //go:generate go tool enumer -type Usage -trimprefix Usage -transform=lower -text 6 | const ( 7 | UsageGrid Usage = iota 8 | UsagePV 9 | UsageBattery 10 | UsageCharge 11 | UsageAux 12 | ) 13 | -------------------------------------------------------------------------------- /api/actionconfig_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestActionConfigString(t *testing.T) { 10 | var a ActionConfig 11 | assert.NotPanics(t, func() { 12 | _ = a.String() 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /server/modbus/readonlymode.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | // go:generate go tool enumer -type ReadOnlyMode -trimprefix ReadOnly -transform=lower 4 | 5 | type ReadOnlyMode int 6 | 7 | const ( 8 | ReadOnlyFalse ReadOnlyMode = iota 9 | ReadOnlyDeny 10 | ReadOnlyTrue 11 | ) 12 | -------------------------------------------------------------------------------- /tariff/smartenergy/types.go: -------------------------------------------------------------------------------- 1 | package smartenergy 2 | 3 | import "time" 4 | 5 | const URI = "https://apis.smartenergy.at/market/v1/price" 6 | 7 | type Prices struct { 8 | Data []Price 9 | } 10 | 11 | type Price struct { 12 | Date time.Time 13 | Value float64 14 | } 15 | -------------------------------------------------------------------------------- /templates/definition/vehicle/jaguar-landrover.yaml: -------------------------------------------------------------------------------- 1 | template: jaguar-landrover 2 | deprecated: true 3 | products: 4 | - brand: Jaguar 5 | - brand: Land Rover 6 | params: 7 | - preset: vehicle-base 8 | render: | 9 | type: jaguar 10 | {{ include "vehicle-base" . }} 11 | -------------------------------------------------------------------------------- /templates/definition/vehicle/dacia.yaml: -------------------------------------------------------------------------------- 1 | template: dacia 2 | products: 3 | - brand: Dacia 4 | params: 5 | - preset: vehicle-base 6 | - preset: vehicle-features 7 | render: | 8 | type: dacia 9 | {{ include "vehicle-base" . }} 10 | {{ include "vehicle-features" . }} 11 | -------------------------------------------------------------------------------- /tests/password.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `settings` ( 2 | `key` text 3 | , `value` text 4 | , PRIMARY KEY(`key`) 5 | ); 6 | 7 | -- password: secret 8 | INSERT INTO settings("key", value) VALUES('adminPassword', '$2a$10$HNLoqiTO5oLwopczA/wcPOebfO79S.hnAA5HOkx5p6o3g5a2E30v2'); 9 | -------------------------------------------------------------------------------- /templates/definition/vehicle/nissan-ariya.yaml: -------------------------------------------------------------------------------- 1 | template: nissan-ariya 2 | products: 3 | - brand: Nissan 4 | description: 5 | generic: Ariya 6 | params: 7 | - preset: vehicle-base 8 | render: | 9 | type: nissan 10 | version: v2 11 | {{ include "vehicle-base" . }} 12 | -------------------------------------------------------------------------------- /templates/definition/vehicle/seat.yaml: -------------------------------------------------------------------------------- 1 | template: seat 2 | products: 3 | - brand: Seat 4 | description: 5 | generic: CupraConnect Gen3 (Ateca, Leon, Formentor, Tarraco) 6 | params: 7 | - preset: vehicle-base 8 | render: | 9 | type: seat 10 | {{ include "vehicle-base" . }} 11 | -------------------------------------------------------------------------------- /templates/definition/vehicle/volvo.yaml: -------------------------------------------------------------------------------- 1 | template: volvo 2 | deprecated: true 3 | products: 4 | - brand: Volvo 5 | description: 6 | de: veraltet 7 | en: legacy 8 | params: 9 | - preset: vehicle-base 10 | render: | 11 | type: volvo 12 | {{ include "vehicle-base" . }} 13 | -------------------------------------------------------------------------------- /util/duration.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | func ParseDuration(s string) (time.Duration, error) { 9 | v, err := strconv.Atoi(s) 10 | if err != nil { 11 | return 0, err 12 | } 13 | 14 | return time.Duration(v) * time.Second, nil 15 | } 16 | -------------------------------------------------------------------------------- /util/locale/internal/types.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // ContextKey is just an empty struct. It exists so context values can be 4 | // an immutable public variable with a unique type. It's immutable 5 | // because nobody else can create a ContextKey, being unexported. 6 | type ContextKey struct{} 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .storybook 3 | .vscode 4 | *.conf 5 | *.Dockerfile 6 | *.gz 7 | *.image 8 | *.json 9 | *.sh 10 | *.yaml 11 | Dockerfile 12 | evcc 13 | evcc.exe 14 | builddir 15 | node_modules 16 | release 17 | !entrypoint.sh 18 | !evcc.dist.yaml 19 | !package*.json 20 | !vue.config.js 21 | -------------------------------------------------------------------------------- /templates/definition/charger/openwb-pro.yaml: -------------------------------------------------------------------------------- 1 | template: openwb-pro 2 | products: 3 | - brand: openWB 4 | description: 5 | generic: Pro 6 | capabilities: ["1p3p", "mA", "iso151182"] 7 | params: 8 | - name: host 9 | render: | 10 | type: openwbpro 11 | uri: http://{{ .host }} 12 | -------------------------------------------------------------------------------- /cmd/cache.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // cacheCmd represents the cache command 8 | var cacheCmd = &cobra.Command{ 9 | Use: "cache", 10 | Short: "Manage cache entries", 11 | } 12 | 13 | func init() { 14 | rootCmd.AddCommand(cacheCmd) 15 | } 16 | -------------------------------------------------------------------------------- /server/db/settings/api.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | //go:generate go tool mockgen -package settings -destination mock.go -mock_names API=MockAPI github.com/evcc-io/evcc/server/db/settings API 4 | 5 | type API interface { 6 | String(key string) (string, error) 7 | SetString(key string, value string) 8 | } 9 | -------------------------------------------------------------------------------- /templates/definition/charger/phoenix-ev-ser.yaml: -------------------------------------------------------------------------------- 1 | template: phoenix-ev-ser 2 | products: 3 | - brand: Phoenix Contact 4 | description: 5 | generic: EV-SER 6 | params: 7 | - name: modbus 8 | choice: ["rs485"] 9 | render: | 10 | type: phoenix-ev-ser 11 | {{- include "modbus" . }} 12 | -------------------------------------------------------------------------------- /templates/definition/charger/porsche-pmcp.yaml: -------------------------------------------------------------------------------- 1 | template: pmcp 2 | products: 3 | - brand: Porsche 4 | description: 5 | generic: Mobile Charger Plus 6 | requirements: 7 | evcc: ["eebus"] 8 | params: 9 | - preset: eebus 10 | render: | 11 | {{ include "eebus" . }} 12 | meter: true 13 | -------------------------------------------------------------------------------- /templates/definition/charger/v2c.yaml: -------------------------------------------------------------------------------- 1 | template: v2c 2 | covers: ["trydan"] 3 | products: 4 | - brand: V2C 5 | description: 6 | generic: Trydan 7 | requirements: 8 | evcc: ["sponsorship"] 9 | params: 10 | - name: host 11 | render: | 12 | type: trydan 13 | uri: http://{{ .host }} 14 | -------------------------------------------------------------------------------- /tests/custom-css.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --evcc-dark-green: rebeccapurple; 3 | --evcc-yellow: hotpink; 4 | } 5 | 6 | [data-testid="header"], 7 | [data-testid="footer"] { 8 | display: none !important; 9 | } 10 | 11 | /* test against markup injection */ 12 | 21 | -------------------------------------------------------------------------------- /assets/js/components/Savings/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface LiveCommunityData { 2 | totalClients: number; 3 | activeClients: number; 4 | totalInstances: number; 5 | activeInstances: number; 6 | chargePower: number; 7 | greenPower: number; 8 | maxChargePower: number; 9 | maxGreenPower: number; 10 | chargeEnergy: number; 11 | greenEnergy: number; 12 | } 13 | export type Period = "30d" | "365d" | "thisYear" | "total"; 14 | -------------------------------------------------------------------------------- /core/session/format_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "golang.org/x/text/language" 8 | "golang.org/x/text/message" 9 | ) 10 | 11 | func TestFormatValue(t *testing.T) { 12 | mp := message.NewPrinter(language.Make("en")) 13 | 14 | f := 1.2345 15 | assert.Equal(t, "1.234", formatValue(mp, f, 3)) 16 | assert.Equal(t, "1.234", formatValue(mp, &f, 3)) 17 | } 18 | -------------------------------------------------------------------------------- /core/soc/helper.go: -------------------------------------------------------------------------------- 1 | package soc 2 | 3 | import "fmt" 4 | 5 | // Guard checks soc value for validity 6 | func Guard(soc float64, err error) (float64, error) { 7 | switch { 8 | case err != nil: 9 | return soc, err 10 | 11 | case soc < 0: 12 | return 0, fmt.Errorf("invalid soc: %.1f", soc) 13 | 14 | case soc > 100: 15 | return 100, fmt.Errorf("invalid soc: %.1f", soc) 16 | 17 | default: 18 | return soc, nil 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /templates/definition/charger/amperfied-solar.yaml: -------------------------------------------------------------------------------- 1 | template: amperfied-solar 2 | products: 3 | - brand: Amperfied 4 | description: 5 | generic: Wallbox connect.solar 6 | capabilities: ["mA", "rfid", "1p3p"] 7 | requirements: 8 | evcc: ["sponsorship"] 9 | params: 10 | - name: modbus 11 | choice: ["tcpip"] 12 | id: 255 13 | render: | 14 | type: amperfied 15 | {{- include "modbus" . }} 16 | phases1p3p: true 17 | -------------------------------------------------------------------------------- /templates/definition/meter/thor.yaml: -------------------------------------------------------------------------------- 1 | template: thor 2 | products: 3 | - brand: my-PV 4 | description: 5 | generic: AC•THOR 6 | params: 7 | - name: usage 8 | choice: ["aux"] 9 | - name: host 10 | render: | 11 | type: custom 12 | power: 13 | source: http 14 | uri: http://{{ .host }}/data.jsn 15 | jq: if .power_act == null then 0 else .power_act end + if .power_ac9 == null then 0 else .power_ac9 end 16 | -------------------------------------------------------------------------------- /util/test/errors.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Acceptable checks if a test error is in the list of acceptable errors 8 | func Acceptable(err error, acceptable []string) bool { 9 | for _, msg := range acceptable { 10 | err := strings.TrimSpace(err.Error()) 11 | if strings.HasPrefix(err, msg) || strings.HasSuffix(err, msg) { 12 | return true 13 | } 14 | } 15 | 16 | return false 17 | } 18 | -------------------------------------------------------------------------------- /util/transport/bearer.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // BearerAuth creates an HTTP transport performing HTTP authorization using an OAuth 2.0 Bearer Token 8 | func BearerAuth(token string, base http.RoundTripper) http.RoundTripper { 9 | return &Decorator{ 10 | Decorator: DecorateHeaders(map[string]string{ 11 | "Authorization": "Bearer " + token, 12 | }), 13 | Base: base, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /templates/definition/charger/abb.yaml: -------------------------------------------------------------------------------- 1 | template: abb 2 | products: 3 | - brand: ABB 4 | description: 5 | generic: Terra AC 6 | capabilities: ["mA"] 7 | requirements: 8 | description: 9 | de: Erfordert Firmware >= 1.6.5 10 | en: Requires firmware >= 1.6.5 11 | evcc: ["sponsorship"] 12 | params: 13 | - name: modbus 14 | choice: ["rs485", "tcpip"] 15 | render: | 16 | type: abb 17 | {{- include "modbus" . }} 18 | -------------------------------------------------------------------------------- /assets/js/featureflags.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import settings from "./settings"; 3 | 4 | export function setHiddenFeatures(value: boolean) { 5 | settings.hiddenFeatures = value; 6 | } 7 | 8 | export function getHiddenFeatures() { 9 | return settings.hiddenFeatures; 10 | } 11 | 12 | export default { 13 | install: (app: App) => { 14 | app.config.globalProperties.$hiddenFeatures = getHiddenFeatures; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /templates/definition/charger/eprowallbox.yaml: -------------------------------------------------------------------------------- 1 | template: eprowallbox 2 | products: 3 | - brand: Free2Move 4 | description: 5 | generic: eProWallbox 6 | - brand: Free2Move 7 | description: 8 | generic: eProWallbox Move 9 | capabilities: ["mA"] 10 | requirements: 11 | evcc: ["sponsorship"] 12 | params: 13 | - name: modbus 14 | choice: ["rs485"] 15 | render: | 16 | type: eprowallbox 17 | {{- include "modbus" . }} 18 | -------------------------------------------------------------------------------- /vehicle/tesla/types.go: -------------------------------------------------------------------------------- 1 | package tesla 2 | 3 | import ( 4 | tesla "github.com/evcc-io/tesla-proxy-client" 5 | ) 6 | 7 | type ( 8 | Vehicle = tesla.Vehicle 9 | VehicleData = tesla.VehicleData 10 | CommandResponse = tesla.CommandResponse 11 | ) 12 | 13 | type RegionResponse struct { 14 | Response Region 15 | } 16 | 17 | type Region struct { 18 | Region string 19 | FleetApiBaseUrl string `json:"fleet_api_base_url"` 20 | } 21 | -------------------------------------------------------------------------------- /api/feature.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type Feature int 4 | 5 | //go:generate go tool enumer -type Feature -text 6 | const ( 7 | _ Feature = iota 8 | CoarseCurrent // charger 9 | IntegratedDevice // charger 10 | Heating // charger 11 | Cacheable // tariff 12 | Offline // vehicle 13 | Retryable // vehicle 14 | WelcomeCharge // vehicle 15 | ) 16 | -------------------------------------------------------------------------------- /core/site/vehicles.go: -------------------------------------------------------------------------------- 1 | package site 2 | 3 | import ( 4 | "github.com/evcc-io/evcc/api" 5 | "github.com/evcc-io/evcc/core/vehicle" 6 | ) 7 | 8 | type Vehicles interface { 9 | // Settings returns the list of vehicle adapters 10 | Settings() []vehicle.API 11 | 12 | // ByName returns a single vehicle adapter by name 13 | ByName(string) (vehicle.API, error) 14 | 15 | // All returns the list of vehicle instances 16 | Instances() []api.Vehicle 17 | } 18 | -------------------------------------------------------------------------------- /templates/definition/charger/versicharge.yaml: -------------------------------------------------------------------------------- 1 | template: versicharge 2 | products: 3 | - brand: Siemens 4 | description: 5 | generic: Versicharge GEN3 6 | requirements: 7 | evcc: ["sponsorship"] 8 | description: 9 | de: Erfordert Firmware >= 2.135 10 | en: Requires firmware >= 2.135 11 | params: 12 | - name: modbus 13 | choice: ["tcpip"] 14 | id: 2 15 | render: | 16 | type: versicharge 17 | {{- include "modbus" . }} 18 | -------------------------------------------------------------------------------- /templates/definition/charger/pulsatrix.yaml: -------------------------------------------------------------------------------- 1 | template: pulsatrix 2 | products: 3 | - brand: Pulsatrix 4 | requirements: 5 | evcc: ["sponsorship"] 6 | params: 7 | - name: host #IP address or hostname (can be found on 3rd page of SECC display) 8 | required: true 9 | help: 10 | en: Shown on 3rd page of SECC display 11 | de: Wird auf der dritten Displayseite des SECC angezeigt 12 | render: | 13 | type: pulsatrix 14 | host: {{ .host }} 15 | -------------------------------------------------------------------------------- /util/param_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParam(t *testing.T) { 10 | lp := 2 11 | p := Param{ 12 | Key: "power", 13 | Val: 4711, 14 | } 15 | assert.Equal(t, "power", p.UniqueID()) 16 | 17 | p.Loadpoint = &lp 18 | assert.Equal(t, "2.power", p.UniqueID()) 19 | } 20 | 21 | func TestParamCache(t *testing.T) { 22 | NewParamCache().Add("foo", Param{}) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/error_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestError(t *testing.T) { 11 | res := &ClassError{ 12 | ClassMeter, 13 | &DeviceError{ 14 | "0815", 15 | errors.New("foo"), 16 | }, 17 | } 18 | 19 | b, err := res.MarshalJSON() 20 | require.NoError(t, err) 21 | require.Equal(t, `{"class":"meter","device":"0815","error":"[0815] foo"}`, string(b)) 22 | } 23 | -------------------------------------------------------------------------------- /templates/definition/meter/tapo.yaml: -------------------------------------------------------------------------------- 1 | template: tapo 2 | products: 3 | - brand: TP-Link 4 | description: 5 | generic: Tapo P-Series Smart Plug 6 | group: switchsockets 7 | params: 8 | - name: usage 9 | choice: ["pv"] 10 | - name: host 11 | - name: user 12 | required: true 13 | - name: password 14 | required: true 15 | render: | 16 | type: tapo 17 | uri: http://{{ .host }} 18 | user: {{ .user }} 19 | password: {{ .password }} 20 | -------------------------------------------------------------------------------- /templates/definition/vehicle/mg.yaml: -------------------------------------------------------------------------------- 1 | template: mg 2 | products: 3 | - brand: MG 4 | params: 5 | - preset: vehicle-base 6 | - name: vin 7 | required: true 8 | - name: region 9 | description: 10 | de: Region 11 | en: Region 12 | type: choice 13 | choice: ["EU", "AU"] 14 | default: EU 15 | required: true 16 | advanced: true 17 | render: | 18 | type: mg 19 | {{ include "vehicle-base" . }} 20 | region: {{ .region }} 21 | -------------------------------------------------------------------------------- /tests/config-one-lp.evcc.yaml: -------------------------------------------------------------------------------- 1 | site: 2 | title: Hello World 3 | 4 | loadpoints: 5 | - title: Carport 6 | charger: charger 7 | 8 | chargers: 9 | - name: charger 10 | type: custom 11 | enable: 12 | source: js 13 | script: 14 | enabled: 15 | source: js 16 | script: | 17 | false 18 | status: 19 | source: js 20 | script: | 21 | "B" 22 | maxcurrent: 23 | source: js 24 | script: 25 | -------------------------------------------------------------------------------- /templates/definition/meter/sma-energymeter.yaml: -------------------------------------------------------------------------------- 1 | template: sma-energy-meter 2 | products: 3 | - brand: SMA 4 | description: 5 | generic: Energy Meter 6 | params: 7 | - name: usage 8 | choice: ["grid", "pv"] 9 | - name: host 10 | - name: interface 11 | render: | 12 | type: sma 13 | uri: {{ .host }} 14 | {{- if .interface }} 15 | interface: {{ .interface }} 16 | {{- end }} 17 | {{- if eq .usage "pv" }} 18 | scale: -1 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /templates/definition/tariff/energinet.yaml: -------------------------------------------------------------------------------- 1 | template: energinet 2 | deprecated: true 3 | products: 4 | - brand: Energinet 5 | requirements: 6 | evcc: ["skiptest"] 7 | group: price 8 | countries: ["DK"] 9 | params: 10 | - name: region 11 | example: dk1 12 | type: choice 13 | choice: ["dk1", "dk2"] 14 | required: true 15 | - preset: tariff-base 16 | render: | 17 | type: energinet 18 | region: {{ .region }} 19 | {{ include "tariff-base" . }} 20 | -------------------------------------------------------------------------------- /assets/js/components/Config/defaultYaml/hems.yaml: -------------------------------------------------------------------------------- 1 | ## external control via binary in put 2 | 3 | #type: relay 4 | #maxPower: 4200 # limit loadpoints to 4.2 kW total 5 | #limit: # limit signal, plugin 6 | # source: mqtt 7 | # topic: hems/limit/status # 0/false = normal, 1/true = limit active 8 | 9 | ## external control via EEBus protocol 10 | 11 | #type: eebus # general EEBus setup (cert gen) required 12 | #ski: "1234-5678-90AB-CDEF" # SKI of control box (grid operator) 13 | -------------------------------------------------------------------------------- /cmd/discuss.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ if .CfgError -}} 6 | Fehlermeldung: 7 | 8 | ``` 9 | {{ .CfgError }} 10 | ``` 11 | 12 | {{ end -}} 13 | 14 | {{ if .CfgContent -}} 15 |
Konfiguration{{ if .CfgFile }} ({{ .CfgFile }}){{ end }} 16 | 17 | ```yaml 18 | {{ .CfgContent }} 19 | ``` 20 | 21 |
22 | {{ end -}} 23 | 24 | {{ if .Version -}} 25 | Version: `{{ .Version }}` 26 | {{ end -}} 27 | -------------------------------------------------------------------------------- /LICENSES/icons.md: -------------------------------------------------------------------------------- 1 | # Icon Licenses 2 | 3 | ## Material Symbols 4 | 5 | - **Source**: https://github.com/google/material-design-icons 6 | - **License**: Apache License 2.0 7 | - **Files**: /assets/js/components/MaterialIcon/\*.vue 8 | - **Notes**: Repackaged as Vue components, some modified/adapted versions 9 | 10 | ## H2D2 Shopicons 11 | 12 | - **Source**: https://github.com/H2D2-Design/h2d2-shopicons 13 | - **License**: Apache License 2.0 14 | - **Usage**: Regular dependency 15 | -------------------------------------------------------------------------------- /api/plans.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type RepeatingPlanStruct struct { 4 | Weekdays []int `json:"weekdays"` // 0-6 (Sunday-Saturday) 5 | Time string `json:"time"` // HH:MM 6 | Tz string `json:"tz"` // timezone in IANA format 7 | Soc int `json:"soc"` // target soc 8 | Precondition int64 `json:"precondition"` // precondition duration in seconds 9 | Active bool `json:"active"` // active flag 10 | } 11 | -------------------------------------------------------------------------------- /templates/definition/charger/amperfied.yaml: -------------------------------------------------------------------------------- 1 | template: amperfied 2 | products: 3 | - brand: Amperfied 4 | description: 5 | generic: Wallbox connect.home 6 | - brand: Amperfied 7 | description: 8 | generic: Wallbox connect.business 9 | capabilities: ["mA", "rfid"] 10 | requirements: 11 | evcc: ["sponsorship"] 12 | params: 13 | - name: modbus 14 | choice: ["tcpip"] 15 | id: 255 16 | render: | 17 | type: amperfied 18 | {{- include "modbus" . }} 19 | -------------------------------------------------------------------------------- /templates/definition/charger/mennekes-hcc3.yaml: -------------------------------------------------------------------------------- 1 | template: mennekes-hcc3 2 | covers: ["amtron", "menneckes-hcc3"] 3 | products: 4 | - brand: Mennekes 5 | description: 6 | generic: AMTRON Xtra 7 | - brand: Mennekes 8 | description: 9 | generic: AMTRON Premium 10 | requirements: 11 | evcc: ["sponsorship"] 12 | params: 13 | - name: modbus 14 | choice: ["tcpip"] 15 | id: 255 16 | render: | 17 | type: mennekes-hcc3 18 | {{- include "modbus" . }} 19 | -------------------------------------------------------------------------------- /templates/definition/charger/tinkerforge-warp3-smart.yaml: -------------------------------------------------------------------------------- 1 | template: tinkerforge-warp3-smart 2 | products: 3 | - brand: TinkerForge 4 | description: 5 | generic: WARP3 Charger Smart 6 | capabilities: ["mA", "1p3p", "rfid"] 7 | requirements: 8 | evcc: ["skiptest"] 9 | params: 10 | - preset: mqtt 11 | - name: topic 12 | default: warp 13 | render: | 14 | type: warp2 15 | {{ include "mqtt" . }} 16 | topic: {{ .topic }} 17 | energymanager: {{ .topic }} 18 | -------------------------------------------------------------------------------- /api/proto/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // protoc proto/auth.proto --go_out=. --go-grpc_out=. 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | option go_package = "proto/pb"; 8 | 9 | service Auth { 10 | rpc IsAuthorized (AuthRequest) returns (AuthReply) {} 11 | } 12 | 13 | message AuthRequest { 14 | string token = 1; 15 | } 16 | 17 | message AuthReply { 18 | bool authorized = 1; 19 | string subject = 2; 20 | google.protobuf.Timestamp expires_at = 3; 21 | } 22 | -------------------------------------------------------------------------------- /api/proto/victron.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // protoc proto/victron.proto --go_out=. --go-grpc_out=. 4 | 5 | option go_package = "proto/pb"; 6 | 7 | service Victron { 8 | rpc IsValidDevice (VictronRequest) returns (VictronReply) {} 9 | } 10 | 11 | message VictronRequest { 12 | string productId = 1; 13 | string vrmId = 2; 14 | string serial = 3; 15 | string board = 4; 16 | } 17 | 18 | message VictronReply { 19 | bool authorized = 1; 20 | string subject = 2; 21 | } 22 | -------------------------------------------------------------------------------- /assets/js/utils/convertRates.ts: -------------------------------------------------------------------------------- 1 | import type { Rate } from "../types/evcc"; 2 | import type { ForecastSlot } from "../components/Forecast/types"; 3 | 4 | function convertRate(slot: ForecastSlot): Rate { 5 | return { 6 | start: new Date(slot.start), 7 | end: new Date(slot.end), 8 | value: slot.value, 9 | }; 10 | } 11 | 12 | export default function convertRates(slots: ForecastSlot[] | null): Rate[] { 13 | if (!slots) return []; 14 | return slots.map(convertRate); 15 | } 16 | -------------------------------------------------------------------------------- /charger/nrgble.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package charger 4 | 5 | import ( 6 | "errors" 7 | 8 | "github.com/evcc-io/evcc/api" 9 | ) 10 | 11 | func init() { 12 | registry.Add("nrgkick-bluetooth", NewNRGKickBLEFromConfig) 13 | } 14 | 15 | // NewNRGKickBLEFromConfig creates a NRGKickBLE charger from generic config 16 | func NewNRGKickBLEFromConfig(other map[string]interface{}) (api.Charger, error) { 17 | return nil, errors.New("NRGKick bluetooth is only supported on linux") 18 | } 19 | -------------------------------------------------------------------------------- /templates/definition/charger/innogy-ebox.yaml: -------------------------------------------------------------------------------- 1 | template: innogy-ebox 2 | products: 3 | - brand: Innogy 4 | description: 5 | generic: eBox 6 | - brand: E.ON Drive 7 | description: 8 | generic: eBox 9 | - brand: Compleo 10 | description: 11 | generic: eBox 12 | capabilities: ["mA"] 13 | requirements: 14 | evcc: ["sponsorship"] 15 | params: 16 | - name: modbus 17 | choice: ["tcpip"] 18 | render: | 19 | type: innogy 20 | {{- include "modbus" . }} 21 | -------------------------------------------------------------------------------- /templates/definition/charger/tapo.yaml: -------------------------------------------------------------------------------- 1 | template: tapo 2 | products: 3 | - brand: TP-Link 4 | description: 5 | generic: Tapo P-Series Smart Plug 6 | group: switchsockets 7 | params: 8 | - name: host 9 | - name: user 10 | required: true 11 | - name: password 12 | required: true 13 | - preset: switchsocket 14 | render: | 15 | type: tapo 16 | uri: http://{{ .host }} 17 | user: {{ .user }} 18 | password: {{ .password }} 19 | {{ include "switchsocket" . }} 20 | -------------------------------------------------------------------------------- /templates/definition/meter/ac-elwa-e.yaml: -------------------------------------------------------------------------------- 1 | template: ac-elwa-e 2 | covers: ["elwa-e"] 3 | products: 4 | - brand: my-PV 5 | description: 6 | generic: AC ELWA-E 7 | params: 8 | - name: usage 9 | choice: ["aux"] 10 | - name: host 11 | render: | 12 | type: custom 13 | power: 14 | source: http 15 | uri: http://{{ .host }}/data.jsn 16 | jq: .power 17 | soc: 18 | source: http 19 | uri: http://{{ .host }}/data.jsn 20 | jq: .temp1 21 | scale: 0.1 22 | -------------------------------------------------------------------------------- /assets/js/components/Config/defaultYaml/sgreadyBoost.yaml: -------------------------------------------------------------------------------- 1 | ## required attributes [type: sgready-boost] 2 | 3 | charger: # configuration of a switch controlling the sg-ready port 4 | type: template 5 | template: shelly # example shelly 6 | host: 192.168.0.101 # 7 | 8 | ## optional attributes (read-only) 9 | 10 | #temp: # current temperature (°C) 11 | # source: const 12 | # value: 42 13 | #limittemp: # temperature limit (°C) configured in device 14 | # source: const 15 | # value: 84 16 | -------------------------------------------------------------------------------- /core/wrapper/chargemeter_test.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.uber.org/mock/gomock" 7 | ) 8 | 9 | func TestProxyChargeMeter(t *testing.T) { 10 | ctrl := gomock.NewController(t) 11 | defer ctrl.Finish() 12 | 13 | tc := []float64{600, 1000, 2000} 14 | m := ChargeMeter{} 15 | 16 | for _, f := range tc { 17 | m.SetPower(f) 18 | 19 | if p, err := m.CurrentPower(); p != f || err != nil { 20 | t.Errorf("power: %.1f %v", p, err) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /templates/definition/charger/em2go-home.yaml: -------------------------------------------------------------------------------- 1 | template: em2go-home 2 | products: 3 | - brand: EM2GO 4 | description: 5 | generic: Home 6 | capabilities: ["1p3p", "mA"] 7 | requirements: 8 | description: 9 | de: "Benötigt FW version >= E3C_V1.1. mA Regelung benötigt FW version >= E3C_V1.3." 10 | en: "Requires FW Version >= E3C_V1.1. mA regulation requires FW version >= E3C_V1.3." 11 | params: 12 | - name: host 13 | render: | 14 | type: em2go-home 15 | uri: {{ .host }} 16 | -------------------------------------------------------------------------------- /templates/definition/tariff/elering.yaml: -------------------------------------------------------------------------------- 1 | template: elering 2 | deprecated: true 3 | products: 4 | - brand: Nordpool 5 | description: 6 | generic: "Elering" 7 | group: price 8 | countries: ["EE", "LT", "LV", "FI"] 9 | params: 10 | - name: region 11 | example: ee 12 | type: choice 13 | choice: ["ee", "lt", "lv", "fi"] 14 | required: true 15 | - preset: tariff-base 16 | render: | 17 | type: elering 18 | region: {{ .region }} 19 | {{ include "tariff-base" . }} 20 | -------------------------------------------------------------------------------- /vehicle/vw/id/params.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/evcc-io/evcc/vehicle/vag/cariad" 7 | ) 8 | 9 | const LoginURL = cariad.BaseURL + "/user-login/v1/authorize" 10 | 11 | var AuthParams = url.Values{ 12 | "response_type": {"code id_token token"}, 13 | "client_id": {"a24fba63-34b3-4d43-b181-942111e6bda8@apps_vw-dilab_com"}, 14 | "redirect_uri": {"weconnect://authenticated"}, 15 | "scope": {"openid profile badge cars vin"}, // dealers 16 | } 17 | -------------------------------------------------------------------------------- /util/encode/encode_test.go: -------------------------------------------------------------------------------- 1 | package encode 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestEncode(t *testing.T) { 12 | enc := NewEncoder(WithDuration()) 13 | assert.Equal(t, nil, enc.Encode(time.Time{})) 14 | assert.Equal(t, nil, enc.Encode(math.NaN())) 15 | assert.Equal(t, nil, enc.Encode(math.Inf(0))) 16 | assert.Equal(t, 30, enc.Encode(30*time.Second)) 17 | assert.Equal(t, 3.142, enc.Encode(math.Pi)) 18 | } 19 | -------------------------------------------------------------------------------- /assets/js/components/Loadpoints/session.ts: -------------------------------------------------------------------------------- 1 | import settings from "@/settings"; 2 | import type { SessionInfoKey } from "@/types/evcc"; 3 | 4 | export function getSessionInfo(index: number): SessionInfoKey | undefined { 5 | return settings.sessionInfo[index - 1]; 6 | } 7 | 8 | export function setSessionInfo(index: number, value: SessionInfoKey) { 9 | const clone = [...settings.sessionInfo]; 10 | clone[index - 1] = value; 11 | clone.map((v) => v || ""); 12 | settings.sessionInfo = clone; 13 | } 14 | -------------------------------------------------------------------------------- /templates/definition/charger/obo.yaml: -------------------------------------------------------------------------------- 1 | template: obo 2 | products: 3 | - brand: EcoHarmony 4 | description: 5 | generic: EVSE EPC 2.0 Plus 6 | - brand: OBO Bettermann 7 | description: 8 | generic: Ion 9 | - brand: Viridian EV 10 | description: 11 | generic: EVSE EPC 2.0 Plus 12 | params: 13 | - name: modbus 14 | choice: ["rs485", "tcpip"] 15 | baudrate: 19200 16 | comset: 8E1 17 | id: 101 18 | render: | 19 | type: obo 20 | {{- include "modbus" . }} 21 | -------------------------------------------------------------------------------- /templates/definition/charger/ocpp-enercab.yaml: -------------------------------------------------------------------------------- 1 | template: ocpp-enercab 2 | products: 3 | - brand: enercab 4 | description: 5 | generic: smart 6 | - brand: eledio 7 | description: 8 | generic: go 9 | capabilities: ["1p3p"] 10 | requirements: 11 | description: 12 | generic: | 13 | https://www.enercab.at/index.php?controller=attachment&id_attachment=311 14 | evcc: ["sponsorship", "skiptest"] 15 | params: 16 | - preset: ocpp 17 | render: | 18 | {{ include "ocpp" . }} 19 | -------------------------------------------------------------------------------- /templates/definition/charger/openevse.yaml: -------------------------------------------------------------------------------- 1 | template: openevse 2 | products: 3 | - brand: OpenEVSE 4 | requirements: 5 | description: 6 | en: Requires firmware 7.0 or later. 7 | de: Benötigt mindestens Firmware 7.0 oder neuer. 8 | params: 9 | - name: host 10 | - name: user 11 | required: false 12 | - name: password 13 | required: false 14 | mask: true 15 | render: | 16 | type: openevse 17 | uri: http://{{ .host }} 18 | user: {{ .user }} 19 | password: {{ .password }} 20 | -------------------------------------------------------------------------------- /server/mcp/tools.go: -------------------------------------------------------------------------------- 1 | package mcp 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/modelcontextprotocol/go-sdk/mcp" 7 | ) 8 | 9 | func docsTool(_ context.Context, _ *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { 10 | return &mcp.CallToolResult{ 11 | Content: []mcp.Content{ 12 | &mcp.ResourceLink{ 13 | URI: "https://docs.evcc.io", 14 | Name: "evcc-docs", 15 | Title: "evcc documentation", 16 | MIMEType: "text/html", 17 | }, 18 | }, 19 | }, nil, nil 20 | } 21 | -------------------------------------------------------------------------------- /templates/definition/charger/ocpp-evbox-elvi.yaml: -------------------------------------------------------------------------------- 1 | template: ocpp-evbox-elvi 2 | covers: ["elvi"] 3 | products: 4 | - brand: EVBox 5 | description: 6 | generic: Elvi 7 | requirements: 8 | evcc: ["sponsorship", "skiptest"] 9 | params: 10 | - preset: ocpp 11 | - name: meter 12 | type: bool 13 | default: true 14 | - name: meterinterval 15 | deprecated: true 16 | render: | 17 | {{ include "ocpp" . }} 18 | {{- if eq .meter "false" }} 19 | metervalues: Current.Offered 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /templates/definition/charger/phoenix-charx.yaml: -------------------------------------------------------------------------------- 1 | template: phoenix-charx 2 | products: 3 | - brand: Phoenix Contact 4 | description: 5 | generic: CHARX 6 | - brand: LadeFoxx 7 | description: 8 | generic: EvLoad 9 | - brand: LadeFoxx 10 | description: 11 | generic: Mikro 2.0 12 | params: 13 | - name: modbus 14 | choice: ["tcpip"] 15 | id: 255 16 | - name: connector 17 | render: | 18 | type: phoenix-charx 19 | {{- include "modbus" . }} 20 | connector: {{ .connector }} 21 | -------------------------------------------------------------------------------- /templates/definition/tariff/pun.yaml: -------------------------------------------------------------------------------- 1 | template: pun 2 | products: 3 | - brand: PUN Orario 4 | requirements: 5 | evcc: ["skiptest"] 6 | description: 7 | de: "Preisdaten von https://www.mercatoelettrico.org/it/. Wird oft zur Einspeisung ins Netz verwendet." 8 | en: "Price data from https://www.mercatoelettrico.org/it/. Often used for feeding into the grid." 9 | group: price 10 | countries: ["IT"] 11 | params: 12 | - preset: tariff-base 13 | render: | 14 | type: pun 15 | {{ include "tariff-base" . }} 16 | -------------------------------------------------------------------------------- /tariff/energinet/types.go: -------------------------------------------------------------------------------- 1 | package energinet 2 | 3 | const ( 4 | URI = "https://api.energidataservice.dk/dataset/Elspotprices?offset=0&start=%s&end=%s&filter={\"PriceArea\":[\"%s\"]}&timezone=dk&limit=48" 5 | TimeFormat = "2006-01-02T15:04" // RFC3339 short 6 | ) 7 | 8 | type Prices struct { 9 | Records []PriceInfo `json:"records"` 10 | } 11 | 12 | type PriceInfo struct { 13 | HourUTC string 14 | HourDK string 15 | PriceArea string 16 | SpotPriceDKK float64 17 | SpotPriceEUR float64 18 | } 19 | -------------------------------------------------------------------------------- /templates/definition/charger/abl-em4.yaml: -------------------------------------------------------------------------------- 1 | template: abl-em4 2 | products: 3 | - brand: ABL 4 | description: 5 | generic: eM4 Single (SBCx) 6 | - brand: ABL 7 | description: 8 | generic: eM4 Twin (SBCx) 9 | capabilities: ["mA"] 10 | requirements: 11 | evcc: ["sponsorship"] 12 | params: 13 | - name: modbus 14 | choice: ["tcpip"] 15 | id: 255 16 | - name: connector 17 | default: 1 18 | render: | 19 | type: abl-em4 20 | {{- include "modbus" . }} 21 | connector: {{ .connector }} 22 | -------------------------------------------------------------------------------- /templates/definition/meter/dzg.yaml: -------------------------------------------------------------------------------- 1 | template: dzg 2 | products: 3 | - brand: DZG 4 | description: 5 | generic: DVH4013 6 | params: 7 | - name: usage 8 | choice: ["charge"] 9 | - name: modbus 10 | choice: ["rs485"] 11 | render: | 12 | type: mbmd 13 | {{- include "modbus" . }} 14 | model: dzg 15 | power: ImportPower 16 | energy: Import 17 | currents: 18 | - CurrentL1 19 | - CurrentL2 20 | - CurrentL3 21 | voltages: 22 | - VoltageL1 23 | - VoltageL2 24 | - VoltageL3 25 | -------------------------------------------------------------------------------- /util/templates/includes/mqtt.tpl: -------------------------------------------------------------------------------- 1 | {{ define "mqtt" }} 2 | broker: {{ .host }}:{{ .port }} 3 | {{- if .user }} 4 | user: {{ .user }} 5 | {{- end }} 6 | {{- if .password }} 7 | password: {{ .password }} 8 | {{- end }} 9 | {{- if ne .timeout "30s" }} 10 | timeout: {{ .timeout }} 11 | {{- end }} 12 | {{- if .caCert }} 13 | caCert: {{ .caCert }} 14 | {{- end }} 15 | {{- if .clientCert }} 16 | clientCert: {{ .clientCert }} 17 | {{- end }} 18 | {{- if .clientKey }} 19 | clientKey: {{ .clientKey }} 20 | {{- end }} 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /meter/config.go: -------------------------------------------------------------------------------- 1 | package meter 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/evcc-io/evcc/api" 7 | "github.com/evcc-io/evcc/meter/config" 8 | ) 9 | 10 | var registry = config.Registry 11 | 12 | // Types returns the list of types 13 | func Types() []string { 14 | return registry.Types() 15 | } 16 | 17 | // NewFromConfig creates meter from configuration 18 | func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}) (api.Meter, error) { 19 | return config.NewFromConfig(ctx, typ, other) 20 | } 21 | -------------------------------------------------------------------------------- /templates/definition/charger/hardybarth-salia.yaml: -------------------------------------------------------------------------------- 1 | template: hardybarth-salia 2 | products: 3 | - brand: Hardy Barth 4 | description: 5 | generic: cPH2 6 | - brand: Hardy Barth 7 | description: 8 | generic: cPμ2 9 | - brand: echarge 10 | description: 11 | generic: cPH2 12 | - brand: echarge 13 | description: 14 | generic: cPμ2 15 | requirements: 16 | evcc: ["sponsorship"] 17 | params: 18 | - name: host 19 | render: | 20 | type: hardybarth-salia 21 | uri: http://{{ .host }} 22 | -------------------------------------------------------------------------------- /templates/definition/charger/ocpp-zaptec.yaml: -------------------------------------------------------------------------------- 1 | template: ocpp-zaptec 2 | products: 3 | - brand: Zaptec 4 | description: 5 | generic: Go (OCPP) 6 | capabilities: ["rfid"] 7 | requirements: 8 | description: 9 | generic: | 10 | OCPP Native mode 11 | 12 | https://help.zaptec.com/hc/en-001/articles/22330328601489-Zaptec-Go-OCPP-Native-configuration-guide#h_01HP261F5NP6Z9VY0MVHJCZEBJ 13 | evcc: ["sponsorship", "skiptest"] 14 | params: 15 | - preset: ocpp 16 | render: | 17 | {{ include "ocpp" . }} 18 | -------------------------------------------------------------------------------- /templates/definition/meter/hoymiles-opendtu.yaml: -------------------------------------------------------------------------------- 1 | template: hoymiles-opendtu 2 | products: 3 | - brand: Hoymiles 4 | description: 5 | generic: HM & HMS Series (via OpenDTU) 6 | params: 7 | - name: usage 8 | choice: ["pv"] 9 | - name: host 10 | render: | 11 | type: custom 12 | power: 13 | source: http 14 | uri: http://{{ .host }}/api/livedata/status 15 | jq: .total.Power.v 16 | energy: 17 | source: http 18 | uri: http://{{ .host }}/api/livedata/status 19 | jq: .total.YieldTotal.v 20 | -------------------------------------------------------------------------------- /charger/config.go: -------------------------------------------------------------------------------- 1 | package charger 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/evcc-io/evcc/api" 7 | "github.com/evcc-io/evcc/charger/config" 8 | ) 9 | 10 | var registry = config.Registry 11 | 12 | // Types returns the list of types 13 | func Types() []string { 14 | return registry.Types() 15 | } 16 | 17 | // NewFromConfig creates charger from configuration 18 | func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}) (api.Charger, error) { 19 | return config.NewFromConfig(ctx, typ, other) 20 | } 21 | -------------------------------------------------------------------------------- /templates/definition/charger/pcelectric-garo.yaml: -------------------------------------------------------------------------------- 1 | template: pcelectric-garo 2 | products: 3 | - brand: PC Electric 4 | description: 5 | generic: Garo 6 | requirements: 7 | evcc: ["sponsorship"] 8 | description: 9 | de: Es können momentan nur als Master konfigurierte Geräte verwendet werden! 10 | en: Only devices configured as master can be used right now! 11 | params: 12 | - name: host 13 | - name: port 14 | default: 8080 15 | render: | 16 | type: garo 17 | uri: http://{{ .host }}:{{ .port }}/servlet 18 | -------------------------------------------------------------------------------- /.github/workflows/schema.yml: -------------------------------------------------------------------------------- 1 | name: Validate Schema 2 | 3 | on: 4 | push: 5 | paths: 6 | - "*.yaml" 7 | - "*.json" 8 | pull_request: 9 | paths: 10 | - "*.yaml" 11 | - "*.json" 12 | 13 | jobs: 14 | build: 15 | runs-on: depot-ubuntu-24.04-arm 16 | 17 | steps: 18 | - uses: actions/checkout@v5 19 | - uses: nwisbeta/validate-yaml-schema@v2.0.0 20 | with: 21 | yamlSchemasJson: | 22 | { 23 | "./schema.json": ["evcc.dist.yaml"] 24 | } 25 | -------------------------------------------------------------------------------- /templates/definition/charger/ocpp-abl.yaml: -------------------------------------------------------------------------------- 1 | template: ocpp-abl 2 | products: 3 | - brand: ABL 4 | description: 5 | generic: eMH2 (OCPP) 6 | - brand: ABL 7 | description: 8 | generic: eMH3 (OCPP) 9 | - brand: ABL 10 | description: 11 | generic: eM4 Single (OCPP) 12 | - brand: ABL 13 | description: 14 | generic: eM4 Twin (OCPP) 15 | capabilities: ["mA", "rfid"] 16 | requirements: 17 | evcc: ["sponsorship", "skiptest"] 18 | params: 19 | - preset: ocpp 20 | render: | 21 | {{ include "ocpp" . }} 22 | -------------------------------------------------------------------------------- /templates/definition/meter/bosch-bpt.yaml: -------------------------------------------------------------------------------- 1 | template: bosch-bpt 2 | # UDP implementation is broken 3 | # deprecated: true 4 | products: 5 | - brand: Bosch 6 | description: 7 | generic: BPT-S 5 Hybrid 8 | requirements: 9 | evcc: ["skiptest"] 10 | params: 11 | - name: usage 12 | choice: ["grid", "pv", "battery"] 13 | allinone: true 14 | - name: uri 15 | - name: capacity 16 | advanced: true 17 | render: | 18 | type: bosch-bpt 19 | usage: {{ .usage }} 20 | uri: {{ .uri }} 21 | capacity: {{ .capacity }} # kWh 22 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: "Stale issue handler" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | issue_comment: 8 | types: [created] 9 | 10 | jobs: 11 | stale: 12 | runs-on: depot-ubuntu-24.04-arm 13 | steps: 14 | - uses: actions/stale@v9 15 | id: stale 16 | with: 17 | days-before-stale: 7 18 | days-before-close: 5 19 | exempt-issue-labels: "pinned,security,backlog,bug" 20 | exempt-pr-labels: "pinned,security,backlog,bug" 21 | -------------------------------------------------------------------------------- /assets/js/components/Config/defaultYaml/tariffs.yaml: -------------------------------------------------------------------------------- 1 | #currency: EUR 2 | 3 | #grid: # price using energy from the grid 4 | # type: fixed 5 | # price: 0.294 # EUR/kWh 6 | 7 | #feedin: # price for feeding solar energy to the grid 8 | # type: fixed 9 | # price: 0.08 # EUR/kWh 10 | 11 | #co2: # carbon intensity forecast 12 | # type: template 13 | # template: grünstromindex 14 | # zip: 15 | 16 | #solar: # list of pv generation forecast (additive) 17 | #- type: template 18 | # template: solcast 19 | # site: 20 | # token: 21 | -------------------------------------------------------------------------------- /packaging/init/evcc.service: -------------------------------------------------------------------------------- 1 | # evcc.service 2 | # 3 | 4 | [Unit] 5 | Description=evcc 6 | Requires=network-online.target 7 | After=syslog.target network.target network-online.target 8 | Wants=network-online.target 9 | StartLimitIntervalSec=10 10 | StartLimitBurst=10 11 | 12 | [Service] 13 | AmbientCapabilities=CAP_NET_BIND_SERVICE 14 | ExecStart=/usr/bin/evcc 15 | Environment="EVCC_DATABASE_DSN=/var/lib/evcc/evcc.db" 16 | Restart=always 17 | RestartSec=10 18 | 19 | User=evcc 20 | Group=evcc 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /server/eebus/eebus_test.go: -------------------------------------------------------------------------------- 1 | package eebus 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "go.yaml.in/yaml/v4" 8 | ) 9 | 10 | func TestConfig(t *testing.T) { 11 | conf := ` 12 | certificate: 13 | private: | 14 | -----BEGIN EC PRIVATE KEY----- 15 | MHcCfoo== 16 | -----END EC PRIVATE KEY----- 17 | public: | 18 | -----BEGIN CERTIFICATE----- 19 | MIIBbar= 20 | -----END CERTIFICATE----- 21 | ` 22 | 23 | var res Config 24 | require.NoError(t, yaml.Unmarshal([]byte(conf), &res)) 25 | } 26 | -------------------------------------------------------------------------------- /templates/definition/meter/apsystems-ez1.yaml: -------------------------------------------------------------------------------- 1 | template: apsystems-ez1 2 | products: 3 | - brand: APsystems 4 | description: 5 | generic: EZ1 6 | params: 7 | - name: usage 8 | choice: ["pv"] 9 | - name: host 10 | render: | 11 | type: custom 12 | {{- if eq .usage "pv" }} 13 | power: 14 | source: http 15 | uri: http://{{ .host }}:8050/getOutputData 16 | jq: .data.p1+.data.p2 17 | energy: 18 | source: http 19 | uri: http://{{ .host }}:8050/getOutputData 20 | jq: .data.te1+.data.te2 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /assets/js/components/Savings/LiveCommunity.stories.ts: -------------------------------------------------------------------------------- 1 | import LiveCommunity from "./LiveCommunity.vue"; 2 | import type { Meta, StoryFn } from "@storybook/vue3"; 3 | 4 | export default { 5 | title: "Savings/LiveCommunity", 6 | component: LiveCommunity, 7 | parameters: { 8 | layout: "centered", 9 | }, 10 | } as Meta; 11 | 12 | const Template: StoryFn = () => ({ 13 | components: { LiveCommunity }, 14 | template: "", 15 | }); 16 | 17 | export const Default = Template.bind({}); 18 | -------------------------------------------------------------------------------- /templates/definition/charger/ocpp-abb-tac.yaml: -------------------------------------------------------------------------------- 1 | template: ocpp-abb-tac 2 | covers: ["ocpp-abb"] 3 | products: 4 | - brand: ABB 5 | description: 6 | generic: Terra AC (OCPP) 7 | capabilities: ["mA", "rfid"] 8 | requirements: 9 | evcc: ["sponsorship", "skiptest"] 10 | description: 11 | generic: https://library.e.abb.com/public/8f07987a3a284da6bf4e4f8f53cd6502/ABB_Terra_AC_Charger_OCPP1.6_ImplementationOverview%20_v1.8_FW1.6.6.pdf 12 | params: 13 | - preset: ocpp 14 | render: | 15 | {{ include "ocpp" . }} 16 | stacklevelzero: true 17 | -------------------------------------------------------------------------------- /templates/definition/charger/em2go.yaml: -------------------------------------------------------------------------------- 1 | template: em2go 2 | products: 3 | - brand: EM2GO 4 | description: 5 | generic: Pro Power (OCPP/ONC) 6 | capabilities: ["mA"] 7 | requirements: 8 | description: 9 | de: "Aktuelle Firmware mit Modbus-Unterstützung notwendig (Pro Power: 1.01 bzw. OCPP/ONC: 3.15)" 10 | en: "Recent firmware with Modbus support required (Pro Power: 1.01 and OCPP/ONC: 3.15)" 11 | params: 12 | - name: modbus 13 | choice: ["tcpip"] 14 | id: 255 15 | render: | 16 | type: em2go 17 | {{- include "modbus" . }} 18 | -------------------------------------------------------------------------------- /templates/definition/charger/fronius-wattpilot.yaml: -------------------------------------------------------------------------------- 1 | template: fronius-wattpilot 2 | deprecated: true 3 | products: 4 | - brand: Fronius 5 | description: 6 | generic: Wattpilot 7 | capabilities: ["1p3p", "rfid"] 8 | requirements: 9 | description: 10 | de: | 11 | Benötigt mindestens Firmware 36.3 oder neuer. 12 | en: | 13 | Requires firmware 36.3 or later. 14 | params: 15 | - name: host 16 | - name: password 17 | mask: true 18 | render: | 19 | type: wattpilot 20 | uri: {{ .host }} 21 | password: {{ .password }} 22 | -------------------------------------------------------------------------------- /templates/definition/meter/discovergy.yaml: -------------------------------------------------------------------------------- 1 | template: discovergy 2 | products: 3 | - description: 4 | generic: Discovergy 5 | params: 6 | - name: usage 7 | choice: ["grid", "pv"] 8 | - name: user 9 | required: true 10 | - name: password 11 | required: true 12 | - name: meter 13 | required: true 14 | example: 1ESY1161229886 15 | render: | 16 | type: discovergy 17 | user: {{ .user }} 18 | password: {{ .password }} # password 19 | meter: {{ .meter }} 20 | {{- if eq .usage "pv" }} 21 | scale: -1 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /tests/plan-fixed-tariff.evcc.yaml: -------------------------------------------------------------------------------- 1 | interval: 0.1s 2 | 3 | loadpoints: 4 | - title: Loadpoint 5 | charger: charger 6 | 7 | chargers: 8 | - name: charger 9 | type: custom 10 | enable: 11 | source: js 12 | script: 13 | enabled: 14 | source: js 15 | script: | 16 | false 17 | status: 18 | source: js 19 | script: | 20 | "B" 21 | maxcurrent: 22 | source: js 23 | script: 24 | 25 | tariffs: 26 | currency: EUR 27 | grid: 28 | type: fixed 29 | price: 0.4 # EUR/kWh 30 | -------------------------------------------------------------------------------- /tests/sponsor.evcc.yaml: -------------------------------------------------------------------------------- 1 | # expired sponsor token 2 | sponsortoken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJldmNjLmlvIiwic3ViIjoidHJpYWwiLCJleHAiOjE3NTQ5OTI4MDAsImlhdCI6MTc1MzY5NjgwMCwic3BlIjp0cnVlLCJzcmMiOiJtYSJ9.XKa5DHT-icCM9awcX4eS8feW0J_KIjsx2IxjcRRQOcQ 3 | 4 | site: 5 | title: Sponsor Test 6 | 7 | loadpoints: 8 | - title: Carport 9 | charger: easee_charger 10 | 11 | chargers: 12 | - name: easee_charger 13 | type: template 14 | template: easee 15 | user: test@example.org 16 | password: none 17 | charger: EH123456 18 | -------------------------------------------------------------------------------- /meter/template.go: -------------------------------------------------------------------------------- 1 | package meter 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/evcc-io/evcc/api" 7 | "github.com/evcc-io/evcc/util/templates" 8 | ) 9 | 10 | func init() { 11 | registry.AddCtx("template", NewMeterFromTemplateConfig) 12 | } 13 | 14 | func NewMeterFromTemplateConfig(ctx context.Context, other map[string]interface{}) (api.Meter, error) { 15 | instance, err := templates.RenderInstance(templates.Meter, other) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return NewFromConfig(ctx, instance.Type, instance.Other) 21 | } 22 | -------------------------------------------------------------------------------- /templates/definition/charger/abl.yaml: -------------------------------------------------------------------------------- 1 | template: abl 2 | products: 3 | - brand: ABL 4 | description: 5 | generic: eMH1 6 | - brand: ABL 7 | description: 8 | generic: eMH2 9 | - brand: SENEC 10 | description: 11 | generic: Wallbox pro 12 | capabilities: ["mA"] 13 | requirements: 14 | evcc: ["sponsorship"] 15 | params: 16 | - name: modbus 17 | choice: ["rs485"] 18 | baudrate: 38400 19 | comset: 8E1 20 | - name: timeout 21 | render: | 22 | type: abl 23 | {{- include "modbus" . }} 24 | timeout: {{ .timeout }} 25 | -------------------------------------------------------------------------------- /templates/definition/meter/ac-elwa-2.yaml: -------------------------------------------------------------------------------- 1 | template: ac-elwa-2 2 | products: 3 | - brand: my-PV 4 | description: 5 | generic: AC ELWA 2 6 | params: 7 | - name: usage 8 | choice: ["aux"] 9 | - name: host 10 | - name: tempsource 11 | choice: ["1", "2"] 12 | default: "1" 13 | render: | 14 | type: custom 15 | power: 16 | source: http 17 | uri: http://{{ .host }}/data.jsn 18 | jq: .power_elwa2 19 | soc: 20 | source: http 21 | uri: http://{{ .host }}/data.jsn 22 | jq: .temp{{ .tempsource }} 23 | scale: 0.1 24 | -------------------------------------------------------------------------------- /templates/definition/meter/goodwe-wifi.yaml: -------------------------------------------------------------------------------- 1 | template: goodwe-wifi 2 | # UDP implementation is broken 3 | # deprecated: true 4 | products: 5 | - brand: GoodWe 6 | description: 7 | generic: GoodWe over Wifi 8 | requirements: 9 | evcc: ["skiptest"] 10 | params: 11 | - name: usage 12 | choice: ["grid", "pv", "battery"] 13 | allinone: true 14 | - name: uri 15 | description: 16 | en: IP address or hostname 17 | de: IP-Adresse des Hostname 18 | render: | 19 | type: goodwe-wifi 20 | usage: {{ .usage }} 21 | uri: {{ .uri }} 22 | -------------------------------------------------------------------------------- /assets/js/components/VehicleIcon/Desktop.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /templates/definition/charger/etrel-duo.yaml: -------------------------------------------------------------------------------- 1 | template: etrel-duo 2 | products: 3 | - brand: Etrel 4 | description: 5 | generic: INCH Duo 6 | capabilities: ["mA"] 7 | requirements: 8 | evcc: ["sponsorship"] 9 | description: 10 | de: Die Wallbox muss sich im "Power" Modus befinden. 11 | en: The charger must be switched to "Power" charging mode. 12 | params: 13 | - name: connector 14 | - name: host 15 | - name: port 16 | default: 502 17 | render: | 18 | type: etrel 19 | connector: {{ .connector }} 20 | uri: {{ .host }}:{{ .port }} 21 | -------------------------------------------------------------------------------- /charger/template.go: -------------------------------------------------------------------------------- 1 | package charger 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/evcc-io/evcc/api" 7 | "github.com/evcc-io/evcc/util/templates" 8 | ) 9 | 10 | func init() { 11 | registry.AddCtx("template", NewChargerFromTemplateConfig) 12 | } 13 | 14 | func NewChargerFromTemplateConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) { 15 | instance, err := templates.RenderInstance(templates.Charger, other) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return NewFromConfig(ctx, instance.Type, instance.Other) 21 | } 22 | -------------------------------------------------------------------------------- /tariff/template.go: -------------------------------------------------------------------------------- 1 | package tariff 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/evcc-io/evcc/api" 7 | "github.com/evcc-io/evcc/util/templates" 8 | ) 9 | 10 | func init() { 11 | registry.AddCtx("template", NewTariffFromTemplateConfig) 12 | } 13 | 14 | func NewTariffFromTemplateConfig(ctx context.Context, other map[string]interface{}) (api.Tariff, error) { 15 | instance, err := templates.RenderInstance(templates.Tariff, other) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return NewCachedFromConfig(ctx, instance.Type, instance.Other) 21 | } 22 | -------------------------------------------------------------------------------- /templates/definition/charger/etrel.yaml: -------------------------------------------------------------------------------- 1 | template: etrel 2 | products: 3 | - brand: Etrel 4 | description: 5 | generic: INCH 6 | - brand: Sonnen 7 | description: 8 | generic: sonnenCharger 9 | capabilities: ["mA"] 10 | requirements: 11 | evcc: ["sponsorship"] 12 | description: 13 | de: Die Wallbox muss sich im "Power" Modus befinden. 14 | en: The charger must be switched to "Power" charging mode. 15 | params: 16 | - name: host 17 | - name: port 18 | default: 502 19 | render: | 20 | type: etrel 21 | uri: {{ .host }}:{{ .port }} 22 | -------------------------------------------------------------------------------- /templates/definition/charger/nrgkick-bluetooth.yaml: -------------------------------------------------------------------------------- 1 | template: nrgkick-bluetooth 2 | deprecated: true 3 | products: 4 | - brand: NRGkick 5 | description: 6 | generic: Bluetooth 7 | requirements: 8 | description: 9 | de: NRGkick Ladeeinheit via Bluetooth (älter als 2022/2023) 10 | en: NRGkick charging unit via Bluetooth (older than 2022/2023) 11 | params: 12 | - name: mac 13 | required: true 14 | - name: pin 15 | required: true 16 | mask: true 17 | render: | 18 | type: nrgkick-bluetooth 19 | mac: {{ .mac }} 20 | pin: {{ .pin }} 21 | -------------------------------------------------------------------------------- /templates/definition/meter/eastron-sdm220_230.yaml: -------------------------------------------------------------------------------- 1 | template: eastron-sdm220_230 2 | products: 3 | - brand: Eastron 4 | description: 5 | generic: SDM220/230 6 | - brand: Weidmüller 7 | description: 8 | generic: EM110-RTU-2P 9 | - brand: Weidmüller 10 | description: 11 | generic: EM111-RTU-2P 12 | params: 13 | - name: usage 14 | choice: ["grid", "charge"] 15 | - name: modbus 16 | choice: ["rs485"] 17 | render: | 18 | type: mbmd 19 | {{- include "modbus" . }} 20 | model: sdm220 21 | power: Power 22 | energy: Import 23 | -------------------------------------------------------------------------------- /vehicle/template.go: -------------------------------------------------------------------------------- 1 | package vehicle 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/evcc-io/evcc/api" 7 | "github.com/evcc-io/evcc/util/templates" 8 | ) 9 | 10 | func init() { 11 | registry.AddCtx("template", NewVehicleFromTemplateConfig) 12 | } 13 | 14 | func NewVehicleFromTemplateConfig(ctx context.Context, other map[string]interface{}) (api.Vehicle, error) { 15 | instance, err := templates.RenderInstance(templates.Vehicle, other) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return NewFromConfig(ctx, instance.Type, instance.Other) 21 | } 22 | -------------------------------------------------------------------------------- /vehicle/toyota/provider.go: -------------------------------------------------------------------------------- 1 | package toyota 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/evcc-io/evcc/util" 7 | ) 8 | 9 | type Provider struct { 10 | status func() (Status, error) 11 | } 12 | 13 | func NewProvider(api *API, vin string, cache time.Duration) *Provider { 14 | impl := &Provider{ 15 | status: util.Cached(func() (Status, error) { 16 | return api.Status(vin) 17 | }, cache), 18 | } 19 | return impl 20 | } 21 | 22 | func (v *Provider) Soc() (float64, error) { 23 | res, err := v.status() 24 | return float64(res.Payload.BatteryLevel), err 25 | } 26 | -------------------------------------------------------------------------------- /templates/definition/vehicle/vw.yaml: -------------------------------------------------------------------------------- 1 | template: vw 2 | covers: ["id"] 3 | products: 4 | - brand: Volkswagen 5 | description: 6 | generic: We Connect ID 7 | requirements: 8 | description: 9 | de: e-Golf, e-Up, ID Familie 10 | en: e-Golf, e-Up, ID family 11 | params: 12 | - preset: vehicle-base 13 | - name: vin 14 | example: WVWZZZ... 15 | - name: timeout 16 | default: 10s 17 | - preset: vehicle-features 18 | render: | 19 | type: vw 20 | {{ include "vehicle-base" . }} 21 | {{ include "vehicle-features" . }} 22 | timeout: {{ .timeout }} 23 | -------------------------------------------------------------------------------- /charger/connectiq/types.go: -------------------------------------------------------------------------------- 1 | package connectiq 2 | 3 | type ChargeStatus struct { 4 | Amps int64 `json:"amps"` 5 | Pp int64 `json:"pp"` 6 | Status string `json:"status"` 7 | Std int64 `json:"std"` 8 | } 9 | 10 | type ChargeMaxAmps struct { 11 | Max int64 `json:"max"` 12 | } 13 | 14 | type MeterStatus struct { 15 | App []float64 `json:"app"` 16 | Curr []float64 `json:"curr"` 17 | Fac []float64 `json:"fac"` 18 | Pow []float64 `json:"pow"` 19 | Volt []float64 `json:"volt"` 20 | } 21 | 22 | type MeterRead struct { 23 | Energy float64 `json:"energy"` 24 | } 25 | -------------------------------------------------------------------------------- /plugin/auth/nop.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/evcc-io/evcc/util" 8 | ) 9 | 10 | type nop struct{} 11 | 12 | func init() { 13 | registry.AddCtx("nop", NewNopFromConfig) 14 | } 15 | 16 | func NewNopFromConfig(ctx context.Context, other map[string]any) (Authorizer, error) { 17 | var cc struct{} 18 | if err := util.DecodeOther(other, &cc); err != nil { 19 | return nil, err 20 | } 21 | 22 | return new(nop), nil 23 | } 24 | 25 | func (p *nop) Transport(base http.RoundTripper) http.RoundTripper { 26 | return base 27 | } 28 | -------------------------------------------------------------------------------- /tests/config-circuit.evcc.yaml: -------------------------------------------------------------------------------- 1 | site: 2 | meters: 3 | grid: grid 4 | 5 | meters: 6 | - name: grid 7 | type: template 8 | template: demo-meter 9 | power: 2070 10 | currentL1: 3 11 | currentL2: 3 12 | currentL3: 3 13 | 14 | loadpoints: 15 | - title: Carport 16 | charger: charger 17 | circuit: main 18 | 19 | circuits: 20 | - name: main 21 | meter: grid 22 | maxcurrent: 16 23 | 24 | chargers: 25 | - name: charger 26 | type: template 27 | template: demo-charger 28 | status: C 29 | enabled: true 30 | power: 1000 31 | -------------------------------------------------------------------------------- /assets/js/components/MaterialIcon/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /assets/js/components/MaterialIcon/Total.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /cmd/openapi/openapi.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | 8 | "github.com/getkin/kin-openapi/openapi3" 9 | ) 10 | 11 | func main() { 12 | doc, err := openapi3.NewLoader().LoadFromFile(os.Args[1]) 13 | if err != nil { 14 | log.Fatal("failed to load OpenAPI spec:", err) 15 | } 16 | 17 | // omit servers 18 | doc.Servers = nil 19 | 20 | b, err := json.MarshalIndent(doc, "", " ") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | if err := os.WriteFile(os.Args[2], b, 0o644); err != nil { 26 | log.Fatal(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /meter/shelly/types.go: -------------------------------------------------------------------------------- 1 | package shelly 2 | 3 | // DeviceInfo is the common /shelly endpoint response 4 | // https://shelly-api-docs.shelly.cloud/gen1/#shelly 5 | // https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Shelly#http-endpoint-shelly 6 | type DeviceInfo struct { 7 | Mac string `json:"mac"` 8 | Gen int `json:"gen"` 9 | Model string `json:"model"` 10 | Type string `json:"type"` 11 | Auth bool `json:"auth"` 12 | AuthEn bool `json:"auth_en"` 13 | NumMeters int `json:"num_meters"` 14 | Profile string `json:"profile"` 15 | } 16 | -------------------------------------------------------------------------------- /templates/definition/charger/go-e.yaml: -------------------------------------------------------------------------------- 1 | template: go-e 2 | products: 3 | - brand: go-e 4 | description: 5 | generic: Charger HOMEfix 6 | - brand: go-e 7 | description: 8 | generic: Charger PRO 9 | capabilities: ["rfid"] 10 | requirements: 11 | description: 12 | en: Requires firmware 040.0 or later. HTTP API v1 or v2 must be activated. 13 | de: Benötigt mindestens Firmware 040.0 oder neuer. Das HTTP API v1 oder v2 muss aktiviert sein. 14 | evcc: ["sponsorship"] 15 | params: 16 | - name: host 17 | render: | 18 | type: go-e 19 | uri: http://{{ .host }} 20 | -------------------------------------------------------------------------------- /templates/definition/charger/victron-evcs.yaml: -------------------------------------------------------------------------------- 1 | template: victron-evcs 2 | products: 3 | - brand: Victron 4 | description: 5 | generic: EV Charging Station 6 | requirements: 7 | description: 8 | en: Enter the host of the charger (not the GX device) and ensure that the charger is in manual mode. 9 | de: Trage den Host der Wallbox (nicht des GX-Geräts) ein und stelle sicher, dass die Wallbox sich im Modus "Manual" befindet. 10 | params: 11 | - name: modbus 12 | choice: ["tcpip"] 13 | id: 1 14 | render: | 15 | type: victron-evcs 16 | {{- include "modbus" . }} 17 | -------------------------------------------------------------------------------- /assets/js/components/Savings/communityApi.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const api = axios.create({ 4 | baseURL: "https://api.evcc.io/v1/", 5 | headers: { 6 | Accept: "application/json", 7 | }, 8 | }); 9 | 10 | // global error handling 11 | api.interceptors.response.use( 12 | (response) => response, 13 | (error) => { 14 | const url = error.config.baseURL + error.config.url; 15 | const message = `${error.message}: API request failed ${url}`; 16 | window.app.raise({ message }); 17 | return Promise.reject(error); 18 | } 19 | ); 20 | 21 | export default api; 22 | -------------------------------------------------------------------------------- /assets/js/utils/fatal.ts: -------------------------------------------------------------------------------- 1 | import type { FatalError } from "@/types/evcc"; 2 | 3 | const FATALS = ["configfile", "database"]; 4 | 5 | function isError(fatal: FatalError[]) { 6 | return fatal.length > 0; 7 | } 8 | 9 | export function isUserConfigError(fatal: FatalError[]) { 10 | if (!isError(fatal)) { 11 | return false; 12 | } 13 | 14 | if (fatal.some((f) => FATALS.includes(f.class ?? ""))) { 15 | return false; 16 | } 17 | 18 | return true; 19 | } 20 | 21 | export function isSystemError(fatal: FatalError[]) { 22 | return isError(fatal) && !isUserConfigError(fatal); 23 | } 24 | -------------------------------------------------------------------------------- /meter/tplink.go: -------------------------------------------------------------------------------- 1 | package meter 2 | 3 | import ( 4 | "github.com/evcc-io/evcc/api" 5 | "github.com/evcc-io/evcc/meter/tplink" 6 | "github.com/evcc-io/evcc/util" 7 | ) 8 | 9 | func init() { 10 | registry.Add("tplink", NewTPLinkFromConfig) 11 | } 12 | 13 | // NewTPLinkFromConfig creates a tapo meter from generic config 14 | func NewTPLinkFromConfig(other map[string]interface{}) (api.Meter, error) { 15 | var cc struct { 16 | URI string 17 | } 18 | 19 | if err := util.DecodeOther(other, &cc); err != nil { 20 | return nil, err 21 | } 22 | 23 | return tplink.NewConnection(cc.URI) 24 | } 25 | -------------------------------------------------------------------------------- /templates/definition/tariff/amber.yaml: -------------------------------------------------------------------------------- 1 | template: amber 2 | products: 3 | - brand: Amber Electric 4 | group: price 5 | countries: ["AU"] 6 | params: 7 | - name: token 8 | required: true 9 | - name: siteid 10 | description: 11 | generic: Site ID 12 | required: true 13 | - name: channel 14 | type: choice 15 | choice: ["general", "feedIn", "controlledLoad"] 16 | required: true 17 | - preset: tariff-base 18 | render: | 19 | type: amber 20 | token: {{ .token }} 21 | siteid: {{ .siteid }} 22 | channel: {{ .channel }} 23 | {{ include "tariff-base" . }} 24 | -------------------------------------------------------------------------------- /vehicle/vag/loginapps/token.go: -------------------------------------------------------------------------------- 1 | package loginapps 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "golang.org/x/oauth2" 8 | ) 9 | 10 | // Token is the loginapps token 11 | type Token oauth2.Token 12 | 13 | func (t *Token) UnmarshalJSON(data []byte) error { 14 | var s struct { 15 | AccessToken string 16 | RefreshToken string 17 | } 18 | 19 | err := json.Unmarshal(data, &s) 20 | if err == nil { 21 | t.TokenType = "bearer" 22 | t.AccessToken = s.AccessToken 23 | t.RefreshToken = s.RefreshToken 24 | t.Expiry = time.Now().Add(time.Hour) 25 | } 26 | 27 | return err 28 | } 29 | -------------------------------------------------------------------------------- /charger/embed.go: -------------------------------------------------------------------------------- 1 | package charger 2 | 3 | import ( 4 | "github.com/evcc-io/evcc/api" 5 | ) 6 | 7 | type embed struct { 8 | Icon_ string `mapstructure:"icon"` 9 | Features_ []api.Feature `mapstructure:"features"` 10 | } 11 | 12 | var _ api.IconDescriber = (*embed)(nil) 13 | 14 | // Icon implements the api.IconDescriber interface 15 | func (v *embed) Icon() string { 16 | return v.Icon_ 17 | } 18 | 19 | var _ api.FeatureDescriber = (*embed)(nil) 20 | 21 | // Features implements the api.FeatureDescriber interface 22 | func (v *embed) Features() []api.Feature { 23 | return v.Features_ 24 | } 25 | -------------------------------------------------------------------------------- /templates/definition/meter/shelly-pro-3em.yaml: -------------------------------------------------------------------------------- 1 | template: shelly-pro-3em 2 | products: 3 | - brand: Shelly 4 | description: 5 | generic: Pro 3 EM 6 | params: 7 | - name: usage 8 | choice: ["grid", "pv", "charge"] 9 | - name: host 10 | - name: user 11 | - name: password 12 | render: | 13 | type: shelly 14 | uri: http://{{ .host }} # shelly device ip address (local) 15 | {{- if .user }} 16 | user: {{ .user }} 17 | {{- end }} 18 | {{- if .password }} 19 | password: {{ .password }} 20 | {{- end }} 21 | usage: {{ .usage }} 22 | channel: 0 # shelly device relay channel 23 | -------------------------------------------------------------------------------- /api/proto/vehicle.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // protoc proto/vehicle.proto --go_out=. --go-grpc_out=. 4 | 5 | option go_package = "proto/pb"; 6 | 7 | service Vehicle { 8 | rpc New (NewRequest) returns (NewReply) {} 9 | rpc SoC (SoCRequest) returns (SoCReply) {} 10 | } 11 | 12 | message NewRequest { 13 | string token = 1; 14 | string type = 2; 15 | map config = 3; 16 | } 17 | 18 | message NewReply { 19 | int64 vehicle_id = 1; 20 | } 21 | 22 | message SoCRequest { 23 | string token = 1; 24 | int64 vehicle_id = 2; 25 | } 26 | 27 | message SoCReply { 28 | double soc = 1; 29 | } 30 | -------------------------------------------------------------------------------- /assets/js/components/Loadpoints/SettingsButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /assets/js/components/MaterialIcon/Play.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /assets/js/components/Top/baseapi.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const { protocol, hostname, port, pathname } = window.location; 4 | 5 | const baseAPI = axios.create({ 6 | baseURL: protocol + "//" + hostname + (port ? ":" + port : "") + pathname, 7 | }); 8 | 9 | // global error handling 10 | baseAPI.interceptors.response.use( 11 | (response) => response, 12 | (error) => { 13 | const url = error.config.baseURL + error.config.url; 14 | const message = `${error.message}: API request failed ${url}`; 15 | window.app.raise({ message }); 16 | } 17 | ); 18 | 19 | export default baseAPI; 20 | -------------------------------------------------------------------------------- /core/settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import "time" 4 | 5 | type Settings interface { 6 | SetString(key string, val string) 7 | SetInt(key string, val int64) 8 | SetFloat(key string, val float64) 9 | SetFloatPtr(key string, val *float64) 10 | SetTime(key string, val time.Time) 11 | SetJson(key string, val any) error 12 | SetBool(key string, val bool) 13 | String(key string) (string, error) 14 | Int(key string) (int64, error) 15 | Float(key string) (float64, error) 16 | Time(key string) (time.Time, error) 17 | Bool(key string) (bool, error) 18 | Json(key string, res any) error 19 | } 20 | -------------------------------------------------------------------------------- /templates/definition/charger/ocpp-goe.yaml: -------------------------------------------------------------------------------- 1 | template: ocpp-goe 2 | covers: ["ocpp-fronius-wattpilot"] 3 | products: 4 | - brand: go-e 5 | description: 6 | generic: Charger V3 (OCPP) 7 | - brand: go-e 8 | description: 9 | generic: Charger Gemini (OCPP) 10 | - brand: go-e 11 | description: 12 | generic: Charger PRO (OCPP) 13 | - brand: Fronius 14 | description: 15 | generic: Wattpilot (OCPP) 16 | capabilities: ["rfid", "1p3p"] 17 | requirements: 18 | evcc: ["sponsorship", "skiptest"] 19 | params: 20 | - preset: ocpp 21 | render: | 22 | {{ include "ocpp" . }} 23 | -------------------------------------------------------------------------------- /templates/definition/vehicle/ford.yaml: -------------------------------------------------------------------------------- 1 | template: ford 2 | deprecated: true 3 | products: 4 | - brand: Ford 5 | requirements: 6 | description: 7 | de: "Hinweis: Ford hat kürzlich den API-Zugriff für seine Benutzer deaktiviert." 8 | en: "Note: Ford has recently disabled API-access for their users." 9 | params: 10 | - preset: vehicle-base 11 | - name: vin 12 | example: WF0FXX... 13 | - name: domain 14 | type: choice 15 | choice: ["com", "de"] 16 | default: com 17 | required: true 18 | render: | 19 | type: ford 20 | {{ include "vehicle-base" . }} 21 | domain: {{ .domain }} 22 | -------------------------------------------------------------------------------- /server/updater/run.go: -------------------------------------------------------------------------------- 1 | //go:build !gokrazy 2 | 3 | package updater 4 | 5 | import ( 6 | "github.com/evcc-io/evcc/util" 7 | "github.com/google/go-github/v32/github" 8 | ) 9 | 10 | // Run regularly checks version 11 | func Run(log *util.Logger, httpd webServer, outChan chan<- util.Param) { 12 | u := &watch{ 13 | log: log, 14 | outChan: outChan, 15 | repo: NewRepo(log, owner, repository), 16 | } 17 | 18 | c := make(chan *github.RepositoryRelease, 1) 19 | go u.watchReleases(util.Version, c) // endless 20 | 21 | for rel := range c { 22 | u.Send("availableVersion", *rel.TagName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/tariff.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | //go:generate go tool enumer -type TariffType -trimprefix TariffType -transform=lower -text 4 | //go:generate go tool enumer -type TariffUsage -trimprefix TariffUsage -transform=lower 5 | 6 | type TariffType int 7 | 8 | const ( 9 | _ TariffType = iota 10 | TariffTypePriceStatic 11 | TariffTypePriceDynamic 12 | TariffTypePriceForecast 13 | TariffTypeCo2 14 | TariffTypeSolar 15 | ) 16 | 17 | type TariffUsage int 18 | 19 | const ( 20 | _ TariffUsage = iota 21 | TariffUsageCo2 22 | TariffUsageFeedIn 23 | TariffUsageGrid 24 | TariffUsagePlanner 25 | TariffUsageSolar 26 | ) 27 | -------------------------------------------------------------------------------- /core/metrics/types.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | type SqlTime time.Time 10 | 11 | var _ sql.Scanner = (*SqlTime)(nil) 12 | 13 | func (st *SqlTime) Scan(value any) error { 14 | switch v := value.(type) { 15 | case time.Time: 16 | *st = SqlTime(v) 17 | case int64: 18 | *st = SqlTime(time.Unix(v, 0)) 19 | case string: 20 | t, err := time.Parse(time.DateTime+"-07:00", v) 21 | if err == nil { 22 | *st = SqlTime(t) 23 | } 24 | return err 25 | default: 26 | return errors.New("unsupported timestamp type") 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /templates/definition/charger/nrgkick-connect.yaml: -------------------------------------------------------------------------------- 1 | template: nrgkick-connect 2 | products: 3 | - brand: NRGkick 4 | description: 5 | generic: Connect 6 | requirements: 7 | description: 8 | de: NRGkick Ladeeinheit via HTTP (älter als 2022/2023) 9 | en: NRGkick charging unit via HTTP (older than 2022/2023) 10 | params: 11 | - name: host 12 | - name: mac 13 | required: true 14 | - name: password 15 | required: true 16 | render: | 17 | type: nrgkick-connect 18 | uri: http://{{ .host }} 19 | mac: {{ .mac }} # BT device MAC address (sudo hcitool lescan) 20 | password: {{ .password }} 21 | -------------------------------------------------------------------------------- /vehicle/seat/params.go: -------------------------------------------------------------------------------- 1 | package seat 2 | 3 | import "net/url" 4 | 5 | const ( 6 | Brand = "VW" 7 | Country = "ES" 8 | 9 | // Authorization ClientID 10 | AuthClientID = "9dcc70f0-8e79-423a-a3fa-4065d99088b4" 11 | ) 12 | 13 | // Authorization parameters 14 | var AuthParams = url.Values{ 15 | "response_type": {"code id_token"}, // token 16 | "client_id": {"3c8e98bc-3ae9-4277-a563-d5ee65ddebba@apps_vw-dilab_com"}, 17 | "redirect_uri": {"seatconnect://identity-kit/login"}, 18 | "scope": {"openid profile"}, // address phone email birthdate nationalIdentifier cars mbb dealers badge nationality 19 | } 20 | -------------------------------------------------------------------------------- /core/progress.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Progress struct { 4 | min, step, current float64 5 | } 6 | 7 | func NewProgress(min, step float64) *Progress { 8 | return &Progress{ 9 | min: min, 10 | step: step, 11 | current: min, 12 | } 13 | } 14 | 15 | func (p *Progress) NextStep(value float64) bool { 16 | // test guard 17 | if p != nil && value >= p.current { 18 | for p.current <= value { 19 | p.current += p.step 20 | } 21 | 22 | return true 23 | } 24 | 25 | return false 26 | } 27 | 28 | func (p *Progress) Reset() { 29 | // test guard 30 | if p != nil { 31 | p.current = p.min 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tariff/types.go: -------------------------------------------------------------------------------- 1 | package tariff 2 | 3 | type Typed struct { 4 | Type string `json:"type"` 5 | Tariff string `json:"tariff"` 6 | Other map[string]any `mapstructure:",remain" yaml:",inline"` 7 | } 8 | 9 | func (t Typed) Name() string { 10 | if t.Type == "template" { 11 | return t.Tariff 12 | } 13 | return t.Type 14 | } 15 | 16 | type FromTo struct { 17 | From, To int 18 | } 19 | 20 | func (ft FromTo) IsActive(hour int) bool { 21 | return ft.From == 0 && ft.To == 0 || 22 | ft.From < ft.To && ft.From <= hour && hour <= ft.To || 23 | ft.From > ft.To && (ft.From <= hour || hour <= ft.To) 24 | } 25 | -------------------------------------------------------------------------------- /templates/definition/meter/wago-879-30xx.yaml: -------------------------------------------------------------------------------- 1 | template: wago-879-30xx 2 | products: 3 | - brand: Wago 4 | description: 5 | generic: 879-30xx 6 | params: 7 | - name: usage 8 | choice: ["grid", "charge"] 9 | - name: modbus 10 | choice: ["rs485"] 11 | render: | 12 | type: mbmd 13 | {{- include "modbus" . }} 14 | model: wago87930 15 | power: Power 16 | energy: Import 17 | currents: 18 | - CurrentL1 19 | - CurrentL2 20 | - CurrentL3 21 | powers: 22 | - PowerL1 23 | - PowerL2 24 | - PowerL3 25 | voltages: 26 | - VoltageL1 27 | - VoltageL2 28 | - VoltageL3 29 | -------------------------------------------------------------------------------- /vehicle/bmw/types.go: -------------------------------------------------------------------------------- 1 | package bmw 2 | 3 | type Vehicle struct { 4 | VIN string 5 | Model string 6 | AppVehicleType string 7 | } 8 | 9 | type VehicleStatus struct { 10 | StatusCode int 11 | Message string 12 | State struct { 13 | CurrentMileage int64 14 | Range int64 15 | ElectricChargingState struct { 16 | ChargingLevelPercent int64 17 | Range int64 18 | IsChargerConnected bool 19 | ChargingStatus string 20 | ChargingTarget int64 21 | } 22 | ClimateControlState struct { 23 | Activity string 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /assets/js/experimental.js: -------------------------------------------------------------------------------- 1 | import settings from "./settings"; 2 | 3 | const KM = "km"; 4 | const MILES = "mi"; 5 | export const UNITS = [KM, MILES]; 6 | 7 | const MILES_FACTOR = 0.6213711922; 8 | 9 | function isMiles() { 10 | return settings.unit === MILES; 11 | } 12 | 13 | export function distanceValue(value) { 14 | return isMiles() ? value * MILES_FACTOR : value; 15 | } 16 | 17 | export function distanceUnit() { 18 | return isMiles() ? "mi" : "km"; 19 | } 20 | 21 | export function getUnits() { 22 | return isMiles() ? MILES : KM; 23 | } 24 | 25 | export function setUnits(value) { 26 | settings.unit = value; 27 | } 28 | -------------------------------------------------------------------------------- /templates/definition/meter/fritzgrid.yaml: -------------------------------------------------------------------------------- 1 | template: fritzgrid 2 | products: 3 | - brand: "FRITZ!" 4 | description: 5 | generic: "FRITZ!Smart Energy 250" 6 | params: 7 | - name: usage 8 | choice: ["grid"] 9 | - name: uri 10 | default: https://fritz.box 11 | - name: user 12 | required: true 13 | - name: password 14 | required: true 15 | - name: ain 16 | required: true 17 | render: | 18 | type: fritzdect 19 | uri: {{ .uri }} 20 | user: {{ .user }} 21 | password: {{ .password }} 22 | ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) 23 | -------------------------------------------------------------------------------- /assets/js/components/VehicleIcon/Floorlamp.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /core/progress_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestProgress(t *testing.T) { 11 | p := NewProgress(0, 10) 12 | 13 | tc := []struct { 14 | value float64 15 | res bool 16 | }{ 17 | {-1, false}, 18 | {0, true}, 19 | {1, false}, 20 | {5, false}, 21 | {10, true}, 22 | {15, false}, 23 | {25, true}, 24 | {30, true}, 25 | {60, true}, 26 | {65, false}, 27 | {70, true}, 28 | } 29 | 30 | for _, tc := range tc { 31 | require.Equal(t, tc.res, p.NextStep(tc.value), fmt.Sprintf("%.0f%%", tc.value)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/definition/charger/wallbe.yaml: -------------------------------------------------------------------------------- 1 | template: wallbe 2 | deprecated: true 3 | products: 4 | - brand: Wallbe 5 | description: 6 | generic: Eco 7 | - brand: Wallbe 8 | description: 9 | generic: Pro 10 | requirements: 11 | description: 12 | en: The Wallbe must be connected using Ethernet and the DIP switch 10 must be set to 'ON'. 13 | de: Die Wallbox muss über ein Netzwerkkabel angebunden sein und im Gerät muss der DIP Schalter 10 auf 'ON' gestellt sein. 14 | params: 15 | - name: host 16 | - name: port 17 | default: 502 18 | render: | 19 | type: wallbe 20 | uri: {{ .host }}:{{ .port }} 21 | -------------------------------------------------------------------------------- /assets/js/components/Config/Markdown.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 21 | 31 | -------------------------------------------------------------------------------- /templates/definition/charger/victron.yaml: -------------------------------------------------------------------------------- 1 | template: victron 2 | products: 3 | - brand: Victron 4 | description: 5 | generic: EV Charging Station (via GX) 6 | requirements: 7 | description: 8 | en: Enter the host of the GX device (not the charger). The charger has to be in manual mode and Modbus has to be configured for ID 100. 9 | de: Trage den Host des GX-Gerätes (nicht der Wallbox) ein. Die Wallbox muss sich im Modus "Manual" befinden und Modbus ID 100 konfiguriert sein. 10 | params: 11 | - name: modbus 12 | choice: ["tcpip"] 13 | id: 100 14 | render: | 15 | type: victron 16 | {{- include "modbus" . }} 17 | -------------------------------------------------------------------------------- /tests/config-grid-only.evcc.yaml: -------------------------------------------------------------------------------- 1 | site: 2 | title: Hello World 3 | meters: 4 | grid: grid 5 | 6 | meters: 7 | - name: grid 8 | type: custom 9 | power: 10 | source: js 11 | script: | 12 | 1000 13 | 14 | loadpoints: 15 | - title: Carport 16 | charger: charger 17 | 18 | chargers: 19 | - name: charger 20 | type: custom 21 | enable: 22 | source: js 23 | script: 24 | enabled: 25 | source: js 26 | script: | 27 | false 28 | status: 29 | source: js 30 | script: | 31 | "B" 32 | maxcurrent: 33 | source: js 34 | script: 35 | -------------------------------------------------------------------------------- /plugin/auth/config.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | reg "github.com/evcc-io/evcc/util/registry" 9 | ) 10 | 11 | var registry = reg.New[Authorizer]("auth") 12 | 13 | // NewFromConfig creates auth from configuration 14 | func NewFromConfig(ctx context.Context, typ string, other map[string]any) (Authorizer, error) { 15 | factory, err := registry.Get(strings.ToLower(typ)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | v, err := factory(ctx, other) 21 | if err != nil { 22 | err = fmt.Errorf("cannot create auth type '%s': %w", typ, err) 23 | } 24 | 25 | return v, err 26 | } 27 | -------------------------------------------------------------------------------- /templates/definition/meter/tibber-pulse.yaml: -------------------------------------------------------------------------------- 1 | template: tibber-pulse 2 | products: 3 | - brand: Tibber 4 | description: 5 | generic: Pulse 6 | requirements: 7 | evcc: ["skiptest"] 8 | params: 9 | - name: usage 10 | choice: ["grid"] 11 | - name: token 12 | mask: true 13 | required: true 14 | example: 5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE 15 | - name: homeid 16 | description: 17 | generic: Home ID 18 | example: 96a14971-525a-4420-aae9-e5aedaa129ff 19 | - name: timeout 20 | deprecated: true 21 | render: | 22 | type: tibber-pulse 23 | token: {{ .token }} 24 | homeid: {{ .homeid }} 25 | -------------------------------------------------------------------------------- /util/logstash/levels.go: -------------------------------------------------------------------------------- 1 | package logstash 2 | 3 | import ( 4 | "strings" 5 | 6 | jww "github.com/spf13/jwalterweatherman" 7 | ) 8 | 9 | // LogLevelToThreshold converts log level string to a jww Threshold 10 | func LogLevelToThreshold(level string) jww.Threshold { 11 | switch strings.ToUpper(level) { 12 | case "FATAL": 13 | return jww.LevelFatal 14 | case "ERROR": 15 | return jww.LevelError 16 | case "WARN": 17 | return jww.LevelWarn 18 | case "INFO": 19 | return jww.LevelInfo 20 | case "DEBUG": 21 | return jww.LevelDebug 22 | case "TRACE": 23 | return jww.LevelTrace 24 | default: 25 | return jww.LevelError 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /util/templates/merge_test.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestMergeMaps(t *testing.T) { 10 | target := map[string]any{ 11 | "foo": "bar", 12 | "nested": map[string]any{ 13 | "bar": "baz", 14 | }, 15 | } 16 | other := map[string]any{ 17 | "Foo": 1, 18 | "Nested": map[string]any{ 19 | "Bar": 2, 20 | }, 21 | "baz": 3, 22 | } 23 | 24 | require.NoError(t, mergeMaps(other, target)) 25 | require.Equal(t, map[string]any{ 26 | "foo": 1, 27 | "nested": map[string]any{ 28 | "bar": 2, 29 | }, 30 | "baz": 3, 31 | }, target) 32 | } 33 | -------------------------------------------------------------------------------- /assets/js/restart.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import api from "./api"; 3 | 4 | const restart = reactive({ 5 | restartNeeded: false, 6 | restarting: false, 7 | }); 8 | 9 | export async function performRestart() { 10 | try { 11 | await api.post("/system/shutdown"); 12 | restart.restarting = true; 13 | } catch (e) { 14 | alert(`Unable to restart server. ${e}`); 15 | } 16 | } 17 | 18 | export function restartComplete() { 19 | restart.restarting = false; 20 | restart.restartNeeded = false; 21 | } 22 | 23 | export function showRestarting() { 24 | restart.restarting = true; 25 | } 26 | 27 | export default restart; 28 | -------------------------------------------------------------------------------- /meter/obis/obis.go: -------------------------------------------------------------------------------- 1 | package obis 2 | 3 | // https://www.kbr.de/de/obis-kennzeichen/elektrizitaet 4 | 5 | const ( 6 | PowerConsumption = "1-0:1.4.0" 7 | EnergyConsumption = "1-0:1.8.0" 8 | PowerFeedIn = "1-0:2.4.0" 9 | EnergyFeedIn = "1-0:2.8.0" 10 | 11 | PowerConsumptionL1 = "1-0:21.4.0" 12 | EnergyConsumptionL1 = "1-0:21.8.0" 13 | CurrentL1 = "1-0:31.4.0" 14 | 15 | PowerConsumptionL2 = "1-0:41.4.0" 16 | EnergyConsumptionL2 = "1-0:41.8.0" 17 | CurrentL2 = "1-0:51.4.0" 18 | 19 | PowerConsumptionL3 = "1-0:61.4.0" 20 | EnergyConsumptionL3 = "1-0:61.8.0" 21 | CurrentL3 = "1-0:71.4.0" 22 | ) 23 | -------------------------------------------------------------------------------- /templates/definition/charger/webasto-next.yaml: -------------------------------------------------------------------------------- 1 | template: webasto-next 2 | products: 3 | - brand: Ampure 4 | description: 5 | generic: NEXT 6 | - brand: Webasto 7 | description: 8 | generic: NEXT 9 | capabilities: ["rfid"] 10 | requirements: 11 | description: 12 | de: Modus "HEMS activated" muss aktiviert sein. RFID-Tags können durch evcc nur gelesen werden. 13 | en: Mode "HEMS activated" must be enabled. RFID tags can only be read by evcc. 14 | evcc: ["sponsorship"] 15 | params: 16 | - name: host 17 | - name: port 18 | default: 502 19 | render: | 20 | type: webasto-next 21 | uri: {{ .host }}:{{ .port }} 22 | -------------------------------------------------------------------------------- /util/transport/basicauth.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "encoding/base64" 5 | "net/http" 6 | ) 7 | 8 | // BasicAuthHeader returns the basic auth header 9 | func BasicAuthHeader(user, password string) string { 10 | return "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) 11 | } 12 | 13 | // BasicAuth creates an http transport performing basic auth 14 | func BasicAuth(user, password string, base http.RoundTripper) http.RoundTripper { 15 | return &Decorator{ 16 | Decorator: DecorateHeaders(map[string]string{ 17 | "Authorization": BasicAuthHeader(user, password), 18 | }), 19 | Base: base, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cmd/shutdown/shutdown.go: -------------------------------------------------------------------------------- 1 | package shutdown 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var ( 8 | mu sync.Mutex 9 | handlers = make([]func(), 0) 10 | ) 11 | 12 | // Register registers a function for executing on application shutdown 13 | func Register(cb func()) { 14 | mu.Lock() 15 | handlers = append(handlers, cb) 16 | mu.Unlock() 17 | } 18 | 19 | // Cleanup executes the registered shutdown functions when the stop channel closes 20 | func Cleanup(doneC chan struct{}) { 21 | var wg sync.WaitGroup 22 | 23 | mu.Lock() 24 | for _, cb := range handlers { 25 | wg.Go(cb) 26 | } 27 | mu.Unlock() 28 | 29 | wg.Wait() 30 | close(doneC) 31 | } 32 | -------------------------------------------------------------------------------- /templates/definition/charger/hardybarth-ecb1.yaml: -------------------------------------------------------------------------------- 1 | template: hardybarth-ecb1 2 | products: 3 | - brand: Hardy Barth 4 | description: 5 | generic: cPH1 6 | - brand: echarge 7 | description: 8 | generic: cPH1 9 | requirements: 10 | evcc: ["sponsorship"] 11 | description: 12 | de: Als Betriebsmodus muss `manual` ausgewählt sein 13 | en: Charge mode must be configured as `manual` 14 | params: 15 | - name: host 16 | - name: connector 17 | default: 1 18 | advanced: true 19 | render: | 20 | type: hardybarth-ecb1 21 | uri: http://{{ .host }} 22 | chargecontrol: {{ .connector }} 23 | meter: {{ .connector }} 24 | -------------------------------------------------------------------------------- /util/oauth/tokensource_test.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/oauth2" 7 | ) 8 | 9 | func TestMerge(t *testing.T) { 10 | ts := &TokenSource{ 11 | token: &oauth2.Token{ 12 | AccessToken: "access", 13 | RefreshToken: "refresh", 14 | }, 15 | } 16 | 17 | r := &oauth2.Token{ 18 | AccessToken: "new", 19 | } 20 | 21 | if err := ts.mergeToken(r); err != nil { 22 | t.Error(err) 23 | } 24 | 25 | if ts.token.AccessToken != "new" { 26 | t.Error("unexpected access token", ts.token) 27 | } 28 | if ts.token.RefreshToken != "refresh" { 29 | t.Error("unexpected refresh token", ts.token) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /vehicle/tronity/auth.go: -------------------------------------------------------------------------------- 1 | package tronity 2 | 3 | import ( 4 | "golang.org/x/oauth2" 5 | ) 6 | 7 | const URI = "https://api.tronity.tech" 8 | 9 | func OAuth2Config(id, secret string) (*oauth2.Config, error) { 10 | return &oauth2.Config{ 11 | ClientID: id, 12 | ClientSecret: secret, 13 | Endpoint: oauth2.Endpoint{ 14 | AuthURL: "https://auth.tronity.io/oauth/v2/authorize", 15 | TokenURL: "https://api.tronity.tech/authentication", 16 | }, 17 | Scopes: []string{"read_vin", "read_vehicle_info", "read_odometer", "read_charge", "read_charge", "read_battery", "read_location", "write_charge_start_stop", "write_wake_up"}, 18 | }, nil 19 | } 20 | -------------------------------------------------------------------------------- /cmd/sponsor.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/evcc-io/evcc/util/sponsor" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // sponsorCmd represents the vehicle command 9 | var sponsorCmd = &cobra.Command{ 10 | Use: "sponsor [name]", 11 | Short: "Validate sponsor token", 12 | Args: cobra.ExactArgs(1), 13 | Run: runSponsor, 14 | } 15 | 16 | func init() { 17 | rootCmd.AddCommand(sponsorCmd) 18 | } 19 | 20 | func runSponsor(cmd *cobra.Command, args []string) { 21 | token := args[0] 22 | 23 | if err := sponsor.ConfigureSponsorship(token); err != nil { 24 | fatal(err) 25 | } 26 | 27 | log.INFO.Println("sponsorship validated") 28 | } 29 | -------------------------------------------------------------------------------- /server/eebus/types.go: -------------------------------------------------------------------------------- 1 | package eebus 2 | 3 | import "github.com/evcc-io/evcc/util" 4 | 5 | const ( 6 | BrandName string = "EVCC" 7 | Model string = "HEMS" 8 | ) 9 | 10 | // used as common name in cert generation 11 | var DeviceCode = util.Getenv("EEBUS_DEVICE_CODE", "EVCC_HEMS_01") 12 | 13 | type Config struct { 14 | URI string 15 | ShipID string 16 | Interfaces []string 17 | Certificate struct { 18 | Public, Private string 19 | } 20 | } 21 | 22 | // Configured returns true if the EEbus server is configured 23 | func (c Config) Configured() bool { 24 | return len(c.Certificate.Public) > 0 && len(c.Certificate.Private) > 0 25 | } 26 | -------------------------------------------------------------------------------- /vehicle/vag/loginapps/token_test.go: -------------------------------------------------------------------------------- 1 | package loginapps 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestUnmarshalJSON(t *testing.T) { 9 | var tok Token 10 | str := `{"accesstoken":"access","refreshtoken":"refresh"}` 11 | 12 | if err := json.Unmarshal([]byte(str), &tok); err != nil { 13 | t.Error(err) 14 | } 15 | 16 | if tok.AccessToken != "access" { 17 | t.Error("AccessToken") 18 | } 19 | 20 | if tok.RefreshToken != "refresh" { 21 | t.Error("RefreshToken") 22 | } 23 | 24 | if tok.TokenType != "bearer" { 25 | t.Error("TokenType") 26 | } 27 | 28 | if tok.Expiry.IsZero() { 29 | t.Error("Expiry") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/coordinator/dummy.go: -------------------------------------------------------------------------------- 1 | package coordinator 2 | 3 | import ( 4 | "github.com/evcc-io/evcc/api" 5 | "github.com/evcc-io/evcc/core/loadpoint" 6 | ) 7 | 8 | type dummy struct{} 9 | 10 | // NewDummy creates a dummy coordinator without vehicles 11 | func NewDummy() API { 12 | return new(dummy) 13 | } 14 | 15 | func (a *dummy) GetVehicles(_ bool) []api.Vehicle { 16 | return nil 17 | } 18 | 19 | func (a *dummy) Owner(api.Vehicle) loadpoint.API { 20 | return nil 21 | } 22 | 23 | func (a *dummy) Acquire(api.Vehicle) {} 24 | 25 | func (a *dummy) Release(api.Vehicle) {} 26 | 27 | func (a *dummy) IdentifyVehicleByStatus() api.Vehicle { 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /core/wrapper/chargemeter.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // ChargeMeter is a replacement for a physical charge meter. 8 | // It uses the charger's actual or max current to calculate power consumption. 9 | type ChargeMeter struct { 10 | sync.Mutex 11 | power float64 12 | } 13 | 14 | // SetPower updates meter's current power 15 | func (m *ChargeMeter) SetPower(power float64) { 16 | m.Lock() 17 | defer m.Unlock() 18 | m.power = power 19 | } 20 | 21 | // CurrentPower implements the api.Meter interface 22 | func (m *ChargeMeter) CurrentPower() (float64, error) { 23 | m.Lock() 24 | defer m.Unlock() 25 | return m.power, nil 26 | } 27 | -------------------------------------------------------------------------------- /meter/discovergy/types.go: -------------------------------------------------------------------------------- 1 | package discovergy 2 | 3 | const API = "https://api.inexogy.com/public/v1" 4 | 5 | type Meter struct { 6 | MeterID string `json:"meterId"` 7 | SerialNumber string `json:"serialNumber"` 8 | FullSerialNumber string `json:"fullSerialNumber"` 9 | } 10 | 11 | type Reading struct { 12 | Time int64 13 | Values struct { 14 | EnergyOut int64 15 | Energy1, Energy2 int64 16 | Voltage1, Voltage2, Voltage3 int64 17 | EnergyOut1, EnergyOut2 int64 18 | Power1, Power2, Power3 int64 19 | Power int64 20 | Energy int64 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /templates/definition/meter/kostal-piko-legacy.yaml: -------------------------------------------------------------------------------- 1 | template: kostal-piko-legacy 2 | products: 3 | - brand: Kostal 4 | description: 5 | generic: Piko (legacy) 6 | params: 7 | - name: usage 8 | choice: ["pv"] 9 | - name: host 10 | - name: user 11 | required: true 12 | - name: password 13 | required: true 14 | render: | 15 | type: custom 16 | power: 17 | {{- if eq .usage "pv" }} 18 | source: http 19 | uri: http://{{ .host }} 20 | auth: 21 | type: basic 22 | user: {{ .user }} 23 | password: {{ .password }} 24 | regex: '(?s)aktuell\s+]+>\s+(\d+)' 25 | default: 0 26 | {{- end }} 27 | -------------------------------------------------------------------------------- /templates/definition/vehicle/toyota.yaml: -------------------------------------------------------------------------------- 1 | template: toyota 2 | products: 3 | - brand: Toyota 4 | requirements: 5 | description: 6 | de: | 7 | Benötigt Toyota Connected Services Account. 8 | en: | 9 | Requires Toyota Connected Services Account. 10 | params: 11 | - preset: vehicle-common 12 | - name: user 13 | required: true 14 | - name: password 15 | required: true 16 | - name: vin 17 | example: JT... 18 | - name: cache 19 | default: 15m 20 | render: | 21 | type: toyota 22 | {{ include "vehicle-common" . }} 23 | user: {{ .user }} 24 | password: {{ .password }} 25 | vin: {{ .vin }} 26 | cache: {{ .cache }} 27 | -------------------------------------------------------------------------------- /tests/mqtt.ts: -------------------------------------------------------------------------------- 1 | import mqtt from "mqtt"; 2 | 3 | export async function isMqttReachable( 4 | broker: string, 5 | username: string, 6 | password: string 7 | ): Promise { 8 | try { 9 | const client = mqtt.connect(`mqtt://${broker}`, { 10 | connectTimeout: 2000, 11 | username, 12 | password, 13 | }); 14 | 15 | await new Promise((resolve, reject) => { 16 | client.once("connect", () => resolve()); 17 | client.once("error", (err) => reject(err)); 18 | }); 19 | 20 | client.end(); 21 | return true; // connection successful 22 | } catch { 23 | return false; // connection failed 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/js/components/Config/defaultYaml/heatpump.yaml: -------------------------------------------------------------------------------- 1 | ## required attributes [type: heatpump] 2 | 3 | setmaxpower: # update the maximum heating power 4 | source: js 5 | script: console.log(setmaxpower); # 6 | 7 | ## optional attributes (read-only) 8 | 9 | #getmaxpower: # heating power in W 10 | # source: const 11 | # value: 5000 12 | #power: # heating power in W 13 | # source: const 14 | # value: 2000 15 | #energy: # meter reading in kWh 16 | # source: const 17 | # value: 42.5 18 | #temp: # current temperature (°C) 19 | # source: const 20 | # value: 45.5 21 | #limittemp: # temperature limit (°C) configured in device 22 | # source: const 23 | # value: 60 24 | -------------------------------------------------------------------------------- /assets/js/components/MaterialIcon/DynamicPrice.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /assets/js/components/VehicleIcon/Climate.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /templates/definition/charger/delta.yaml: -------------------------------------------------------------------------------- 1 | template: delta 2 | products: 3 | - brand: Delta 4 | description: 5 | generic: AC Max Basic 6 | - brand: Delta 7 | description: 8 | generic: AC MAX Smart 9 | - brand: Delta 10 | description: 11 | generic: SLIM Charger 12 | - brand: Delta 13 | description: 14 | generic: Ultra Fast Charger 15 | capabilities: ["mA", "rfid"] 16 | requirements: 17 | evcc: ["sponsorship"] 18 | params: 19 | - name: modbus 20 | choice: ["rs485", "tcpip"] 21 | baudrate: 115200 22 | - name: connector 23 | render: | 24 | type: delta 25 | {{- include "modbus" . }} 26 | connector: {{ .connector }} 27 | -------------------------------------------------------------------------------- /util/logstash/element.go: -------------------------------------------------------------------------------- 1 | package logstash 2 | 3 | import ( 4 | "regexp" 5 | "slices" 6 | 7 | jww "github.com/spf13/jwalterweatherman" 8 | ) 9 | 10 | type element string 11 | 12 | var re = regexp.MustCompile(`^\[(.+?)\s*\] (\w+) `) 13 | 14 | func (e element) areaLevel() (string, jww.Threshold) { 15 | m := re.FindAllStringSubmatch(string(e), 1) 16 | if len(m) != 1 || len(m[0]) != 3 { 17 | return "", jww.LevelError 18 | } 19 | return m[0][1], LogLevelToThreshold(m[0][2]) 20 | } 21 | 22 | func (e element) match(areas []string, level jww.Threshold) bool { 23 | a, l := e.areaLevel() 24 | return (len(areas) == 0 || slices.Contains(areas, a)) && l >= level 25 | } 26 | -------------------------------------------------------------------------------- /vehicle/vw/params.go: -------------------------------------------------------------------------------- 1 | package vw 2 | 3 | import "net/url" 4 | 5 | const ( 6 | Brand = "VW" 7 | Country = "DE" 8 | 9 | // Authorization ClientID 10 | AuthClientID = "38761134-34d0-41f3-9a73-c4be88d7d337" 11 | ) 12 | 13 | // Authorization parameters 14 | var AuthParams = url.Values{ 15 | "response_type": {"code id_token token"}, 16 | "client_id": {"9496332b-ea03-4091-a224-8c746b885068@apps_vw-dilab_com"}, 17 | "redirect_uri": {"carnet://identity-kit/login"}, 18 | "scope": {"openid profile mbb"}, // cars birthdate nickname address phone 19 | } 20 | 21 | // TokenRefreshService parameters 22 | var TRSParams = url.Values{ 23 | "brand": {"vw"}, 24 | } 25 | -------------------------------------------------------------------------------- /templates/definition/meter/solaranzeige-mqtt.yaml: -------------------------------------------------------------------------------- 1 | template: solaranzeige 2 | products: 3 | - brand: Solaranzeige 4 | description: 5 | generic: Solaranzeige 6 | requirements: 7 | evcc: ["skiptest"] 8 | params: 9 | - name: usage 10 | choice: ["grid", "pv"] 11 | - preset: mqtt 12 | - name: topic 13 | default: solaranzeige/box1 14 | render: | 15 | type: custom 16 | power: 17 | source: mqtt 18 | {{- include "mqtt" . | indent 2 }} 19 | {{- if eq .usage "grid" }} 20 | topic: {{ .topic }}/einspeisung_bezug 21 | scale: -1 22 | {{- end }} 23 | {{- if eq .usage "pv" }} 24 | topic: {{ .topic }}/pv_leistung 25 | {{- end }} 26 | -------------------------------------------------------------------------------- /templates/definition/meter/volkszaehler-ws.yaml: -------------------------------------------------------------------------------- 1 | template: volkszaehler-ws 2 | products: 3 | - brand: Volkszähler 4 | description: 5 | generic: WebSocket API 6 | requirements: 7 | evcc: ["skiptest"] 8 | group: generic 9 | params: 10 | - name: usage 11 | choice: ["grid"] 12 | - name: host 13 | - name: port 14 | default: 8082 15 | - name: uuid 16 | required: true 17 | render: | 18 | type: custom 19 | power: # power reading 20 | source: ws # use websocket plugin 21 | uri: ws://{{ .host }}:{{ .port }}/socket 22 | jq: .data | select(.uuid=="{{ unquote .uuid }}") .tuples[0][1] # parse response json 23 | timeout: 30s 24 | scale: 1 25 | -------------------------------------------------------------------------------- /cmd/demo.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | _ "embed" // for yaml 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/evcc-io/evcc/api/globalconfig" 9 | ) 10 | 11 | //go:embed demo.yaml 12 | var demoYaml string 13 | 14 | func demoConfig(conf *globalconfig.All) error { 15 | viper.SetConfigType("yaml") 16 | if err := viper.ReadConfig(strings.NewReader(demoYaml)); err != nil { 17 | return fmt.Errorf("failed decoding demo config: %w", err) 18 | } 19 | 20 | if err := viper.UnmarshalExact(conf); err != nil { 21 | return fmt.Errorf("failed loading demo config: %w", err) 22 | } 23 | 24 | // parse log levels after reading config 25 | parseLogLevels() 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /assets/js/components/MaterialIcon/Shm.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /core/wrapper/chargetimer_test.go: -------------------------------------------------------------------------------- 1 | package wrapper 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/benbjohnson/clock" 8 | ) 9 | 10 | func TestTimer(t *testing.T) { 11 | ct := NewChargeTimer() 12 | clck := clock.NewMock() 13 | ct.clck = clck 14 | 15 | ct.StartCharge(false) 16 | clck.Add(time.Hour) 17 | ct.StopCharge() 18 | clck.Add(time.Hour) 19 | 20 | if d, err := ct.ChargeDuration(); d != 1*time.Hour || err != nil { 21 | t.Error(d, err) 22 | } 23 | 24 | // continue 25 | ct.StartCharge(true) 26 | clck.Add(2 * time.Hour) 27 | ct.StopCharge() 28 | 29 | if d, err := ct.ChargeDuration(); d != 3*time.Hour || err != nil { 30 | t.Error(d, err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /util/config/types.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Typed struct { 8 | Type string `json:"type"` 9 | Other map[string]any `mapstructure:",remain" yaml:",inline"` // TODO JSON serialization 10 | } 11 | 12 | type Named struct { 13 | Name string `json:"name"` 14 | Type string `json:"type"` 15 | Other map[string]any `mapstructure:",remain" yaml:",inline"` // TODO JSON serialization 16 | } 17 | 18 | // Property returns the value of the named property 19 | func (n Named) Property(key string) any { 20 | for k, v := range n.Other { 21 | if strings.EqualFold(k, key) { 22 | return v 23 | } 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /vehicle/audi/etron/params.go: -------------------------------------------------------------------------------- 1 | package etron 2 | 3 | import "net/url" 4 | 5 | // Authorization parameters 6 | var AuthParams = url.Values{ 7 | "response_type": {"code id_token token"}, 8 | "client_id": {"f4d0934f-32bf-4ce4-b3c4-699a7049ad26@apps_vw-dilab_com"}, 9 | "redirect_uri": {"myaudi:///"}, 10 | "scope": {"openid profile mbb"}, // vin badge birthdate nickname email address phone name picture 11 | "prompt": {"login"}, 12 | "ui_locales": {"de-DE"}, 13 | } 14 | 15 | var IDKParams = url.Values{ 16 | "client_id": {"f4d0934f-32bf-4ce4-b3c4-699a7049ad26@apps_vw-dilab_com"}, 17 | "redirect_uri": {"myaudi:///"}, 18 | } 19 | 20 | const AZSConfig = "myaudi" 21 | -------------------------------------------------------------------------------- /assets/js/components/Forecast/types.ts: -------------------------------------------------------------------------------- 1 | export function isForecastSlot(obj?: TimeseriesEntry | ForecastSlot): obj is ForecastSlot { 2 | return (obj as ForecastSlot).start !== undefined; 3 | } 4 | 5 | export interface TimeseriesEntry { 6 | val: number; 7 | ts: string; 8 | } 9 | 10 | export interface ForecastSlot { 11 | start: string; 12 | end: string; 13 | value: number; 14 | } 15 | 16 | export interface EnergyByDay { 17 | energy: number; 18 | complete: boolean; 19 | } 20 | 21 | export interface SolarDetails { 22 | scale?: number; 23 | today?: EnergyByDay; 24 | tomorrow?: EnergyByDay; 25 | dayAfterTomorrow?: EnergyByDay; 26 | timeseries?: TimeseriesEntry[]; 27 | } 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: evcc-io 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /assets/js/components/MaterialIcon/Edit.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /meter/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/evcc-io/evcc/api" 9 | reg "github.com/evcc-io/evcc/util/registry" 10 | ) 11 | 12 | var Registry = reg.New[api.Meter]("meter") 13 | 14 | // NewFromConfig creates meter from configuration 15 | func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}) (api.Meter, error) { 16 | factory, err := Registry.Get(strings.ToLower(typ)) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | v, err := factory(ctx, other) 22 | if err != nil { 23 | return nil, fmt.Errorf("cannot create meter type '%s': %w", typ, err) 24 | } 25 | 26 | return v, nil 27 | } 28 | -------------------------------------------------------------------------------- /meter/measurement/energy.go: -------------------------------------------------------------------------------- 1 | package measurement 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/evcc-io/evcc/plugin" 8 | ) 9 | 10 | type Energy struct { 11 | Power plugin.Config 12 | Energy *plugin.Config // optional 13 | } 14 | 15 | func (cc *Energy) Configure(ctx context.Context) ( 16 | func() (float64, error), 17 | func() (float64, error), 18 | error, 19 | ) { 20 | powerG, err := cc.Power.FloatGetter(ctx) 21 | if err != nil { 22 | return nil, nil, fmt.Errorf("power: %w", err) 23 | } 24 | 25 | energyG, err := cc.Energy.FloatGetter(ctx) 26 | if err != nil { 27 | return nil, nil, fmt.Errorf("energy: %w", err) 28 | } 29 | 30 | return powerG, energyG, nil 31 | } 32 | -------------------------------------------------------------------------------- /templates/definition/charger/daheimladen-pro.yaml: -------------------------------------------------------------------------------- 1 | template: daheimladen-pro 2 | products: 3 | - brand: DaheimLaden 4 | description: 5 | generic: Smart/Touch Pro 6 | requirements: 7 | description: 8 | de: Die Phasenumschaltung benötigt mindestens die Firmware-Version "M3W_3.11". Während der Umschaltung pausiert die Ladung für zwei Minuten. 9 | en: Phase switching requires at least firmware version "M3W_3.11." Charging pauses for two minutes during the phase switching. 10 | capabilities: ["1p3p", "mA"] 11 | params: 12 | - name: host 13 | - name: port 14 | default: 502 15 | render: | 16 | type: daheimladen-mb 17 | uri: {{ .host }}:{{ .port }} 18 | phases1p3p: true 19 | -------------------------------------------------------------------------------- /templates/definition/charger/wallbe-pre2019.yaml: -------------------------------------------------------------------------------- 1 | template: wallbe-pre2019 2 | deprecated: true 3 | products: 4 | - brand: Wallbe 5 | description: 6 | de: Eco (vor ~2019) 7 | en: Eco (pre ~2019) 8 | - brand: Wallbe 9 | description: 10 | de: Pro (vor ~2019) 11 | en: Pro (pre ~2019) 12 | requirements: 13 | description: 14 | en: DIP switch 10 must be set to 'ON'. 15 | de: Im Gerät muss der DIP Schalter 10 auf 'ON' gestellt sein. 16 | params: 17 | - name: host 18 | - name: port 19 | default: 502 20 | render: | 21 | type: wallbe 22 | uri: {{ .host }}:{{ .port }} 23 | legacy: true # set only for older Wallbe devices (pre ~2019, old controller firmware) 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "assets/js/**/*", "tests/**/*"], 4 | "compilerOptions": { 5 | "allowUnreachableCode": false, 6 | "allowUnusedLabels": false, 7 | "noFallthroughCasesInSwitch": true, 8 | "noImplicitReturns": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "composite": true, 13 | "allowJs": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "@/*": ["assets/js/*"] 17 | }, 18 | "plugins": [ 19 | { 20 | "name": "typescript-eslint-language-service" 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /vehicle/ford/autonomic/types.go: -------------------------------------------------------------------------------- 1 | package autonomic 2 | 3 | import "time" 4 | 5 | type IntValue struct { 6 | UpdateTime time.Time 7 | Value int 8 | } 9 | type FloatValue struct { 10 | UpdateTime time.Time 11 | Value float64 12 | } 13 | type StringValue struct { 14 | UpdateTime time.Time 15 | Value string 16 | } 17 | 18 | type MetricsResponse struct { 19 | Metrics struct { 20 | Position struct { 21 | Value struct { 22 | Location struct { 23 | Lat, Lon float64 24 | } 25 | } 26 | } 27 | Odometer FloatValue 28 | XevPlugChargerStatus StringValue 29 | XevBatteryRange FloatValue 30 | XevBatteryStateOfCharge FloatValue 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /assets/js/components/Config/EebusModal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /charger/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/evcc-io/evcc/api" 9 | reg "github.com/evcc-io/evcc/util/registry" 10 | ) 11 | 12 | var Registry = reg.New[api.Charger]("charger") 13 | 14 | // NewFromConfig creates charger from configuration 15 | func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}) (api.Charger, error) { 16 | factory, err := Registry.Get(strings.ToLower(typ)) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | v, err := factory(ctx, other) 22 | if err != nil { 23 | return nil, fmt.Errorf("cannot create charger type '%s': %w", typ, err) 24 | } 25 | 26 | return v, nil 27 | } 28 | -------------------------------------------------------------------------------- /meter/mystrom.go: -------------------------------------------------------------------------------- 1 | package meter 2 | 3 | import ( 4 | "github.com/evcc-io/evcc/api" 5 | "github.com/evcc-io/evcc/meter/mystrom" 6 | "github.com/evcc-io/evcc/util" 7 | ) 8 | 9 | // myStrom switch: 10 | // https://api.mystrom.ch/#fbb2c698-e37a-4584-9324-3f8b2f615fe2 11 | 12 | func init() { 13 | registry.Add("mystrom", NewMyStromFromConfig) 14 | } 15 | 16 | // NewMyStromFromConfig creates a myStrom meter from generic config 17 | func NewMyStromFromConfig(other map[string]interface{}) (api.Meter, error) { 18 | var cc struct { 19 | URI string 20 | } 21 | 22 | if err := util.DecodeOther(other, &cc); err != nil { 23 | return nil, err 24 | } 25 | 26 | return mystrom.NewConnection(cc.URI), nil 27 | } 28 | -------------------------------------------------------------------------------- /vehicle/saic/requests/hashUtils.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha1" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | ) 9 | 10 | func Md5(value string) string { 11 | if len(value) == 0 { 12 | return "" 13 | } 14 | result := md5.Sum([]byte(value)) 15 | return hex.EncodeToString(result[:]) 16 | } 17 | 18 | func Sha1(value string) string { 19 | if len(value) == 0 { 20 | return "" 21 | } 22 | result := sha1.Sum([]byte(value)) 23 | return hex.EncodeToString(result[:]) 24 | } 25 | 26 | func Sha256(value string) string { 27 | if len(value) == 0 { 28 | return "" 29 | } 30 | result := sha256.Sum256([]byte(value)) 31 | return hex.EncodeToString(result[:]) 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /charger/measurement/energy.go: -------------------------------------------------------------------------------- 1 | package measurement 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/evcc-io/evcc/plugin" 8 | ) 9 | 10 | type Energy struct { 11 | Power *plugin.Config // optional 12 | Energy *plugin.Config // optional 13 | } 14 | 15 | func (cc *Energy) Configure(ctx context.Context) ( 16 | func() (float64, error), 17 | func() (float64, error), 18 | error, 19 | ) { 20 | powerG, err := cc.Power.FloatGetter(ctx) 21 | if err != nil { 22 | return nil, nil, fmt.Errorf("power: %w", err) 23 | } 24 | 25 | energyG, err := cc.Energy.FloatGetter(ctx) 26 | if err != nil { 27 | return nil, nil, fmt.Errorf("energy: %w", err) 28 | } 29 | 30 | return powerG, energyG, nil 31 | } 32 | -------------------------------------------------------------------------------- /templates/definition/charger/daheimladen-mb.yaml: -------------------------------------------------------------------------------- 1 | template: daheimladen-mb 2 | products: 3 | - brand: DaheimLaden 4 | description: 5 | generic: Smart/Touch 6 | requirements: 7 | description: 8 | de: Die Wallbox muss über eine aktuelle Firmware mit Modbus-Unterstützung verfügen. In den Einstellungen muss "Nachladen" (Smart) bzw. "RSDA" (Touch) aktiviert sein 9 | en: Wallbox must be operated with a recent firmware including Modbus support. Furthermore, “Nachladen” (Smart) or “RSDA” (Touch) must be activated in settings. 10 | capabilities: ["mA"] 11 | params: 12 | - name: host 13 | - name: port 14 | default: 502 15 | render: | 16 | type: daheimladen-mb 17 | uri: {{ .host }}:{{ .port }} 18 | -------------------------------------------------------------------------------- /templates/definition/meter/inepro.yaml: -------------------------------------------------------------------------------- 1 | template: inepro 2 | products: 3 | - brand: inepro 4 | description: 5 | generic: PRO380-MOD 6 | params: 7 | - name: usage 8 | choice: ["grid", "charge"] 9 | - name: modbus 10 | choice: ["rs485"] 11 | render: | 12 | type: mbmd 13 | {{- include "modbus" . }} 14 | model: inepro 15 | power: Power 16 | energy: Import 17 | currents: 18 | - CurrentL1 19 | - CurrentL2 20 | - CurrentL3 21 | {{- if eq .usage "grid" }} 22 | powers: 23 | - PowerL1 24 | - PowerL2 25 | - PowerL3 26 | {{- end }} 27 | {{- if eq .usage "charge" }} 28 | voltages: 29 | - VoltageL1 30 | - VoltageL2 31 | - VoltageL3 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /templates/definition/meter/iometer.yaml: -------------------------------------------------------------------------------- 1 | template: iometer 2 | products: 3 | - brand: IOmeter 4 | params: 5 | - name: usage 6 | choice: ["grid"] 7 | - name: host 8 | description: 9 | de: IP deines IOmeter 10 | en: IP of your IOmeter 11 | render: | 12 | type: custom 13 | power: 14 | source: http 15 | uri: http://{{ .host }}/v1/reading 16 | method: GET 17 | jq: .meter.reading.registers[] | select(.obis == "01-00:10.07.00*ff") | .value 18 | cache: 10s 19 | energy: 20 | source: http 21 | uri: http://{{ .host }}/v1/reading 22 | method: GET 23 | jq: (.meter.reading.registers[] | select(.obis == "01-00:01.08.00*ff") | .value) / 1000 24 | cache: 10s 25 | -------------------------------------------------------------------------------- /assets/js/components/Issue/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface IssueData { 2 | title: string; 3 | description: string; 4 | steps: string; 5 | version: string; 6 | } 7 | 8 | export interface SectionData { 9 | included: boolean; 10 | content: string; 11 | } 12 | 13 | export interface Sections { 14 | yamlConfig: SectionData; 15 | uiConfig: SectionData; 16 | state: SectionData; 17 | logs: SectionData; 18 | } 19 | 20 | export interface GitHubContent { 21 | body: string; 22 | additional?: string; 23 | } 24 | 25 | // First level items are joint with empty line (\n\n), second level with line wrap (\n) 26 | export type Template = (string | string[])[]; 27 | 28 | export type HelpType = "discussion" | "issue"; 29 | -------------------------------------------------------------------------------- /templates/definition/charger/pracht-alpha.yaml: -------------------------------------------------------------------------------- 1 | template: pracht-alpha 2 | products: 3 | - brand: Pracht 4 | description: 5 | generic: Alpha XT 6 | - brand: Pracht 7 | description: 8 | generic: XT+ 9 | - brand: Pracht 10 | description: 11 | generic: Mono XT 12 | - brand: Pracht 13 | description: 14 | generic: PNI 15 | requirements: 16 | evcc: ["sponsorship"] 17 | params: 18 | - name: modbus 19 | choice: ["rs485", "tcpip"] 20 | baudrate: 9600 21 | comset: 8N1 22 | id: 1 23 | - name: connector 24 | - name: timeout 25 | render: | 26 | type: pracht-alpha 27 | {{- include "modbus" . }} 28 | connector: {{ .connector }} 29 | timeout: {{ .timeout }} 30 | -------------------------------------------------------------------------------- /assets/js/components/Config/ExperimentalBanner.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /charger/ocpp/helper_test.go: -------------------------------------------------------------------------------- 1 | package ocpp 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSortByAge(t *testing.T) { 12 | assert.Equal(t, []types.MeterValue{ 13 | {Timestamp: nil}, 14 | {Timestamp: types.NewDateTime(time.UnixMilli(1))}, 15 | {Timestamp: types.NewDateTime(time.UnixMilli(2))}, 16 | {Timestamp: types.NewDateTime(time.UnixMilli(3))}, 17 | }, sortByAge([]types.MeterValue{ 18 | {Timestamp: types.NewDateTime(time.UnixMilli(3))}, 19 | {Timestamp: types.NewDateTime(time.UnixMilli(1))}, 20 | {Timestamp: nil}, 21 | {Timestamp: types.NewDateTime(time.UnixMilli(2))}, 22 | })) 23 | } 24 | -------------------------------------------------------------------------------- /core/loadpoint/remote.go: -------------------------------------------------------------------------------- 1 | package loadpoint 2 | 3 | import "strings" 4 | 5 | // RemoteDemand defines external status demand 6 | type RemoteDemand string 7 | 8 | // remote status demand definition 9 | const ( 10 | RemoteEnable RemoteDemand = "" 11 | RemoteHardDisable RemoteDemand = "hard" 12 | RemoteSoftDisable RemoteDemand = "soft" 13 | ) 14 | 15 | // RemoteDemandString converts string to RemoteDemand 16 | func RemoteDemandString(demand string) (RemoteDemand, error) { 17 | switch strings.ToLower(demand) { 18 | case string(RemoteHardDisable): 19 | return RemoteHardDisable, nil 20 | case string(RemoteSoftDisable): 21 | return RemoteSoftDisable, nil 22 | default: 23 | return RemoteEnable, nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /templates/definition/meter/solarmax-inverter-smt.yaml: -------------------------------------------------------------------------------- 1 | template: solarmax-smt 2 | products: 3 | - brand: SolarMax 4 | description: 5 | generic: SolarMax SMT 6 | params: 7 | - name: usage 8 | choice: ["pv"] 9 | - name: modbus 10 | choice: ["tcpip"] 11 | id: 1 12 | render: | 13 | type: custom 14 | power: 15 | source: modbus 16 | {{- include "modbus" . | indent 2 }} 17 | register: 18 | address: 4151 # PAC 19 | type: holding 20 | decode: uint32 21 | scale: 0.1 22 | energy: 23 | source: modbus 24 | {{- include "modbus" . | indent 2 }} 25 | register: 26 | address: 4129 # Total 27 | type: holding 28 | decode: uint32 29 | scale: 0.1 30 | -------------------------------------------------------------------------------- /templates/definition/vehicle/offline.yaml: -------------------------------------------------------------------------------- 1 | template: offline 2 | products: 3 | - description: 4 | en: Generic vehicle (without API) 5 | de: Generisches Fahrzeug (ohne API) 6 | requirements: 7 | description: 8 | de: 9 | group: generic 10 | params: 11 | - preset: vehicle-common 12 | - name: coarsecurrent 13 | advanced: true 14 | - name: welcomecharge 15 | advanced: true 16 | render: | 17 | type: custom 18 | {{- include "vehicle-common" . }} 19 | features: 20 | - offline 21 | {{- if eq .coarsecurrent "true" }} 22 | - coarsecurrent 23 | {{- end }} 24 | {{- if eq .welcomecharge "true" }} 25 | - welcomecharge 26 | {{- end }} 27 | soc: 28 | source: const 29 | value: 0 30 | -------------------------------------------------------------------------------- /tests/sessions.evcc.yaml: -------------------------------------------------------------------------------- 1 | interval: 0.1s 2 | 3 | site: 4 | title: Sessions 5 | meters: 6 | grid: grid 7 | 8 | meters: 9 | - name: grid 10 | type: template 11 | template: demo-meter 12 | power: 200 13 | 14 | loadpoints: 15 | - title: Carport 16 | charger: charger1 17 | vehicle: tesla 18 | - title: Garage 19 | charger: charger2 20 | vehicle: egolf 21 | 22 | vehicles: 23 | - name: tesla 24 | title: weißes Model 3 25 | - name: egolf 26 | title: blauer e-Golf 27 | 28 | chargers: 29 | - name: charger1 30 | type: template 31 | template: demo-charger 32 | status: B 33 | - name: charger2 34 | type: template 35 | template: demo-charger 36 | status: B 37 | -------------------------------------------------------------------------------- /assets/js/components/Config/HemsModal.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /templates/definition/meter/be-mpm3pm.yaml: -------------------------------------------------------------------------------- 1 | template: mpm3pm 2 | products: 3 | - brand: Bernecker Engineering 4 | description: 5 | generic: MPM3PM 6 | params: 7 | - name: usage 8 | choice: ["grid", "charge"] 9 | - name: modbus 10 | choice: ["rs485"] 11 | render: | 12 | type: mbmd 13 | {{- include "modbus" . }} 14 | model: MPM 15 | power: Power 16 | energy: Import 17 | currents: 18 | - CurrentL1 19 | - CurrentL2 20 | - CurrentL3 21 | {{- if eq .usage "grid" }} 22 | powers: 23 | - PowerL1 24 | - PowerL2 25 | - PowerL3 26 | {{- end }} 27 | {{- if eq .usage "charge" }} 28 | voltages: 29 | - VoltageL1 30 | - VoltageL2 31 | - VoltageL3 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /templates/definition/tariff/allinpower.yaml: -------------------------------------------------------------------------------- 1 | template: allinpower 2 | deprecated: true 3 | products: 4 | - brand: All in Power 5 | requirements: 6 | evcc: ["skiptest"] 7 | group: price 8 | countries: ["NL"] 9 | params: 10 | - preset: tariff-base 11 | render: | 12 | type: custom 13 | {{ include "tariff-base" . }} 14 | forecast: 15 | source: http 16 | uri: https://api.allinpower.nl/troodon/api/p/spot_market/prices/?product_type=ELK 17 | jq: | 18 | [.timestamps, .prices] | transpose | map({ 19 | "start": .[0] | strptime("%FT%T.%f%z") | strftime("%FT%TZ"), 20 | "end": .[0] | strptime("%FT%T.%f%z") | mktime + 3600 | strftime("%FT%TZ"), 21 | "value": .[1] 22 | }) | tostring 23 | -------------------------------------------------------------------------------- /templates/definition/tariff/gruenstromindex.yaml: -------------------------------------------------------------------------------- 1 | template: grünstromindex 2 | products: 3 | - brand: Grünstromindex 4 | requirements: 5 | description: 6 | de: "Regionale Emissionsdaten von https://gruenstromindex.de" 7 | en: "Regional emission data from https://gruenstromindex.de" 8 | evcc: ["skiptest"] 9 | group: co2 10 | countries: ["DE"] 11 | params: 12 | - name: zip 13 | required: true 14 | - name: token 15 | help: 16 | de: "Token für den Zugriff auf die API von https://console.corrently.io/" 17 | en: "Token for accessing the API from https://console.corrently.io" 18 | render: | 19 | type: grünstromindex 20 | features: ["cacheable"] 21 | zip: {{ .zip }} 22 | token: {{ .token }} 23 | -------------------------------------------------------------------------------- /vehicle/skoda/params.go: -------------------------------------------------------------------------------- 1 | package skoda 2 | 3 | import "net/url" 4 | 5 | const ( 6 | Brand = "VW" 7 | Country = "CZ" 8 | 9 | // Authorization ClientID 10 | AuthClientID = "afb0473b-6d82-42b8-bfea-cead338c46ef" 11 | ) 12 | 13 | // Skoda native api 14 | var AuthParams = url.Values{ 15 | "response_type": {"code id_token"}, 16 | "client_id": {"7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com"}, 17 | "redirect_uri": {"myskoda://redirect/login/"}, 18 | "scope": {"address badge birthdate cars driversLicense dealers email mileage mbb nationalIdentifier openid phone profession profile vin"}, 19 | } 20 | 21 | // TokenRefreshService parameters 22 | var TRSParams = url.Values{ 23 | "brand": {"skoda"}, 24 | } 25 | -------------------------------------------------------------------------------- /assets/js/components/Footer/Footer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | -------------------------------------------------------------------------------- /assets/js/components/MaterialIcon/PlanEnd.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /assets/js/components/MaterialIcon/PlanStart.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /charger/measurement/heating.go: -------------------------------------------------------------------------------- 1 | package measurement 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/evcc-io/evcc/plugin" 8 | ) 9 | 10 | type Temperature struct { 11 | Temp *plugin.Config // optional 12 | LimitTemp *plugin.Config // optional 13 | } 14 | 15 | func (cc *Temperature) Configure(ctx context.Context) ( 16 | func() (float64, error), 17 | func() (int64, error), 18 | error, 19 | ) { 20 | tempG, err := cc.Temp.FloatGetter(ctx) 21 | if err != nil { 22 | return nil, nil, fmt.Errorf("temp: %w", err) 23 | } 24 | 25 | limitTempG, err := cc.LimitTemp.IntGetter(ctx) 26 | if err != nil { 27 | return nil, nil, fmt.Errorf("limit temp: %w", err) 28 | } 29 | 30 | return tempG, limitTempG, nil 31 | } 32 | -------------------------------------------------------------------------------- /util/request/json.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | ) 8 | 9 | // errorReader wraps an error with an io.Reader 10 | type errorReader struct { 11 | err error 12 | } 13 | 14 | func (r *errorReader) Read(p []byte) (int, error) { 15 | return 0, r.err 16 | } 17 | 18 | func (r *errorReader) Seek(offset int64, whence int) (int64, error) { 19 | return 0, r.err 20 | } 21 | 22 | // MarshalJSON marshals JSON into an io.ReadSeeker 23 | func MarshalJSON(data interface{}) io.ReadSeeker { 24 | if data == nil { 25 | return nil 26 | } 27 | 28 | body, err := json.Marshal(data) 29 | if err != nil { 30 | return &errorReader{err: err} 31 | } 32 | 33 | return bytes.NewReader(body) 34 | } 35 | -------------------------------------------------------------------------------- /assets/js/components/Config/TariffsModal.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | --------------------------------------------------------------------------------