Skip to content

Commit

Permalink
Merge branch 'evcc-io:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
xerion3800 authored Aug 13, 2024
2 parents d046a06 + 57b19e7 commit 1057ad1
Show file tree
Hide file tree
Showing 69 changed files with 1,485 additions and 317 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ evcc is an extensible EV Charge Controller and home energy management system. Fe

- simple and clean user interface
- wide range of supported [chargers](https://docs.evcc.io/docs/devices/chargers):
- ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Mennekes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), [openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more
- ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Mennekes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), NRGKick Gen2,[openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more
- experimental EEBus support (Elli, PMCC)
- experimental OCPP support
- Build-your-own: Phoenix Contact (includes ESL Walli), [EVSE DIN](http://evracing.cz/simple-evse-wallbox)
Expand Down
16 changes: 16 additions & 0 deletions assets/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,27 @@ const { protocol, hostname, port, pathname } = window.location;

const base = protocol + "//" + hostname + (port ? ":" + port : "") + pathname;

// override the way axios serializes arrays in query parameters (a=1&a=2&a=3 instead of a[]=1&a[]=2&a[]=3)
function customParamsSerializer(params) {
const queryString = Object.keys(params)
.filter((key) => params[key] !== null)
.map((key) => {
const value = params[key];
if (Array.isArray(value)) {
return value.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join("&");
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
.join("&");
return queryString;
}

const api = axios.create({
baseURL: base + "api/",
headers: {
Accept: "application/json",
},
paramsSerializer: customParamsSerializer,
});

// global error handling
Expand Down
126 changes: 126 additions & 0 deletions assets/js/components/MultiSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<template>
<div>
<button
class="form-select text-start text-nowrap"
type="button"
:id="id"
data-bs-toggle="dropdown"
aria-expanded="false"
data-bs-auto-close="outside"
>
<slot></slot>
</button>
<ul class="dropdown-menu dropdown-menu-end" ref="dropdown" :aria-labelledby="id">
<template v-if="selectAllLabel">
<li class="dropdown-item p-0">
<label class="form-check px-3 py-2">
<input
class="form-check-input ms-0 me-2"
type="checkbox"
value="all"
@change="toggleCheckAll()"
:checked="allOptionsSelected"
/>
<div class="form-check-label">{{ selectAllLabel }}</div>
</label>
</li>
<li><hr class="dropdown-divider" /></li>
</template>
<li v-for="option in options" :key="option.value" class="dropdown-item p-0">
<label class="form-check px-3 py-2 d-flex" :for="option.value">
<input
class="form-check-input ms-0 me-2"
type="checkbox"
:id="option.value"
:value="option.value"
v-model="internalValue"
/>
<div class="form-check-label">
{{ option.name }}
</div>
</label>
</li>
</ul>
</div>
</template>

<script>
import Dropdown from "bootstrap/js/dist/dropdown";
export default {
name: "MultiSelect",
props: {
id: String,
value: { type: Array, default: () => [] },
options: { type: Array, default: () => [] },
selectAllLabel: String,
},
emits: ["open", "update:modelValue"],
data() {
return {
internalValue: [...this.value],
};
},
mounted() {
this.$refs.dropdown.addEventListener("show.bs.dropdown", this.open);
},
unmounted() {
this.$refs.dropdown?.removeEventListener("show.bs.dropdown", this.open);
},
computed: {
allOptionsSelected() {
return this.internalValue.length === this.options.length;
},
noneSelected() {
return this.internalValue.length === 0;
},
},
watch: {
options: {
immediate: true,
handler(newOptions) {
// If value is empty, set internalValue to include all options
if (this.value.length === 0) {
this.internalValue = newOptions.map((option) => option.value);
} else {
// Otherwise, keep selected options that still exist in the new options
this.internalValue = this.internalValue.filter((value) =>
newOptions.some((option) => option.value === value)
);
}
this.$nextTick(() => {
Dropdown.getOrCreateInstance(this.$refs.dropdown).update();
});
},
},
value: {
immediate: true,
handler(newValue) {
this.internalValue =
newValue.length === 0 && this.options.length > 0
? this.options.map((o) => o.value)
: [...newValue];
},
},
internalValue(newValue) {
if (this.allOptionsSelected || this.noneSelected) {
this.$emit("update:modelValue", []);
} else {
this.$emit("update:modelValue", newValue);
}
},
},
methods: {
open() {
this.$emit("open");
},
toggleCheckAll() {
if (this.allOptionsSelected) {
this.internalValue = [];
} else {
this.internalValue = this.options.map((option) => option.value);
}
},
},
};
</script>
7 changes: 7 additions & 0 deletions assets/js/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ export default function setupRouter(i18n) {
path: "/log",
component: () => import("./views/Log.vue"),
beforeEnter: ensureAuth,
props: (route) => {
const { areas, level } = route.query;
return {
areas: areas ? areas.split(",") : undefined,
level,
};
},
},
],
});
Expand Down
93 changes: 65 additions & 28 deletions assets/js/views/Log.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,26 @@
<div class="filterLevel col-6 col-lg-2">
<select
class="form-select"
v-model="level"
:aria-label="$t('log.levelLabel')"
@change="updateLogs()"
:value="level"
@input="changeLevel"
>
<option v-for="level in levels" :key="level" :value="level">
{{ level }}
<option v-for="l in levels" :key="l" :value="l">
{{ l.toUpperCase() }}
</option>
</select>
</div>
<div class="filterAreas col-6 col-lg-2">
<select
class="form-select"
v-model="area"
:aria-label="$t('log.areaLabel')"
@focus="updateAreas()"
@change="updateLogs()"
<MultiSelect
id="logAreasSelect"
:modelValue="areas"
:options="areaOptions"
:selectAllLabel="$t('log.selectAll')"
@update:modelValue="changeAreas"
@open="updateAreas()"
>
<option value="">{{ $t("log.areas") }}</option>
<hr />
<option v-for="area in areas" :key="area" :value="area">
{{ area }}
</option>
</select>
{{ areasLabel }}
</MultiSelect>
</div>
</div>
<hr class="my-0" />
Expand Down Expand Up @@ -112,29 +109,33 @@ import "@h2d2/shopicons/es/regular/download";
import TopHeader from "../components/TopHeader.vue";
import Play from "../components/MaterialIcon/Play.vue";
import Record from "../components/MaterialIcon/Record.vue";
import MultiSelect from "../components/MultiSelect.vue";
import api from "../api";
import store from "../store";
const LEVELS = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
const DEFAULT_LEVEL = "DEBUG";
const LEVELS = ["fatal", "error", "warn", "info", "debug", "trace"];
const DEFAULT_LEVEL = "debug";
const DEFAULT_COUNT = 1000;
const levelMatcher = new RegExp(`\\[.*?\\] (${LEVELS.join("|")})`);
const levelMatcher = new RegExp(`\\[.*?\\] (${LEVELS.map((l) => l.toUpperCase()).join("|")})`);
export default {
name: "Log",
components: {
TopHeader,
Play,
Record,
MultiSelect,
},
props: {
areas: { type: Array, default: () => [] },
level: { type: String, default: DEFAULT_LEVEL },
},
data() {
return {
lines: [],
areas: [],
availableAreas: [],
search: "",
level: DEFAULT_LEVEL,
area: "",
timeout: null,
levels: LEVELS,
busy: false,
Expand Down Expand Up @@ -168,6 +169,18 @@ export default {
return { key, className, line };
});
},
areaOptions() {
return this.availableAreas.map((area) => ({ name: area, value: area }));
},
areasLabel() {
if (this.areas.length === 0) {
return this.$t("log.areas");
} else if (this.areas.length === 1) {
return this.areas[0];
} else {
return this.$t("log.nAreas", { count: this.areas.length });
}
},
showMoreButton() {
return this.lines.length === DEFAULT_COUNT;
},
Expand All @@ -179,16 +192,24 @@ export default {
if (this.level) {
params.append("level", this.level);
}
if (this.area) {
params.append("area", this.area);
}
this.areas.forEach((area) => {
params.append("area", area);
});
params.append("format", "txt");
return `./api/system/log?${params.toString()}`;
},
autoFollow() {
return this.timeout !== null;
},
},
watch: {
selectedAreas() {
this.updateLogs();
},
level() {
this.updateLogs();
},
},
methods: {
async updateLogs(showAll) {
// prevent concurrent requests
Expand All @@ -198,8 +219,8 @@ export default {
this.busy = true;
const response = await api.get("/system/log", {
params: {
level: this.level?.toLocaleLowerCase() || null,
area: this.area || null,
level: this.level || null,
area: this.areas.length ? this.areas : null,
count: showAll ? null : DEFAULT_COUNT,
},
});
Expand Down Expand Up @@ -232,7 +253,7 @@ export default {
async updateAreas() {
try {
const response = await api.get("/system/log/areas");
this.areas = response.data?.result || [];
this.availableAreas = response.data?.result || [];
} catch (e) {
console.error(e);
}
Expand Down Expand Up @@ -260,6 +281,22 @@ export default {
this.startInterval();
}
},
updateQuery({ level, areas }) {
let newLevel = level || this.level;
let newAreas = areas || this.areas;
// reset to default level
if (newLevel === DEFAULT_LEVEL) newLevel = undefined;
newAreas = newAreas.length ? newAreas.join(",") : undefined;
this.$router.push({
query: { level: newLevel, areas: newAreas },
});
},
changeLevel(event) {
this.updateQuery({ level: event.target.value });
},
changeAreas(areas) {
this.updateQuery({ areas });
},
},
};
</script>
Expand Down
3 changes: 1 addition & 2 deletions charger/easee.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,7 @@ func (c *Easee) chargerSite(charger string) (easee.Site, error) {

// connect creates an HTTP connection to the signalR hub
func (c *Easee) connect(ts oauth2.TokenSource) func() (signalr.Connection, error) {
bo := backoff.NewExponentialBackOff()
bo.MaxInterval = time.Minute
bo := backoff.NewExponentialBackOff(backoff.WithMaxInterval(time.Minute))

return func() (conn signalr.Connection, err error) {
defer func() {
Expand Down
Loading

0 comments on commit 1057ad1

Please sign in to comment.