Chore: Typescript: Rewrite Energyflow (#20814)

Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
Maschga 2025-04-28 22:22:49 +02:00 • committed by GitHub
parent 3b672a3f17
commit e7a5860f5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 430 additions and 393 deletions

View file

@ -262,7 +262,7 @@ import collector from "../../mixins/collector.js";
import api from "../../api.js";
import smartCostAvailable from "../../utils/smartCostAvailable.js";
import { defineComponent, type PropType } from "vue";
import type { Battery, SelectOption } from "assets/js/types/evcc.js";
import type { Battery, SelectOption } from "../../types/evcc.js";
export default defineComponent({
name: "BatterySettingsModal",

View file

@ -113,9 +113,9 @@ import collector from "../../mixins/collector.js";
import api from "../../api.js";
import { optionStep, fmtEnergy } from "../../utils/energyOptions.js";
import { defineComponent, type PropType } from "vue";
import type { CURRENCY, Timeout, Vehicle } from "assets/js/types/evcc.js";
import type { CURRENCY, Timeout, Vehicle } from "../../types/evcc.js";
import type { StaticPlan, StaticSocPlan, StaticEnergyPlan, RepeatingPlan } from "./types.js";
import type { Forecast } from "../../utils/forecast.ts";
import type { Forecast } from "../../types/evcc.ts";
const ONE_MINUTE = 60 * 1000;
export default defineComponent({

View file

@ -88,7 +88,7 @@ import type {
StaticEnergyPlan,
PlanResponse,
} from "./types.js";
import type { Forecast } from "../../utils/forecast.ts";
import type { Forecast } from "../../types/evcc.ts";
export default defineComponent({
name: "ChargingPlansSettings",

View file

@ -22,7 +22,7 @@
import { defineComponent, type PropType } from "vue";
import formatter from "../../mixins/formatter.js";
import type { PlanWrapper } from "./types.js";
import type { Tariff } from "assets/js/types/evcc.js";
import type { Tariff } from "../../types/evcc.js";
export default defineComponent({
name: "ChargingPlanWarnings",

View file

@ -1,4 +1,4 @@
import type { Rate } from "assets/js/types/evcc";
import type { Rate } from "../../types/evcc";
export interface RepeatingPlan {
weekdays: number[];

View file

@ -1,34 +0,0 @@
import BatteryIcon from "./BatteryIcon.vue";
export default {
title: "Energyflow/BatteryIcon",
component: BatteryIcon,
parameters: {
layout: "centered",
},
};
const createStory = (args) => {
const story = () => ({
components: { BatteryIcon },
setup() {
return { args };
},
template: '<BatteryIcon v-bind="args" />',
});
story.args = args;
return story;
};
export const Empty = createStory({ soc: 0 });
export const Soc10 = createStory({ soc: 10 });
export const Soc20 = createStory({ soc: 20 });
export const Soc30 = createStory({ soc: 30 });
export const Soc40 = createStory({ soc: 40 });
export const Soc50 = createStory({ soc: 50 });
export const Soc60 = createStory({ soc: 60 });
export const Soc70 = createStory({ soc: 70 });
export const Soc80 = createStory({ soc: 80 });
export const Soc90 = createStory({ soc: 90 });
export const Hold = createStory({ hold: true });
export const GridCharge = createStory({ gridCharge: true });

View file

@ -0,0 +1,58 @@
import BatteryIcon from "./BatteryIcon.vue";
import type { Meta, StoryFn } from "@storybook/vue3";
export default {
title: "Energyflow/BatteryIcon",
component: BatteryIcon,
parameters: {
layout: "centered",
},
} as Meta<typeof BatteryIcon>;
const Template: StoryFn<typeof BatteryIcon> = (args) => {
const story = () => ({
components: { BatteryIcon },
setup() {
return { args };
},
template: '<BatteryIcon v-bind="args" />',
});
story.args = args;
return story;
};
export const Empty = Template.bind({});
Empty.args = { soc: 0 };
export const Soc10 = Template.bind({});
Soc10.args = { soc: 10 };
export const Soc20 = Template.bind({});
Soc20.args = { soc: 20 };
export const Soc30 = Template.bind({});
Soc30.args = { soc: 30 };
export const Soc40 = Template.bind({});
Soc40.args = { soc: 40 };
export const Soc50 = Template.bind({});
Soc50.args = { soc: 50 };
export const Soc60 = Template.bind({});
Soc60.args = { soc: 60 };
export const Soc70 = Template.bind({});
Soc70.args = { soc: 70 };
export const Soc80 = Template.bind({});
Soc80.args = { soc: 80 };
export const Soc90 = Template.bind({});
Soc90.args = { soc: 90 };
export const Hold = Template.bind({});
Hold.args = { hold: true };
export const GridCharge = Template.bind({});
GridCharge.args = { gridCharge: true };

View file

@ -19,10 +19,11 @@
</svg>
</template>
<script>
<script lang="ts">
import { defineComponent } from "vue";
import icon from "../../mixins/icon";
export default {
export default defineComponent({
name: "BatteryIcon",
mixins: [icon],
props: {
@ -36,5 +37,5 @@ export default {
return `M30 38H18v-${height}h12v${height}z`;
},
},
};
});
</script>

View file

@ -1,215 +0,0 @@
import Energyflow from "./Energyflow.vue";
export default {
title: "Energyflow/Energyflow",
component: Energyflow,
};
const Template = (args) => ({
components: { Energyflow },
setup() {
return { args };
},
template: '<Energyflow v-bind="args" />',
});
export const GridAndPV = Template.bind({});
GridAndPV.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 7300,
gridPower: -2300,
homePower: 800,
loadpointsCompact: [
{ power: 1000, icon: "car", charging: true },
{ power: 1000, icon: "bike", charging: true },
{ power: 2200, icon: "car", charging: true },
],
tariffGrid: 0.25,
tariffFeedIn: 0.08,
tariffEffectivePrice: 0.08,
smartCostType: "price",
smartCostAvailable: true,
currency: "EUR",
siteTitle: "Home",
pv: [{ power: 5000 }, { power: 2300 }],
};
export const BatteryAndGrid = Template.bind({});
BatteryAndGrid.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 0,
gridPower: 1200,
homePower: 2000,
batteryPower: 800,
batterySoc: 77,
tariffGrid: 0.25,
tariffFeedIn: 0.08,
tariffEffectivePrice: 0.08,
currency: "EUR",
siteTitle: "Home",
battery: [
{ soc: 44.999, capacity: 13.3, power: 350 },
{ soc: 82.3331, capacity: 21, power: 450 },
],
};
export const BatteryCharging = Template.bind({});
BatteryCharging.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 5000,
gridPower: -1300,
homePower: 800,
loadpointsCompact: [{ power: 1400, icon: "car", charging: true }],
batteryPower: -1500,
batterySoc: 75,
siteTitle: "Home",
};
export const GridPVAndBattery = Template.bind({});
GridPVAndBattery.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 1000,
gridPower: 700,
homePower: 3300,
batteryPower: 1500,
batterySoc: 30,
siteTitle: "Home",
};
export const BatteryThresholds = Template.bind({});
BatteryThresholds.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 8700,
gridPower: -500,
loadpointsCompact: [
{ power: 5000, icon: "car", charging: true },
{ power: 2500, icon: "bus", charging: true },
],
batteryPower: -700,
batterySoc: 95,
siteTitle: "Home",
};
export const PVThresholds = Template.bind({});
PVThresholds.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 300,
gridPower: 6500,
homePower: 1000,
loadpointsCompact: [
{ power: 5000, icon: "car", charging: true },
{ power: 1600, icon: "car", charging: true },
],
batteryPower: 800,
batterySoc: 76,
siteTitle: "Home",
};
export const GridOnly = Template.bind({});
GridOnly.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 0,
gridPower: 6500,
homePower: 1000,
loadpointsCompact: [
{ power: 5500, icon: "car", charging: true },
{ power: 0, icon: "car", charging: false },
{ power: 0, icon: "car", charging: false },
{ power: 0, icon: "car", charging: false },
],
batteryPower: 0,
batterySoc: 0,
siteTitle: "Home",
};
export const LowPower = Template.bind({});
LowPower.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 700,
gridPower: -300,
homePower: 300,
batteryPower: -100,
batterySoc: 55,
tariffGrid: 0.25,
tariffFeedIn: 0.08,
tariffEffectivePrice: 0.08,
currency: "EUR",
siteTitle: "Home",
};
export const CO2 = Template.bind({});
CO2.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 7300,
gridPower: -2300,
homePower: 800,
loadpointsCompact: [
{ power: 1000, icon: "car", charging: true },
{ power: 1000, icon: "car", charging: true },
{ power: 2200, icon: "car", charging: true },
],
tariffGrid: 0.25,
tariffFeedIn: 0.08,
tariffEffectivePrice: 0.08,
tariffCo2: 723,
tariffEffectiveCo2: 0,
smartCostType: "co2",
smartCostAvailable: true,
currency: "EUR",
siteTitle: "Home",
pv: [{ power: 5000 }, { power: 2300 }],
};
export const UnknownInput = Template.bind({});
UnknownInput.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 2000,
gridPower: -2000,
loadpointsCompact: [{ power: 1000, icon: "car", charging: true }],
};
export const UnknownInputFill = Template.bind({});
UnknownInputFill.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 500,
gridPower: 0,
batteryPower: -1000,
loadpointsCompact: [],
};
export const UnknownOutput = Template.bind({});
UnknownOutput.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 3000,
gridPower: -1000,
loadpointsCompact: [{ power: 1700, icon: "car", charging: true }],
};
export const UnknownOutputLessThan10Percent = Template.bind({});
UnknownOutputLessThan10Percent.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 3000,
gridPower: -1000,
loadpointsCompact: [{ power: 1800, icon: "car", charging: true }],
};

View file

@ -0,0 +1,201 @@
import type { Meta, StoryFn } from "@storybook/vue3";
import Energyflow from "./Energyflow.vue";
import { CURRENCY } from "../../types/evcc";
export default {
title: "Energyflow/Energyflow",
component: Energyflow,
} as Meta<typeof Energyflow>;
const Template: StoryFn<typeof Energyflow> = (args) => ({
components: { Energyflow },
setup() {
return { args };
},
template: '<Energyflow v-bind="args" />',
});
export const GridAndPV = Template.bind({});
GridAndPV.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 7300,
gridPower: -2300,
homePower: 800,
loadpointsCompact: [
{ power: 1000, icon: "car", charging: true, title: "Garage" },
{ power: 1000, icon: "bike", charging: true, title: "Garage" },
{ power: 2200, icon: "car", charging: true, title: "Garage" },
],
tariffGrid: 0.25,
tariffFeedIn: 0.08,
smartCostType: "price",
currency: CURRENCY.EUR,
pv: [{ power: 5000 }, { power: 2300 }],
};
export const BatteryAndGrid = Template.bind({});
BatteryAndGrid.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 0,
gridPower: 1200,
homePower: 2000,
batteryPower: 800,
batterySoc: 77,
tariffGrid: 0.25,
tariffFeedIn: 0.08,
currency: CURRENCY.EUR,
battery: [
{ soc: 44.999, capacity: 13.3, power: 350, controllable: true },
{ soc: 82.3331, capacity: 21, power: 450, controllable: false },
],
};
export const BatteryCharging = Template.bind({});
BatteryCharging.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 5000,
gridPower: -1300,
homePower: 800,
loadpointsCompact: [{ power: 1400, icon: "car", charging: true, title: "Garage" }],
batteryPower: -1500,
batterySoc: 75,
};
export const GridPVAndBattery = Template.bind({});
GridPVAndBattery.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 1000,
gridPower: 700,
homePower: 3300,
batteryPower: 1500,
batterySoc: 30,
};
export const BatteryThresholds = Template.bind({});
BatteryThresholds.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 8700,
gridPower: -500,
loadpointsCompact: [
{ power: 5000, icon: "car", charging: true, title: "Garage" },
{ power: 2500, icon: "bus", charging: true, title: "Garage" },
],
batteryPower: -700,
batterySoc: 95,
};
export const PVThresholds = Template.bind({});
PVThresholds.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 300,
gridPower: 6500,
homePower: 1000,
loadpointsCompact: [
{ power: 5000, icon: "car", charging: true, title: "Garage" },
{ power: 1600, icon: "car", charging: true, title: "Garage" },
],
batteryPower: 800,
batterySoc: 76,
};
export const GridOnly = Template.bind({});
GridOnly.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 0,
gridPower: 6500,
homePower: 1000,
loadpointsCompact: [
{ power: 5500, icon: "car", charging: true, title: "Garage" },
{ power: 0, icon: "car", charging: false, title: "Garage" },
{ power: 0, icon: "car", charging: false, title: "Garage" },
{ power: 0, icon: "car", charging: false, title: "Garage" },
],
batteryPower: 0,
batterySoc: 0,
};
export const LowPower = Template.bind({});
LowPower.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 700,
gridPower: -300,
homePower: 300,
batteryPower: -100,
batterySoc: 55,
tariffGrid: 0.25,
tariffFeedIn: 0.08,
currency: CURRENCY.EUR,
};
export const CO2 = Template.bind({});
CO2.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 7300,
gridPower: -2300,
homePower: 800,
loadpointsCompact: [
{ power: 1000, icon: "car", charging: true, title: "Garage" },
{ power: 1000, icon: "car", charging: true, title: "Garage" },
{ power: 2200, icon: "car", charging: true, title: "Garage" },
],
tariffGrid: 0.25,
tariffFeedIn: 0.08,
tariffCo2: 723,
smartCostType: "co2",
currency: CURRENCY.EUR,
pv: [{ power: 5000 }, { power: 2300 }],
};
export const UnknownInput = Template.bind({});
UnknownInput.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 2000,
gridPower: -2000,
loadpointsCompact: [{ power: 1000, icon: "car", charging: true, title: "Garage" }],
};
export const UnknownInputFill = Template.bind({});
UnknownInputFill.args = {
gridConfigured: true,
pvConfigured: true,
batteryConfigured: true,
pvPower: 500,
gridPower: 0,
batteryPower: -1000,
loadpointsCompact: [],
};
export const UnknownOutput = Template.bind({});
UnknownOutput.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 3000,
gridPower: -1000,
loadpointsCompact: [{ power: 1700, icon: "car", charging: true, title: "Garage" }],
};
export const UnknownOutputLessThan10Percent = Template.bind({});
UnknownOutputLessThan10Percent.args = {
gridConfigured: true,
pvConfigured: true,
pvPower: 3000,
gridPower: -1000,
loadpointsCompact: [{ power: 1800, icon: "car", charging: true, title: "Garage" }],
};

View file

@ -94,7 +94,7 @@
@details-clicked="openForecastModal"
@toggle="togglePv"
>
<template v-if="pv.length > 1" #expanded>
<template v-if="multiplePv" #expanded>
<EnergyflowEntry
v-for="(p, index) in pv"
:key="index"
@ -127,7 +127,7 @@
<template v-if="batteryGridChargeLimitSet" #subline>
<div class="d-none d-md-block">&nbsp;</div>
</template>
<template v-if="battery.length > 1" #expanded>
<template v-if="multipleBattery" #expanded>
<EnergyflowEntry
v-for="(b, index) in battery"
:key="index"
@ -174,6 +174,7 @@
/>
<EnergyflowEntry
:name="
// @ts-ignore
$t('main.energyflow.loadpoints', activeLoadpointsCount, {
count: activeLoadpointsCount,
})
@ -248,7 +249,7 @@
</span>
</button>
</template>
<template v-if="battery.length > 1" #expanded>
<template v-if="multipleBattery" #expanded>
<EnergyflowEntry
v-for="(b, index) in battery"
:key="index"
@ -278,7 +279,7 @@
</div>
</template>
<script>
<script lang="ts">
import "@h2d2/shopicons/es/filled/square";
import Modal from "bootstrap/js/dist/modal";
import Visualization from "./Visualization.vue";
@ -288,8 +289,10 @@ import AnimatedNumber from "../Helper/AnimatedNumber.vue";
import settings from "../../settings";
import { CO2_TYPE } from "../../units";
import collector from "../../mixins/collector";
import { defineComponent, type PropType } from "vue";
import type { Battery, CURRENCY, Forecast, LoadpointCompact } from "../../types/evcc";
export default {
export default defineComponent({
name: "Energyflow",
components: {
Visualization,
@ -302,11 +305,11 @@ export default {
gridPower: { type: Number, default: 0 },
homePower: { type: Number, default: 0 },
pvConfigured: Boolean,
pv: { type: Array },
pv: { type: Array as PropType<Pv[]> },
pvPower: { type: Number, default: 0 },
loadpointsCompact: { type: Array, default: () => [] },
loadpointsCompact: { type: Array as PropType<LoadpointCompact[]>, default: () => [] },
batteryConfigured: { type: Boolean },
battery: { type: Array },
battery: { type: Array as PropType<Battery[]> },
batteryPower: { type: Number, default: 0 },
batterySoc: { type: Number, default: 0 },
batteryDischargeControl: { type: Boolean },
@ -314,21 +317,21 @@ export default {
batteryGridChargeActive: { type: Boolean },
batteryMode: { type: String },
tariffGrid: { type: Number },
tariffFeedIn: { type: Number },
tariffFeedIn: { type: Number, default: 0 },
tariffCo2: { type: Number },
tariffPriceHome: { type: Number },
tariffCo2Home: { type: Number },
tariffPriceLoadpoints: { type: Number },
tariffCo2Loadpoints: { type: Number },
smartCostType: { type: String },
currency: { type: String },
currency: { type: String as PropType<CURRENCY> },
prioritySoc: { type: Number },
bufferSoc: { type: Number },
bufferStartSoc: { type: Number },
forecast: { type: Object, default: () => ({}) },
forecast: { type: Object as PropType<Forecast>, default: () => ({}) },
},
data: () => {
return { detailsOpen: false, detailsCompleteHeight: null, ready: false };
return { detailsOpen: false, detailsCompleteHeight: null as number | null, ready: false };
},
computed: {
gridImport() {
@ -401,13 +404,19 @@ export default {
return this.detailsOpen ? this.detailsCompleteHeight + "px" : 0;
},
batteryFmt() {
return (soc) => this.fmtPercentage(soc, 0);
return (soc: number) => this.fmtPercentage(soc, 0);
},
multipleBattery() {
return (this.battery?.length || 0) > 1;
},
multiplePv() {
return (this.pv?.length || 0) > 1;
},
fmtLoadpointSoc() {
return (soc) => this.fmtPercentage(soc, 0);
return (soc: number) => this.fmtPercentage(soc, 0);
},
fmtLoadpointTemp() {
return (temp) => this.fmtTemperature(temp);
return (temp: number) => this.fmtTemperature(temp);
},
co2Available() {
return this.smartCostType === CO2_TYPE;
@ -444,7 +453,8 @@ export default {
}
const { today, scale } = this.forecast.solar || {};
const factor = this.$hiddenFeatures() && settings.solarAdjusted && scale ? scale : 1;
return today.energy * factor;
const energy = today?.energy || 0;
return energy * factor;
},
solarForecastIcon() {
return this.solarForecastExists ? "forecast" : undefined;
@ -494,7 +504,7 @@ export default {
window.removeEventListener("resize", this.updateHeight);
},
methods: {
detailsTooltip(price, co2) {
detailsTooltip(price?: number, co2?: number) {
const result = [];
if (co2 !== undefined) {
result.push(`${this.fmtCo2Long(co2)}`);
@ -504,25 +514,25 @@ export default {
}
return result;
},
detailsValue(price, co2) {
detailsValue(price?: number, co2?: number) {
if (this.co2Available) {
return co2;
}
return price;
},
detailsFmt(value) {
detailsFmt(value: number) {
if (this.co2Available) {
return this.fmtCo2Short(value);
}
return this.fmtPricePerKWh(value, this.currency, true);
},
forecastFmt(value) {
forecastFmt(value: number) {
if (value === null) {
return "";
}
return `${this.fmtWh(value, POWER_UNIT.KW)}`;
},
kw(watt) {
kw(watt: number) {
return this.fmtW(watt, this.powerUnit);
},
toggleDetails() {
@ -531,22 +541,24 @@ export default {
settings.energyflowDetails = this.detailsOpen;
},
updateHeight() {
this.detailsCompleteHeight = this.$refs.detailsInner.offsetHeight;
this.detailsCompleteHeight = this.$refs["detailsInner"]?.offsetHeight ?? 0;
},
openBatterySettingsModal() {
const modal = Modal.getOrCreateInstance(
document.getElementById("batterySettingsModal")
document.getElementById("batterySettingsModal") as HTMLElement
);
modal.show();
},
openForecastModal() {
const modal = Modal.getOrCreateInstance(document.getElementById("forecastModal"));
const modal = Modal.getOrCreateInstance(
document.getElementById("forecastModal") as HTMLElement
);
modal.show();
},
dischargePower(power) {
dischargePower(power: number) {
return Math.abs(Math.max(0, power));
},
chargePower(power) {
chargePower(power: number) {
return Math.abs(Math.min(0, power) * -1);
},
toggleBattery() {
@ -561,14 +573,14 @@ export default {
settings.energyflowLoadpoints = !settings.energyflowLoadpoints;
this.$nextTick(this.updateHeight);
},
genericBatteryTitle(index) {
genericBatteryTitle(index: number) {
return `${this.$t("config.devices.batteryStorage")} #${index + 1}`;
},
genericPvTitle(index) {
genericPvTitle(index: number) {
return `${this.$t("config.devices.solarSystem")} #${index + 1}`;
},
},
};
});
</script>
<style scoped>
.details {

View file

@ -9,7 +9,7 @@
<component :is="`shopicon-regular-${icon}`" v-else></component>
</span>
<div class="d-block flex-grow-1 ms-3 text-truncate">
<span v-if="!$slots.expanded">{{ name }}</span>
<span v-if="!$slots['expanded']">{{ name }}</span>
<button
v-else
class="btn-neutral d-flex align-items-baseline flex-shrink-1 flex-grow-1"
@ -42,7 +42,11 @@
v-if="detailsIcon === 'forecast'"
class="ms-2 me-1 d-inline-block"
/>
<AnimatedNumber v-if="!isNaN(details)" :to="details" :format="detailsFmt" />
<AnimatedNumber
v-if="details !== undefined && !isNaN(details)"
:to="details"
:format="detailsFmt as Function"
/>
</div>
<div ref="power" class="power" data-bs-toggle="tooltip" @click="powerClicked">
<AnimatedNumber ref="powerNumber" :to="power" :format="kw" />
@ -51,31 +55,32 @@
</div>
</div>
<div
v-if="$slots.expanded"
v-if="$slots['expanded']"
class="expandable ms-2"
:class="{ 'expandable--open': expanded }"
>
<slot name="expanded" />
</div>
<div v-if="$slots.subline" class="ms-4 ps-3 mb-2">
<div v-if="$slots['subline']" class="ms-4 ps-3 mb-2">
<slot name="subline" />
</div>
</div>
</template>
<script>
<script lang="ts">
import "@h2d2/shopicons/es/regular/powersupply";
import "@h2d2/shopicons/es/regular/sun";
import "@h2d2/shopicons/es/regular/home";
import "@h2d2/shopicons/es/regular/arrowdropdown";
import Tooltip from "bootstrap/js/dist/tooltip";
import BatteryIcon from "./BatteryIcon.vue";
import formatter from "../../mixins/formatter";
import formatter, { POWER_UNIT } from "../../mixins/formatter";
import AnimatedNumber from "../Helper/AnimatedNumber.vue";
import VehicleIcon from "../VehicleIcon";
import ForecastIcon from "../MaterialIcon/Forecast.vue";
import { defineComponent, type PropType } from "vue";
export default {
export default defineComponent({
name: "EnergyflowEntry",
components: { BatteryIcon, AnimatedNumber, VehicleIcon, ForecastIcon },
mixins: [formatter],
@ -83,20 +88,23 @@ export default {
name: { type: String },
icon: { type: String },
iconProps: { type: Object, default: () => ({}) },
power: { type: Number },
powerTooltip: { type: Array },
powerUnit: { type: String },
power: { type: Number, default: 0 },
powerTooltip: { type: Array as PropType<string[]> },
powerUnit: { type: String as PropType<POWER_UNIT> },
details: { type: Number },
detailsIcon: { type: String },
detailsFmt: { type: Function },
detailsTooltip: { type: Array },
detailsTooltip: { type: Array as PropType<string[]> },
detailsClickable: { type: Boolean },
detailsInactive: { type: Boolean },
expanded: { type: Boolean, default: false },
},
emits: ["details-clicked", "toggle"],
data() {
return { powerTooltipInstance: null, detailsTooltipInstance: null };
return {
powerTooltipInstance: null as Tooltip | null,
detailsTooltipInstance: null as Tooltip | null,
};
},
computed: {
active() {
@ -123,7 +131,7 @@ export default {
powerInKw(newVal, oldVal) {
// force update if unit changes but not the value
if (newVal !== oldVal) {
this.$refs.powerNumber.forceUpdate();
(this.$refs["powerNumber"] as any).forceUpdate();
}
},
},
@ -132,29 +140,29 @@ export default {
this.updateDetailsTooltip();
},
methods: {
kw(watt) {
kw(watt: number) {
return this.fmtW(watt, this.powerUnit);
},
updatePowerTooltip() {
this.powerTooltipInstance = this.updateTooltip(
this.powerTooltipInstance,
this.powerTooltip,
this.$refs.power
this.$refs["power"],
this.powerTooltip
);
},
updateDetailsTooltip() {
this.detailsTooltipInstance = this.updateTooltip(
this.detailsTooltipInstance,
this.detailsTooltip,
this.$refs.details
this.$refs["details"],
this.detailsTooltip
);
},
updateTooltip(instance, content, ref) {
updateTooltip(instance: Tooltip | null, ref: any, content?: string[]) {
if (!Array.isArray(content) || !content.length) {
if (instance) {
instance.dispose();
}
return;
return null;
}
let newInstance = instance;
if (!newInstance) {
@ -164,12 +172,12 @@ export default {
newInstance.setContent({ ".tooltip-inner": html });
return newInstance;
},
powerClicked($event) {
powerClicked($event: Event) {
if (this.powerTooltip) {
$event.stopPropagation();
}
},
detailsClicked($event) {
detailsClicked($event: Event) {
if (this.detailsClickable || this.detailsTooltip) {
$event.stopPropagation();
}
@ -179,12 +187,12 @@ export default {
// hide tooltip, chrome needs a timeout
setTimeout(() => this.detailsTooltipInstance?.hide(), 10);
},
toggle($event) {
toggle($event: Event) {
$event.stopPropagation();
this.$emit("toggle");
},
},
};
});
</script>
<style scoped>
.entry {

View file

@ -18,8 +18,10 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "LabelBar",
props: {
value: { type: Number, default: 0 },
@ -29,7 +31,7 @@ export default {
first: { type: Boolean },
last: { type: Boolean },
},
};
});
</script>
<style scoped>
.label-bar {

View file

@ -106,7 +106,7 @@
</div>
</template>
<script>
<script lang="ts">
import formatter, { POWER_UNIT } from "../../mixins/formatter";
import BatteryIcon from "./BatteryIcon.vue";
import LabelBar from "./LabelBar.vue";
@ -115,8 +115,10 @@ import VehicleIcon from "../VehicleIcon";
import QuestionIcon from "../MaterialIcon/Question.vue";
import "@h2d2/shopicons/es/regular/sun";
import "@h2d2/shopicons/es/regular/home";
import { defineComponent, type PropType } from "vue";
import type { LoadpointCompact } from "../../types/evcc";
export default {
export default defineComponent({
name: "Visualization",
components: { BatteryIcon, LabelBar, AnimatedNumber, VehicleIcon, QuestionIcon },
mixins: [formatter],
@ -125,7 +127,7 @@ export default {
selfPv: { type: Number, default: 0 },
selfBattery: { type: Number, default: 0 },
pvExport: { type: Number, default: 0 },
loadpoints: { type: Array, default: () => [] },
loadpoints: { type: Array as PropType<LoadpointCompact[]>, default: () => [] },
batteryCharge: { type: Number, default: 0 },
batteryDischarge: { type: Number, default: 0 },
batteryHold: { type: Boolean, default: false },
@ -133,7 +135,7 @@ export default {
pvProduction: { type: Number, default: 0 },
homePower: { type: Number, default: 0 },
batterySoc: { type: Number, default: 0 },
powerUnit: { type: String, default: POWER_UNIT.KW },
powerUnit: { type: String as PropType<POWER_UNIT>, default: POWER_UNIT.KW },
inPower: { type: Number, default: 0 },
outPower: { type: Number, default: 0 },
},
@ -197,42 +199,42 @@ export default {
window.removeEventListener("resize", this.updateElementWidth);
},
methods: {
widthTotal(power) {
widthTotal(power: number) {
if (this.totalAdjusted === 0 || power === 0) return "0";
return (100 / this.totalAdjusted) * power + "%";
},
fmtBarValue(watt) {
fmtBarValue(watt: number) {
if (!this.enoughSpaceForValue(watt)) {
return "";
}
const withUnit = this.enoughSpaceForUnit(watt);
return this.fmtW(watt, this.powerUnit, withUnit);
},
powerLabelAvailableSpace(power) {
powerLabelAvailableSpace(power: number) {
if (this.totalAdjusted === 0) return 0;
const percent = (100 / this.totalAdjusted) * power;
return (this.width / 100) * percent;
},
enoughSpaceForValue(power) {
enoughSpaceForValue(power: number) {
return this.powerLabelAvailableSpace(power) > 40;
},
enoughSpaceForUnit(power) {
enoughSpaceForUnit(power: number) {
return this.powerLabelAvailableSpace(power) > 60;
},
hideLabelIcon(power, minWidth = 32) {
hideLabelIcon(power: number, minWidth = 32) {
if (this.totalAdjusted === 0) return true;
const percent = (100 / this.totalAdjusted) * power;
return (this.width / 100) * percent < minWidth;
},
applyThreshold(power, threshold = 2) {
applyThreshold(power: number, threshold = 2) {
const percent = (100 / this.totalRaw) * power;
return percent < threshold ? 0 : power;
},
updateElementWidth() {
this.width = this.$refs.site_progress.getBoundingClientRect().width;
this.width = this.$refs["site_progress"]?.getBoundingClientRect().width ?? 0;
},
labelBarProps(position, name, val) {
const value = val === undefined ? this[name] : val;
labelBarProps(position: string, name: string, val?: number) {
const value = val === undefined ? (this as any)[name] : val;
const minWidth = 40;
return {
value,
@ -242,7 +244,7 @@ export default {
};
},
},
};
});
</script>
<style scoped>
.site-progress {

View file

@ -1,3 +0,0 @@
import Energyflow from "./Energyflow.vue";
export default Energyflow;

View file

@ -0,0 +1,4 @@
interface Pv {
title?: string;
power: number;
}

View file

@ -9,10 +9,9 @@
</template>
<script lang="ts">
import { defineComponent } from "vue";
import type { PropType } from "vue";
import { isForecastSlot, type ForecastSlot, type TimeseriesEntry } from "../../utils/forecast";
import { defineComponent, type PropType } from "vue";
import formatter from "../../mixins/formatter";
import { isForecastSlot, type ForecastSlot, type TimeseriesEntry } from "./types.js";
export default defineComponent({
name: "ForecastActiveSlot",

View file

@ -35,14 +35,9 @@ import "chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm";
import { registerChartComponents, commonOptions } from "../Sessions/chartConfig";
import formatter, { POWER_UNIT } from "../../mixins/formatter.ts";
import colors, { lighterColor } from "../../colors.ts";
import {
highestSlotIndexByDay,
ForecastType,
type ForecastSlot,
type SolarDetails,
type TimeseriesEntry,
} from "../../utils/forecast.ts";
import type { CURRENCY } from "assets/js/types/evcc.ts";
import type { CURRENCY } from "../../types/evcc.ts";
import { ForecastType, highestSlotIndexByDay } from "../../utils/forecast.ts";
import type { ForecastSlot, SolarDetails, TimeseriesEntry } from "./types.ts";
registerChartComponents([
BarController,

View file

@ -64,10 +64,11 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { ForecastType, type ForecastSlot, type SolarDetails } from "../../utils/forecast.ts";
import formatter, { POWER_UNIT } from "../../mixins/formatter.ts";
import AnimatedNumber from "../Helper/AnimatedNumber.vue";
import type { CURRENCY, Timeout } from "assets/js/types/evcc.ts";
import type { CURRENCY, Timeout } from "../../types/evcc.ts";
import { ForecastType } from "../../utils/forecast.ts";
import type { ForecastSlot, SolarDetails } from "./types.ts";
const LOCALES_WITHOUT_DAY_AFTER_TOMORROW = ["en", "tr"];
export interface Energy {

View file

@ -58,16 +58,12 @@ import Chart from "./Chart.vue";
import TypeSelect from "./TypeSelect.vue";
import Details from "./Details.vue";
import ActiveSlot from "./ActiveSlot.vue";
import {
type ForecastSlot,
type TimeseriesEntry,
type Forecast,
ForecastType,
adjustedSolar,
} from "../../utils/forecast.ts";
import formatter from "../../mixins/formatter.ts";
import settings from "../../settings";
import type { CURRENCY } from "assets/js/types/evcc.ts";
import type { CURRENCY, Forecast } from "../../types/evcc.ts";
import { ForecastType, adjustedSolar } from "../../utils/forecast.ts";
import type { ForecastSlot, TimeseriesEntry } from "./types.ts";
export default defineComponent({
name: "ForecastModal",
components: {

View file

@ -0,0 +1,27 @@
export function isForecastSlot(obj?: TimeseriesEntry | ForecastSlot): obj is ForecastSlot {
return (obj as ForecastSlot).start !== undefined;
}
export interface TimeseriesEntry {
val: number;
ts: string;
}
export interface ForecastSlot {
start: string;
end: string;
value: number;
}
export interface EnergyByDay {
energy: number;
complete: boolean;
}
export interface SolarDetails {
scale?: number;
today?: EnergyByDay;
tomorrow?: EnergyByDay;
dayAfterTomorrow?: EnergyByDay;
timeseries?: TimeseriesEntry[];
}

View file

@ -96,8 +96,8 @@ export default defineComponent({
maximumFractionDigits: 0,
}).format(value);
},
fmtCo2Short(gramms: number) {
return `${this.fmtNumber(gramms, 0)} g`;
fmtCo2Short(gramms?: number) {
return gramms ? `${this.fmtNumber(gramms, 0)} g` : "?";
},
fmtCo2Medium(gramms: number) {
return `${this.fmtNumber(gramms, 0)} g/kWh`;

View file

@ -1,4 +1,5 @@
import type { StaticPlan, RepeatingPlan } from "../components/ChargingPlans/types";
import type { ForecastSlot, SolarDetails } from "../components/Forecast/types";
declare global {
interface Window {
@ -10,12 +11,20 @@ export interface State {
offline: boolean;
startup?: boolean;
loadpoints: [];
forecast?: any;
forecast?: Forecast;
currency?: CURRENCY;
fatal?: {
error: any;
};
}
export interface LoadpointCompact {
icon: string;
title: string;
charging: boolean;
soc?: number;
power: number;
heating?: boolean;
}
export enum CURRENCY {
EUR = "EUR",
@ -64,6 +73,13 @@ export interface Slot {
selectable?: boolean | null;
}
export interface Forecast {
grid?: ForecastSlot[];
co2?: ForecastSlot[];
solar?: SolarDetails;
planner?: ForecastSlot[];
}
export interface SelectOption<T> {
name: string;
value: T;

View file

@ -1,5 +1,5 @@
import type { Rate } from "../types/evcc";
import type { ForecastSlot } from "./forecast";
import type { ForecastSlot } from "../components/Forecast/types";
function convertRate(slot: ForecastSlot): Rate {
return {

View file

@ -1,40 +1,6 @@
import type { TimeseriesEntry, SolarDetails } from "../components/Forecast/types";
import deepCopy from "./deepClone";
export interface TimeseriesEntry {
val: number;
ts: string;
}
export interface ForecastSlot {
start: string;
end: string;
value: number;
}
export function isForecastSlot(obj?: TimeseriesEntry | ForecastSlot): obj is ForecastSlot {
return (obj as ForecastSlot).start !== undefined;
}
export interface EnergyByDay {
energy: number;
complete: boolean;
}
export interface SolarDetails {
scale?: number;
today?: EnergyByDay;
tomorrow?: EnergyByDay;
dayAfterTomorrow?: EnergyByDay;
timeseries?: TimeseriesEntry[];
}
export interface Forecast {
grid?: ForecastSlot[];
co2?: ForecastSlot[];
planner?: ForecastSlot[];
solar?: SolarDetails;
}
export enum ForecastType {
Solar = "solar",
Price = "price",

View file

@ -47,9 +47,10 @@ import Chart from "../components/Forecast/Chart.vue";
import IconSelectItem from "../components/Helper/IconSelectItem.vue";
import IconSelectGroup from "../components/Helper/IconSelectGroup.vue";
import DynamicPriceIcon from "../components/MaterialIcon/DynamicPrice.vue";
import { ForecastType, type ForecastSlot, type SolarDetails } from "../utils/forecast";
import formatter from "../mixins/formatter.ts";
import store from "../store.ts";
import type { ForecastSlot, SolarDetails } from "../components/Forecast/types.ts";
import { ForecastType } from "../utils/forecast.ts";
interface Forecast {
grid?: ForecastSlot[];