Chore: Typescript: Rewrite Loadpoints (#20859)
Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
parent
e0421cc7d8
commit
67449ed79a
20 changed files with 636 additions and 425 deletions
|
|
@ -23,9 +23,30 @@ GridAndPV.args = {
|
|||
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" },
|
||||
{
|
||||
power: 1000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
power: 1000,
|
||||
icon: "bike",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
power: 2200,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 2200,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
tariffGrid: 0.25,
|
||||
tariffFeedIn: 0.08,
|
||||
|
|
@ -61,7 +82,16 @@ BatteryCharging.args = {
|
|||
pvPower: 5000,
|
||||
gridPower: -1300,
|
||||
homePower: 800,
|
||||
loadpointsCompact: [{ power: 1400, icon: "car", charging: true, title: "Garage" }],
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1400,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1400,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
batteryPower: -1500,
|
||||
batterySoc: 75,
|
||||
};
|
||||
|
|
@ -86,8 +116,22 @@ BatteryThresholds.args = {
|
|||
pvPower: 8700,
|
||||
gridPower: -500,
|
||||
loadpointsCompact: [
|
||||
{ power: 5000, icon: "car", charging: true, title: "Garage" },
|
||||
{ power: 2500, icon: "bus", charging: true, title: "Garage" },
|
||||
{
|
||||
power: 5000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 5000,
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
power: 2500,
|
||||
icon: "bus",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 2500,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
batteryPower: -700,
|
||||
batterySoc: 95,
|
||||
|
|
@ -102,8 +146,22 @@ PVThresholds.args = {
|
|||
gridPower: 6500,
|
||||
homePower: 1000,
|
||||
loadpointsCompact: [
|
||||
{ power: 5000, icon: "car", charging: true, title: "Garage" },
|
||||
{ power: 1600, icon: "car", charging: true, title: "Garage" },
|
||||
{
|
||||
power: 5000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 5000,
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
power: 1600,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1600,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
batteryPower: 800,
|
||||
batterySoc: 76,
|
||||
|
|
@ -118,10 +176,38 @@ GridOnly.args = {
|
|||
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" },
|
||||
{
|
||||
power: 5500,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 5500,
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
power: 0,
|
||||
icon: "car",
|
||||
charging: false,
|
||||
title: "Garage",
|
||||
chargePower: 0,
|
||||
connected: false,
|
||||
},
|
||||
{
|
||||
power: 0,
|
||||
icon: "car",
|
||||
charging: false,
|
||||
title: "Garage",
|
||||
chargePower: 0,
|
||||
connected: false,
|
||||
},
|
||||
{
|
||||
power: 0,
|
||||
icon: "car",
|
||||
charging: false,
|
||||
title: "Garage",
|
||||
chargePower: 0,
|
||||
connected: false,
|
||||
},
|
||||
],
|
||||
batteryPower: 0,
|
||||
batterySoc: 0,
|
||||
|
|
@ -150,9 +236,30 @@ CO2.args = {
|
|||
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" },
|
||||
{
|
||||
power: 1000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
power: 1000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
},
|
||||
{
|
||||
power: 2200,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 2200,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
tariffGrid: 0.25,
|
||||
tariffFeedIn: 0.08,
|
||||
|
|
@ -168,7 +275,16 @@ UnknownInput.args = {
|
|||
pvConfigured: true,
|
||||
pvPower: 2000,
|
||||
gridPower: -2000,
|
||||
loadpointsCompact: [{ power: 1000, icon: "car", charging: true, title: "Garage" }],
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const UnknownInputFill = Template.bind({});
|
||||
|
|
@ -188,7 +304,16 @@ UnknownOutput.args = {
|
|||
pvConfigured: true,
|
||||
pvPower: 3000,
|
||||
gridPower: -1000,
|
||||
loadpointsCompact: [{ power: 1700, icon: "car", charging: true, title: "Garage" }],
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1700,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1700,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const UnknownOutputLessThan10Percent = Template.bind({});
|
||||
|
|
@ -197,5 +322,14 @@ UnknownOutputLessThan10Percent.args = {
|
|||
pvConfigured: true,
|
||||
pvPower: 3000,
|
||||
gridPower: -1000,
|
||||
loadpointsCompact: [{ power: 1800, icon: "car", charging: true, title: "Garage" }],
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1800,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1800,
|
||||
connected: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@
|
|||
<slot>
|
||||
<h3 class="value m-0" :class="valueClass">
|
||||
<slot name="value">
|
||||
<AnimatedNumber v-if="valueFmt" :to="value" :format="valueFmt" />
|
||||
<AnimatedNumber
|
||||
v-if="valueFmt && typeof value === 'number'"
|
||||
:to="value"
|
||||
:format="valueFmt"
|
||||
/>
|
||||
<span v-else>{{ value }}</span>
|
||||
<div v-if="extraValue != null" class="extraValue text-nowrap">
|
||||
{{ extraValue || " " }}
|
||||
|
|
@ -26,7 +30,7 @@ export default defineComponent({
|
|||
components: { AnimatedNumber },
|
||||
props: {
|
||||
label: String,
|
||||
value: [Number],
|
||||
value: [Number, String],
|
||||
valueFmt: Function as PropType<(n: number) => string>,
|
||||
extraValue: String,
|
||||
align: { type: String, default: "center" },
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
import Loadpoint from "./Loadpoint.vue";
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Loadpoint",
|
||||
component: Loadpoint,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
};
|
||||
|
||||
const baseState = {
|
||||
id: 0,
|
||||
pvConfigured: true,
|
||||
chargePower: 2800,
|
||||
chargedEnergy: 11e3,
|
||||
chargeDuration: 95 * 60,
|
||||
vehicleTitle: "Mein Auto",
|
||||
vehicleName: "meinauto",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
mode: "pv",
|
||||
charging: true,
|
||||
vehicleSoc: 66,
|
||||
limitSoc: 90,
|
||||
offeredCurrent: 7,
|
||||
minCurrent: 6,
|
||||
maxCurrent: 16,
|
||||
activePhases: 2,
|
||||
};
|
||||
|
||||
const createStory = (props) => {
|
||||
const story = (args) => ({
|
||||
components: { Loadpoint },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Loadpoint v-bind="args" />',
|
||||
});
|
||||
story.args = { ...baseState, ...props };
|
||||
return story;
|
||||
};
|
||||
|
||||
export const Standard = createStory({});
|
||||
|
||||
export const WithoutSoc = createStory({
|
||||
vehicleTitle: "",
|
||||
vehicleName: "",
|
||||
});
|
||||
|
||||
export const Idle = createStory({
|
||||
enabled: false,
|
||||
connected: false,
|
||||
vehicleName: "",
|
||||
mode: "off",
|
||||
charging: false,
|
||||
offeredCurrent: 0,
|
||||
});
|
||||
|
||||
export const DisabledLongTitle = createStory({
|
||||
title: "Charging point with a very very very long title!!!1!",
|
||||
remoteDisabled: "soft",
|
||||
remoteDisabledSource: "Sunny Home Manager",
|
||||
mode: "now",
|
||||
enabled: false,
|
||||
charging: false,
|
||||
chargePower: 0,
|
||||
});
|
||||
|
||||
export const ChargerIconNoVehicle = createStory({
|
||||
chargerIcon: "heater",
|
||||
title: "Heating device with long name",
|
||||
mode: "now",
|
||||
chargerFeatureIntegratedDevice: true,
|
||||
});
|
||||
78
assets/js/components/Loadpoints/Loadpoint.stories.ts
Normal file
78
assets/js/components/Loadpoints/Loadpoint.stories.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import Loadpoint from "./Loadpoint.vue";
|
||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Loadpoint",
|
||||
component: Loadpoint,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} as Meta<typeof Loadpoint>;
|
||||
|
||||
const baseState = {
|
||||
id: 0,
|
||||
pvConfigured: true,
|
||||
chargePower: 2800,
|
||||
chargedEnergy: 11e3,
|
||||
chargeDuration: 95 * 60,
|
||||
vehicleTitle: "Mein Auto",
|
||||
vehicleName: "meinauto",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
mode: "pv",
|
||||
charging: true,
|
||||
vehicleSoc: 66,
|
||||
limitSoc: 90,
|
||||
offeredCurrent: 7,
|
||||
minCurrent: 6,
|
||||
maxCurrent: 16,
|
||||
activePhases: 2,
|
||||
};
|
||||
|
||||
const Template: StoryFn<typeof Loadpoint> = (args) => {
|
||||
const story = () => ({
|
||||
components: { Loadpoint },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Loadpoint v-bind="args" />',
|
||||
});
|
||||
story.args = { ...baseState, ...args };
|
||||
return story;
|
||||
};
|
||||
|
||||
export const Standard = Template.bind({});
|
||||
|
||||
export const WithoutSoc = Template.bind({});
|
||||
WithoutSoc.args = {
|
||||
vehicleName: "",
|
||||
};
|
||||
|
||||
export const Idle = Template.bind({});
|
||||
Idle.args = {
|
||||
enabled: false,
|
||||
connected: false,
|
||||
vehicleName: "",
|
||||
mode: "off",
|
||||
charging: false,
|
||||
offeredCurrent: 0,
|
||||
};
|
||||
|
||||
export const DisabledLongTitle = Template.bind({});
|
||||
DisabledLongTitle.args = {
|
||||
title: "Charging point with a very very very long title!!!1!",
|
||||
remoteDisabled: "soft",
|
||||
remoteDisabledSource: "Sunny Home Manager",
|
||||
mode: "now",
|
||||
enabled: false,
|
||||
charging: false,
|
||||
chargePower: 0,
|
||||
};
|
||||
|
||||
export const ChargerIconNoVehicle = Template.bind({});
|
||||
ChargerIconNoVehicle.args = {
|
||||
chargerIcon: "heater",
|
||||
title: "Heating device with long name",
|
||||
mode: "now",
|
||||
chargerFeatureIntegratedDevice: true,
|
||||
};
|
||||
|
|
@ -72,7 +72,8 @@
|
|||
<LabelAndValue
|
||||
v-show="socBasedCharging"
|
||||
:label="$t('main.loadpoint.charged')"
|
||||
:value="fmtEnergy(chargedEnergy)"
|
||||
:value="chargedEnergy"
|
||||
:valueFmt="fmtEnergy"
|
||||
align="center"
|
||||
/>
|
||||
<LoadpointSessionInfo v-bind="sessionInfoProps" />
|
||||
|
|
@ -90,12 +91,12 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/regular/lightning";
|
||||
import "@h2d2/shopicons/es/regular/adjust";
|
||||
import api from "@/api";
|
||||
import Mode from "./Mode.vue";
|
||||
import Vehicle from "../Vehicles/Vehicle.vue";
|
||||
import VehicleComponent from "../Vehicles/Vehicle.vue";
|
||||
import Phases from "./Phases.vue";
|
||||
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
||||
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
||||
|
|
@ -106,12 +107,14 @@ import VehicleIcon from "../VehicleIcon";
|
|||
import SessionInfo from "./SessionInfo.vue";
|
||||
import smartCostAvailable from "@/utils/smartCostAvailable";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { CHARGE_MODE, PHASES, Timeout, Vehicle, Forecast } from "@/types/evcc";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "Loadpoint",
|
||||
components: {
|
||||
Mode,
|
||||
Vehicle,
|
||||
Vehicle: VehicleComponent,
|
||||
Phases,
|
||||
LabelAndValue,
|
||||
LoadpointSettingsButton: SettingsButton,
|
||||
|
|
@ -129,9 +132,9 @@ export default {
|
|||
mode: String,
|
||||
effectiveLimitSoc: Number,
|
||||
limitEnergy: Number,
|
||||
remoteDisabled: Boolean,
|
||||
remoteDisabled: String,
|
||||
remoteDisabledSource: String,
|
||||
chargeDuration: Number,
|
||||
chargeDuration: { type: Number, default: 0 },
|
||||
charging: Boolean,
|
||||
batteryBoost: Boolean,
|
||||
batteryConfigured: Boolean,
|
||||
|
|
@ -155,15 +158,15 @@ export default {
|
|||
enabled: Boolean,
|
||||
vehicleDetectionActive: Boolean,
|
||||
vehicleRange: Number,
|
||||
vehicleSoc: Number,
|
||||
vehicleSoc: { type: Number, default: 0 },
|
||||
vehicleName: String,
|
||||
vehicleIcon: String,
|
||||
vehicleLimitSoc: Number,
|
||||
vehicles: Array,
|
||||
vehicles: Array as PropType<Vehicle[]>,
|
||||
planActive: Boolean,
|
||||
planProjectedStart: String,
|
||||
planProjectedEnd: String,
|
||||
planOverrun: Number,
|
||||
planOverrun: { type: Number, default: 0 },
|
||||
planEnergy: Number,
|
||||
planPrecondition: Number,
|
||||
planTime: String,
|
||||
|
|
@ -176,9 +179,9 @@ export default {
|
|||
// details
|
||||
vehicleClimaterActive: Boolean,
|
||||
vehicleWelcomeActive: Boolean,
|
||||
chargePower: Number,
|
||||
chargedEnergy: Number,
|
||||
chargeRemainingDuration: Number,
|
||||
chargePower: { type: Number, default: 0 },
|
||||
chargedEnergy: { type: Number, default: 0 },
|
||||
chargeRemainingDuration: { type: Number, default: 0 },
|
||||
|
||||
// other information
|
||||
phasesConfigured: Number,
|
||||
|
|
@ -192,8 +195,8 @@ export default {
|
|||
chargeCurrents: Array,
|
||||
chargeRemainingEnergy: Number,
|
||||
phaseAction: String,
|
||||
phaseRemaining: Number,
|
||||
pvRemaining: Number,
|
||||
phaseRemaining: { type: Number, default: 0 },
|
||||
pvRemaining: { type: Number, default: 0 },
|
||||
pvAction: String,
|
||||
smartCostLimit: { type: Number, default: null },
|
||||
smartCostType: String,
|
||||
|
|
@ -205,11 +208,11 @@ export default {
|
|||
multipleLoadpoints: Boolean,
|
||||
gridConfigured: Boolean,
|
||||
pvConfigured: Boolean,
|
||||
forecast: Object, // as PropType<Forecast>,
|
||||
forecast: Object as PropType<Forecast>,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tickerHandler: null,
|
||||
tickerHandler: null as Timeout,
|
||||
phaseRemainingInterpolated: this.phaseRemaining,
|
||||
pvRemainingInterpolated: this.pvRemaining,
|
||||
chargeDurationInterpolated: this.chargeDuration,
|
||||
|
|
@ -245,7 +248,7 @@ export default {
|
|||
return this.collectProps(SettingsModal);
|
||||
},
|
||||
vehicleProps() {
|
||||
return this.collectProps(Vehicle);
|
||||
return this.collectProps(VehicleComponent);
|
||||
},
|
||||
showChargingIndicator() {
|
||||
return this.charging && this.chargePower > 0;
|
||||
|
|
@ -269,7 +272,7 @@ export default {
|
|||
return this.vehicleHasSoc || this.vehicleSoc > 0;
|
||||
},
|
||||
socBasedPlanning() {
|
||||
return this.socBasedCharging && this.vehicle?.capacity > 0;
|
||||
return this.socBasedCharging && this.vehicle?.capacity && this.vehicle?.capacity > 0;
|
||||
},
|
||||
pvPossible() {
|
||||
return this.pvConfigured || this.gridConfigured;
|
||||
|
|
@ -281,7 +284,12 @@ export default {
|
|||
return this.batteryConfigured && this.$hiddenFeatures();
|
||||
},
|
||||
batteryBoostActive() {
|
||||
return this.batteryBoost && this.charging && !["off", "now"].includes(this.mode);
|
||||
return (
|
||||
this.batteryBoost &&
|
||||
this.charging &&
|
||||
this.mode &&
|
||||
!["off", "now"].includes(this.mode)
|
||||
);
|
||||
},
|
||||
plannerForecast() {
|
||||
return this.forecast?.planner;
|
||||
|
|
@ -305,7 +313,9 @@ export default {
|
|||
this.tickerHandler = setInterval(this.tick, 1000);
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval(this.tickerHandler);
|
||||
if (this.tickerHandler) {
|
||||
clearInterval(this.tickerHandler);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tick() {
|
||||
|
|
@ -322,50 +332,50 @@ export default {
|
|||
this.chargeRemainingDurationInterpolated--;
|
||||
}
|
||||
},
|
||||
apiPath(func) {
|
||||
apiPath(func: string) {
|
||||
return "loadpoints/" + this.id + "/" + func;
|
||||
},
|
||||
setTargetMode(mode) {
|
||||
setTargetMode(mode: CHARGE_MODE) {
|
||||
api.post(this.apiPath("mode") + "/" + mode);
|
||||
},
|
||||
setLimitSoc(soc) {
|
||||
setLimitSoc(soc: number) {
|
||||
api.post(this.apiPath("limitsoc") + "/" + soc);
|
||||
},
|
||||
setLimitEnergy(kWh) {
|
||||
setLimitEnergy(kWh: number) {
|
||||
api.post(this.apiPath("limitenergy") + "/" + kWh);
|
||||
},
|
||||
setMaxCurrent(maxCurrent) {
|
||||
setMaxCurrent(maxCurrent: number) {
|
||||
api.post(this.apiPath("maxcurrent") + "/" + maxCurrent);
|
||||
},
|
||||
setMinCurrent(minCurrent) {
|
||||
setMinCurrent(minCurrent: number) {
|
||||
api.post(this.apiPath("mincurrent") + "/" + minCurrent);
|
||||
},
|
||||
setPhasesConfigured(phases) {
|
||||
setPhasesConfigured(phases: PHASES) {
|
||||
api.post(this.apiPath("phases") + "/" + phases);
|
||||
},
|
||||
changeVehicle(name) {
|
||||
changeVehicle(name: string) {
|
||||
api.post(this.apiPath("vehicle") + `/${name}`);
|
||||
},
|
||||
removeVehicle() {
|
||||
api.delete(this.apiPath("vehicle"));
|
||||
},
|
||||
setBatteryBoost(batteryBoost) {
|
||||
setBatteryBoost(batteryBoost: boolean) {
|
||||
api.post(this.apiPath("batteryboost") + `/${batteryBoost ? "1" : "0"}`);
|
||||
},
|
||||
fmtPower(value) {
|
||||
fmtPower(value: number) {
|
||||
return this.fmtW(value, POWER_UNIT.AUTO);
|
||||
},
|
||||
fmtEnergy(value) {
|
||||
fmtEnergy(value: number) {
|
||||
return this.fmtWh(value, POWER_UNIT.AUTO);
|
||||
},
|
||||
openSettingsModal() {
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById(`loadpointSettingsModal_${this.id}`)
|
||||
document.getElementById(`loadpointSettingsModal_${this.id}`) as HTMLElement
|
||||
);
|
||||
modal.show();
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
import Loadpoints from "./Loadpoints.vue";
|
||||
|
||||
function loadpoint(opts = {}) {
|
||||
const base = {
|
||||
id: 0,
|
||||
pvConfigured: true,
|
||||
chargePower: 2800,
|
||||
chargedEnergy: 11e3,
|
||||
chargeDuration: 95 * 60,
|
||||
vehicleName: "tesla",
|
||||
vehicleTitle: "Tesla Model 3",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
mode: "pv",
|
||||
charging: true,
|
||||
vehicleSoc: 66,
|
||||
limitSoc: 90,
|
||||
offeredCurrent: 7,
|
||||
minCurrent: 6,
|
||||
maxCurrent: 16,
|
||||
activePhases: 2,
|
||||
};
|
||||
return { ...base, ...opts };
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Loadpoints",
|
||||
component: Loadpoints,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => ({
|
||||
components: { Loadpoints },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Loadpoints v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Standard = Template.bind({});
|
||||
Standard.args = {
|
||||
vehicles: [],
|
||||
loadpoints: [
|
||||
loadpoint({ title: "Carport" }),
|
||||
loadpoint({
|
||||
title: "Water Heater",
|
||||
chargerFeatureIntegratedDevice: true,
|
||||
chargerIcon: "waterheater",
|
||||
}),
|
||||
],
|
||||
};
|
||||
57
assets/js/components/Loadpoints/Loadpoints.stories.ts
Normal file
57
assets/js/components/Loadpoints/Loadpoints.stories.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import Loadpoints from "./Loadpoints.vue";
|
||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
function loadpoint(opts = {}) {
|
||||
const base = {
|
||||
id: 0,
|
||||
pvConfigured: true,
|
||||
chargePower: 2800,
|
||||
chargedEnergy: 11e3,
|
||||
chargeDuration: 95 * 60,
|
||||
vehicleName: "tesla",
|
||||
vehicleTitle: "Tesla Model 3",
|
||||
enabled: true,
|
||||
connected: true,
|
||||
mode: "pv",
|
||||
charging: true,
|
||||
vehicleSoc: 66,
|
||||
limitSoc: 90,
|
||||
offeredCurrent: 7,
|
||||
minCurrent: 6,
|
||||
maxCurrent: 16,
|
||||
activePhases: 2,
|
||||
icon: "car",
|
||||
title: "Garage",
|
||||
power: 2100,
|
||||
};
|
||||
return { ...base, ...opts };
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Loadpoints",
|
||||
component: Loadpoints,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} as Meta<typeof Loadpoints>;
|
||||
|
||||
const Template: StoryFn<typeof Loadpoints> = (args) => ({
|
||||
components: { Loadpoints },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Loadpoints v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Standard = Template.bind({});
|
||||
Standard.args = {
|
||||
vehicles: [],
|
||||
loadpoints: [
|
||||
loadpoint({ title: "Carport" }),
|
||||
loadpoint({
|
||||
title: "Water Heater",
|
||||
chargerFeatureIntegratedDevice: true,
|
||||
chargerIcon: "waterheater",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
|
@ -54,24 +54,26 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/filled/circle";
|
||||
import "@h2d2/shopicons/es/bold/circle";
|
||||
import "@h2d2/shopicons/es/filled/lightning";
|
||||
|
||||
import Loadpoint from "./Loadpoint.vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { LoadpointCompact, Timeout, Vehicle } from "@/types/evcc";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "Loadpoints",
|
||||
components: { Loadpoint },
|
||||
props: {
|
||||
loadpoints: Array,
|
||||
vehicles: Array,
|
||||
loadpoints: { type: Array as PropType<LoadpointCompact[]>, default: () => [] },
|
||||
vehicles: { type: Array as PropType<Vehicle[]> },
|
||||
smartCostType: String,
|
||||
tariffGrid: Number,
|
||||
tariffCo2: Number,
|
||||
currency: String,
|
||||
selectedIndex: Number,
|
||||
selectedIndex: { type: Number, default: 0 },
|
||||
gridConfigured: Boolean,
|
||||
pvConfigured: Boolean,
|
||||
batteryConfigured: Boolean,
|
||||
|
|
@ -79,7 +81,11 @@ export default {
|
|||
},
|
||||
emits: ["index-changed"],
|
||||
data() {
|
||||
return { snapTimeout: null, scrollTimeout: null, highlightedIndex: 0 };
|
||||
return {
|
||||
snapTimeout: null as Timeout,
|
||||
scrollTimeout: null as Timeout,
|
||||
highlightedIndex: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
selectedIndex(newIndex) {
|
||||
|
|
@ -88,54 +94,60 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
if (this.selectedIndex > 0) {
|
||||
this.$refs.carousel.scrollTo({ top: 0, left: this.left(this.selectedIndex) });
|
||||
this.$refs["carousel"]?.scrollTo({ top: 0, left: this.left(this.selectedIndex) });
|
||||
}
|
||||
this.$refs.carousel.addEventListener("scroll", this.handleCarouselScroll);
|
||||
this.$refs["carousel"]?.addEventListener("scroll", this.handleCarouselScroll);
|
||||
},
|
||||
unmounted() {
|
||||
if (this.$refs.carousel) {
|
||||
this.$refs.carousel.removeEventListener("scroll", this.handleCarouselScroll);
|
||||
}
|
||||
this.$refs["carousel"]?.removeEventListener("scroll", this.handleCarouselScroll);
|
||||
},
|
||||
methods: {
|
||||
handleCarouselScroll() {
|
||||
const { scrollLeft } = this.$refs.carousel;
|
||||
const { offsetWidth } = this.$refs.carousel.children[0];
|
||||
const { scrollLeft } = this.$refs["carousel"] as HTMLElement;
|
||||
const { offsetWidth } = this.$refs["carousel"]?.children[0] as HTMLElement;
|
||||
this.highlightedIndex = Math.round((scrollLeft - 7.5) / offsetWidth);
|
||||
|
||||
// save scroll position to url if not changing for 2s
|
||||
clearTimeout(this.scrollTimeout);
|
||||
if (this.scrollTimeout) {
|
||||
clearTimeout(this.scrollTimeout);
|
||||
}
|
||||
this.scrollTimeout = setTimeout(() => {
|
||||
if (this.highlightedIndex !== this.selectedIndex) {
|
||||
this.$emit("index-changed", this.highlightedIndex);
|
||||
}
|
||||
}, 2000);
|
||||
},
|
||||
goTo(index) {
|
||||
goTo(index: number) {
|
||||
this.$emit("index-changed", index);
|
||||
},
|
||||
isCharging(lp) {
|
||||
isCharging(lp: LoadpointCompact) {
|
||||
return lp.charging && lp.chargePower > 0;
|
||||
},
|
||||
selected(index) {
|
||||
selected(index: number) {
|
||||
return this.highlightedIndex === index;
|
||||
},
|
||||
left(index) {
|
||||
return this.$refs.carousel.children[0].offsetWidth * index;
|
||||
left(index: number) {
|
||||
return (this.$refs["carousel"]?.children[0] as HTMLElement).offsetWidth * index;
|
||||
},
|
||||
scrollTo(index) {
|
||||
scrollTo(index: number) {
|
||||
this.highlightedIndex = index;
|
||||
const $carousel = this.$refs.carousel;
|
||||
$carousel.style.scrollSnapType = "none";
|
||||
$carousel.scrollTo({ top: 0, left: this.left(index), behavior: "smooth" });
|
||||
const $carousel = this.$refs["carousel"];
|
||||
if ($carousel) {
|
||||
$carousel.style.scrollSnapType = "none";
|
||||
$carousel?.scrollTo({ top: 0, left: this.left(index), behavior: "smooth" });
|
||||
}
|
||||
|
||||
clearTimeout(this.snapTimeout);
|
||||
if (this.snapTimeout) {
|
||||
clearTimeout(this.snapTimeout);
|
||||
}
|
||||
this.snapTimeout = setTimeout(() => {
|
||||
this.$refs.carousel.style.scrollSnapType = "x mandatory";
|
||||
if (this.$refs["carousel"]) {
|
||||
this.$refs["carousel"].style.scrollSnapType = "x mandatory";
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.container--loadpoint {
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import Mode from "./Mode.vue";
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Mode",
|
||||
component: Mode,
|
||||
argTypes: {
|
||||
mode: {
|
||||
control: "select",
|
||||
options: ["off", "now", "minpv", "pv"],
|
||||
description: "Charging mode",
|
||||
},
|
||||
pvPossible: { control: "boolean", description: "Whether PV is possible" },
|
||||
hasSmartCost: { control: "boolean", description: "Whether smart cost is available" },
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
};
|
||||
|
||||
const createStory = (props) => {
|
||||
const story = (args) => ({
|
||||
components: { Mode },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Mode v-bind="args" />',
|
||||
});
|
||||
story.args = props;
|
||||
return story;
|
||||
};
|
||||
|
||||
export const Minimal = createStory({ mode: "now" });
|
||||
|
||||
export const Full = createStory({
|
||||
mode: "pv",
|
||||
pvPossible: true,
|
||||
hasSmartCost: true,
|
||||
});
|
||||
|
||||
export const SmartGridOnly = createStory({
|
||||
mode: "pv",
|
||||
pvPossible: false,
|
||||
hasSmartCost: true,
|
||||
});
|
||||
48
assets/js/components/Loadpoints/Mode.stories.ts
Normal file
48
assets/js/components/Loadpoints/Mode.stories.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import Mode from "./Mode.vue";
|
||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Mode",
|
||||
component: Mode,
|
||||
argTypes: {
|
||||
mode: {
|
||||
control: "select",
|
||||
options: ["off", "now", "minpv", "pv"],
|
||||
description: "Charging mode",
|
||||
},
|
||||
pvPossible: { control: "boolean", description: "Whether PV is possible" },
|
||||
hasSmartCost: { control: "boolean", description: "Whether smart cost is available" },
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} as Meta<typeof Mode>;
|
||||
|
||||
const Template: StoryFn<typeof Mode> = (args) => {
|
||||
const story = () => ({
|
||||
components: { Mode },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Mode v-bind="args" />',
|
||||
});
|
||||
story.args = args;
|
||||
return story;
|
||||
};
|
||||
|
||||
export const Minimal = Template.bind({});
|
||||
Minimal.args = { mode: "now" };
|
||||
|
||||
export const Full = Template.bind({});
|
||||
Full.args = {
|
||||
mode: "pv",
|
||||
pvPossible: true,
|
||||
hasSmartCost: true,
|
||||
};
|
||||
|
||||
export const SmartGridOnly = Template.bind({});
|
||||
SmartGridOnly.args = {
|
||||
mode: "pv",
|
||||
pvPossible: false,
|
||||
hasSmartCost: true,
|
||||
};
|
||||
|
|
@ -14,8 +14,13 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
<script lang="ts">
|
||||
import { CHARGE_MODE } from "@/types/evcc";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
const { OFF, PV, MINPV, NOW } = CHARGE_MODE;
|
||||
|
||||
export default defineComponent({
|
||||
name: "Mode",
|
||||
props: {
|
||||
mode: String,
|
||||
|
|
@ -25,33 +30,33 @@ export default {
|
|||
emits: ["updated"],
|
||||
|
||||
computed: {
|
||||
modes() {
|
||||
modes(): CHARGE_MODE[] {
|
||||
if (this.pvPossible) {
|
||||
return ["off", "pv", "minpv", "now"];
|
||||
return [OFF, PV, MINPV, NOW];
|
||||
}
|
||||
if (this.hasSmartCost) {
|
||||
return ["off", "pv", "now"];
|
||||
return [OFF, PV, NOW];
|
||||
}
|
||||
return ["off", "now"];
|
||||
return [OFF, NOW];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
label(mode) {
|
||||
label(mode: CHARGE_MODE) {
|
||||
// rename pv mode to smart for non-pv and dynamic tariffs scenarios
|
||||
// TODO: rollout smart name for everyting later
|
||||
if (mode === "pv" && !this.pvPossible && this.hasSmartCost) {
|
||||
if (mode === PV && !this.pvPossible && this.hasSmartCost) {
|
||||
return this.$t("main.mode.smart");
|
||||
}
|
||||
return this.$t(`main.mode.${mode}`);
|
||||
},
|
||||
isActive(mode) {
|
||||
isActive(mode: CHARGE_MODE) {
|
||||
return this.mode === mode;
|
||||
},
|
||||
setTargetMode(mode) {
|
||||
setTargetMode(mode: CHARGE_MODE) {
|
||||
this.$emit("updated", mode);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
import Phases from "./Phases.vue";
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Phases",
|
||||
component: Phases,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
phasesActive: { control: { type: "number" } },
|
||||
minCurrent: { control: { type: "number" } },
|
||||
maxCurrent: { control: { type: "number" } },
|
||||
offeredCurrent: { control: { type: "number" } },
|
||||
chargeCurrents: { control: { type: "object" } },
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => ({
|
||||
components: { Phases },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Phases v-bind="args" />',
|
||||
});
|
||||
|
||||
export const OnePhase = Template.bind({});
|
||||
OnePhase.args = {
|
||||
phasesActive: 1,
|
||||
minCurrent: 6,
|
||||
maxCurrent: 16,
|
||||
offeredCurrent: 8,
|
||||
chargeCurrents: null,
|
||||
};
|
||||
|
||||
export const TwoPhase = Template.bind({});
|
||||
TwoPhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 2,
|
||||
};
|
||||
|
||||
export const ThreePhase = Template.bind({});
|
||||
ThreePhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 3,
|
||||
};
|
||||
|
||||
export const RealCurrents = Template.bind({});
|
||||
RealCurrents.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 3,
|
||||
offeredCurrent: 13,
|
||||
chargeCurrents: [11, 9, 12],
|
||||
};
|
||||
|
||||
export const OnePhaseMoreAvailable = Template.bind({});
|
||||
OnePhaseMoreAvailable.args = {
|
||||
...OnePhase.args,
|
||||
offeredCurrent: 12,
|
||||
chargeCurrents: [6, 0.2, 0],
|
||||
};
|
||||
|
||||
export const TwoPhasesActive = Template.bind({});
|
||||
TwoPhasesActive.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 2,
|
||||
offeredCurrent: 16,
|
||||
chargeCurrents: [16, 16, 0.3],
|
||||
};
|
||||
|
||||
export const AsymetricPhases = Template.bind({});
|
||||
AsymetricPhases.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 2,
|
||||
offeredCurrent: 16,
|
||||
chargeCurrents: [8, 0.9, 14],
|
||||
};
|
||||
|
||||
export const OnlySecondPhase = Template.bind({});
|
||||
OnlySecondPhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 1,
|
||||
offeredCurrent: 13,
|
||||
chargeCurrents: [0, 13, 0],
|
||||
};
|
||||
|
||||
export const MainlyThirdPhase = Template.bind({});
|
||||
MainlyThirdPhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 1,
|
||||
offeredCurrent: 10,
|
||||
chargeCurrents: [0.007, 0.009, 5.945],
|
||||
minCurrent: 6,
|
||||
maxCurrent: 20,
|
||||
};
|
||||
95
assets/js/components/Loadpoints/Phases.stories.ts
Normal file
95
assets/js/components/Loadpoints/Phases.stories.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import Phases from "./Phases.vue";
|
||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Phases",
|
||||
component: Phases,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
phasesActive: { control: { type: "number" } },
|
||||
minCurrent: { control: { type: "number" } },
|
||||
maxCurrent: { control: { type: "number" } },
|
||||
offeredCurrent: { control: { type: "number" } },
|
||||
chargeCurrents: { control: { type: "object" } },
|
||||
},
|
||||
} as Meta<typeof Phases>;
|
||||
|
||||
const Template: StoryFn<typeof Phases> = (args) => ({
|
||||
components: { Phases },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Phases v-bind="args" />',
|
||||
});
|
||||
|
||||
export const OnePhase = Template.bind({});
|
||||
OnePhase.args = {
|
||||
phasesActive: 1,
|
||||
minCurrent: 6,
|
||||
maxCurrent: 16,
|
||||
offeredCurrent: 8,
|
||||
chargeCurrents: undefined,
|
||||
};
|
||||
|
||||
export const TwoPhase = Template.bind({});
|
||||
TwoPhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 2,
|
||||
};
|
||||
|
||||
export const ThreePhase = Template.bind({});
|
||||
ThreePhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 3,
|
||||
};
|
||||
|
||||
export const RealCurrents = Template.bind({});
|
||||
RealCurrents.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 3,
|
||||
offeredCurrent: 13,
|
||||
chargeCurrents: [11, 9, 12],
|
||||
};
|
||||
|
||||
export const OnePhaseMoreAvailable = Template.bind({});
|
||||
OnePhaseMoreAvailable.args = {
|
||||
...OnePhase.args,
|
||||
offeredCurrent: 12,
|
||||
chargeCurrents: [6, 0.2, 0],
|
||||
};
|
||||
|
||||
export const TwoPhasesActive = Template.bind({});
|
||||
TwoPhasesActive.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 2,
|
||||
offeredCurrent: 16,
|
||||
chargeCurrents: [16, 16, 0.3],
|
||||
};
|
||||
|
||||
export const AsymetricPhases = Template.bind({});
|
||||
AsymetricPhases.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 2,
|
||||
offeredCurrent: 16,
|
||||
chargeCurrents: [8, 0.9, 14],
|
||||
};
|
||||
|
||||
export const OnlySecondPhase = Template.bind({});
|
||||
OnlySecondPhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 1,
|
||||
offeredCurrent: 13,
|
||||
chargeCurrents: [0, 13, 0],
|
||||
};
|
||||
|
||||
export const MainlyThirdPhase = Template.bind({});
|
||||
MainlyThirdPhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 1,
|
||||
offeredCurrent: 10,
|
||||
chargeCurrents: [0.007, 0.009, 5.945],
|
||||
minCurrent: 6,
|
||||
maxCurrent: 20,
|
||||
};
|
||||
|
|
@ -12,21 +12,24 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import type { PHASES } from "@/types/evcc";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
const MIN_ACTIVE_CURRENT = 1;
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "Phases",
|
||||
props: {
|
||||
offeredCurrent: { type: Number },
|
||||
chargeCurrents: { type: Array },
|
||||
phasesActive: { type: Number },
|
||||
minCurrent: { type: Number },
|
||||
maxCurrent: { type: Number },
|
||||
offeredCurrent: { type: Number, default: 0 },
|
||||
chargeCurrents: { type: Array as PropType<number[]> },
|
||||
phasesActive: { type: Number as PropType<PHASES> },
|
||||
minCurrent: { type: Number, default: 6 },
|
||||
maxCurrent: { type: Number, default: 16 },
|
||||
},
|
||||
computed: {
|
||||
chargeCurrentsActive() {
|
||||
return this.chargeCurrents?.filter((c) => c >= MIN_ACTIVE_CURRENT).length > 0;
|
||||
if (!this.chargeCurrents) return false;
|
||||
return this.chargeCurrents.filter((c) => c >= MIN_ACTIVE_CURRENT).length > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -37,21 +40,21 @@ export default {
|
|||
);
|
||||
return (100 / this.maxCurrent) * current;
|
||||
},
|
||||
realWidth(num) {
|
||||
realWidth(num: number) {
|
||||
if (this.chargeCurrents) {
|
||||
const current = this.chargeCurrents[num - 1] || 0;
|
||||
return (100 / this.maxCurrent) * current;
|
||||
}
|
||||
return this.targetWidth();
|
||||
},
|
||||
isPhaseActive(num) {
|
||||
if (this.chargeCurrentsActive) {
|
||||
isPhaseActive(num: number) {
|
||||
if (this.chargeCurrentsActive && this.chargeCurrents) {
|
||||
return this.chargeCurrents[num - 1] >= MIN_ACTIVE_CURRENT;
|
||||
}
|
||||
return num <= this.phasesActive;
|
||||
return num <= (this.phasesActive || 0);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -26,13 +26,15 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
||||
import CustomSelect from "../Helper/CustomSelect.vue";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import { getSessionInfo, setSessionInfo } from "./session";
|
||||
import type { CURRENCY, SelectOption } from "@/types/evcc";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "LoadpointSessionInfo",
|
||||
components: {
|
||||
LabelAndValue,
|
||||
|
|
@ -40,13 +42,13 @@ export default {
|
|||
},
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
id: Number,
|
||||
id: { type: Number, default: 0 },
|
||||
sessionCo2PerKWh: { type: Number, default: 0 },
|
||||
sessionPricePerKWh: { type: Number, default: 0 },
|
||||
sessionPrice: { type: Number, default: 0 },
|
||||
currency: String,
|
||||
currency: String as PropType<CURRENCY>,
|
||||
sessionSolarPercentage: { type: Number, default: 0 },
|
||||
chargeRemainingDurationInterpolated: Number,
|
||||
chargeRemainingDurationInterpolated: { type: Number, default: 0 },
|
||||
chargeDurationInterpolated: Number,
|
||||
tariffCo2: Number,
|
||||
tariffGrid: Number,
|
||||
|
|
@ -101,7 +103,7 @@ export default {
|
|||
optionKeys() {
|
||||
return this.options.map((option) => option.key);
|
||||
},
|
||||
selectOptions() {
|
||||
selectOptions(): SelectOption<string>[] {
|
||||
return this.optionKeys.map((key) => ({
|
||||
name: this.$t(`main.loadpoint.${key}`),
|
||||
value: key,
|
||||
|
|
@ -126,11 +128,11 @@ export default {
|
|||
},
|
||||
finishTime() {
|
||||
const remainingSeconds = this.chargeRemainingDurationInterpolated;
|
||||
const now = new Date();
|
||||
if (remainingSeconds > 0) {
|
||||
const now = new Date();
|
||||
return new Date(now.getTime() + remainingSeconds * 1000);
|
||||
}
|
||||
return undefined;
|
||||
return now;
|
||||
},
|
||||
solarFormatted() {
|
||||
return this.fmtPercentage(this.sessionSolarPercentage, 1);
|
||||
|
|
@ -142,10 +144,10 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
fmtAvgPrice(value) {
|
||||
fmtAvgPrice(value: number) {
|
||||
return this.fmtPricePerKWh(value, this.currency, false);
|
||||
},
|
||||
fmtAvgPriceShort(value) {
|
||||
fmtAvgPriceShort(value: number) {
|
||||
return this.fmtPricePerKWh(value, this.currency, true);
|
||||
},
|
||||
nextSessionInfo() {
|
||||
|
|
@ -153,7 +155,7 @@ export default {
|
|||
this.selectedKey = this.optionKeys[index + 1] || this.optionKeys[0];
|
||||
this.presist();
|
||||
},
|
||||
selectOption(value) {
|
||||
selectOption(value: string) {
|
||||
this.selectedKey = value;
|
||||
this.presist();
|
||||
},
|
||||
|
|
@ -161,7 +163,7 @@ export default {
|
|||
setSessionInfo(this.id, this.selectedKey);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -37,14 +37,16 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import { CHARGE_MODE } from "@/types/evcc";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
formId: Function,
|
||||
mode: String,
|
||||
formId: { type: Function as PropType<(s: string) => string>, default: (s: string) => s },
|
||||
mode: String as PropType<CHARGE_MODE>,
|
||||
batteryBoost: Boolean,
|
||||
},
|
||||
emits: ["batteryboost-updated"],
|
||||
|
|
@ -55,7 +57,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
disabled() {
|
||||
return ["off", "now"].includes(this.mode);
|
||||
return this.mode && [CHARGE_MODE.OFF, CHARGE_MODE.NOW].includes(this.mode);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -68,5 +70,5 @@ export default {
|
|||
this.$emit("batteryboost-updated", this.selectedEnabled);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@
|
|||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/regular/adjust";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "LoadpointSettingsButton",
|
||||
props: {
|
||||
id: [String, Number],
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
v-if="isModalVisible"
|
||||
v-bind="smartCostLimitProps"
|
||||
:possible="smartCostAvailable"
|
||||
:smartCostLimit="smartCostLimit"
|
||||
class="mt-2"
|
||||
/>
|
||||
<LoadpointSettingsBatteryBoost
|
||||
|
|
@ -155,23 +156,21 @@
|
|||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import collector from "@/mixins/collector";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import SmartCostLimit from "../Tariff/SmartCostLimit.vue";
|
||||
import smartCostAvailable from "@/utils/smartCostAvailable";
|
||||
import SettingsBatteryBoost from "./SettingsBatteryBoost.vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { PHASES } from "@/types/evcc";
|
||||
|
||||
const V = 230;
|
||||
|
||||
const PHASES_AUTO = 0;
|
||||
const PHASES_1 = 1;
|
||||
const PHASES_3 = 3;
|
||||
|
||||
const range = (start, stop, step = -1) =>
|
||||
const range = (start: number, stop: number, step = -1) =>
|
||||
Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);
|
||||
|
||||
const insertSorted = (arr, num) => {
|
||||
const insertSorted = (arr: number[], num: number) => {
|
||||
const uniqueSet = new Set(arr);
|
||||
uniqueSet.add(num);
|
||||
return [...uniqueSet].sort((a, b) => b - a);
|
||||
|
|
@ -180,23 +179,25 @@ const insertSorted = (arr, num) => {
|
|||
// TODO: add max physical current to loadpoint (config ui) and only allow user to select values in side that range (main ui, here)
|
||||
const MAX_CURRENT = 64;
|
||||
|
||||
export default {
|
||||
const { AUTO, THREE_PHASES, ONE_PHASE } = PHASES;
|
||||
|
||||
export default defineComponent({
|
||||
name: "LoadpointSettingsModal",
|
||||
components: { SmartCostLimit, LoadpointSettingsBatteryBoost: SettingsBatteryBoost },
|
||||
mixins: [formatter, collector],
|
||||
props: {
|
||||
id: [String, Number],
|
||||
phasesConfigured: Number,
|
||||
phasesConfigured: { type: Number, default: 0 },
|
||||
chargerPhases1p3p: Boolean,
|
||||
chargerSinglePhase: Boolean,
|
||||
batteryBoost: Boolean,
|
||||
batteryBoostAvailable: Boolean,
|
||||
mode: String,
|
||||
minSoc: Number,
|
||||
maxCurrent: Number,
|
||||
minCurrent: Number,
|
||||
maxCurrent: { type: Number, default: 0 },
|
||||
minCurrent: { type: Number, default: 0 },
|
||||
title: String,
|
||||
smartCostLimit: Number,
|
||||
smartCostLimit: { type: Number as PropType<number | null>, default: null },
|
||||
smartCostType: String,
|
||||
tariffGrid: Number,
|
||||
currency: String,
|
||||
|
|
@ -223,20 +224,20 @@ export default {
|
|||
}
|
||||
if (this.chargerPhases1p3p) {
|
||||
// automatic switching
|
||||
return [PHASES_AUTO, PHASES_3, PHASES_1];
|
||||
return [AUTO, THREE_PHASES, ONE_PHASE];
|
||||
}
|
||||
// 1p or 3p possible
|
||||
return [PHASES_3, PHASES_1];
|
||||
return [THREE_PHASES, ONE_PHASE];
|
||||
},
|
||||
batteryBoostProps() {
|
||||
return this.collectProps(SettingsBatteryBoost);
|
||||
},
|
||||
maxPower() {
|
||||
if (this.chargerPhases1p3p) {
|
||||
if (this.phasesConfigured === PHASES_AUTO) {
|
||||
return this.maxPowerPhases(3);
|
||||
if (this.phasesConfigured === AUTO) {
|
||||
return this.maxPowerPhases(THREE_PHASES);
|
||||
}
|
||||
if ([PHASES_3, PHASES_1].includes(this.phasesConfigured)) {
|
||||
if ([THREE_PHASES, ONE_PHASE].includes(this.phasesConfigured)) {
|
||||
return this.maxPowerPhases(this.phasesConfigured);
|
||||
}
|
||||
}
|
||||
|
|
@ -244,10 +245,10 @@ export default {
|
|||
},
|
||||
minPower() {
|
||||
if (this.chargerPhases1p3p) {
|
||||
if (this.phasesConfigured === PHASES_AUTO) {
|
||||
return this.minPowerPhases(1);
|
||||
if (this.phasesConfigured === AUTO) {
|
||||
return this.minPowerPhases(ONE_PHASE);
|
||||
}
|
||||
if ([PHASES_3, PHASES_1].includes(this.phasesConfigured)) {
|
||||
if ([THREE_PHASES, ONE_PHASE].includes(this.phasesConfigured)) {
|
||||
return this.minPowerPhases(this.phasesConfigured);
|
||||
}
|
||||
}
|
||||
|
|
@ -285,26 +286,23 @@ export default {
|
|||
phasesConfigured(value) {
|
||||
this.selectedPhases = value;
|
||||
},
|
||||
minSoc(value) {
|
||||
this.selectedMinSoc = value;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.modal.addEventListener("show.bs.modal", this.modalVisible);
|
||||
this.$refs.modal.addEventListener("hidden.bs.modal", this.modalInvisible);
|
||||
this.$refs["modal"]?.addEventListener("show.bs.modal", this.modalVisible);
|
||||
this.$refs["modal"]?.addEventListener("hidden.bs.modal", this.modalInvisible);
|
||||
},
|
||||
unmounted() {
|
||||
this.$refs.modal?.removeEventListener("show.bs.modal", this.modalVisible);
|
||||
this.$refs.modal?.removeEventListener("hidden.bs.modal", this.modalInvisible);
|
||||
this.$refs["modal"]?.removeEventListener("show.bs.modal", this.modalVisible);
|
||||
this.$refs["modal"]?.removeEventListener("hidden.bs.modal", this.modalInvisible);
|
||||
},
|
||||
methods: {
|
||||
maxPowerPhases(phases) {
|
||||
maxPowerPhases(phases: PHASES) {
|
||||
return this.fmtW(this.maxCurrent * V * phases);
|
||||
},
|
||||
minPowerPhases(phases) {
|
||||
minPowerPhases(phases: PHASES) {
|
||||
return this.fmtW(this.minCurrent * V * phases);
|
||||
},
|
||||
formId(name) {
|
||||
formId(name: string) {
|
||||
return `loadpoint_${this.id}_${name}`;
|
||||
},
|
||||
changeMaxCurrent() {
|
||||
|
|
@ -316,8 +314,8 @@ export default {
|
|||
changePhasesConfigured() {
|
||||
this.$emit("phasesconfigured-updated", this.selectedPhases);
|
||||
},
|
||||
currentOption(value, isDefault) {
|
||||
let name = `${this.fmtNumber(value)} A`;
|
||||
currentOption(value: number, isDefault: boolean) {
|
||||
let name = `${this.fmtNumber(value, 0)} A`;
|
||||
if (isDefault) {
|
||||
name += ` (${this.$t("main.loadpointSettings.default")})`;
|
||||
}
|
||||
|
|
@ -329,11 +327,11 @@ export default {
|
|||
modalInvisible() {
|
||||
this.isModalVisible = false;
|
||||
},
|
||||
changeBatteryBoost(boost) {
|
||||
changeBatteryBoost(boost: boolean) {
|
||||
this.$emit("batteryboost-updated", boost);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.container {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import settings from "@/settings";
|
||||
|
||||
export function getSessionInfo(index: number, fallback: string) {
|
||||
export function getSessionInfo(index: number, fallback?: string) {
|
||||
return settings.sessionInfo[index - 1] || fallback;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,12 +29,37 @@ export interface LoadpointCompact {
|
|||
heating?: boolean;
|
||||
}
|
||||
|
||||
export interface LoadpointCompact {
|
||||
icon: string;
|
||||
title: string;
|
||||
charging: boolean;
|
||||
soc?: number;
|
||||
power: number;
|
||||
heating?: boolean;
|
||||
chargePower: number;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
export enum CURRENCY {
|
||||
EUR = "EUR",
|
||||
USD = "USD",
|
||||
DKK = "DKK",
|
||||
}
|
||||
|
||||
export enum CHARGE_MODE {
|
||||
OFF = "off",
|
||||
NOW = "now",
|
||||
MINPV = "minpv",
|
||||
PV = "pv",
|
||||
}
|
||||
|
||||
export enum PHASES {
|
||||
AUTO = 0,
|
||||
ONE_PHASE = 1,
|
||||
TWO_PHASES = 2,
|
||||
THREE_PHASES = 3,
|
||||
}
|
||||
|
||||
export interface Sponsor {
|
||||
name: string;
|
||||
expiresAt: Date;
|
||||
|
|
@ -55,6 +80,8 @@ export interface Vehicle {
|
|||
plan?: StaticPlan;
|
||||
repeatingPlans: RepeatingPlan[];
|
||||
title: string;
|
||||
features?: string[];
|
||||
capacity?: number;
|
||||
}
|
||||
|
||||
export type Timeout = ReturnType<typeof setInterval> | null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue