Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b21ebda
chore: speed up the Playwright run (#35325)
andig Oct 26, 2026
d217b8b
Optimizer: estimate vehicle state from charged energy without soc (#3…
andig Oct 26, 2026
15f843d
Tariff (Octopus DE): fix time-of-use slots shifting after DST change …
github-actions[bot] Oct 26, 2026
5498ea2
EEBus: support single-phase chargers and dynamic phase limits (#35133)
denisivanov Oct 26, 2026
e05d5bf
Shelly Top AC: handle charger_insert work state (#35345)
github-actions[bot] Oct 26, 2026
12a658a
Tariff (Home Assistant): support sub-hourly solar forecast slots (#35…
github-actions[bot] Oct 26, 2026
1b4e112
chore: speed up Playwright with reduced motion and faster polling (#3…
naltatis Oct 26, 2026
223c48c
EcoFlow Stream: detect leader among multiple serials (#35334)
andig Oct 26, 2026
b51e0fc
Loadpoint: adopt charger phases while disabled (#35383)
andig Oct 26, 2026
c69db00
fix(eudataact): support Auth0 Universal Login Page for CUPRA/VW (#35385)
juergenbosch Oct 26, 2026
12169b7
sonnenBatterie: repeat charge setpoint while charging (#35391)
andig Oct 26, 2026
18ad321
Tariff (Energinet): include current quarter-hour (#35396)
andig Oct 26, 2026
c4d4571
Optimizer: suggest the battery mode control applies (#35402)
andig Oct 26, 2026
08caa0e
chore: move Playwright simulator off the EEBus port range (#35381)
naltatis Oct 26, 2026
3a456cb
Config: fix legacy circuits modal docs link (#35422)
github-actions[bot] Oct 26, 2026
4b02df4
UI: render markdown with marked instead of snarkdown (#35295)
andig Oct 27, 2026
d983b91
UI: ignore safe-area insets when embedded in an iframe (#35448)
naltatis Oct 27, 2026
0880587
Config/Energyflow: sort consumers alphabetically (#35433)
naltatis Oct 27, 2026
2997876
Optimizer: don't treat pv battery charging as grid charging (#35428)
andig Oct 27, 2026
9890c9f
Kostal Piko MP Plus: cache measurements request (#35404)
andig Oct 27, 2026
927ed3c
Heating: demand predictor selector in loadpoint settings (#34342)
daniel309 Oct 27, 2026
77125c5
Merge remote-tracking branch 'origin/master' into resolve/upstream-20…
jeffborg Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,13 @@ input[list]::-webkit-list-button {
}
}

html.embedded {
--safe-area-inset-top: 0px;
--safe-area-inset-bottom: 0px;
--safe-area-inset-left: 0px;
--safe-area-inset-right: 0px;
}

html.app .hide-in-app {
display: none !important;
}
Expand Down
2 changes: 1 addition & 1 deletion assets/js/components/Config/CircuitsLegacyModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
name="circuitslegacy"
:title="`${$t('config.circuits.title')} (${$t('config.general.legacy')})`"
:description="$t('config.circuits.description')"
docs="/reference/configuration/circuits"
docs="/features/loadmanagement"
:defaultYaml="defaultYaml"
endpoint="/config/circuits"
removeKey="circuits"
Expand Down
2 changes: 1 addition & 1 deletion assets/js/components/Config/DeviceTags.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import TariffChart from "../Tariff/TariffChart.vue";
import { generateRateSlots, calculateCostRange } from "@/utils/tariffSlots";
import { distanceValue, distanceUnit } from "@/units";

const HIDDEN_TAGS = ["icon", "heating", "integratedDevice", "continuous"];
const HIDDEN_TAGS = ["icon", "heating", "integratedDevice", "continuous", "demandPredictor"];

const PHASE_TAGS = ["phaseCurrents", "phaseVoltages", "phasePowers"];

Expand Down
1 change: 1 addition & 0 deletions assets/js/components/Config/FormRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
{{ $t("config.general.docsLink") }}
</a>
</div>
<slot name="footer" />
<div v-if="danger" class="alert alert-danger my-2" role="alert">
<strong>{{ $t("config.form.danger") }}:</strong> {{ danger }}
</div>
Expand Down
91 changes: 90 additions & 1 deletion assets/js/components/Config/LoadpointModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,51 @@
</div>

<div v-if="chargerIsHeating">
<h6>{{ $t("config.loadpoint.temperatureRangeTitle") }}</h6>
<h6 class="mb-2">{{ $t("config.loadpoint.demandPredictor.title") }}</h6>
<p class="text-muted">
{{ $t("config.loadpoint.demandPredictor.description") }}
</p>
<FormRow
id="loadpointDemandPredictor"
:label="$t('config.loadpoint.demandPredictor.label')"
:help="demandPredictorHelp"
>
<select
id="loadpointDemandPredictor"
v-model="demandPredictorSelection"
class="form-select"
>
<optgroup
v-for="group in demandPredictorGroups"
:key="group.label"
:label="group.label"
>
<option
v-for="option in group.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</optgroup>
</select>
<template #footer>
<div v-if="temperatureTariffMissing" class="text-warning mt-1">
{{ $t("config.loadpoint.demandPredictor.noTempTariff") }}
<a href="#" @click.prevent="addTemperatureTariff">
{{
$t(
"config.loadpoint.demandPredictor.noTempTariffLink"
)
}}
</a>
</div>
</template>
</FormRow>
</div>

<div v-if="chargerIsHeating">
<h6 class="mb-2">{{ $t("config.loadpoint.temperatureRangeTitle") }}</h6>
<p class="text-muted">
{{ $t("config.loadpoint.temperatureRangeHelp") }}
</p>
Expand Down Expand Up @@ -568,10 +612,13 @@ import {
type VehicleOption,
type ConfigCircuit,
type ConfigLoadpoint,
type DemandPredictor,
} from "@/types/evcc";

const nsPerMin = 60 * 1e9;

const DEMAND_PREDICTORS: DemandPredictor[] = ["daily", "weekday", "temperature"];

const { OFF, SMART, NOW } = CHARGE_MODE;

const defaultValues = {
Expand Down Expand Up @@ -627,6 +674,7 @@ export default {
},
voltage: Number,
gridConnection: String as PropType<GRID_CONNECTION>,
temperatureTariffConfigured: Boolean,
},
emits: ["changed", "dismissed", "disable"],
data() {
Expand All @@ -645,6 +693,44 @@ export default {
id(): number | undefined {
return getModal("loadpoint")?.id;
},
defaultDemandPredictor(): DemandPredictor {
return this.chargerStatus?.demandPredictor?.value ?? "daily";
},
demandPredictorGroups() {
const t = (key: string) => this.$t(`config.loadpoint.demandPredictor.${key}`);
const label = (profile: DemandPredictor) => t(`${profile}.label`);
return [
{
label: t("defaultGroup"),
options: [{ value: "", label: label(this.defaultDemandPredictor) }],
},
{
label: t("profilesGroup"),
options: DEMAND_PREDICTORS.map((value) => ({ value, label: label(value) })),
},
];
},
demandPredictorSelection: {
get(): string {
return this.values.demandPredictor ?? "";
},
set(value: string) {
this.values.demandPredictor = (value as DemandPredictor) || undefined;
},
},
activeDemandPredictor(): DemandPredictor {
return this.values.demandPredictor || this.defaultDemandPredictor;
},
demandPredictorHelp(): string {
return this.$t(
`config.loadpoint.demandPredictor.${this.activeDemandPredictor}.description`
);
},
temperatureTariffMissing(): boolean {
return (
this.activeDemandPredictor === "temperature" && !this.temperatureTariffConfigured
);
},
thresholdsSet(): boolean {
const { enable, disable } = this.values.thresholds;
return !!(enable.threshold || disable.threshold);
Expand Down Expand Up @@ -1020,6 +1106,9 @@ export default {
this.values.charger = "";
}
},
addTemperatureTariff() {
openModal("tariff", { type: "temperature" });
},
async editMeter() {
const meter = this.meters.find((m) => m.name === this.values.meter);
const result = await openModal("meter", {
Expand Down
35 changes: 35 additions & 0 deletions assets/js/components/Config/Markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { mount } from "@vue/test-utils";
import { describe, expect, test } from "vite-plus/test";
import Markdown from "./Markdown.vue";

const render = (markdown: string) => mount(Markdown, { props: { markdown } }).html();

describe("Markdown", () => {
test("keeps underscores inside words as text", () => {
const html = render("charge_before_export and a_b_c stay flat");
expect(html).toContain("charge_before_export and a_b_c stay flat");
expect(html).not.toContain("<em>");
});

test("keeps underscores in firmware versions", () => {
const html = render("requires M3W_3.11STP or M3W_4.03PTB");
expect(html).toContain("requires M3W_3.11STP or M3W_4.03PTB");
expect(html).not.toContain("<em>");
});

test("opens links in new window", () => {
expect(render("[docs](https://docs.evcc.io)")).toContain(
'<a target="_blank" rel="noopener noreferrer" href="https://docs.evcc.io">docs</a>'
);
});

test("still emphasises whole words", () => {
expect(render("so _early_ and __bold__")).toContain(
"so <em>early</em> and <strong>bold</strong>"
);
});

test("keeps underscores inside code", () => {
expect(render("`charge_before_export`")).toContain("<code>charge_before_export</code>");
});
});
9 changes: 6 additions & 3 deletions assets/js/components/Config/Markdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
</template>

<script>
import snarkdown from "snarkdown";
import { marked } from "marked";

export default {
name: "MarkdownRenderer",
Expand All @@ -13,7 +13,7 @@ export default {
},
computed: {
compiledMarkdown() {
const html = snarkdown(this.markdown);
const html = marked.parse(this.markdown ?? "", { async: false });
// open all links in new window
return html.replace(/<a href=/g, '<a target="_blank" rel="noopener noreferrer" href=');
},
Expand All @@ -24,7 +24,10 @@ export default {
.root {
max-width: 100%;
}
.root :deep(pre.code) {
.root > :deep(:last-child) {
margin-bottom: 0;
}
.root :deep(pre) {
overflow-x: auto;
margin: 1em 0;
hyphens: none;
Expand Down
5 changes: 4 additions & 1 deletion assets/js/components/Energyflow/Energyflow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,10 @@ export default defineComponent({
});
},
allConsumers() {
return [...this.consumers, ...this.aux];
const label = (m: Meter) => m.title || m.name || "";
return [...this.consumers, ...this.aux].sort((a, b) =>
label(a).localeCompare(label(b), this.$i18n.locale)
);
},
},
watch: {
Expand Down
8 changes: 0 additions & 8 deletions assets/js/components/Optimize/PlanExplanation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,6 @@ export default defineComponent({
</script>

<style scoped>
/* snarkdown renders the blank lines around a list as bare <br>, the list margin replaces them */
.explanation :deep(ul) {
margin: 1rem 0;
}
.explanation :deep(ul + br) {
display: none;
}

/* busy state of the update link: a light sweeps across the text, same box as the idle link */
.sweep {
background: linear-gradient(
Expand Down
2 changes: 1 addition & 1 deletion assets/js/components/Sessions/PeriodHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default defineComponent({
--vertical-shift: 0rem;
left: 0;
right: 0;
top: max(0rem, env(safe-area-inset-top)) !important;
top: max(0rem, var(--safe-area-inset-top)) !important;
margin: 0 calc(calc(1.5rem + var(--vertical-shift)) * -1);
background-color: var(--evcc-background);
box-shadow: 0 1px 8px 0px var(--evcc-background);
Expand Down
2 changes: 1 addition & 1 deletion assets/js/components/Sessions/SessionTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ export default defineComponent({
z-index: 2;
}
.sticky-bottom th {
padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
padding-bottom: max(0.5rem, var(--safe-area-inset-bottom));
border-bottom: none;
}
@media (max-width: 576px) {
Expand Down
4 changes: 4 additions & 0 deletions assets/js/types/evcc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,9 @@ export interface LoadpointThreshold {
threshold: number;
}

/** Demand forecast strategy of a heating loadpoint. */
export type DemandPredictor = "daily" | "weekday" | "temperature";

export interface ConfigLoadpoint {
id?: number;
name?: string;
Expand All @@ -571,6 +574,7 @@ export interface ConfigLoadpoint {
limitEnergy?: number;
limitSoc?: number;
minSoc?: number;
demandPredictor?: DemandPredictor;
circuit?: string;
thresholds: {
enable: LoadpointThreshold;
Expand Down
2 changes: 2 additions & 0 deletions assets/js/utils/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ function receiveFromApp(message: FromAppMessage) {

export function appDetection(r: Router) {
router = r;
// Safari applies root safe-area insets inside iframes, see https://github.com/w3c/csswg-drafts/issues/10506
document.documentElement.classList.toggle("embedded", window.self !== window.top);
if (!isApp()) return;
document.querySelector("html")?.classList.add("app");

Expand Down
35 changes: 21 additions & 14 deletions assets/js/views/Config.vue
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,10 @@
<ConfigSection v-bind="sectionProps('consumers')">
<div class="p-0 config-list box-pull-out">
<MeterCard
v-for="meter in consumerMeters"
v-for="{ meter, meterType } in consumerAndAuxMeters"
:key="meter.name"
:meter="meter"
meter-type="consumer"
:has-error="hasDeviceError('meter', meter.name)"
:tags="deviceTags('meter', meter.name)"
@edit="(type, id) => openModal('meter', { type, id })"
@enable="handleDisable('meter', meter.id, false)"
/>
<MeterCard
v-for="meter in auxMeters"
:key="meter.name"
:meter="meter"
meter-type="aux"
:meter-type="meterType"
:has-error="hasDeviceError('meter', meter.name)"
:tags="deviceTags('meter', meter.name)"
@edit="(type, id) => openModal('meter', { type, id })"
Expand Down Expand Up @@ -541,6 +531,7 @@
:hasDeviceError="hasDeviceError"
:voltage="voltage"
:gridConnection="gridConnection"
:temperatureTariffConfigured="temperatureTariffConfigured"
@changed="loadpointChanged"
@dismissed="loadpointDismissed"
@disable="({ id, disable }) => handleDisable('loadpoint', id, disable)"
Expand Down Expand Up @@ -1076,8 +1067,7 @@ export default defineComponent({
return this.getMetersByNames(names);
},
auxMeters() {
const names = this.site?.aux;
return this.getMetersByNames(names);
return this.getMetersByNames(this.site?.aux);
},
extMeters() {
const names = this.site?.ext;
Expand All @@ -1092,6 +1082,18 @@ export default defineComponent({
consumerMeters() {
return this.getMetersByNames(this.site?.consumer);
},
consumerAndAuxMeters(): { meter: ConfigMeter; meterType: MeterType; label: string }[] {
const entries = (meters: ConfigMeter[], meterType: MeterType, fallback: string) =>
meters.map((meter) => ({
meter,
meterType,
label: meter.deviceTitle || meter.config?.template || fallback,
}));
return [
...entries(this.consumerMeters, "consumer", this.$t("config.devices.consumer")),
...entries(this.auxMeters, "aux", this.$t("config.devices.auxMeter")),
].sort((a, b) => a.label.localeCompare(b.label, this.$i18n.locale));
},
gridTariff() {
const name = this.tariffRefs?.grid;
return name ? this.tariffs.find((t) => t.name === name) : null;
Expand All @@ -1112,6 +1114,11 @@ export default defineComponent({
const name = this.tariffRefs?.temperature;
return name ? this.tariffs.find((t) => t.name === name) : null;
},
temperatureTariffConfigured(): boolean {
return this.tariffsYamlVisible
? store.state?.tariffTemperature !== undefined
: !!this.temperatureTariff;
},
solarTariffs() {
const names = this.tariffRefs?.solar || [];
return names.map((name) => this.tariffs.find((t) => t.name === name)).filter(Boolean);
Expand Down
Loading
Loading