Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ac21468
Add forecast configuration UI with provider forms and manual refresh
seaspotter Aug 8, 2026
1d2c33a
Improve forecast forms with orientation hints and multi-array inputs
seaspotter Aug 8, 2026
063af2e
Simplify PVNode UI to API key and plant ID only
seaspotter Aug 8, 2026
0060ced
Replace forecast JSON strings with row-based orientation editor
seaspotter Aug 8, 2026
1509081
fix(ui): replace wildcard security access subscriptions
seaspotter Aug 8, 2026
60718b4
fix(ui): normalize forecast provider topic for editable config
seaspotter Aug 8, 2026
7d9abc1
feat(ui): improve forecast provider forms and workflow
seaspotter Aug 8, 2026
04f3a05
fix(ui): stabilize forecast provider switching and reset flow
seaspotter Aug 8, 2026
a115b83
fix(ui): persist provider switch immediately
seaspotter Aug 8, 2026
88d3c16
feat(debug): add forecast log card
seaspotter Aug 8, 2026
f8bea82
fix(forecast): publish provider switch directly
seaspotter Aug 8, 2026
7498523
fix(forecast-ui): allow decimal kWp inputs and use string names as ca…
seaspotter Aug 8, 2026
456e828
fix(forecast-ui): cache provider component to prevent flicker
seaspotter Aug 8, 2026
9049d80
fix(forecast-ui): stabilize pvnode credential field editing
seaspotter Aug 8, 2026
03a4449
fix(ui): keep retained topic values across overlapping wildcard subsc…
seaspotter Aug 8, 2026
3852fc0
fix(ui): keep last topic values on route unsubscribe
seaspotter Aug 8, 2026
46d35cf
fix(forecast-ui): normalize provider aliases and align action buttons
seaspotter Aug 8, 2026
624d4ed
refactor(forecast-ui): use configurable providers and add forecast AC…
seaspotter Aug 8, 2026
9fbbfaa
chore(navbar): require forecast ACL topic for PV forecast link
seaspotter Aug 8, 2026
3e99493
refactor(security-ui): subscribe ACL topics via wildcard
seaspotter Aug 8, 2026
e645d50
fix(security-ui): subscribe ACL topics explicitly incl. forecast
seaspotter Aug 8, 2026
77556f5
refactor(security-ui): restore wildcard ACL subscription
seaspotter Aug 8, 2026
4c38adb
fix(ui): revert wildcard subscription workaround
seaspotter Aug 8, 2026
7c7854e
fix(ui): restore explicit security access subscriptions
seaspotter Aug 8, 2026
3089d70
fix(ui): keep wildcard security subscribe and add forecast access topic
seaspotter Aug 8, 2026
62bf9bd
fix(forecast-ui): ignore empty init events in provider select
seaspotter Aug 8, 2026
d3ccc21
fix(ui): fallback to explicit security topics if wildcard subscribe f…
seaspotter Aug 8, 2026
bc3d659
fix(ui): stabilize settings access and forecast provider selection
seaspotter Aug 8, 2026
54d0e08
fix(forecast-ui): ensure provider select emits and avoid normalizatio…
seaspotter Aug 8, 2026
3524a6d
fix(lint): exclude dist from eslint and fix computed side effect in f…
seaspotter Aug 9, 2026
001b83a
feat(forecast-ui): show next retry time in status field on fault
seaspotter Aug 9, 2026
aaefbbf
fix(forecast-ui): remove irradiance_to_power_factor and redundant top…
seaspotter Aug 9, 2026
7b93108
fix(pvnode-ui): show Plant ID before API Key
seaspotter Aug 9, 2026
fd0dc0e
fix(store): guard against undefined state.examples in production build
seaspotter Aug 9, 2026
804818a
feat(forecast-ui): rename to Dachfläche, move add-button below, perma…
seaspotter Aug 9, 2026
7f91912
feat(forecast-ui): rename cards, day filter, hover tooltips, bigger c…
seaspotter Aug 9, 2026
4a14607
feat(forecast-ui): icon buttons for add/remove, azimuth hint as blue …
seaspotter Aug 9, 2026
f8045c3
fix(forecast-ui): fix umlauts, rename labels, add help texts and rate…
seaspotter Aug 9, 2026
6f1f5cb
fix(forecast-ui): fix remaining umlaut substitutes across all forecas…
seaspotter Aug 9, 2026
34a82e9
refactor(App): revert fallback topic list and isTopicCovered - use wi…
seaspotter Aug 9, 2026
371980a
feat(forecastsolar-ui): add optional API key field
seaspotter Aug 9, 2026
69cb0ac
docs: Add Forecast.Solar public API limitation note and remove mislea…
seaspotter Aug 9, 2026
fec90dc
docs: Move Forecast.Solar API limitation warning to provider-specific…
seaspotter Aug 9, 2026
119aac8
style: Change reset button to red and update text to 'Prognose'
seaspotter Aug 9, 2026
df18b88
chore: revert non-forecast changes (eslint, App.vue, BaseSelectInput)
seaspotter Aug 9, 2026
0f63d92
chore: revert non-forecast store changes
seaspotter Aug 9, 2026
2562226
Revert "chore: revert non-forecast store changes"
seaspotter Aug 9, 2026
b78adce
Add PVNode API limits info
seaspotter Aug 9, 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
8 changes: 8 additions & 0 deletions src/components/OpenwbPageNavbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@
>
Lastmanagement
</router-link>
<router-link
v-if="accessAllowed('ForecastConfiguration')"
to="/ForecastConfiguration"
class="dropdown-item"
active-class="active disabled"
>
PV-Prognose
</router-link>
<router-link
v-if="accessAllowed('ChargePointInstallation')"
to="/ChargePointInstallation"
Expand Down
13 changes: 13 additions & 0 deletions src/components/forecast/ForecastConfigMixin.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
export default {
props: {
forecast: { type: Object, required: true },
},
emits: ["update:configuration"],
methods: {
updateConfiguration(event, path = undefined) {
this.$emit("update:configuration", { value: event, object: path });
},
},
};
</script>
36 changes: 36 additions & 0 deletions src/components/forecast/OpenwbForecastConfigFallback.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<template>
<div class="forecast-fallback">
<openwb-base-alert
v-if="Object.keys(forecast.configuration || {}).length == 0"
subtype="info"
>
Der Anbieter "{{ forecast.name }}" bietet keine Einstellungen.
</openwb-base-alert>
<div v-else>
<openwb-base-alert subtype="warning">
Es wurde keine Konfigurationsseite für den Anbieter "{{ forecast.name }}" gefunden. Die Einstellungen können als
JSON direkt bearbeitet werden.
</openwb-base-alert>
<openwb-base-textarea
title="Konfiguration"
subtype="json"
:model-value="forecast.configuration"
@update:model-value="updateConfiguration($event, 'configuration')"
>
<template #help> Bitte prüfe, ob die Eingaben richtig interpretiert werden. </template>
</openwb-base-textarea>
<openwb-base-alert subtype="info">
<pre>{{ JSON.stringify(forecast.configuration, undefined, 2) }}</pre>
</openwb-base-alert>
</div>
</div>
</template>

<script>
import ForecastConfigMixin from "./ForecastConfigMixin.vue";

export default {
name: "OpenwbForecastConfigFallback",
mixins: [ForecastConfigMixin],
};
</script>
94 changes: 94 additions & 0 deletions src/components/forecast/OpenwbForecastProxy.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<template>
<openwb-base-heading> Einstellungen für Modul "{{ forecast.name }}" </openwb-base-heading>
<component
:is="forecastComponent"
:forecast="forecast"
@update:configuration="updateConfiguration($event)"
/>
</template>

<script>
import { defineAsyncComponent, markRaw } from "vue";
import OpenwbForecastConfigFallback from "./OpenwbForecastConfigFallback.vue";

const FORECAST_COMPONENT_MODULES = import.meta.glob("./*/forecast.vue");

export default {
name: "OpenwbForecastProxy",
props: {
forecast: { type: Object, required: true },
},
emits: ["update:configuration"],
data() {
return {
providerComponentCache: {},
};
},
computed: {
providerComponentLoaders() {
return Object.entries(FORECAST_COMPONENT_MODULES).reduce((loaders, [path, loader]) => {
const match = path.match(/^\.\/([^/]+)\/forecast\.vue$/);
if (!match) {
return loaders;
}
return {
...loaders,
[match[1]]: loader,
};
}, {});
},
normalizedProviderType() {
const rawType =
typeof this.forecast?.type === "string" && this.forecast.type.trim().length > 0
? this.forecast.type
: this.forecast?.name;
if (typeof rawType !== "string") {
return null;
}
const trimmed = rawType.trim();
if (!trimmed) {
return null;
}
const loaderTypes = Object.keys(this.providerComponentLoaders);
if (loaderTypes.includes(trimmed)) {
return trimmed;
}
const lowered = trimmed.toLowerCase();
if (loaderTypes.includes(lowered)) {
return lowered;
}
const simplified = lowered.replace(/[^a-z0-9]/g, "");
return (
loaderTypes.find((providerType) => providerType.toLowerCase().replace(/[^a-z0-9]/g, "") === simplified) || null
);
},
forecastComponent() {
const providerType = this.normalizedProviderType;
if (!providerType) {
return OpenwbForecastConfigFallback;
}
const loader = this.providerComponentLoaders[providerType];
if (!loader) {
return OpenwbForecastConfigFallback;
}
return this.getProviderComponent(providerType, loader);
},
},
methods: {
getProviderComponent(providerType, loader) {
if (!this.providerComponentCache[providerType]) {
this.providerComponentCache[providerType] = markRaw(
defineAsyncComponent({
loader,
errorComponent: OpenwbForecastConfigFallback,
}),
);
}
return this.providerComponentCache[providerType];
},
updateConfiguration(event) {
this.$emit("update:configuration", event);
},
},
};
</script>
172 changes: 172 additions & 0 deletions src/components/forecast/forecastsolar/forecast.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<template>
<div>
<openwb-base-alert subtype="info">
Forecast.Solar wird von der Community gepflegt. Ohne API Key sind 12 Anfragen pro Stunde möglich.
</openwb-base-alert>
<openwb-base-alert subtype="warning">
<strong>Hinweis zur kostenlosen API:</strong> Die kostenlose API zeigt nur noch die verbleibenden Stunden des aktuellen Tages.
Bei mehrfachen Abfragen pro Tag verringert sich daher der Prognosewert kontinuierlich. Mit einem kostenpflichtigen API-Key
erhält man die vollständige Tageshistorie und dieser Effekt tritt nicht auf.
</openwb-base-alert>
<openwb-base-number-input
title="Breitengrad"
:step="0.000001"
required
:model-value="forecast.configuration.latitude"
@update:model-value="updateConfiguration($event, 'configuration.latitude')"
>
<template #help> Dezimalgrad, z.B. 51.123456 </template>
</openwb-base-number-input>
<openwb-base-number-input
title="Längengrad"
:step="0.000001"
required
:model-value="forecast.configuration.longitude"
@update:model-value="updateConfiguration($event, 'configuration.longitude')"
>
<template #help> Dezimalgrad, z.B. 7.654321 </template>
</openwb-base-number-input>
<openwb-base-text-input
title="API Key (optional)"
subtype="password"
:model-value="forecast.configuration.api_key"
@update:model-value="updateConfiguration($event || null, 'configuration.api_key')"
>
<template #help>
Optional: API Key für einen bezahlten Forecast.Solar Account. Ohne Key gelten die Limits des Free Tiers.
</template>
</openwb-base-text-input>
<openwb-base-alert subtype="info">
Jede Dachfläche muss einzeln erfasst werden, auch wenn es nur eine Dachfläche gibt.
</openwb-base-alert>
<openwb-base-alert
v-if="stringRows.length >= 6"
subtype="warning"
>
Maximal 6 Dachflächen sind erlaubt.
</openwb-base-alert>
<openwb-base-alert
v-if="stringRows.length === 0"
subtype="secondary"
>
Noch keine Dachflächen konfiguriert.
</openwb-base-alert>
<openwb-base-card
v-for="(row, index) in stringRows"
:key="`fs-string-${index}`"
:title="row.name && row.name.trim() ? row.name : `Dachfläche ${index + 1}`"
class="mb-2"
>
<openwb-base-text-input
title="Name der Dachfläche"
:model-value="row.name"
@update:model-value="updateStringField(index, 'name', $event)"
/>
<openwb-base-number-input
title="Leistung Dachfläche/String"
unit="kWp"
:min="0"
:step="0.01"
required
:model-value="row.peak_power_kw"
@update:model-value="updateStringField(index, 'peak_power_kw', $event)"
/>
<openwb-base-number-input
title="Dachneigung"
unit="Grad"
:min="0"
:max="90"
:step="1"
required
:model-value="row.tilt"
@update:model-value="updateStringField(index, 'tilt', $event)"
/>
<openwb-base-number-input
title="Ausrichtung"
unit="Grad"
:min="-180"
:max="180"
:step="1"
required
:model-value="row.azimuth"
@update:model-value="updateStringField(index, 'azimuth', $event)"
/>
<openwb-base-alert subtype="info">
Ausrichtung: 0&deg; = S&uuml;den | -90&deg; = Osten | 90&deg; = Westen | 180&deg; = Norden
</openwb-base-alert>
<div class="d-flex justify-content-end">
<button
type="button"
class="btn btn-danger btn-sm"
title="Dachfläche entfernen"
@click="removeStringRow(index)"
>
<font-awesome-icon :icon="['fas', 'trash']" />
</button>
</div>
</openwb-base-card>
<button
type="button"
class="btn btn-success btn-sm mb-2"
:disabled="stringRows.length >= 6"
title="Dachfläche hinzufügen"
@click="addStringRow"
>
<font-awesome-icon :icon="['fas', 'plus']" />
Dachfläche hinzufügen
</button>
</div>
</template>

<script>
import ForecastConfigMixin from "../ForecastConfigMixin.vue";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faPlus as fasPlus, faTrash as fasTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";

library.add(fasPlus, fasTrash);

export default {
name: "ForecastSolarForecastConfig",
components: { FontAwesomeIcon },
mixins: [ForecastConfigMixin],
computed: {
stringRows() {
return Array.isArray(this.forecast.configuration.strings) ? this.forecast.configuration.strings : [];
},
},
methods: {
addStringRow() {
if (this.stringRows.length >= 6) {
return;
}
const next = [
...this.stringRows,
{
name: `Ausrichtung ${this.stringRows.length + 1}`,
peak_power_kw: this.forecast.configuration.peak_power_kw || 1,
tilt: this.forecast.configuration.tilt ?? 30,
azimuth: this.forecast.configuration.azimuth ?? 0,
},
];
this.updateConfiguration(next, "configuration.strings");
},
removeStringRow(index) {
const next = this.stringRows.filter((_, rowIndex) => rowIndex !== index);
this.updateConfiguration(next, "configuration.strings");
},
updateStringField(index, key, value) {
const next = this.stringRows.map((row, rowIndex) => {
if (rowIndex !== index) {
return row;
}
return {
...row,
[key]: value,
};
});
this.updateConfiguration(next, "configuration.strings");
},
},
};
</script>
Loading
Loading