chore: fix npm run lint; spaces > tabs for .ts (#21879)
This commit is contained in:
parent
7c8b9f588e
commit
404c97b654
77 changed files with 4813 additions and 4846 deletions
|
|
@ -14,6 +14,6 @@ indent_size = 4
|
|||
[*.css]
|
||||
indent_size = 2
|
||||
|
||||
[*.{js,html,yml,yaml,json,md}]
|
||||
[*.{js,html,yml,yaml,json,md,ts}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
|
|
|||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
tests/custom-css.css
|
||||
|
|
@ -7,63 +7,61 @@ const base = protocol + "//" + hostname + (port ? ":" + port : "") + pathname;
|
|||
|
||||
// override the way axios serializes arrays in query parameters (a=1&a=2&a=3 instead of a[]=1&a[]=2&a[]=3)
|
||||
function customParamsSerializer(params: { [key: string]: any }) {
|
||||
return Object.keys(params)
|
||||
.filter((key) => params[key] !== null)
|
||||
.map((key) => {
|
||||
const value = params[key];
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`)
|
||||
.join("&");
|
||||
}
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join("&");
|
||||
return Object.keys(params)
|
||||
.filter((key) => params[key] !== null)
|
||||
.map((key) => {
|
||||
const value = params[key];
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join("&");
|
||||
}
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join("&");
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: base + "api/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
paramsSerializer: customParamsSerializer,
|
||||
baseURL: base + "api/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
paramsSerializer: customParamsSerializer,
|
||||
});
|
||||
|
||||
// global error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// handle unauthorized errors
|
||||
if (error.response?.status === 401) {
|
||||
openLoginModal();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// handle unauthorized errors
|
||||
if (error.response?.status === 401) {
|
||||
openLoginModal();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const message = [`${error.message}.`];
|
||||
if (error.response?.data?.error) {
|
||||
message.push(`${error.response.data.error}.`);
|
||||
}
|
||||
if (error.config) {
|
||||
const method = error.config.method.toUpperCase();
|
||||
const url = error.request.responseURL;
|
||||
message.push(`${method} ${url}`);
|
||||
}
|
||||
window.app.raise({ message });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
const message = [`${error.message}.`];
|
||||
if (error.response?.data?.error) {
|
||||
message.push(`${error.response.data.error}.`);
|
||||
}
|
||||
if (error.config) {
|
||||
const method = error.config.method.toUpperCase();
|
||||
const url = error.request.responseURL;
|
||||
message.push(`${method} ${url}`);
|
||||
}
|
||||
window.app.raise({ message });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
||||
export const i18n = axios.create({
|
||||
baseURL: base + "i18n/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
baseURL: base + "i18n/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
export const allowClientError = {
|
||||
validateStatus(status: number) {
|
||||
return status >= 200 && status < 500;
|
||||
},
|
||||
validateStatus(status: number) {
|
||||
return status >= 200 && status < 500;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
106
assets/js/app.ts
106
assets/js/app.ts
|
|
@ -12,58 +12,58 @@ import type { Notification } from "./types/evcc";
|
|||
|
||||
// lazy load smoothscroll polyfill. mainly for safari < 15.4
|
||||
if (!window.CSS.supports("scroll-behavior", "smooth")) {
|
||||
console.log("no native smoothscroll support. polyfilling...");
|
||||
import("smoothscroll-polyfill").then((module) => {
|
||||
module.polyfill();
|
||||
});
|
||||
console.log("no native smoothscroll support. polyfilling...");
|
||||
import("smoothscroll-polyfill").then((module) => {
|
||||
module.polyfill();
|
||||
});
|
||||
}
|
||||
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
data() {
|
||||
return { notifications: [] as Notification[], offline: false };
|
||||
},
|
||||
watch: {
|
||||
offline(value) {
|
||||
console.log(`we are ${value ? "offline" : "online"}`);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
raise(msg: Notification) {
|
||||
if (this.offline) return;
|
||||
if (!msg.level) msg.level = "error";
|
||||
const now = new Date();
|
||||
const latestMsg = this.notifications[0];
|
||||
if (latestMsg && latestMsg.message === msg.message && latestMsg.lp === msg.lp) {
|
||||
latestMsg.count++;
|
||||
latestMsg.time = now;
|
||||
} else {
|
||||
this.notifications = [
|
||||
{
|
||||
...msg,
|
||||
count: 1,
|
||||
time: now,
|
||||
},
|
||||
...this.notifications,
|
||||
];
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
this.notifications = [];
|
||||
},
|
||||
setOnline() {
|
||||
this.offline = false;
|
||||
sendToApp({ type: "online" });
|
||||
},
|
||||
setOffline() {
|
||||
this.offline = true;
|
||||
sendToApp({ type: "offline" });
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return h(App, { notifications: this.notifications, offline: this.offline });
|
||||
},
|
||||
})
|
||||
defineComponent({
|
||||
data() {
|
||||
return { notifications: [] as Notification[], offline: false };
|
||||
},
|
||||
watch: {
|
||||
offline(value) {
|
||||
console.log(`we are ${value ? "offline" : "online"}`);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
raise(msg: Notification) {
|
||||
if (this.offline) return;
|
||||
if (!msg.level) msg.level = "error";
|
||||
const now = new Date();
|
||||
const latestMsg = this.notifications[0];
|
||||
if (latestMsg && latestMsg.message === msg.message && latestMsg.lp === msg.lp) {
|
||||
latestMsg.count++;
|
||||
latestMsg.time = now;
|
||||
} else {
|
||||
this.notifications = [
|
||||
{
|
||||
...msg,
|
||||
count: 1,
|
||||
time: now,
|
||||
},
|
||||
...this.notifications,
|
||||
];
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
this.notifications = [];
|
||||
},
|
||||
setOnline() {
|
||||
this.offline = false;
|
||||
sendToApp({ type: "online" });
|
||||
},
|
||||
setOffline() {
|
||||
this.offline = true;
|
||||
sendToApp({ type: "offline" });
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return h(App, { notifications: this.notifications, offline: this.offline });
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const i18n = setupI18n();
|
||||
|
|
@ -80,8 +80,8 @@ watchThemeChanges();
|
|||
appDetection();
|
||||
|
||||
if (window.evcc.customCss === "true") {
|
||||
const link = document.createElement("link");
|
||||
link.href = `./custom.css`;
|
||||
link.rel = "stylesheet";
|
||||
document.head.appendChild(link);
|
||||
const link = document.createElement("link");
|
||||
link.href = `./custom.css`;
|
||||
link.rel = "stylesheet";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,79 +8,79 @@ import { reactive } from "vue";
|
|||
// const COLORS = [ "#0077B6FF", "#00B4D8FF", "#90E0EFFF", "#40A578FF", "#9DDE8BFF", "#F8961EFF", "#F9C74FFF", "#E6FF94FF"];
|
||||
|
||||
const colors: {
|
||||
text: string | null;
|
||||
muted: string | null;
|
||||
border: string | null;
|
||||
self: string | null;
|
||||
grid: string | null;
|
||||
co2PerKWh: string | null;
|
||||
pricePerKWh: string | null;
|
||||
price: string | null;
|
||||
co2: string | null;
|
||||
background: string | null;
|
||||
light: string | null;
|
||||
selfPalette: string[];
|
||||
palette: string[];
|
||||
text: string | null;
|
||||
muted: string | null;
|
||||
border: string | null;
|
||||
self: string | null;
|
||||
grid: string | null;
|
||||
co2PerKWh: string | null;
|
||||
pricePerKWh: string | null;
|
||||
price: string | null;
|
||||
co2: string | null;
|
||||
background: string | null;
|
||||
light: string | null;
|
||||
selfPalette: string[];
|
||||
palette: string[];
|
||||
} = reactive({
|
||||
text: null,
|
||||
muted: null,
|
||||
border: null,
|
||||
self: null,
|
||||
grid: null,
|
||||
co2PerKWh: null,
|
||||
pricePerKWh: null,
|
||||
price: null,
|
||||
co2: null,
|
||||
background: null,
|
||||
light: null,
|
||||
selfPalette: ["#0FDE41FF", "#FFBD2FFF", "#FD6158FF", "#03C1EFFF", "#0F662DFF", "#FF922EFF"],
|
||||
palette: [
|
||||
"#03C1EFFF",
|
||||
"#FD6158FF",
|
||||
"#31AB4AFF",
|
||||
"#41517AFF",
|
||||
"#FF922EFF",
|
||||
"#0F662DFF",
|
||||
"#0470D4FF",
|
||||
"#FFBD2FFF",
|
||||
"#77C93EFF",
|
||||
"#4E1D10FF",
|
||||
"#0AAFBFFF",
|
||||
"#813504FF",
|
||||
],
|
||||
text: null,
|
||||
muted: null,
|
||||
border: null,
|
||||
self: null,
|
||||
grid: null,
|
||||
co2PerKWh: null,
|
||||
pricePerKWh: null,
|
||||
price: null,
|
||||
co2: null,
|
||||
background: null,
|
||||
light: null,
|
||||
selfPalette: ["#0FDE41FF", "#FFBD2FFF", "#FD6158FF", "#03C1EFFF", "#0F662DFF", "#FF922EFF"],
|
||||
palette: [
|
||||
"#03C1EFFF",
|
||||
"#FD6158FF",
|
||||
"#31AB4AFF",
|
||||
"#41517AFF",
|
||||
"#FF922EFF",
|
||||
"#0F662DFF",
|
||||
"#0470D4FF",
|
||||
"#FFBD2FFF",
|
||||
"#77C93EFF",
|
||||
"#4E1D10FF",
|
||||
"#0AAFBFFF",
|
||||
"#813504FF",
|
||||
],
|
||||
});
|
||||
|
||||
export const dimColor = (color: string | null) => {
|
||||
return color?.toLowerCase().replace(/ff$/, "20");
|
||||
return color?.toLowerCase().replace(/ff$/, "20");
|
||||
};
|
||||
|
||||
export const lighterColor = (color: string | null) => {
|
||||
return color?.toLowerCase().replace(/ff$/, "aa");
|
||||
return color?.toLowerCase().replace(/ff$/, "aa");
|
||||
};
|
||||
|
||||
export const fullColor = (color: string | null) => {
|
||||
return color?.toLowerCase().replace(/20$/, "ff");
|
||||
return color?.toLowerCase().replace(/20$/, "ff");
|
||||
};
|
||||
|
||||
function updateCssColors() {
|
||||
const style = window.getComputedStyle(document.documentElement);
|
||||
colors.text = style.getPropertyValue("--evcc-default-text");
|
||||
colors.muted = style.getPropertyValue("--bs-gray-medium");
|
||||
colors.border = style.getPropertyValue("--bs-border-color-translucent");
|
||||
colors.self = style.getPropertyValue("--evcc-self");
|
||||
colors.grid = style.getPropertyValue("--evcc-grid");
|
||||
colors.price = style.getPropertyValue("--evcc-price");
|
||||
colors.co2 = style.getPropertyValue("--evcc-co2");
|
||||
colors.background = style.getPropertyValue("--evcc-background");
|
||||
colors.pricePerKWh = style.getPropertyValue("--bs-gray-medium");
|
||||
colors.co2PerKWh = style.getPropertyValue("--bs-gray-medium");
|
||||
colors.light = style.getPropertyValue("--bs-gray-light");
|
||||
const style = window.getComputedStyle(document.documentElement);
|
||||
colors.text = style.getPropertyValue("--evcc-default-text");
|
||||
colors.muted = style.getPropertyValue("--bs-gray-medium");
|
||||
colors.border = style.getPropertyValue("--bs-border-color-translucent");
|
||||
colors.self = style.getPropertyValue("--evcc-self");
|
||||
colors.grid = style.getPropertyValue("--evcc-grid");
|
||||
colors.price = style.getPropertyValue("--evcc-price");
|
||||
colors.co2 = style.getPropertyValue("--evcc-co2");
|
||||
colors.background = style.getPropertyValue("--evcc-background");
|
||||
colors.pricePerKWh = style.getPropertyValue("--bs-gray-medium");
|
||||
colors.co2PerKWh = style.getPropertyValue("--bs-gray-medium");
|
||||
colors.light = style.getPropertyValue("--bs-gray-light");
|
||||
}
|
||||
|
||||
// update colors on theme change
|
||||
const darkModeMatcher = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
|
||||
if (darkModeMatcher && darkModeMatcher.addEventListener) {
|
||||
darkModeMatcher.addEventListener("change", updateCssColors);
|
||||
darkModeMatcher.addEventListener("change", updateCssColors);
|
||||
}
|
||||
// initialize colors
|
||||
updateCssColors();
|
||||
|
|
|
|||
|
|
@ -5,101 +5,101 @@ import Modal from "bootstrap/js/dist/modal";
|
|||
import { isSystemError } from "@/utils/fatal.js";
|
||||
|
||||
const auth = reactive({
|
||||
configured: true,
|
||||
loggedIn: null as boolean | null, // true / false / null (unknown)
|
||||
nextUrl: null as string | null, // url to navigate to after login
|
||||
nextModal: null as Modal | null, // modal instance to show after login
|
||||
configured: true,
|
||||
loggedIn: null as boolean | null, // true / false / null (unknown)
|
||||
nextUrl: null as string | null, // url to navigate to after login
|
||||
nextModal: null as Modal | null, // modal instance to show after login
|
||||
});
|
||||
|
||||
export async function updateAuthStatus() {
|
||||
if (store.state.offline || isSystemError(store.state.fatal)) {
|
||||
// system not ready, skip auth check
|
||||
return;
|
||||
}
|
||||
if (store.state.offline || isSystemError(store.state.fatal)) {
|
||||
// system not ready, skip auth check
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await api.get("/auth/status", {
|
||||
validateStatus: (code) => [200, 501, 500].includes(code),
|
||||
});
|
||||
if (res.status === 501) {
|
||||
auth.configured = false;
|
||||
}
|
||||
if (res.status === 200) {
|
||||
auth.configured = true;
|
||||
auth.loggedIn = res.data === true;
|
||||
}
|
||||
if (res.status === 500) {
|
||||
auth.loggedIn = null;
|
||||
console.log("unable to fetch auth status", res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("unable to fetch auth status", e);
|
||||
}
|
||||
try {
|
||||
const res = await api.get("/auth/status", {
|
||||
validateStatus: (code) => [200, 501, 500].includes(code),
|
||||
});
|
||||
if (res.status === 501) {
|
||||
auth.configured = false;
|
||||
}
|
||||
if (res.status === 200) {
|
||||
auth.configured = true;
|
||||
auth.loggedIn = res.data === true;
|
||||
}
|
||||
if (res.status === 500) {
|
||||
auth.loggedIn = null;
|
||||
console.log("unable to fetch auth status", res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("unable to fetch auth status", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
try {
|
||||
await api.post("/auth/logout");
|
||||
await updateAuthStatus();
|
||||
auth.nextUrl = null;
|
||||
} catch (e) {
|
||||
console.log("unable to logout", e);
|
||||
}
|
||||
try {
|
||||
await api.post("/auth/logout");
|
||||
await updateAuthStatus();
|
||||
auth.nextUrl = null;
|
||||
} catch (e) {
|
||||
console.log("unable to logout", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
return auth.loggedIn === true;
|
||||
return auth.loggedIn === true;
|
||||
}
|
||||
|
||||
export function statusUnknown() {
|
||||
return auth.loggedIn === null;
|
||||
return auth.loggedIn === null;
|
||||
}
|
||||
|
||||
export function isConfigured() {
|
||||
return auth.configured;
|
||||
return auth.configured;
|
||||
}
|
||||
|
||||
export function getAndClearNextUrl() {
|
||||
const nextUrl = auth.nextUrl;
|
||||
auth.nextUrl = null;
|
||||
return nextUrl;
|
||||
const nextUrl = auth.nextUrl;
|
||||
auth.nextUrl = null;
|
||||
return nextUrl;
|
||||
}
|
||||
|
||||
export function getAndClearNextModal() {
|
||||
const nextModal = auth.nextModal;
|
||||
auth.nextModal = null;
|
||||
return nextModal;
|
||||
const nextModal = auth.nextModal;
|
||||
auth.nextModal = null;
|
||||
return nextModal;
|
||||
}
|
||||
|
||||
export function openLoginModal(nextUrl: string | null = null, nextModal: Modal | null = null) {
|
||||
auth.nextUrl = nextUrl;
|
||||
auth.nextModal = nextModal;
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("loginModal") as HTMLElement);
|
||||
modal.show();
|
||||
auth.nextUrl = nextUrl;
|
||||
auth.nextModal = nextModal;
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("loginModal") as HTMLElement);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// show/hide password modal based on auth status
|
||||
watch(
|
||||
() => auth.configured,
|
||||
(configured) => {
|
||||
console.log("configured", configured);
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("passwordModal") as HTMLElement
|
||||
);
|
||||
if (configured) {
|
||||
modal.hide();
|
||||
} else {
|
||||
modal.show();
|
||||
}
|
||||
}
|
||||
() => auth.configured,
|
||||
(configured) => {
|
||||
console.log("configured", configured);
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("passwordModal") as HTMLElement
|
||||
);
|
||||
if (configured) {
|
||||
modal.hide();
|
||||
} else {
|
||||
modal.show();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let timeoutId: number | undefined = undefined;
|
||||
function debounedUpdateAuthStatus() {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
updateAuthStatus();
|
||||
}, 500) as unknown as number;
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
updateAuthStatus();
|
||||
}, 500) as unknown as number;
|
||||
}
|
||||
|
||||
// update auth status on reconnect or server restart
|
||||
|
|
|
|||
|
|
@ -2,55 +2,55 @@ import ChargingPlan from "./ChargingPlan.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
const hoursFromNow = (hours: number) => {
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() + hours);
|
||||
return now.toISOString();
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() + hours);
|
||||
return now.toISOString();
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "ChargingPlans/ChargingPlan",
|
||||
component: ChargingPlan,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
id: { control: "text" },
|
||||
effectivePlanTime: { control: "text" },
|
||||
effectivePlanSoc: { control: "number" },
|
||||
planEnergy: { control: "number" },
|
||||
socBasedCharging: { control: "boolean" },
|
||||
socBasedPlanning: { control: "boolean" },
|
||||
vehicleSoc: { control: "number" },
|
||||
},
|
||||
title: "ChargingPlans/ChargingPlan",
|
||||
component: ChargingPlan,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
id: { control: "text" },
|
||||
effectivePlanTime: { control: "text" },
|
||||
effectivePlanSoc: { control: "number" },
|
||||
planEnergy: { control: "number" },
|
||||
socBasedCharging: { control: "boolean" },
|
||||
socBasedPlanning: { control: "boolean" },
|
||||
vehicleSoc: { control: "number" },
|
||||
},
|
||||
} as Meta<typeof ChargingPlan>;
|
||||
|
||||
const Template: StoryFn<typeof ChargingPlan> = (args) => ({
|
||||
components: { ChargingPlan },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ChargingPlan v-bind="args" />',
|
||||
components: { ChargingPlan },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<ChargingPlan v-bind="args" />',
|
||||
});
|
||||
|
||||
export const None = Template.bind({});
|
||||
None.args = {
|
||||
id: "1",
|
||||
id: "1",
|
||||
};
|
||||
|
||||
export const SocBasedCharging = Template.bind({});
|
||||
SocBasedCharging.args = {
|
||||
id: "1",
|
||||
effectivePlanTime: hoursFromNow(4),
|
||||
effectivePlanSoc: 77,
|
||||
socBasedCharging: true,
|
||||
socBasedPlanning: true,
|
||||
id: "1",
|
||||
effectivePlanTime: hoursFromNow(4),
|
||||
effectivePlanSoc: 77,
|
||||
socBasedCharging: true,
|
||||
socBasedPlanning: true,
|
||||
};
|
||||
|
||||
export const EnergyBasedCharging = Template.bind({});
|
||||
EnergyBasedCharging.args = {
|
||||
id: "1",
|
||||
effectivePlanTime: hoursFromNow(12),
|
||||
planEnergy: 77,
|
||||
socBasedCharging: false,
|
||||
socBasedPlanning: false,
|
||||
id: "1",
|
||||
effectivePlanTime: hoursFromNow(12),
|
||||
planEnergy: 77,
|
||||
socBasedCharging: false,
|
||||
socBasedPlanning: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,100 +5,99 @@ import type { StoryFn } from "@storybook/vue3";
|
|||
const now = new Date();
|
||||
|
||||
function createDate(hoursFromNow: number) {
|
||||
const result = new Date(now.getTime());
|
||||
result.setHours(result.getHours() + hoursFromNow);
|
||||
return result;
|
||||
const result = new Date(now.getTime());
|
||||
result.setHours(result.getHours() + hoursFromNow);
|
||||
return result;
|
||||
}
|
||||
|
||||
function createRate(value: number, hoursFromNow: number, durationHours = 1): Rate {
|
||||
const start = new Date(now.getTime());
|
||||
start.setHours(start.getHours() + hoursFromNow);
|
||||
start.setMinutes(0);
|
||||
start.setSeconds(0);
|
||||
start.setMilliseconds(0);
|
||||
const end = new Date(start.getTime());
|
||||
end.setHours(start.getHours() + durationHours);
|
||||
end.setMinutes(0);
|
||||
end.setSeconds(0);
|
||||
end.setMilliseconds(0);
|
||||
return { start, end, value };
|
||||
const start = new Date(now.getTime());
|
||||
start.setHours(start.getHours() + hoursFromNow);
|
||||
start.setMinutes(0);
|
||||
start.setSeconds(0);
|
||||
start.setMilliseconds(0);
|
||||
const end = new Date(start.getTime());
|
||||
end.setHours(start.getHours() + durationHours);
|
||||
end.setMinutes(0);
|
||||
end.setSeconds(0);
|
||||
end.setMilliseconds(0);
|
||||
return { start, end, value };
|
||||
}
|
||||
|
||||
// Scenario data
|
||||
const co2Data = {
|
||||
rates: [
|
||||
545, 518, 545, 518, 0, 545, 527, 527, 536, 518, 400, 336, 336, 339, 344, 336, 336, 336, 372,
|
||||
400, 555, 555, 545, 555, 564, 545, 555, 545, 536, 545, 527, 536, 518, 545, 509, 336, 336,
|
||||
336,
|
||||
].map((value, i) => createRate(value, i)),
|
||||
duration: 8695,
|
||||
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
|
||||
smartCostType: "co2",
|
||||
targetTime: createDate(14),
|
||||
rates: [
|
||||
545, 518, 545, 518, 0, 545, 527, 527, 536, 518, 400, 336, 336, 339, 344, 336, 336, 336, 372,
|
||||
400, 555, 555, 545, 555, 564, 545, 555, 545, 536, 545, 527, 536, 518, 545, 509, 336, 336, 336,
|
||||
].map((value, i) => createRate(value, i)),
|
||||
duration: 8695,
|
||||
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
|
||||
smartCostType: "co2",
|
||||
targetTime: createDate(14),
|
||||
};
|
||||
|
||||
const fixedData = {
|
||||
rates: [createRate(0.442, 0, 50)],
|
||||
duration: 8695,
|
||||
plan: [createRate(0.442, 12, 3)],
|
||||
smartCostType: "price",
|
||||
currency: CURRENCY.EUR,
|
||||
targetTime: createDate(14),
|
||||
rates: [createRate(0.442, 0, 50)],
|
||||
duration: 8695,
|
||||
plan: [createRate(0.442, 12, 3)],
|
||||
smartCostType: "price",
|
||||
currency: CURRENCY.EUR,
|
||||
targetTime: createDate(14),
|
||||
};
|
||||
|
||||
const zonedData = {
|
||||
rates: [
|
||||
createRate(3.72, 0, 4),
|
||||
createRate(2.39, 4, 12),
|
||||
createRate(3.72, 16, 12),
|
||||
createRate(2.39, 28, 12),
|
||||
createRate(3.72, 40, 12),
|
||||
],
|
||||
duration: 8695,
|
||||
plan: [createRate(2.39, 13, 3)],
|
||||
smartCostType: "price",
|
||||
currency: CURRENCY.DKK,
|
||||
targetTime: createDate(17),
|
||||
rates: [
|
||||
createRate(3.72, 0, 4),
|
||||
createRate(2.39, 4, 12),
|
||||
createRate(3.72, 16, 12),
|
||||
createRate(2.39, 28, 12),
|
||||
createRate(3.72, 40, 12),
|
||||
],
|
||||
duration: 8695,
|
||||
plan: [createRate(2.39, 13, 3)],
|
||||
smartCostType: "price",
|
||||
currency: CURRENCY.DKK,
|
||||
targetTime: createDate(17),
|
||||
};
|
||||
|
||||
const unknownData = {
|
||||
rates: co2Data.rates.slice(0, 16),
|
||||
duration: 8695,
|
||||
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
|
||||
smartCostType: "co2",
|
||||
targetTime: createDate(14),
|
||||
rates: co2Data.rates.slice(0, 16),
|
||||
duration: 8695,
|
||||
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
|
||||
smartCostType: "co2",
|
||||
targetTime: createDate(14),
|
||||
};
|
||||
|
||||
const dynamicData = {
|
||||
rates: [
|
||||
0.12, 0.15, 0, -0.05, -0.11, -0.24, -0.08, 0.12, 0.25, 0.29, 0.22, 0.31, 0.31, 0.33,
|
||||
].map((value, i) => createRate(value, i)),
|
||||
duration: 8695,
|
||||
plan: [createRate(0.23, 2, 5)],
|
||||
smartCostType: "price",
|
||||
currency: CURRENCY.EUR,
|
||||
targetTime: createDate(13),
|
||||
rates: [0.12, 0.15, 0, -0.05, -0.11, -0.24, -0.08, 0.12, 0.25, 0.29, 0.22, 0.31, 0.31, 0.33].map(
|
||||
(value, i) => createRate(value, i)
|
||||
),
|
||||
duration: 8695,
|
||||
plan: [createRate(0.23, 2, 5)],
|
||||
smartCostType: "price",
|
||||
currency: CURRENCY.EUR,
|
||||
targetTime: createDate(13),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "ChargingPlans/Preview",
|
||||
component: Preview,
|
||||
argTypes: {
|
||||
rates: { control: "object" },
|
||||
duration: { control: "number" },
|
||||
plan: { control: "object" },
|
||||
smartCostType: { control: "select", options: ["co2", "price"] },
|
||||
currency: { control: "text" },
|
||||
targetTime: { control: "text" },
|
||||
},
|
||||
title: "ChargingPlans/Preview",
|
||||
component: Preview,
|
||||
argTypes: {
|
||||
rates: { control: "object" },
|
||||
duration: { control: "number" },
|
||||
plan: { control: "object" },
|
||||
smartCostType: { control: "select", options: ["co2", "price"] },
|
||||
currency: { control: "text" },
|
||||
targetTime: { control: "text" },
|
||||
},
|
||||
};
|
||||
|
||||
const Template: StoryFn<typeof Preview> = (args) => ({
|
||||
components: { Preview },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Preview v-bind="args" />',
|
||||
components: { Preview },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Preview v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Co2 = Template.bind({});
|
||||
|
|
|
|||
|
|
@ -7,134 +7,134 @@ config.global.mocks["$i18n"] = { locale: "de-DE" };
|
|||
config.global.mocks["$t"] = (a: any) => a;
|
||||
|
||||
describe("basics", () => {
|
||||
const DATE_START = new Date("2023-01-11T11:00:00+01:00");
|
||||
const DATE_TARGET = new Date("2023-01-11T13:00:00+01:00");
|
||||
const TARIFF_FIXED = [
|
||||
{
|
||||
start: new Date("2023-01-11T11:00:00+01:00"),
|
||||
end: new Date("2023-01-22T00:00:00+01:00"),
|
||||
value: 0.4,
|
||||
},
|
||||
];
|
||||
const PLAN = [
|
||||
{
|
||||
start: new Date("2023-01-11T12:00:00+01:00"),
|
||||
end: new Date("2023-01-11T13:00:00+01:00"),
|
||||
value: 0.2,
|
||||
},
|
||||
];
|
||||
const DATE_START = new Date("2023-01-11T11:00:00+01:00");
|
||||
const DATE_TARGET = new Date("2023-01-11T13:00:00+01:00");
|
||||
const TARIFF_FIXED = [
|
||||
{
|
||||
start: new Date("2023-01-11T11:00:00+01:00"),
|
||||
end: new Date("2023-01-22T00:00:00+01:00"),
|
||||
value: 0.4,
|
||||
},
|
||||
];
|
||||
const PLAN = [
|
||||
{
|
||||
start: new Date("2023-01-11T12:00:00+01:00"),
|
||||
end: new Date("2023-01-11T13:00:00+01:00"),
|
||||
value: 0.2,
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = mount(Preview, {
|
||||
props: {
|
||||
plan: PLAN,
|
||||
targetTime: DATE_TARGET,
|
||||
rates: TARIFF_FIXED,
|
||||
},
|
||||
});
|
||||
wrapper.setData({ startTime: DATE_START });
|
||||
const wrapper = mount(Preview, {
|
||||
props: {
|
||||
plan: PLAN,
|
||||
targetTime: DATE_TARGET,
|
||||
rates: TARIFF_FIXED,
|
||||
},
|
||||
});
|
||||
wrapper.setData({ startTime: DATE_START });
|
||||
|
||||
let result: Slot[];
|
||||
let result: Slot[];
|
||||
|
||||
beforeAll(() => {
|
||||
result = wrapper.vm.slots;
|
||||
});
|
||||
beforeAll(() => {
|
||||
result = wrapper.vm.slots;
|
||||
});
|
||||
|
||||
test("should return 39 slots", () => {
|
||||
expect(result.length).eq(39);
|
||||
});
|
||||
test("should return 39 slots", () => {
|
||||
expect(result.length).eq(39);
|
||||
});
|
||||
|
||||
test("slots should be an hour apart", () => {
|
||||
expect(result[0].startHour).eq(11);
|
||||
expect(result[0].endHour).eq(12);
|
||||
expect(result[0].day).eq("Mi");
|
||||
test("slots should be an hour apart", () => {
|
||||
expect(result[0].startHour).eq(11);
|
||||
expect(result[0].endHour).eq(12);
|
||||
expect(result[0].day).eq("Mi");
|
||||
|
||||
expect(result[1].startHour).eq(12);
|
||||
expect(result[1].endHour).eq(13);
|
||||
expect(result[1].day).eq("Mi");
|
||||
expect(result[1].startHour).eq(12);
|
||||
expect(result[1].endHour).eq(13);
|
||||
expect(result[1].day).eq("Mi");
|
||||
|
||||
expect(result[11].startHour).eq(22);
|
||||
expect(result[11].endHour).eq(23);
|
||||
expect(result[11].day).eq("Mi");
|
||||
expect(result[11].startHour).eq(22);
|
||||
expect(result[11].endHour).eq(23);
|
||||
expect(result[11].day).eq("Mi");
|
||||
|
||||
expect(result[24].startHour).eq(11);
|
||||
expect(result[24].endHour).eq(12);
|
||||
expect(result[24].day).eq("Do");
|
||||
});
|
||||
expect(result[24].startHour).eq(11);
|
||||
expect(result[24].endHour).eq(12);
|
||||
expect(result[24].day).eq("Do");
|
||||
});
|
||||
|
||||
test("slots after target should be toLate", () => {
|
||||
expect(result[0].toLate).eq(false);
|
||||
expect(result[1].toLate).eq(false);
|
||||
expect(result[2].toLate).eq(true);
|
||||
expect(result[3].toLate).eq(true);
|
||||
});
|
||||
test("slots after target should be toLate", () => {
|
||||
expect(result[0].toLate).eq(false);
|
||||
expect(result[1].toLate).eq(false);
|
||||
expect(result[2].toLate).eq(true);
|
||||
expect(result[3].toLate).eq(true);
|
||||
});
|
||||
|
||||
test("slots are marked if charging is happening in them", () => {
|
||||
expect(result[0].charging).eq(false);
|
||||
expect(result[1].charging).eq(true);
|
||||
expect(result[2].charging).eq(false);
|
||||
expect(result[3].charging).eq(false);
|
||||
});
|
||||
test("slots are marked if charging is happening in them", () => {
|
||||
expect(result[0].charging).eq(false);
|
||||
expect(result[1].charging).eq(true);
|
||||
expect(result[2].charging).eq(false);
|
||||
expect(result[3].charging).eq(false);
|
||||
});
|
||||
|
||||
test("all slots have the same fixed value", () => {
|
||||
result.forEach((slot) => expect(slot.value).eq(0.4));
|
||||
});
|
||||
test("all slots have the same fixed value", () => {
|
||||
result.forEach((slot) => expect(slot.value).eq(0.4));
|
||||
});
|
||||
});
|
||||
|
||||
describe("zoned tariffs", () => {
|
||||
const DATE_START = new Date("2023-01-11T11:00:00+01:00");
|
||||
const DATE_TARGET = new Date("2023-01-11T16:00:00+01:00");
|
||||
const TARIFF_ZONED = [
|
||||
{
|
||||
start: new Date("2023-01-11T11:00:00+01:00"),
|
||||
end: new Date("2023-01-11T12:00:00+01:00"),
|
||||
value: 0.2,
|
||||
},
|
||||
{
|
||||
start: new Date("2023-01-11T12:00:00+01:00"),
|
||||
end: new Date("2023-01-22T00:00:00+01:00"),
|
||||
value: 0.4,
|
||||
},
|
||||
];
|
||||
const PLAN = [
|
||||
{
|
||||
start: new Date("2023-01-11T11:30:00+01:00"),
|
||||
end: new Date("2023-01-11T13:00:00+01:00"),
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
start: new Date("2023-01-11T14:30:00+01:00"),
|
||||
end: new Date("2023-01-11T16:00:00+01:00"),
|
||||
value: 0.2,
|
||||
},
|
||||
];
|
||||
const DATE_START = new Date("2023-01-11T11:00:00+01:00");
|
||||
const DATE_TARGET = new Date("2023-01-11T16:00:00+01:00");
|
||||
const TARIFF_ZONED = [
|
||||
{
|
||||
start: new Date("2023-01-11T11:00:00+01:00"),
|
||||
end: new Date("2023-01-11T12:00:00+01:00"),
|
||||
value: 0.2,
|
||||
},
|
||||
{
|
||||
start: new Date("2023-01-11T12:00:00+01:00"),
|
||||
end: new Date("2023-01-22T00:00:00+01:00"),
|
||||
value: 0.4,
|
||||
},
|
||||
];
|
||||
const PLAN = [
|
||||
{
|
||||
start: new Date("2023-01-11T11:30:00+01:00"),
|
||||
end: new Date("2023-01-11T13:00:00+01:00"),
|
||||
value: 0.3,
|
||||
},
|
||||
{
|
||||
start: new Date("2023-01-11T14:30:00+01:00"),
|
||||
end: new Date("2023-01-11T16:00:00+01:00"),
|
||||
value: 0.2,
|
||||
},
|
||||
];
|
||||
|
||||
let result: Slot[];
|
||||
let result: Slot[];
|
||||
|
||||
const wrapper = mount(Preview, {
|
||||
props: {
|
||||
plan: PLAN,
|
||||
targetTime: DATE_TARGET,
|
||||
rates: TARIFF_ZONED,
|
||||
},
|
||||
});
|
||||
wrapper.setData({ startTime: DATE_START });
|
||||
const wrapper = mount(Preview, {
|
||||
props: {
|
||||
plan: PLAN,
|
||||
targetTime: DATE_TARGET,
|
||||
rates: TARIFF_ZONED,
|
||||
},
|
||||
});
|
||||
wrapper.setData({ startTime: DATE_START });
|
||||
|
||||
beforeAll(() => {
|
||||
result = wrapper.vm.slots;
|
||||
});
|
||||
beforeAll(() => {
|
||||
result = wrapper.vm.slots;
|
||||
});
|
||||
|
||||
test("handle multiple charging slots", () => {
|
||||
expect(result[0].charging).eq(true);
|
||||
expect(result[1].charging).eq(true);
|
||||
expect(result[2].charging).eq(false);
|
||||
expect(result[3].charging).eq(true);
|
||||
expect(result[4].charging).eq(true);
|
||||
expect(result[5].charging).eq(false);
|
||||
});
|
||||
test("handle multiple charging slots", () => {
|
||||
expect(result[0].charging).eq(true);
|
||||
expect(result[1].charging).eq(true);
|
||||
expect(result[2].charging).eq(false);
|
||||
expect(result[3].charging).eq(true);
|
||||
expect(result[4].charging).eq(true);
|
||||
expect(result[5].charging).eq(false);
|
||||
});
|
||||
|
||||
test("first slot is cheap, others are expensive", () => {
|
||||
const [first, ...others] = result;
|
||||
expect(first.value).eq(0.2);
|
||||
others.forEach((slot) => expect(slot.value).eq(0.4));
|
||||
});
|
||||
test("first slot is cheap, others are expensive", () => {
|
||||
const [first, ...others] = result;
|
||||
expect(first.value).eq(0.2);
|
||||
others.forEach((slot) => expect(slot.value).eq(0.4));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
38
assets/js/components/ChargingPlans/types.d.ts
vendored
38
assets/js/components/ChargingPlans/types.d.ts
vendored
|
|
@ -1,37 +1,37 @@
|
|||
import type { Rate } from "@/types/evcc";
|
||||
|
||||
export interface RepeatingPlan {
|
||||
weekdays: number[];
|
||||
time: string;
|
||||
tz: string; // timezone like "Europe/Berlin"
|
||||
soc: number;
|
||||
active: boolean;
|
||||
precondition: number;
|
||||
weekdays: number[];
|
||||
time: string;
|
||||
tz: string; // timezone like "Europe/Berlin"
|
||||
soc: number;
|
||||
active: boolean;
|
||||
precondition: number;
|
||||
}
|
||||
|
||||
export interface PlanWrapper {
|
||||
planId: number;
|
||||
planTime: Date;
|
||||
duration: number;
|
||||
plan: Rate[];
|
||||
power: number;
|
||||
planId: number;
|
||||
planTime: Date;
|
||||
duration: number;
|
||||
plan: Rate[];
|
||||
power: number;
|
||||
}
|
||||
|
||||
export interface PlanResponse {
|
||||
status: number;
|
||||
data: { result: PlanWrapper };
|
||||
status: number;
|
||||
data: { result: PlanWrapper };
|
||||
}
|
||||
|
||||
export type StaticPlan = StaticSocPlan | StaticEnergyPlan;
|
||||
|
||||
export interface StaticSocPlan {
|
||||
soc: number;
|
||||
time: Date;
|
||||
precondition: number;
|
||||
soc: number;
|
||||
time: Date;
|
||||
precondition: number;
|
||||
}
|
||||
|
||||
export interface StaticEnergyPlan {
|
||||
energy: number;
|
||||
time: Date;
|
||||
precondition: number;
|
||||
energy: number;
|
||||
time: Date;
|
||||
precondition: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,128 +2,128 @@ import type { DeviceType } from "@/types/evcc";
|
|||
import api from "@/api";
|
||||
|
||||
export type Product = {
|
||||
group: string;
|
||||
name: string;
|
||||
template: string;
|
||||
group: string;
|
||||
name: string;
|
||||
template: string;
|
||||
};
|
||||
|
||||
export type Template = {
|
||||
Params: TemplateParam[];
|
||||
Requirements: {
|
||||
Description: string;
|
||||
};
|
||||
Params: TemplateParam[];
|
||||
Requirements: {
|
||||
Description: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TemplateType = "vehicle" | "battery" | "grid" | "pv" | "charger" | "aux" | "ext";
|
||||
|
||||
export type TemplateParam = {
|
||||
Name: string;
|
||||
Required: boolean;
|
||||
Advanced: boolean;
|
||||
Deprecated: boolean;
|
||||
Default?: string | number | boolean;
|
||||
Choice?: string[];
|
||||
Usages?: TemplateType[];
|
||||
Name: string;
|
||||
Required: boolean;
|
||||
Advanced: boolean;
|
||||
Deprecated: boolean;
|
||||
Default?: string | number | boolean;
|
||||
Choice?: string[];
|
||||
Usages?: TemplateType[];
|
||||
};
|
||||
|
||||
export type ModbusCapability = "rs485" | "tcpip";
|
||||
|
||||
export type ModbusParam = TemplateParam & {
|
||||
ID?: string;
|
||||
Comset?: string;
|
||||
Baudrate?: number;
|
||||
Port?: number;
|
||||
ID?: string;
|
||||
Comset?: string;
|
||||
Baudrate?: number;
|
||||
Port?: number;
|
||||
};
|
||||
|
||||
export type DeviceValues = {
|
||||
type: ConfigType;
|
||||
icon: string | undefined;
|
||||
deviceProduct: string | undefined;
|
||||
yaml: string | undefined;
|
||||
template: string | null;
|
||||
[key: string]: any;
|
||||
type: ConfigType;
|
||||
icon: string | undefined;
|
||||
deviceProduct: string | undefined;
|
||||
yaml: string | undefined;
|
||||
template: string | null;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export function handleError(e: any, msg: string) {
|
||||
console.error(e);
|
||||
let message = msg;
|
||||
const { error } = e.response.data || {};
|
||||
if (error) message += `: ${error}`;
|
||||
alert(message);
|
||||
console.error(e);
|
||||
let message = msg;
|
||||
const { error } = e.response.data || {};
|
||||
if (error) message += `: ${error}`;
|
||||
alert(message);
|
||||
}
|
||||
|
||||
export const timeout = 15000;
|
||||
|
||||
export enum ConfigType {
|
||||
Template = "template",
|
||||
Custom = "custom",
|
||||
Template = "template",
|
||||
Custom = "custom",
|
||||
}
|
||||
|
||||
export function applyDefaultsFromTemplate(template: Template | null, values: DeviceValues) {
|
||||
const params = template?.Params || [];
|
||||
params
|
||||
.filter((p) => p.Default && !values[p.Name])
|
||||
.forEach((p) => {
|
||||
values[p.Name] = p.Default;
|
||||
});
|
||||
const params = template?.Params || [];
|
||||
params
|
||||
.filter((p) => p.Default && !values[p.Name])
|
||||
.forEach((p) => {
|
||||
values[p.Name] = p.Default;
|
||||
});
|
||||
}
|
||||
|
||||
export function createDeviceUtils(deviceType: DeviceType) {
|
||||
function test(id: number | undefined, data: any) {
|
||||
let url = `config/test/${deviceType}`;
|
||||
if (id !== undefined) {
|
||||
url += `/merge/${id}`;
|
||||
}
|
||||
return api.post(url, data, { timeout });
|
||||
}
|
||||
function test(id: number | undefined, data: any) {
|
||||
let url = `config/test/${deviceType}`;
|
||||
if (id !== undefined) {
|
||||
url += `/merge/${id}`;
|
||||
}
|
||||
return api.post(url, data, { timeout });
|
||||
}
|
||||
|
||||
function update(id: number, data: any) {
|
||||
return api.put(`config/devices/${deviceType}/${id}`, data);
|
||||
}
|
||||
function update(id: number, data: any) {
|
||||
return api.put(`config/devices/${deviceType}/${id}`, data);
|
||||
}
|
||||
|
||||
function remove(id: number) {
|
||||
return api.delete(`config/devices/${deviceType}/${id}`);
|
||||
}
|
||||
function remove(id: number) {
|
||||
return api.delete(`config/devices/${deviceType}/${id}`);
|
||||
}
|
||||
|
||||
async function load(id: number) {
|
||||
const response = await api.get(`config/devices/${deviceType}/${id}`);
|
||||
return response.data.result;
|
||||
}
|
||||
async function load(id: number) {
|
||||
const response = await api.get(`config/devices/${deviceType}/${id}`);
|
||||
return response.data.result;
|
||||
}
|
||||
|
||||
async function create(data: any) {
|
||||
const response = await api.post(`config/devices/${deviceType}`, data);
|
||||
return response.data.result;
|
||||
}
|
||||
async function create(data: any) {
|
||||
const response = await api.post(`config/devices/${deviceType}`, data);
|
||||
return response.data.result;
|
||||
}
|
||||
|
||||
async function loadProducts(lang?: string, usage?: string) {
|
||||
const params: Record<string, string | undefined> = { lang };
|
||||
if (usage) {
|
||||
params["usage"] = usage;
|
||||
}
|
||||
const response = await api.get(`config/products/${deviceType}`, { params });
|
||||
return response.data.result;
|
||||
}
|
||||
async function loadProducts(lang?: string, usage?: string) {
|
||||
const params: Record<string, string | undefined> = { lang };
|
||||
if (usage) {
|
||||
params["usage"] = usage;
|
||||
}
|
||||
const response = await api.get(`config/products/${deviceType}`, { params });
|
||||
return response.data.result;
|
||||
}
|
||||
|
||||
async function loadTemplate(templateName: string, lang?: string) {
|
||||
if (!templateName) return null;
|
||||
async function loadTemplate(templateName: string, lang?: string) {
|
||||
if (!templateName) return null;
|
||||
|
||||
const opts = {
|
||||
params: {
|
||||
lang,
|
||||
name: templateName,
|
||||
},
|
||||
};
|
||||
const response = await api.get(`config/templates/${deviceType}`, opts);
|
||||
return response.data.result;
|
||||
}
|
||||
const opts = {
|
||||
params: {
|
||||
lang,
|
||||
name: templateName,
|
||||
},
|
||||
};
|
||||
const response = await api.get(`config/templates/${deviceType}`, opts);
|
||||
return response.data.result;
|
||||
}
|
||||
|
||||
return {
|
||||
test,
|
||||
update,
|
||||
remove,
|
||||
load,
|
||||
create,
|
||||
loadProducts,
|
||||
loadTemplate,
|
||||
};
|
||||
return {
|
||||
test,
|
||||
update,
|
||||
remove,
|
||||
load,
|
||||
create,
|
||||
loadProducts,
|
||||
loadTemplate,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +1,55 @@
|
|||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export type TestState = {
|
||||
isUnknown: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
isRunning: boolean;
|
||||
result: Record<string, any> | null;
|
||||
error: string | null;
|
||||
errorLine: number | null;
|
||||
isUnknown: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
isRunning: boolean;
|
||||
result: Record<string, any> | null;
|
||||
error: string | null;
|
||||
errorLine: number | null;
|
||||
};
|
||||
|
||||
export const initialTestState = (): TestState => ({
|
||||
isUnknown: true,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
isRunning: false,
|
||||
result: null,
|
||||
error: null,
|
||||
errorLine: null,
|
||||
isUnknown: true,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
isRunning: false,
|
||||
result: null,
|
||||
error: null,
|
||||
errorLine: null,
|
||||
});
|
||||
|
||||
export const performTest = async (
|
||||
state: TestState,
|
||||
api: () => Promise<AxiosResponse<any, any>>,
|
||||
form: HTMLElement | undefined
|
||||
state: TestState,
|
||||
api: () => Promise<AxiosResponse<any, any>>,
|
||||
form: HTMLElement | undefined
|
||||
) => {
|
||||
if (form && !(form as HTMLFormElement).reportValidity()) return false;
|
||||
state.isUnknown = false;
|
||||
state.isSuccess = false;
|
||||
state.isError = false;
|
||||
state.isRunning = true;
|
||||
state.errorLine = null;
|
||||
try {
|
||||
const res = await api();
|
||||
for (const [key, value] of Object.entries(res.data.result)) {
|
||||
const { error } = value as { error?: string };
|
||||
if (error) {
|
||||
state.isError = true;
|
||||
state.error = `${key}: ${error}`;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state.isSuccess = true;
|
||||
state.result = res.data.result;
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
state.isError = true;
|
||||
state.error = e.response?.data?.error || e.message;
|
||||
state.errorLine = e.response?.data?.line || null;
|
||||
} finally {
|
||||
state.isRunning = false;
|
||||
}
|
||||
return false;
|
||||
if (form && !(form as HTMLFormElement).reportValidity()) return false;
|
||||
state.isUnknown = false;
|
||||
state.isSuccess = false;
|
||||
state.isError = false;
|
||||
state.isRunning = true;
|
||||
state.errorLine = null;
|
||||
try {
|
||||
const res = await api();
|
||||
for (const [key, value] of Object.entries(res.data.result)) {
|
||||
const { error } = value as { error?: string };
|
||||
if (error) {
|
||||
state.isError = true;
|
||||
state.error = `${key}: ${error}`;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state.isSuccess = true;
|
||||
state.result = res.data.result;
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
state.isError = true;
|
||||
state.error = e.response?.data?.error || e.message;
|
||||
state.errorLine = e.response?.data?.line || null;
|
||||
} finally {
|
||||
state.isRunning = false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,23 +2,23 @@ import BatteryIcon from "./BatteryIcon.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Energyflow/BatteryIcon",
|
||||
component: BatteryIcon,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
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;
|
||||
const story = () => ({
|
||||
components: { BatteryIcon },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<BatteryIcon v-bind="args" />',
|
||||
});
|
||||
story.args = args;
|
||||
return story;
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
|
|
|
|||
|
|
@ -3,412 +3,412 @@ import Energyflow from "./Energyflow.vue";
|
|||
import { CURRENCY } from "@/types/evcc";
|
||||
|
||||
export default {
|
||||
title: "Energyflow/Energyflow",
|
||||
component: Energyflow,
|
||||
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" />',
|
||||
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",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 1000,
|
||||
icon: "bike",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 2200,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 2200,
|
||||
connected: true,
|
||||
index: 2,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
tariffGrid: 0.25,
|
||||
tariffFeedIn: 0.08,
|
||||
smartCostType: "price",
|
||||
currency: CURRENCY.EUR,
|
||||
pv: [{ power: 5000 }, { power: 2300 }],
|
||||
forecast: {
|
||||
solar: {
|
||||
today: { energy: 1000, complete: true },
|
||||
tomorrow: { energy: 1000, complete: false },
|
||||
dayAfterTomorrow: { energy: 1000, complete: false },
|
||||
},
|
||||
},
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
pvPower: 7300,
|
||||
gridPower: -2300,
|
||||
homePower: 800,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 1000,
|
||||
icon: "bike",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 2200,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 2200,
|
||||
connected: true,
|
||||
index: 2,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
tariffGrid: 0.25,
|
||||
tariffFeedIn: 0.08,
|
||||
smartCostType: "price",
|
||||
currency: CURRENCY.EUR,
|
||||
pv: [{ power: 5000 }, { power: 2300 }],
|
||||
forecast: {
|
||||
solar: {
|
||||
today: { energy: 1000, complete: true },
|
||||
tomorrow: { energy: 1000, complete: false },
|
||||
dayAfterTomorrow: { energy: 1000, complete: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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 },
|
||||
],
|
||||
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",
|
||||
chargePower: 1400,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
batteryPower: -1500,
|
||||
batterySoc: 75,
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
pvPower: 5000,
|
||||
gridPower: -1300,
|
||||
homePower: 800,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1400,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1400,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
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,
|
||||
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",
|
||||
chargePower: 5000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 2500,
|
||||
icon: "bus",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 2500,
|
||||
connected: true,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
batteryPower: -700,
|
||||
batterySoc: 95,
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
pvPower: 8700,
|
||||
gridPower: -500,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 5000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 5000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 2500,
|
||||
icon: "bus",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 2500,
|
||||
connected: true,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
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",
|
||||
chargePower: 5000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 1600,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1600,
|
||||
connected: true,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
batteryPower: 800,
|
||||
batterySoc: 76,
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
pvPower: 300,
|
||||
gridPower: 6500,
|
||||
homePower: 1000,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 5000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 5000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 1600,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1600,
|
||||
connected: true,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
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",
|
||||
chargePower: 5500,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 0,
|
||||
icon: "car",
|
||||
charging: false,
|
||||
title: "Garage",
|
||||
chargePower: 0,
|
||||
connected: false,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 0,
|
||||
icon: "car",
|
||||
charging: false,
|
||||
title: "Garage",
|
||||
chargePower: 0,
|
||||
connected: false,
|
||||
index: 2,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 0,
|
||||
icon: "car",
|
||||
charging: false,
|
||||
title: "Garage",
|
||||
chargePower: 0,
|
||||
connected: false,
|
||||
index: 3,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
batteryPower: 0,
|
||||
batterySoc: 0,
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
pvPower: 0,
|
||||
gridPower: 6500,
|
||||
homePower: 1000,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 5500,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 5500,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 0,
|
||||
icon: "car",
|
||||
charging: false,
|
||||
title: "Garage",
|
||||
chargePower: 0,
|
||||
connected: false,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 0,
|
||||
icon: "car",
|
||||
charging: false,
|
||||
title: "Garage",
|
||||
chargePower: 0,
|
||||
connected: false,
|
||||
index: 2,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 0,
|
||||
icon: "car",
|
||||
charging: false,
|
||||
title: "Garage",
|
||||
chargePower: 0,
|
||||
connected: false,
|
||||
index: 3,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
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,
|
||||
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",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 1000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 2200,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 2200,
|
||||
connected: true,
|
||||
index: 2,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
tariffGrid: 0.25,
|
||||
tariffFeedIn: 0.08,
|
||||
tariffCo2: 723,
|
||||
smartCostType: "co2",
|
||||
currency: CURRENCY.EUR,
|
||||
pv: [{ power: 5000 }, { power: 2300 }],
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
pvPower: 7300,
|
||||
gridPower: -2300,
|
||||
homePower: 800,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 1000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 1,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
{
|
||||
power: 2200,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 2200,
|
||||
connected: true,
|
||||
index: 2,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
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",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
pvPower: 2000,
|
||||
gridPower: -2000,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1000,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1000,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const UnknownInputFill = Template.bind({});
|
||||
UnknownInputFill.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
pvPower: 500,
|
||||
gridPower: 0,
|
||||
batteryPower: -1000,
|
||||
loadpointsCompact: [],
|
||||
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",
|
||||
chargePower: 1700,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
pvPower: 3000,
|
||||
gridPower: -1000,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1700,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1700,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const UnknownOutputLessThan10Percent = Template.bind({});
|
||||
UnknownOutputLessThan10Percent.args = {
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
pvPower: 3000,
|
||||
gridPower: -1000,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1800,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1800,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
pvPower: 3000,
|
||||
gridPower: -1000,
|
||||
loadpointsCompact: [
|
||||
{
|
||||
power: 1800,
|
||||
icon: "car",
|
||||
charging: true,
|
||||
title: "Garage",
|
||||
chargePower: 1800,
|
||||
connected: true,
|
||||
index: 0,
|
||||
vehicleName: "",
|
||||
vehicleSoc: 50,
|
||||
chargerFeatureHeating: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
4
assets/js/components/Energyflow/types.d.ts
vendored
4
assets/js/components/Energyflow/types.d.ts
vendored
|
|
@ -1,4 +1,4 @@
|
|||
interface Pv {
|
||||
title?: string;
|
||||
power: number;
|
||||
title?: string;
|
||||
power: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,58 +2,58 @@ import Footer from "./Footer.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Footer/Footer",
|
||||
component: Footer,
|
||||
argTypes: {
|
||||
version: { control: "object" },
|
||||
savings: { control: "object" },
|
||||
},
|
||||
title: "Footer/Footer",
|
||||
component: Footer,
|
||||
argTypes: {
|
||||
version: { control: "object" },
|
||||
savings: { control: "object" },
|
||||
},
|
||||
} as Meta<typeof Footer>;
|
||||
|
||||
const Template: StoryFn<typeof Footer> = (args) => ({
|
||||
components: { Footer },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Footer v-bind="args" />',
|
||||
components: { Footer },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Footer v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
version: {
|
||||
installed: "0.401",
|
||||
available: "0.401",
|
||||
},
|
||||
savings: {
|
||||
statistics: {
|
||||
"30d": { solarPercentage: 50 },
|
||||
},
|
||||
},
|
||||
version: {
|
||||
installed: "0.401",
|
||||
available: "0.401",
|
||||
},
|
||||
savings: {
|
||||
statistics: {
|
||||
"30d": { solarPercentage: 50 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const UpdateNightly = Template.bind({});
|
||||
UpdateNightly.args = {
|
||||
version: {
|
||||
installed: "0.400",
|
||||
available: "0.400",
|
||||
commit: "5ce7be4",
|
||||
},
|
||||
savings: {
|
||||
statistics: {
|
||||
"30d": { solarPercentage: 22 },
|
||||
},
|
||||
},
|
||||
version: {
|
||||
installed: "0.400",
|
||||
available: "0.400",
|
||||
commit: "5ce7be4",
|
||||
},
|
||||
savings: {
|
||||
statistics: {
|
||||
"30d": { solarPercentage: 22 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const UpdateAvailable = Template.bind({});
|
||||
UpdateAvailable.args = {
|
||||
version: {
|
||||
installed: "0.400",
|
||||
available: "0.500",
|
||||
},
|
||||
savings: {
|
||||
statistics: {
|
||||
"30d": { solarPercentage: 100 },
|
||||
},
|
||||
},
|
||||
version: {
|
||||
installed: "0.400",
|
||||
available: "0.500",
|
||||
},
|
||||
savings: {
|
||||
statistics: {
|
||||
"30d": { solarPercentage: 100 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,32 +2,32 @@ import OfflineIndicator from "./OfflineIndicator.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Footer/OfflineIndicator",
|
||||
component: OfflineIndicator,
|
||||
argTypes: {
|
||||
offline: { control: "boolean" },
|
||||
fatal: { control: "object" },
|
||||
},
|
||||
title: "Footer/OfflineIndicator",
|
||||
component: OfflineIndicator,
|
||||
argTypes: {
|
||||
offline: { control: "boolean" },
|
||||
fatal: { control: "object" },
|
||||
},
|
||||
} as Meta<typeof OfflineIndicator>;
|
||||
|
||||
const Template: StoryFn<typeof OfflineIndicator> = (args) => ({
|
||||
components: { OfflineIndicator },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<OfflineIndicator v-bind="args" />',
|
||||
components: { OfflineIndicator },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<OfflineIndicator v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Offline = Template.bind({});
|
||||
Offline.args = {
|
||||
offline: true,
|
||||
offline: true,
|
||||
};
|
||||
|
||||
export const Fatal = Template.bind({});
|
||||
Fatal.args = {
|
||||
fatal: {
|
||||
error: `cannot create charger 'wallbox_cphl': cannot create charger type 'template': cannot create
|
||||
fatal: {
|
||||
error: `cannot create charger 'wallbox_cphl': cannot create charger type 'template': cannot create
|
||||
charger type 'hardybarth-ecbl: Post http://192.168.2.219/api/v1/chargecontrols/1/mode: context deadline exceeded (Client.Timeout
|
||||
exceeded while awaiting headers)`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,61 +2,61 @@ import Version from "./Version.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
const releaseNotes =
|
||||
'<h1>0.141</h1><p>Bug fixes:</p><ul><li>Withdraw Kia and Hyundai apis (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="817450535" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/699" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/699/hovercard" href="https://github.com/evcc-io/evcc/pull/699">#699</a>)</li><li>Simplify Tesla integration and fix upstream changes (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="817525274" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/700" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/700/hovercard" href="https://github.com/evcc-io/evcc/pull/700">#700</a>)</li><li>SHM: Check connected status (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="811567860" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/673" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/673/hovercard" href="https://github.com/evcc-io/evcc/pull/673">#673</a>)</li></ul><p>Enhancements:</p><ul><li>Add Seat api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="812885392" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/681" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/681/hovercard" href="https://github.com/evcc-io/evcc/pull/681">#681</a>)</li><li>Add Skoda api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="812883796" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/680" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/680/hovercard" href="https://github.com/evcc-io/evcc/pull/680">#680</a>)</li><li>Add Peugeot/Opel/Citroen api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="815612446" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/692" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/692/hovercard" href="https://github.com/evcc-io/evcc/pull/692">#692</a>)</li><li>KEBA: Support mA current resolution adjustments (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="801358501" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/646" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/646/hovercard" href="https://github.com/evcc-io/evcc/pull/646">#646</a>)</li></ul>';
|
||||
'<h1>0.141</h1><p>Bug fixes:</p><ul><li>Withdraw Kia and Hyundai apis (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="817450535" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/699" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/699/hovercard" href="https://github.com/evcc-io/evcc/pull/699">#699</a>)</li><li>Simplify Tesla integration and fix upstream changes (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="817525274" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/700" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/700/hovercard" href="https://github.com/evcc-io/evcc/pull/700">#700</a>)</li><li>SHM: Check connected status (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="811567860" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/673" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/673/hovercard" href="https://github.com/evcc-io/evcc/pull/673">#673</a>)</li></ul><p>Enhancements:</p><ul><li>Add Seat api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="812885392" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/681" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/681/hovercard" href="https://github.com/evcc-io/evcc/pull/681">#681</a>)</li><li>Add Skoda api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="812883796" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/680" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/680/hovercard" href="https://github.com/evcc-io/evcc/pull/680">#680</a>)</li><li>Add Peugeot/Opel/Citroen api (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="815612446" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/692" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/692/hovercard" href="https://github.com/evcc-io/evcc/pull/692">#692</a>)</li><li>KEBA: Support mA current resolution adjustments (<a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="801358501" data-permission-text="Title is private" data-url="https://github.com/evcc-io/evcc/issues/646" data-hovercard-type="pull_request" data-hovercard-url="/evcc-io/evcc/pull/646/hovercard" href="https://github.com/evcc-io/evcc/pull/646">#646</a>)</li></ul>';
|
||||
|
||||
export default {
|
||||
title: "Footer/Version",
|
||||
component: Version,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
installed: { control: "text" },
|
||||
available: { control: "text" },
|
||||
commit: { control: "text" },
|
||||
releaseNotes: { control: "text" },
|
||||
hasUpdater: { control: "boolean" },
|
||||
},
|
||||
title: "Footer/Version",
|
||||
component: Version,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
installed: { control: "text" },
|
||||
available: { control: "text" },
|
||||
commit: { control: "text" },
|
||||
releaseNotes: { control: "text" },
|
||||
hasUpdater: { control: "boolean" },
|
||||
},
|
||||
} as Meta<typeof Version>;
|
||||
|
||||
const Template: StoryFn<typeof Version> = (args) => ({
|
||||
components: { Version },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Version v-bind="args" />',
|
||||
components: { Version },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Version v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Latest = Template.bind({});
|
||||
Latest.args = {
|
||||
installed: "0.141",
|
||||
available: "0.141",
|
||||
installed: "0.141",
|
||||
available: "0.141",
|
||||
};
|
||||
|
||||
export const Nightly = Template.bind({});
|
||||
Nightly.args = {
|
||||
installed: "0.141",
|
||||
available: "0.141",
|
||||
commit: "5ce7be4",
|
||||
installed: "0.141",
|
||||
available: "0.141",
|
||||
commit: "5ce7be4",
|
||||
};
|
||||
|
||||
export const UpdateAvailable = Template.bind({});
|
||||
UpdateAvailable.args = {
|
||||
installed: "0.141",
|
||||
available: "0.142",
|
||||
releaseNotes,
|
||||
installed: "0.141",
|
||||
available: "0.142",
|
||||
releaseNotes,
|
||||
};
|
||||
|
||||
export const AutoUpdater = Template.bind({});
|
||||
AutoUpdater.args = {
|
||||
installed: "0.141",
|
||||
available: "0.142",
|
||||
releaseNotes,
|
||||
hasUpdater: true,
|
||||
installed: "0.141",
|
||||
available: "0.142",
|
||||
releaseNotes,
|
||||
hasUpdater: true,
|
||||
};
|
||||
|
||||
export const NoReleaseNotes = Template.bind({});
|
||||
NoReleaseNotes.args = {
|
||||
installed: "0.141",
|
||||
available: "0.142",
|
||||
installed: "0.141",
|
||||
available: "0.142",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
export function isForecastSlot(obj?: TimeseriesEntry | ForecastSlot): obj is ForecastSlot {
|
||||
return (obj as ForecastSlot).start !== undefined;
|
||||
return (obj as ForecastSlot).start !== undefined;
|
||||
}
|
||||
|
||||
export interface TimeseriesEntry {
|
||||
val: number;
|
||||
ts: string;
|
||||
val: number;
|
||||
ts: string;
|
||||
}
|
||||
|
||||
export interface ForecastSlot {
|
||||
start: string;
|
||||
end: string;
|
||||
value: number;
|
||||
start: string;
|
||||
end: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface EnergyByDay {
|
||||
energy: number;
|
||||
complete: boolean;
|
||||
energy: number;
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
export interface SolarDetails {
|
||||
scale?: number;
|
||||
today?: EnergyByDay;
|
||||
tomorrow?: EnergyByDay;
|
||||
dayAfterTomorrow?: EnergyByDay;
|
||||
timeseries?: TimeseriesEntry[];
|
||||
scale?: number;
|
||||
today?: EnergyByDay;
|
||||
tomorrow?: EnergyByDay;
|
||||
dayAfterTomorrow?: EnergyByDay;
|
||||
timeseries?: TimeseriesEntry[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,77 +2,77 @@ import Loadpoint from "./Loadpoint.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Loadpoint",
|
||||
component: Loadpoint,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
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,
|
||||
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;
|
||||
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: "",
|
||||
vehicleName: "",
|
||||
};
|
||||
|
||||
export const Idle = Template.bind({});
|
||||
Idle.args = {
|
||||
enabled: false,
|
||||
connected: false,
|
||||
vehicleName: "",
|
||||
mode: "off",
|
||||
charging: false,
|
||||
offeredCurrent: 0,
|
||||
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,
|
||||
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,
|
||||
chargerIcon: "heater",
|
||||
title: "Heating device with long name",
|
||||
mode: "now",
|
||||
chargerFeatureIntegratedDevice: true,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,59 +2,59 @@ 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,
|
||||
index: 0,
|
||||
chargerFeatureHeating: false,
|
||||
};
|
||||
return { ...base, ...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,
|
||||
index: 0,
|
||||
chargerFeatureHeating: false,
|
||||
};
|
||||
return { ...base, ...opts };
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "Loadpoints/Loadpoints",
|
||||
component: Loadpoints,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
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" />',
|
||||
components: { Loadpoints },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Loadpoints v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Standard = Template.bind({});
|
||||
Standard.args = {
|
||||
vehicles: [],
|
||||
loadpoints: [
|
||||
loadpoint({ title: "Carport", index: 0 }),
|
||||
loadpoint({
|
||||
title: "Water Heater",
|
||||
chargerFeatureIntegratedDevice: true,
|
||||
chargerIcon: "waterheater",
|
||||
index: 1,
|
||||
}),
|
||||
],
|
||||
vehicles: [],
|
||||
loadpoints: [
|
||||
loadpoint({ title: "Carport", index: 0 }),
|
||||
loadpoint({
|
||||
title: "Water Heater",
|
||||
chargerFeatureIntegratedDevice: true,
|
||||
chargerIcon: "waterheater",
|
||||
index: 1,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,32 +2,32 @@ 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",
|
||||
},
|
||||
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;
|
||||
const story = () => ({
|
||||
components: { Mode },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Mode v-bind="args" />',
|
||||
});
|
||||
story.args = args;
|
||||
return story;
|
||||
};
|
||||
|
||||
export const Minimal = Template.bind({});
|
||||
|
|
@ -35,14 +35,14 @@ Minimal.args = { mode: "now" };
|
|||
|
||||
export const Full = Template.bind({});
|
||||
Full.args = {
|
||||
mode: "pv",
|
||||
pvPossible: true,
|
||||
hasSmartCost: true,
|
||||
mode: "pv",
|
||||
pvPossible: true,
|
||||
hasSmartCost: true,
|
||||
};
|
||||
|
||||
export const SmartGridOnly = Template.bind({});
|
||||
SmartGridOnly.args = {
|
||||
mode: "pv",
|
||||
pvPossible: false,
|
||||
hasSmartCost: true,
|
||||
mode: "pv",
|
||||
pvPossible: false,
|
||||
hasSmartCost: true,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,94 +2,94 @@ 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" } },
|
||||
},
|
||||
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" />',
|
||||
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,
|
||||
phasesActive: 1,
|
||||
minCurrent: 6,
|
||||
maxCurrent: 16,
|
||||
offeredCurrent: 8,
|
||||
chargeCurrents: undefined,
|
||||
};
|
||||
|
||||
export const TwoPhase = Template.bind({});
|
||||
TwoPhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 2,
|
||||
...OnePhase.args,
|
||||
phasesActive: 2,
|
||||
};
|
||||
|
||||
export const ThreePhase = Template.bind({});
|
||||
ThreePhase.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 3,
|
||||
...OnePhase.args,
|
||||
phasesActive: 3,
|
||||
};
|
||||
|
||||
export const RealCurrents = Template.bind({});
|
||||
RealCurrents.args = {
|
||||
...OnePhase.args,
|
||||
phasesActive: 3,
|
||||
offeredCurrent: 13,
|
||||
chargeCurrents: [11, 9, 12],
|
||||
...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],
|
||||
...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],
|
||||
...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],
|
||||
...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],
|
||||
...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,
|
||||
...OnePhase.args,
|
||||
phasesActive: 1,
|
||||
offeredCurrent: 10,
|
||||
chargeCurrents: [0.007, 0.009, 5.945],
|
||||
minCurrent: 6,
|
||||
maxCurrent: 20,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import settings from "@/settings";
|
||||
|
||||
export function getSessionInfo(index: number, fallback?: string) {
|
||||
return settings.sessionInfo[index - 1] || fallback;
|
||||
return settings.sessionInfo[index - 1] || fallback;
|
||||
}
|
||||
|
||||
export function setSessionInfo(index: number, value: string) {
|
||||
const clone = [...settings.sessionInfo];
|
||||
clone[index - 1] = value;
|
||||
clone.map((v) => v || "");
|
||||
settings.sessionInfo = clone;
|
||||
const clone = [...settings.sessionInfo];
|
||||
clone[index - 1] = value;
|
||||
clone.map((v) => v || "");
|
||||
settings.sessionInfo = clone;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,36 +3,36 @@ import MultiIcon from "./MultiIcon.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "MultiIcon/MultiIcon",
|
||||
component: MultiIcon,
|
||||
argTypes: {
|
||||
count: {
|
||||
control: { type: "number", min: 1, max: 10 },
|
||||
description: "Number of icons to display",
|
||||
},
|
||||
size: {
|
||||
options: Object.values(ICON_SIZE),
|
||||
control: { type: "select" },
|
||||
description: "Size of the icons",
|
||||
},
|
||||
},
|
||||
title: "MultiIcon/MultiIcon",
|
||||
component: MultiIcon,
|
||||
argTypes: {
|
||||
count: {
|
||||
control: { type: "number", min: 1, max: 10 },
|
||||
description: "Number of icons to display",
|
||||
},
|
||||
size: {
|
||||
options: Object.values(ICON_SIZE),
|
||||
control: { type: "select" },
|
||||
description: "Size of the icons",
|
||||
},
|
||||
},
|
||||
} as Meta<typeof MultiIcon>;
|
||||
|
||||
export const SingleIcon: StoryFn<typeof MultiIcon> = (args) => ({
|
||||
components: { MultiIcon },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<MultiIcon v-bind="args" />',
|
||||
components: { MultiIcon },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<MultiIcon v-bind="args" />',
|
||||
});
|
||||
|
||||
export const AllCounts: StoryFn<typeof MultiIcon> = (args) => ({
|
||||
components: { MultiIcon },
|
||||
setup() {
|
||||
const counts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
return { counts, args };
|
||||
},
|
||||
template: `
|
||||
components: { MultiIcon },
|
||||
setup() {
|
||||
const counts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
return { counts, args };
|
||||
},
|
||||
template: `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 30px;">
|
||||
<div v-for="count in counts" :key="count" style="display: flex; flex-direction: column; align-items: center; gap: 10px;">
|
||||
<MultiIcon :count="count" :size="args.size" />
|
||||
|
|
@ -43,5 +43,5 @@ export const AllCounts: StoryFn<typeof MultiIcon> = (args) => ({
|
|||
});
|
||||
|
||||
AllCounts.args = {
|
||||
size: ICON_SIZE.XL,
|
||||
size: ICON_SIZE.XL,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ import LiveCommunity from "./LiveCommunity.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Savings/LiveCommunity",
|
||||
component: LiveCommunity,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
title: "Savings/LiveCommunity",
|
||||
component: LiveCommunity,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} as Meta<typeof LiveCommunity>;
|
||||
|
||||
const Template: StoryFn<typeof LiveCommunity> = () => ({
|
||||
components: { LiveCommunity },
|
||||
template: "<LiveCommunity />",
|
||||
components: { LiveCommunity },
|
||||
template: "<LiveCommunity />",
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
|
|
|||
|
|
@ -2,34 +2,34 @@ import Sponsor from "./Sponsor.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Savings/Sponsor",
|
||||
component: Sponsor,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
name: {
|
||||
control: "text",
|
||||
description: "Sponsor name (use 'trial' or 'victron' for special states)",
|
||||
},
|
||||
expiresAt: {
|
||||
control: "text",
|
||||
description: "When the sponsorship expires (ISO date string)",
|
||||
},
|
||||
expiresSoon: {
|
||||
control: "boolean",
|
||||
description: "Whether the sponsorship is expiring soon",
|
||||
},
|
||||
},
|
||||
title: "Savings/Sponsor",
|
||||
component: Sponsor,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
name: {
|
||||
control: "text",
|
||||
description: "Sponsor name (use 'trial' or 'victron' for special states)",
|
||||
},
|
||||
expiresAt: {
|
||||
control: "text",
|
||||
description: "When the sponsorship expires (ISO date string)",
|
||||
},
|
||||
expiresSoon: {
|
||||
control: "boolean",
|
||||
description: "Whether the sponsorship is expiring soon",
|
||||
},
|
||||
},
|
||||
} as Meta<typeof Sponsor>;
|
||||
|
||||
// Template for rendering the component
|
||||
const Template: StoryFn<typeof Sponsor> = (args) => ({
|
||||
components: { Sponsor },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Sponsor v-bind="args" />',
|
||||
components: { Sponsor },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Sponsor v-bind="args" />',
|
||||
});
|
||||
|
||||
// Create stories for each variant
|
||||
|
|
@ -38,23 +38,23 @@ NoSponsor.args = {};
|
|||
|
||||
export const Trial = Template.bind({});
|
||||
Trial.args = {
|
||||
name: "trial",
|
||||
name: "trial",
|
||||
};
|
||||
|
||||
export const IndividualSponsor = Template.bind({});
|
||||
IndividualSponsor.args = {
|
||||
name: "naltatis",
|
||||
name: "naltatis",
|
||||
};
|
||||
|
||||
export const VictronDevice = Template.bind({});
|
||||
VictronDevice.args = {
|
||||
name: "victron",
|
||||
name: "victron",
|
||||
};
|
||||
|
||||
// Add an extra story showing the expiring state
|
||||
export const ExpiringSponsor = Template.bind({});
|
||||
ExpiringSponsor.args = {
|
||||
name: "naltatis",
|
||||
expiresSoon: true,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now
|
||||
name: "naltatis",
|
||||
expiresSoon: true,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,32 +2,32 @@ import SponsorTokenExpires from "./SponsorTokenExpires.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Savings/SponsorTokenExpires",
|
||||
component: SponsorTokenExpires,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
expiresSoon: { control: "boolean", description: "Whether sponsorship is expiring soon" },
|
||||
expiresAt: { control: "text", description: "Date when the sponsor token expires" },
|
||||
name: { control: "text", description: "Name of the sponsor token" },
|
||||
},
|
||||
title: "Savings/SponsorTokenExpires",
|
||||
component: SponsorTokenExpires,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
expiresSoon: { control: "boolean", description: "Whether sponsorship is expiring soon" },
|
||||
expiresAt: { control: "text", description: "Date when the sponsor token expires" },
|
||||
name: { control: "text", description: "Name of the sponsor token" },
|
||||
},
|
||||
} as Meta<typeof SponsorTokenExpires>;
|
||||
|
||||
// Template for rendering the component
|
||||
const Template: StoryFn<typeof SponsorTokenExpires> = (args) => ({
|
||||
components: { SponsorTokenExpires },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<SponsorTokenExpires v-bind="args" />',
|
||||
components: { SponsorTokenExpires },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<SponsorTokenExpires v-bind="args" />',
|
||||
});
|
||||
|
||||
// Create stories for each variant
|
||||
export const SomeDay = Template.bind({});
|
||||
SomeDay.args = {
|
||||
expiresSoon: true,
|
||||
expiresAt: new Date(Date.now() + 22 * 20 * 60 * 1000).toISOString(),
|
||||
expiresSoon: true,
|
||||
expiresAt: new Date(Date.now() + 22 * 20 * 60 * 1000).toISOString(),
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
|
|
|
|||
|
|
@ -2,37 +2,37 @@ import Tile from "./Tile.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Savings/Tile",
|
||||
component: Tile,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
icon: { control: "text", description: "Icon name to display" },
|
||||
title: { control: "text", description: "Title of the tile" },
|
||||
value: { control: "text", description: "Primary value to display" },
|
||||
unit: { control: "text", description: "Unit for the value" },
|
||||
sub1: { control: "text", description: "First subtitle" },
|
||||
sub2: { control: "text", description: "Second subtitle" },
|
||||
},
|
||||
title: "Savings/Tile",
|
||||
component: Tile,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
icon: { control: "text", description: "Icon name to display" },
|
||||
title: { control: "text", description: "Title of the tile" },
|
||||
value: { control: "text", description: "Primary value to display" },
|
||||
unit: { control: "text", description: "Unit for the value" },
|
||||
sub1: { control: "text", description: "First subtitle" },
|
||||
sub2: { control: "text", description: "Second subtitle" },
|
||||
},
|
||||
} as Meta<typeof Tile>;
|
||||
|
||||
// Template for rendering the component
|
||||
const Template: StoryFn<typeof Tile> = (args) => ({
|
||||
components: { Tile },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Tile v-bind="args" />',
|
||||
components: { Tile },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Tile v-bind="args" />',
|
||||
});
|
||||
|
||||
// Create default story with the example props
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
icon: "coinjar",
|
||||
title: "Ersparnis",
|
||||
value: "14,2",
|
||||
unit: "€",
|
||||
sub1: "gegenüber Netzbezug",
|
||||
sub2: "seit Dezember 2022",
|
||||
icon: "coinjar",
|
||||
title: "Ersparnis",
|
||||
value: "14,2",
|
||||
unit: "€",
|
||||
sub1: "gegenüber Netzbezug",
|
||||
sub2: "seit Dezember 2022",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,24 +3,24 @@ const source = "https://ourworldindata.org/grapher/carbon-intensity-electricity?
|
|||
|
||||
// This is a manual selection of countries. If yours is missing, please add it with data from the source above.
|
||||
const regions = [
|
||||
{ name: "Australia", co2: 503 },
|
||||
{ name: "Austria", co2: 158 },
|
||||
{ name: "Canada", co2: 128 },
|
||||
{ name: "Czech Republic", co2: 415 },
|
||||
{ name: "Denmark", co2: 181 },
|
||||
{ name: "Estonia", co2: 464 },
|
||||
{ name: "Europe", co2: 278 },
|
||||
{ name: "Finland", co2: 131 },
|
||||
{ name: "France", co2: 85 },
|
||||
{ name: "Germany", co2: 385 },
|
||||
{ name: "Netherlands", co2: 356 },
|
||||
{ name: "Norway", co2: 29 },
|
||||
{ name: "Poland", co2: 635 },
|
||||
{ name: "Sweden", co2: 45 },
|
||||
{ name: "Switzerland", co2: 46 },
|
||||
{ name: "United Kingdom", co2: 257 },
|
||||
{ name: "United States", co2: 367 },
|
||||
{ name: "World", co2: 436 },
|
||||
{ name: "Australia", co2: 503 },
|
||||
{ name: "Austria", co2: 158 },
|
||||
{ name: "Canada", co2: 128 },
|
||||
{ name: "Czech Republic", co2: 415 },
|
||||
{ name: "Denmark", co2: 181 },
|
||||
{ name: "Estonia", co2: 464 },
|
||||
{ name: "Europe", co2: 278 },
|
||||
{ name: "Finland", co2: 131 },
|
||||
{ name: "France", co2: 85 },
|
||||
{ name: "Germany", co2: 385 },
|
||||
{ name: "Netherlands", co2: 356 },
|
||||
{ name: "Norway", co2: 29 },
|
||||
{ name: "Poland", co2: 635 },
|
||||
{ name: "Sweden", co2: 45 },
|
||||
{ name: "Switzerland", co2: 46 },
|
||||
{ name: "United Kingdom", co2: 257 },
|
||||
{ name: "United States", co2: 367 },
|
||||
{ name: "World", co2: 436 },
|
||||
];
|
||||
|
||||
export default { regions, source };
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import axios from "axios";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: "https://api.evcc.io/v1/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
baseURL: "https://api.evcc.io/v1/",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// global error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const url = error.config.baseURL + error.config.url;
|
||||
const message = `${error.message}: API request failed ${url}`;
|
||||
window.app.raise({ message });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const url = error.config.baseURL + error.config.url;
|
||||
const message = `${error.message}: API request failed ${url}`;
|
||||
window.app.raise({ message });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
|
|
|||
20
assets/js/components/Savings/types.d.ts
vendored
20
assets/js/components/Savings/types.d.ts
vendored
|
|
@ -1,13 +1,13 @@
|
|||
export interface LiveCommunityData {
|
||||
totalClients: number;
|
||||
activeClients: number;
|
||||
totalInstances: number;
|
||||
activeInstances: number;
|
||||
chargePower: number;
|
||||
greenPower: number;
|
||||
maxChargePower: number;
|
||||
maxGreenPower: number;
|
||||
chargeEnergy: number;
|
||||
greenEnergy: number;
|
||||
totalClients: number;
|
||||
activeClients: number;
|
||||
totalInstances: number;
|
||||
activeInstances: number;
|
||||
chargePower: number;
|
||||
greenPower: number;
|
||||
maxChargePower: number;
|
||||
maxGreenPower: number;
|
||||
chargeEnergy: number;
|
||||
greenEnergy: number;
|
||||
}
|
||||
export type Period = "30d" | "365d" | "thisYear" | "total";
|
||||
|
|
|
|||
|
|
@ -1,76 +1,76 @@
|
|||
import {
|
||||
Chart,
|
||||
Tooltip,
|
||||
type ChartComponentLike,
|
||||
type ChartType,
|
||||
type Point,
|
||||
type TooltipItem,
|
||||
Chart,
|
||||
Tooltip,
|
||||
type ChartComponentLike,
|
||||
type ChartType,
|
||||
type Point,
|
||||
type TooltipItem,
|
||||
} from "chart.js";
|
||||
import colors from "@/colors";
|
||||
import type { Context } from "chartjs-plugin-datalabels";
|
||||
// Register common components
|
||||
export function registerChartComponents(components: ChartComponentLike[]) {
|
||||
Chart.register(...components);
|
||||
Chart.register(...components);
|
||||
}
|
||||
|
||||
// Set default configurations immediately
|
||||
Chart.defaults.font.family = window
|
||||
.getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--bs-font-sans-serif");
|
||||
.getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--bs-font-sans-serif");
|
||||
Chart.defaults.font.size = 14;
|
||||
Chart.defaults.layout.padding = 0;
|
||||
|
||||
// Custom tooltip positioners
|
||||
(Tooltip.positioners as any)["center"] = function () {
|
||||
const { chart } = this;
|
||||
return {
|
||||
x: chart.width / 2,
|
||||
y: chart.height / 2,
|
||||
xAlign: "center",
|
||||
yAlign: "center",
|
||||
};
|
||||
const { chart } = this;
|
||||
return {
|
||||
x: chart.width / 2,
|
||||
y: chart.height / 2,
|
||||
xAlign: "center",
|
||||
yAlign: "center",
|
||||
};
|
||||
};
|
||||
|
||||
(Tooltip.positioners as any)["topBottomCenter"] = function (
|
||||
_context: Context,
|
||||
eventPosition: Point
|
||||
_context: Context,
|
||||
eventPosition: Point
|
||||
) {
|
||||
const { chart } = this;
|
||||
const { height, width } = chart;
|
||||
const { chart } = this;
|
||||
const { height, width } = chart;
|
||||
|
||||
const isTop = eventPosition.y > height / 2;
|
||||
const yPadding = height / 5;
|
||||
const y = isTop ? yPadding : height - yPadding;
|
||||
const x = width / 2;
|
||||
const isTop = eventPosition.y > height / 2;
|
||||
const yPadding = height / 5;
|
||||
const y = isTop ? yPadding : height - yPadding;
|
||||
const x = width / 2;
|
||||
|
||||
return { x, y, xAlign: "center", yAlign: "center" };
|
||||
return { x, y, xAlign: "center", yAlign: "center" };
|
||||
};
|
||||
|
||||
export const commonOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
datalabels: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: "#000000cc",
|
||||
boxPadding: 5,
|
||||
usePointStyle: false,
|
||||
borderWidth: 0.00001,
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
datalabels: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: "#000000cc",
|
||||
boxPadding: 5,
|
||||
usePointStyle: false,
|
||||
borderWidth: 0.00001,
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function tooltipLabelColor(useBorder = false) {
|
||||
return function (item: TooltipItem<ChartType>) {
|
||||
const { backgroundColor, borderColor } = item.element.options;
|
||||
const color = useBorder ? borderColor : backgroundColor;
|
||||
return {
|
||||
borderColor: !item.raw ? colors.muted : "#fff",
|
||||
borderWidth: 1,
|
||||
backgroundColor: color,
|
||||
};
|
||||
};
|
||||
return function (item: TooltipItem<ChartType>) {
|
||||
const { backgroundColor, borderColor } = item.element.options;
|
||||
const color = useBorder ? borderColor : backgroundColor;
|
||||
return {
|
||||
borderColor: !item.raw ? colors.muted : "#fff",
|
||||
borderWidth: 1,
|
||||
backgroundColor: color,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +1,49 @@
|
|||
export interface Session {
|
||||
id: number;
|
||||
created: string;
|
||||
finished: string;
|
||||
loadpoint: string;
|
||||
identifier: string;
|
||||
vehicle: string;
|
||||
odometer: number;
|
||||
meterStart: number;
|
||||
meterStop: number;
|
||||
chargedEnergy: number;
|
||||
chargeDuration: number;
|
||||
solarPercentage: number;
|
||||
price: number | null;
|
||||
pricePerKWh: number | null;
|
||||
co2PerKWh?: number | null;
|
||||
id: number;
|
||||
created: string;
|
||||
finished: string;
|
||||
loadpoint: string;
|
||||
identifier: string;
|
||||
vehicle: string;
|
||||
odometer: number;
|
||||
meterStart: number;
|
||||
meterStop: number;
|
||||
chargedEnergy: number;
|
||||
chargeDuration: number;
|
||||
solarPercentage: number;
|
||||
price: number | null;
|
||||
pricePerKWh: number | null;
|
||||
co2PerKWh?: number | null;
|
||||
}
|
||||
|
||||
export interface Legend {
|
||||
label: string;
|
||||
color: any;
|
||||
value: string | string[];
|
||||
label: string;
|
||||
color: any;
|
||||
value: string | string[];
|
||||
}
|
||||
|
||||
export interface Column {
|
||||
name: string;
|
||||
unit: string;
|
||||
total: number | null;
|
||||
value: (session: Session) => number | null;
|
||||
format: (value: number) => string | undefined;
|
||||
name: string;
|
||||
unit: string;
|
||||
total: number | null;
|
||||
value: (session: Session) => number | null;
|
||||
format: (value: number) => string | undefined;
|
||||
}
|
||||
|
||||
export enum TYPES {
|
||||
SOLAR = "solar",
|
||||
PRICE = "price",
|
||||
CO2 = "co2",
|
||||
SOLAR = "solar",
|
||||
PRICE = "price",
|
||||
CO2 = "co2",
|
||||
}
|
||||
|
||||
export enum GROUPS {
|
||||
NONE = "none",
|
||||
LOADPOINT = "loadpoint",
|
||||
VEHICLE = "vehicle",
|
||||
NONE = "none",
|
||||
LOADPOINT = "loadpoint",
|
||||
VEHICLE = "vehicle",
|
||||
}
|
||||
|
||||
export enum PERIODS {
|
||||
MONTH = "month",
|
||||
YEAR = "year",
|
||||
TOTAL = "total",
|
||||
MONTH = "month",
|
||||
YEAR = "year",
|
||||
TOTAL = "total",
|
||||
}
|
||||
|
|
|
|||
2
assets/js/components/Site/types.d.ts
vendored
2
assets/js/components/Site/types.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
|||
export interface Grid {
|
||||
power?: number;
|
||||
power?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,23 @@ import Navigation from "./Navigation.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
export default {
|
||||
title: "Top/Navigation",
|
||||
component: Navigation,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
vehicleLogins: { control: "object" },
|
||||
sponsor: { control: "object" },
|
||||
},
|
||||
title: "Top/Navigation",
|
||||
component: Navigation,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
vehicleLogins: { control: "object" },
|
||||
sponsor: { control: "object" },
|
||||
},
|
||||
} as Meta<typeof Navigation>;
|
||||
|
||||
const Template: StoryFn<typeof Navigation> = (args) => ({
|
||||
components: { Navigation },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Navigation v-bind="args" />',
|
||||
components: { Navigation },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Navigation v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Standard = Template.bind({});
|
||||
|
|
@ -26,37 +26,37 @@ Standard.args = {};
|
|||
|
||||
export const VehicleLogins = Template.bind({});
|
||||
VehicleLogins.args = {
|
||||
vehicleLogins: {
|
||||
"Mercedes EQS": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-a.test/",
|
||||
},
|
||||
"Nissan Leaf Pro": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-b.test/",
|
||||
},
|
||||
},
|
||||
vehicleLogins: {
|
||||
"Mercedes EQS": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-a.test/",
|
||||
},
|
||||
"Nissan Leaf Pro": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-b.test/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const PendingVehicleLogins = Template.bind({});
|
||||
PendingVehicleLogins.args = {
|
||||
vehicleLogins: {
|
||||
"Mercedes EQS": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-a.test/",
|
||||
},
|
||||
"Nissan Leaf Pro": {
|
||||
authenticated: false,
|
||||
uri: "https://login-provider-b.test/",
|
||||
},
|
||||
},
|
||||
vehicleLogins: {
|
||||
"Mercedes EQS": {
|
||||
authenticated: true,
|
||||
uri: "https://login-provider-a.test/",
|
||||
},
|
||||
"Nissan Leaf Pro": {
|
||||
authenticated: false,
|
||||
uri: "https://login-provider-b.test/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TokenExpires = Template.bind({});
|
||||
TokenExpires.args = {
|
||||
sponsor: {
|
||||
name: "Sponsor",
|
||||
expiresAt: new Date().toISOString(),
|
||||
expiresSoon: true,
|
||||
},
|
||||
sponsor: {
|
||||
name: "Sponsor",
|
||||
expiresAt: new Date().toISOString(),
|
||||
expiresSoon: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,88 +3,87 @@ import Notifications from "./Notifications.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
const timeAgo = (hours = 0, minutes = 0, seconds = 0) => {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - hours);
|
||||
date.setMinutes(date.getMinutes() - minutes);
|
||||
date.setSeconds(date.getSeconds() - seconds);
|
||||
return date;
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - hours);
|
||||
date.setMinutes(date.getMinutes() - minutes);
|
||||
date.setSeconds(date.getSeconds() - seconds);
|
||||
return date;
|
||||
};
|
||||
|
||||
const notificationsData: Notification[] = [
|
||||
{
|
||||
message: "Server unavailable",
|
||||
level: "error",
|
||||
time: timeAgo(),
|
||||
count: 1,
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "charger out of sync: expected disabled, got enabled",
|
||||
level: "warn",
|
||||
count: 4,
|
||||
time: timeAgo(0, 0, 42),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "Sponsortoken: x509: certificate has expired",
|
||||
level: "error",
|
||||
count: 1,
|
||||
time: timeAgo(1, 12, 44),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Block device full: can not write to /tmp/evcc/foobarloremawefhwuiehfwuiehfwiauhefjkajowaeigjwalvmoweivwail",
|
||||
level: "error",
|
||||
count: 1,
|
||||
time: timeAgo(1, 12, 44),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "charger out of sync: expected disabled, got enabled",
|
||||
level: "warn",
|
||||
count: 4,
|
||||
time: timeAgo(1, 22, 0),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"vehicle remote charge start: invalid character '<' looking for beginning of value",
|
||||
level: "warn",
|
||||
count: 3,
|
||||
time: timeAgo(4, 2, 0),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Amet irure quis incididunt voluptate esse. Commodo ea sunt est ipsum tempor nisi laboris voluptate labore elit laborum. Ex irure commodo reprehenderit consequat consequat do ad tempor aliquip deserunt eu. Laboris minim nostrud quis nisi. Dolor occaecat reprehenderit velit dolore exercitation cupidatat et voluptate. Nulla pariatur deserunt esse minim nisi nisi nulla. Sit eiusmod do incididunt sint minim pariatur aute.",
|
||||
level: "warn",
|
||||
count: 1,
|
||||
time: timeAgo(5, 2, 44),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "Server unavailable",
|
||||
level: "error",
|
||||
time: timeAgo(),
|
||||
count: 1,
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "charger out of sync: expected disabled, got enabled",
|
||||
level: "warn",
|
||||
count: 4,
|
||||
time: timeAgo(0, 0, 42),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "Sponsortoken: x509: certificate has expired",
|
||||
level: "error",
|
||||
count: 1,
|
||||
time: timeAgo(1, 12, 44),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Block device full: can not write to /tmp/evcc/foobarloremawefhwuiehfwuiehfwiauhefjkajowaeigjwalvmoweivwail",
|
||||
level: "error",
|
||||
count: 1,
|
||||
time: timeAgo(1, 12, 44),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "charger out of sync: expected disabled, got enabled",
|
||||
level: "warn",
|
||||
count: 4,
|
||||
time: timeAgo(1, 22, 0),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message: "vehicle remote charge start: invalid character '<' looking for beginning of value",
|
||||
level: "warn",
|
||||
count: 3,
|
||||
time: timeAgo(4, 2, 0),
|
||||
lp: 1,
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Amet irure quis incididunt voluptate esse. Commodo ea sunt est ipsum tempor nisi laboris voluptate labore elit laborum. Ex irure commodo reprehenderit consequat consequat do ad tempor aliquip deserunt eu. Laboris minim nostrud quis nisi. Dolor occaecat reprehenderit velit dolore exercitation cupidatat et voluptate. Nulla pariatur deserunt esse minim nisi nisi nulla. Sit eiusmod do incididunt sint minim pariatur aute.",
|
||||
level: "warn",
|
||||
count: 1,
|
||||
time: timeAgo(5, 2, 44),
|
||||
lp: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
title: "Top/Notifications",
|
||||
component: Notifications,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
notifications: { control: "object" },
|
||||
},
|
||||
title: "Top/Notifications",
|
||||
component: Notifications,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
argTypes: {
|
||||
notifications: { control: "object" },
|
||||
},
|
||||
} as Meta<typeof Notifications>;
|
||||
|
||||
const Template: StoryFn<typeof Notifications> = (args) => ({
|
||||
components: { Notifications },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Notifications v-bind="args" />',
|
||||
components: { Notifications },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Notifications v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
notifications: notificationsData,
|
||||
notifications: notificationsData,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import axios from "axios";
|
|||
const { protocol, hostname, port, pathname } = window.location;
|
||||
|
||||
const baseAPI = axios.create({
|
||||
baseURL: protocol + "//" + hostname + (port ? ":" + port : "") + pathname,
|
||||
baseURL: protocol + "//" + hostname + (port ? ":" + port : "") + pathname,
|
||||
});
|
||||
|
||||
// global error handling
|
||||
baseAPI.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const url = error.config.baseURL + error.config.url;
|
||||
const message = `${error.message}: API request failed ${url}`;
|
||||
window.app.raise({ message });
|
||||
}
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const url = error.config.baseURL + error.config.url;
|
||||
const message = `${error.message}: API request failed ${url}`;
|
||||
window.app.raise({ message });
|
||||
}
|
||||
);
|
||||
|
||||
export default baseAPI;
|
||||
|
|
|
|||
8
assets/js/components/Top/types.d.ts
vendored
8
assets/js/components/Top/types.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
export interface Provider {
|
||||
title: string;
|
||||
loggedIn: boolean;
|
||||
loginPath: string;
|
||||
logoutPath: string;
|
||||
title: string;
|
||||
loggedIn: boolean;
|
||||
loginPath: string;
|
||||
logoutPath: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,99 +3,99 @@ import VehicleIcon from "./VehicleIcon.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
const icons = [
|
||||
"car",
|
||||
"bike",
|
||||
"bus",
|
||||
"moped",
|
||||
"motorcycle",
|
||||
"rocket",
|
||||
"scooter",
|
||||
"taxi",
|
||||
"tractor",
|
||||
"rickshaw",
|
||||
"shuttle",
|
||||
"van",
|
||||
"airpurifier",
|
||||
"battery",
|
||||
"bulb",
|
||||
"climate",
|
||||
"coffeemaker",
|
||||
"compute",
|
||||
"cooking",
|
||||
"cooler",
|
||||
"desktop",
|
||||
"device",
|
||||
"dishwasher",
|
||||
"dryer",
|
||||
"floorlamp",
|
||||
"generic",
|
||||
"heater",
|
||||
"heatexchange",
|
||||
"heatpump",
|
||||
"kettle",
|
||||
"laundry",
|
||||
"laundry2",
|
||||
"machine",
|
||||
"meter",
|
||||
"smartconsumer",
|
||||
"microwave",
|
||||
"pump",
|
||||
"tool",
|
||||
"waterheater",
|
||||
"car",
|
||||
"bike",
|
||||
"bus",
|
||||
"moped",
|
||||
"motorcycle",
|
||||
"rocket",
|
||||
"scooter",
|
||||
"taxi",
|
||||
"tractor",
|
||||
"rickshaw",
|
||||
"shuttle",
|
||||
"van",
|
||||
"airpurifier",
|
||||
"battery",
|
||||
"bulb",
|
||||
"climate",
|
||||
"coffeemaker",
|
||||
"compute",
|
||||
"cooking",
|
||||
"cooler",
|
||||
"desktop",
|
||||
"device",
|
||||
"dishwasher",
|
||||
"dryer",
|
||||
"floorlamp",
|
||||
"generic",
|
||||
"heater",
|
||||
"heatexchange",
|
||||
"heatpump",
|
||||
"kettle",
|
||||
"laundry",
|
||||
"laundry2",
|
||||
"machine",
|
||||
"meter",
|
||||
"smartconsumer",
|
||||
"microwave",
|
||||
"pump",
|
||||
"tool",
|
||||
"waterheater",
|
||||
];
|
||||
|
||||
export default {
|
||||
title: "VehicleIcon/VehicleIcon",
|
||||
component: VehicleIcon,
|
||||
argTypes: {
|
||||
name: {
|
||||
control: "select",
|
||||
options: icons,
|
||||
},
|
||||
names: { control: "object" },
|
||||
size: {
|
||||
control: "select",
|
||||
options: Object.values(ICON_SIZE),
|
||||
defaultValue: ICON_SIZE.XL,
|
||||
},
|
||||
},
|
||||
title: "VehicleIcon/VehicleIcon",
|
||||
component: VehicleIcon,
|
||||
argTypes: {
|
||||
name: {
|
||||
control: "select",
|
||||
options: icons,
|
||||
},
|
||||
names: { control: "object" },
|
||||
size: {
|
||||
control: "select",
|
||||
options: Object.values(ICON_SIZE),
|
||||
defaultValue: ICON_SIZE.XL,
|
||||
},
|
||||
},
|
||||
} as Meta<typeof VehicleIcon>;
|
||||
|
||||
const Template: StoryFn<typeof VehicleIcon> = (args) => ({
|
||||
components: { VehicleIcon },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<VehicleIcon v-bind="args" />',
|
||||
components: { VehicleIcon },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<VehicleIcon v-bind="args" />',
|
||||
});
|
||||
|
||||
// Single icon story that can be controlled via args
|
||||
export const SingleIcon = Template.bind({});
|
||||
SingleIcon.args = {
|
||||
name: "car",
|
||||
size: ICON_SIZE.XL,
|
||||
name: "car",
|
||||
size: ICON_SIZE.XL,
|
||||
};
|
||||
|
||||
// Multiple icons stories
|
||||
export const TwoCars = Template.bind({});
|
||||
TwoCars.args = {
|
||||
names: ["car", "car"],
|
||||
size: ICON_SIZE.XL,
|
||||
names: ["car", "car"],
|
||||
size: ICON_SIZE.XL,
|
||||
};
|
||||
|
||||
export const CarAndThreeBikes = Template.bind({});
|
||||
CarAndThreeBikes.args = {
|
||||
names: ["car", "bike", "bike", "bike"],
|
||||
size: ICON_SIZE.XL,
|
||||
names: ["car", "bike", "bike", "bike"],
|
||||
size: ICON_SIZE.XL,
|
||||
};
|
||||
|
||||
// Story showing all icons at once
|
||||
export const AllIcons = () => ({
|
||||
components: { VehicleIcon },
|
||||
setup() {
|
||||
return { icons };
|
||||
},
|
||||
template: `
|
||||
components: { VehicleIcon },
|
||||
setup() {
|
||||
return { icons };
|
||||
},
|
||||
template: `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 30px;">
|
||||
<div v-for="icon in icons" :key="icon" style="display: flex; flex-direction: column; align-items: center; gap: 10px;">
|
||||
<VehicleIcon :name="icon" size="xl" />
|
||||
|
|
|
|||
|
|
@ -3,296 +3,293 @@ import { describe, expect, test } from "vitest";
|
|||
import Status from "./Status.vue";
|
||||
import { CURRENCY } from "@/types/evcc";
|
||||
|
||||
const serializeData = (data: Object) => (data ? `:${JSON.stringify(data)}` : "");
|
||||
config.global.mocks["$t"] = (key: string, data: Object) => `${key}${serializeData(data)}`;
|
||||
const serializeData = (data: object) => (data ? `:${JSON.stringify(data)}` : "");
|
||||
config.global.mocks["$t"] = (key: string, data: object) => `${key}${serializeData(data)}`;
|
||||
config.global.mocks["$i18n"] = { locale: "de-DE" };
|
||||
|
||||
const allEntries = {
|
||||
pvtimer: false,
|
||||
phasetimer: false,
|
||||
solar: false,
|
||||
climater: false,
|
||||
minsoc: false,
|
||||
limit: false,
|
||||
smartcost: false,
|
||||
planactive: false,
|
||||
planstart: false,
|
||||
pvtimer: false,
|
||||
phasetimer: false,
|
||||
solar: false,
|
||||
climater: false,
|
||||
minsoc: false,
|
||||
limit: false,
|
||||
smartcost: false,
|
||||
planactive: false,
|
||||
planstart: false,
|
||||
};
|
||||
|
||||
const expectEntries = (props: InstanceType<typeof Status>["$props"], entries: Object) => {
|
||||
const expectedEntries = { ...allEntries, ...entries };
|
||||
const expectEntries = (props: InstanceType<typeof Status>["$props"], entries: object) => {
|
||||
const expectedEntries = { ...allEntries, ...entries };
|
||||
|
||||
const wrapper = mount(Status, { props });
|
||||
const wrapper = mount(Status, { props });
|
||||
|
||||
Object.entries(expectedEntries).forEach(([key, value]) => {
|
||||
const selector = `[data-testid=vehicle-status-${key}]`;
|
||||
if (typeof value === "boolean") {
|
||||
expect(wrapper.find(selector).exists(), selector).eq(value);
|
||||
} else {
|
||||
expect(wrapper.find(selector).exists(), selector).eq(true);
|
||||
expect(wrapper.find(selector).text(), selector).eq(value);
|
||||
}
|
||||
});
|
||||
Object.entries(expectedEntries).forEach(([key, value]) => {
|
||||
const selector = `[data-testid=vehicle-status-${key}]`;
|
||||
if (typeof value === "boolean") {
|
||||
expect(wrapper.find(selector).exists(), selector).eq(value);
|
||||
} else {
|
||||
expect(wrapper.find(selector).exists(), selector).eq(true);
|
||||
expect(wrapper.find(selector).text(), selector).eq(value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
describe("basics", () => {
|
||||
test("no vehicle is connected", () => {
|
||||
expectEntries({ connected: false }, { charger: "main.vehicleStatus.disconnected" });
|
||||
});
|
||||
test("vehicle is connected", () => {
|
||||
expectEntries({ connected: true }, { charger: "main.vehicleStatus.connected" });
|
||||
});
|
||||
test("show waiting for vehicle if charger is enabled but not charging", () => {
|
||||
expectEntries(
|
||||
{ enabled: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle" }
|
||||
);
|
||||
});
|
||||
test("vehicle is charging", () => {
|
||||
expectEntries(
|
||||
{ connected: true, charging: true },
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
});
|
||||
test("no vehicle is connected", () => {
|
||||
expectEntries({ connected: false }, { charger: "main.vehicleStatus.disconnected" });
|
||||
});
|
||||
test("vehicle is connected", () => {
|
||||
expectEntries({ connected: true }, { charger: "main.vehicleStatus.connected" });
|
||||
});
|
||||
test("show waiting for vehicle if charger is enabled but not charging", () => {
|
||||
expectEntries(
|
||||
{ enabled: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle" }
|
||||
);
|
||||
});
|
||||
test("vehicle is charging", () => {
|
||||
expectEntries({ connected: true, charging: true }, { charger: "main.vehicleStatus.charging" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("min charge", () => {
|
||||
test("active when vehicle soc is below", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 10 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: "20 %" }
|
||||
);
|
||||
});
|
||||
test("not active when vehicle soc is above", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 21 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
test("not active when vehicle soc is equal", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 20 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
test("not active when limit is 0", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 0, vehicleSoc: 10 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
test("active when vehicle soc is below", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 10 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: "20 %" }
|
||||
);
|
||||
});
|
||||
test("not active when vehicle soc is above", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 21 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
test("not active when vehicle soc is equal", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 20 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
test("not active when limit is 0", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 0, vehicleSoc: 10 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plan", () => {
|
||||
const effectivePlanTime = "2020-03-16T06:00:00Z";
|
||||
const planProjectedStart = "2020-03-16T02:00:00Z";
|
||||
const planProjectedEnd = "2020-03-16T05:00:00Z";
|
||||
test("charging if target time is set, status is charging but planned slot is not active", () => {
|
||||
expectEntries(
|
||||
{ effectivePlanTime, charging: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
});
|
||||
test("active if target time is set, status is charging and planned slot is active", () => {
|
||||
expectEntries(
|
||||
{ planProjectedEnd, planActive: true, charging: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.charging", planactive: "Mo 06:00" }
|
||||
);
|
||||
});
|
||||
test("waiting for vehicle if a target time is set, the charger is enabled but not charging", () => {
|
||||
expectEntries(
|
||||
{ planProjectedEnd, planActive: true, enabled: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle", planactive: "Mo 06:00" }
|
||||
);
|
||||
});
|
||||
test("show projected start if not enabled yet", () => {
|
||||
expectEntries(
|
||||
{ effectivePlanTime, planProjectedStart, connected: true },
|
||||
{ charger: "main.vehicleStatus.connected", planstart: "Mo 03:00" }
|
||||
);
|
||||
});
|
||||
test("dont show plan status if plan is disabled (e.g. off, fast mode)", () => {
|
||||
expectEntries(
|
||||
{
|
||||
effectivePlanTime,
|
||||
planActive: true,
|
||||
charging: true,
|
||||
connected: true,
|
||||
chargingPlanDisabled: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
expectEntries(
|
||||
{
|
||||
effectivePlanTime,
|
||||
planActive: true,
|
||||
charging: false,
|
||||
enabled: true,
|
||||
connected: true,
|
||||
chargingPlanDisabled: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.waitForVehicle" }
|
||||
);
|
||||
expectEntries(
|
||||
{ effectivePlanTime, planProjectedStart, connected: true, chargingPlanDisabled: true },
|
||||
{ charger: "main.vehicleStatus.connected" }
|
||||
);
|
||||
});
|
||||
const effectivePlanTime = "2020-03-16T06:00:00Z";
|
||||
const planProjectedStart = "2020-03-16T02:00:00Z";
|
||||
const planProjectedEnd = "2020-03-16T05:00:00Z";
|
||||
test("charging if target time is set, status is charging but planned slot is not active", () => {
|
||||
expectEntries(
|
||||
{ effectivePlanTime, charging: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
});
|
||||
test("active if target time is set, status is charging and planned slot is active", () => {
|
||||
expectEntries(
|
||||
{ planProjectedEnd, planActive: true, charging: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.charging", planactive: "Mo 06:00" }
|
||||
);
|
||||
});
|
||||
test("waiting for vehicle if a target time is set, the charger is enabled but not charging", () => {
|
||||
expectEntries(
|
||||
{ planProjectedEnd, planActive: true, enabled: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle", planactive: "Mo 06:00" }
|
||||
);
|
||||
});
|
||||
test("show projected start if not enabled yet", () => {
|
||||
expectEntries(
|
||||
{ effectivePlanTime, planProjectedStart, connected: true },
|
||||
{ charger: "main.vehicleStatus.connected", planstart: "Mo 03:00" }
|
||||
);
|
||||
});
|
||||
test("dont show plan status if plan is disabled (e.g. off, fast mode)", () => {
|
||||
expectEntries(
|
||||
{
|
||||
effectivePlanTime,
|
||||
planActive: true,
|
||||
charging: true,
|
||||
connected: true,
|
||||
chargingPlanDisabled: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
expectEntries(
|
||||
{
|
||||
effectivePlanTime,
|
||||
planActive: true,
|
||||
charging: false,
|
||||
enabled: true,
|
||||
connected: true,
|
||||
chargingPlanDisabled: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.waitForVehicle" }
|
||||
);
|
||||
expectEntries(
|
||||
{ effectivePlanTime, planProjectedStart, connected: true, chargingPlanDisabled: true },
|
||||
{ charger: "main.vehicleStatus.connected" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("climating", () => {
|
||||
test("show climating status", () => {
|
||||
expectEntries(
|
||||
{ connected: true, enabled: true, vehicleClimaterActive: true, charging: true },
|
||||
{ charger: "main.vehicleStatus.charging", climater: true }
|
||||
);
|
||||
expectEntries(
|
||||
{ connected: true, enabled: true, vehicleClimaterActive: true, charging: false },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle", climater: true }
|
||||
);
|
||||
});
|
||||
test("only show climating if enabled", () => {
|
||||
expectEntries(
|
||||
{ connected: true, enabled: false, vehicleClimaterActive: true, charging: false },
|
||||
{ charger: "main.vehicleStatus.connected", climater: true }
|
||||
);
|
||||
});
|
||||
test("show climating status", () => {
|
||||
expectEntries(
|
||||
{ connected: true, enabled: true, vehicleClimaterActive: true, charging: true },
|
||||
{ charger: "main.vehicleStatus.charging", climater: true }
|
||||
);
|
||||
expectEntries(
|
||||
{ connected: true, enabled: true, vehicleClimaterActive: true, charging: false },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle", climater: true }
|
||||
);
|
||||
});
|
||||
test("only show climating if enabled", () => {
|
||||
expectEntries(
|
||||
{ connected: true, enabled: false, vehicleClimaterActive: true, charging: false },
|
||||
{ charger: "main.vehicleStatus.connected", climater: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timer", () => {
|
||||
test("show pv enable timer if not enabled yet and timer exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "enable",
|
||||
connected: true,
|
||||
pvRemainingInterpolated: 90,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.connected", pvtimer: "1:30\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("don't show pv enable timer if value is zero", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "enable",
|
||||
connected: true,
|
||||
pvRemainingInterpolated: 0,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.connected" }
|
||||
);
|
||||
});
|
||||
test("show pv disable timer if charging and timer exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "disable",
|
||||
connected: true,
|
||||
charging: true,
|
||||
pvRemainingInterpolated: 90,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", pvtimer: "1:30\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("show phase enable timer if it exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
phaseAction: "scale1p",
|
||||
connected: true,
|
||||
charging: true,
|
||||
phaseRemainingInterpolated: 91,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("show phase disable timer if it exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
phaseAction: "scale3p",
|
||||
connected: true,
|
||||
charging: true,
|
||||
phaseRemainingInterpolated: 91,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("show pv enable timer if not enabled yet and timer exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "enable",
|
||||
connected: true,
|
||||
pvRemainingInterpolated: 90,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.connected", pvtimer: "1:30\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("don't show pv enable timer if value is zero", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "enable",
|
||||
connected: true,
|
||||
pvRemainingInterpolated: 0,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.connected" }
|
||||
);
|
||||
});
|
||||
test("show pv disable timer if charging and timer exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "disable",
|
||||
connected: true,
|
||||
charging: true,
|
||||
pvRemainingInterpolated: 90,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", pvtimer: "1:30\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("show phase enable timer if it exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
phaseAction: "scale1p",
|
||||
connected: true,
|
||||
charging: true,
|
||||
phaseRemainingInterpolated: 91,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("show phase disable timer if it exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
phaseAction: "scale3p",
|
||||
connected: true,
|
||||
charging: true,
|
||||
phaseRemainingInterpolated: 91,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("vehicle target soc", () => {
|
||||
test("show target reached if charger enabled but soc has reached vehicle limit", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 70,
|
||||
vehicleSoc: 70,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.finished", limit: "70 %" }
|
||||
);
|
||||
});
|
||||
test("show reached message even if vehicle is slightly below its limit", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 70,
|
||||
vehicleSoc: 69,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.finished", limit: "70 %" }
|
||||
);
|
||||
});
|
||||
test("show target reached if charger enabled but soc has reached vehicle limit", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 70,
|
||||
vehicleSoc: 70,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.finished", limit: "70 %" }
|
||||
);
|
||||
});
|
||||
test("show reached message even if vehicle is slightly below its limit", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 70,
|
||||
vehicleSoc: 69,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.finished", limit: "70 %" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("smart grid charging", () => {
|
||||
test("show clean energy message", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffCo2: 600,
|
||||
smartCostLimit: 500,
|
||||
smartCostType: "co2",
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "≤ 500 g" }
|
||||
);
|
||||
});
|
||||
test("show clean energy message if active", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffCo2: 400,
|
||||
smartCostLimit: 500,
|
||||
smartCostType: "co2",
|
||||
smartCostActive: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "400 g ≤ 500 g" }
|
||||
);
|
||||
});
|
||||
test("show cheap energy message", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffGrid: 0.3,
|
||||
smartCostLimit: 0.29,
|
||||
currency: CURRENCY.EUR,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "≤ 29,0 ct" }
|
||||
);
|
||||
});
|
||||
test("show cheap energy message if active", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffGrid: 0.28,
|
||||
smartCostLimit: 0.29,
|
||||
currency: CURRENCY.EUR,
|
||||
smartCostActive: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "28,0 ct ≤ 29,0 ct" }
|
||||
);
|
||||
});
|
||||
test("show clean energy message", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffCo2: 600,
|
||||
smartCostLimit: 500,
|
||||
smartCostType: "co2",
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "≤ 500 g" }
|
||||
);
|
||||
});
|
||||
test("show clean energy message if active", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffCo2: 400,
|
||||
smartCostLimit: 500,
|
||||
smartCostType: "co2",
|
||||
smartCostActive: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "400 g ≤ 500 g" }
|
||||
);
|
||||
});
|
||||
test("show cheap energy message", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffGrid: 0.3,
|
||||
smartCostLimit: 0.29,
|
||||
currency: CURRENCY.EUR,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "≤ 29,0 ct" }
|
||||
);
|
||||
});
|
||||
test("show cheap energy message if active", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffGrid: 0.28,
|
||||
smartCostLimit: 0.29,
|
||||
currency: CURRENCY.EUR,
|
||||
smartCostActive: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "28,0 ct ≤ 29,0 ct" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,185 +3,185 @@ import Vehicle from "./Vehicle.vue";
|
|||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
const baseState = {
|
||||
vehicle: {
|
||||
title: "Mein Auto",
|
||||
icon: "car",
|
||||
capacity: 72,
|
||||
features: [],
|
||||
name: "",
|
||||
repeatingPlans: [],
|
||||
},
|
||||
enabled: false,
|
||||
connected: true,
|
||||
vehicleName: "meinauto",
|
||||
vehicleSoc: 42.742,
|
||||
vehicleRange: 231,
|
||||
limitSoc: 90,
|
||||
chargedEnergy: 14123,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
vehicle: {
|
||||
title: "Mein Auto",
|
||||
icon: "car",
|
||||
capacity: 72,
|
||||
features: [],
|
||||
name: "",
|
||||
repeatingPlans: [],
|
||||
},
|
||||
enabled: false,
|
||||
connected: true,
|
||||
vehicleName: "meinauto",
|
||||
vehicleSoc: 42.742,
|
||||
vehicleRange: 231,
|
||||
limitSoc: 90,
|
||||
chargedEnergy: 14123,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Vehicles/Vehicle",
|
||||
component: Vehicle,
|
||||
argTypes: {
|
||||
chargedEnergy: { control: "number" },
|
||||
charging: { control: "boolean" },
|
||||
vehicleClimaterActive: { control: "boolean" },
|
||||
vehicleWelcomeActive: { control: "boolean" },
|
||||
connected: { control: "boolean" },
|
||||
currency: { control: "text" },
|
||||
effectiveLimitSoc: { control: "number" },
|
||||
effectivePlanSoc: { control: "number" },
|
||||
effectivePlanTime: { control: "text" },
|
||||
batteryBoostActive: { control: "boolean" },
|
||||
enabled: { control: "boolean" },
|
||||
heating: { control: "boolean" },
|
||||
id: { control: "text" },
|
||||
integratedDevice: { control: "boolean" },
|
||||
limitEnergy: { control: "number" },
|
||||
mode: { control: "text" },
|
||||
chargerStatusReason: { control: "text" },
|
||||
vehicle: { control: "object" },
|
||||
vehicleName: { control: "text" },
|
||||
vehicleRange: { control: "number" },
|
||||
vehicleSoc: { control: "number" },
|
||||
vehicleLimitSoc: { control: "number" },
|
||||
socBasedCharging: { control: "boolean" },
|
||||
},
|
||||
title: "Vehicles/Vehicle",
|
||||
component: Vehicle,
|
||||
argTypes: {
|
||||
chargedEnergy: { control: "number" },
|
||||
charging: { control: "boolean" },
|
||||
vehicleClimaterActive: { control: "boolean" },
|
||||
vehicleWelcomeActive: { control: "boolean" },
|
||||
connected: { control: "boolean" },
|
||||
currency: { control: "text" },
|
||||
effectiveLimitSoc: { control: "number" },
|
||||
effectivePlanSoc: { control: "number" },
|
||||
effectivePlanTime: { control: "text" },
|
||||
batteryBoostActive: { control: "boolean" },
|
||||
enabled: { control: "boolean" },
|
||||
heating: { control: "boolean" },
|
||||
id: { control: "text" },
|
||||
integratedDevice: { control: "boolean" },
|
||||
limitEnergy: { control: "number" },
|
||||
mode: { control: "text" },
|
||||
chargerStatusReason: { control: "text" },
|
||||
vehicle: { control: "object" },
|
||||
vehicleName: { control: "text" },
|
||||
vehicleRange: { control: "number" },
|
||||
vehicleSoc: { control: "number" },
|
||||
vehicleLimitSoc: { control: "number" },
|
||||
socBasedCharging: { control: "boolean" },
|
||||
},
|
||||
} as Meta<typeof Vehicle>;
|
||||
|
||||
const Template: StoryFn<typeof Vehicle> = (args) => ({
|
||||
components: { Vehicle },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Vehicle v-bind="args" />',
|
||||
components: { Vehicle },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Vehicle v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Disconnected = Template.bind({});
|
||||
Disconnected.args = {
|
||||
...baseState,
|
||||
connected: false,
|
||||
...baseState,
|
||||
connected: false,
|
||||
};
|
||||
|
||||
export const Connected = Template.bind({});
|
||||
Connected.args = {
|
||||
...baseState,
|
||||
...baseState,
|
||||
};
|
||||
|
||||
export const Ready = Template.bind({});
|
||||
Ready.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export const Charging = Template.bind({});
|
||||
Charging.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
};
|
||||
|
||||
export const UnknownVehicle = Template.bind({});
|
||||
UnknownVehicle.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
vehicleName: "",
|
||||
socBasedCharging: false,
|
||||
vehicle: { ...baseState.vehicle, capacity: undefined },
|
||||
mode: CHARGE_MODE.PV,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
vehicleName: "",
|
||||
socBasedCharging: false,
|
||||
vehicle: { ...baseState.vehicle, capacity: undefined },
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const OfflineVehicle = Template.bind({});
|
||||
OfflineVehicle.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
socBasedCharging: false,
|
||||
vehicle: {
|
||||
...baseState.vehicle,
|
||||
title: "Opel Corsa-e",
|
||||
capacity: 72,
|
||||
features: ["Offline"],
|
||||
},
|
||||
mode: CHARGE_MODE.PV,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
socBasedCharging: false,
|
||||
vehicle: {
|
||||
...baseState.vehicle,
|
||||
title: "Opel Corsa-e",
|
||||
capacity: 72,
|
||||
features: ["Offline"],
|
||||
},
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const OfflineVehicleWithTarget = Template.bind({});
|
||||
OfflineVehicleWithTarget.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
socBasedCharging: false,
|
||||
vehicle: {
|
||||
...baseState.vehicle,
|
||||
title: "Opel Corsa-e",
|
||||
capacity: 72,
|
||||
features: ["Offline"],
|
||||
},
|
||||
mode: CHARGE_MODE.PV,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
socBasedCharging: false,
|
||||
vehicle: {
|
||||
...baseState.vehicle,
|
||||
title: "Opel Corsa-e",
|
||||
capacity: 72,
|
||||
features: ["Offline"],
|
||||
},
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const VehicleLimit = Template.bind({});
|
||||
VehicleLimit.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
vehicleLimitSoc: 80,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
vehicleLimitSoc: 80,
|
||||
};
|
||||
|
||||
export const VehicleLimitReached = Template.bind({});
|
||||
VehicleLimitReached.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 80,
|
||||
vehicleSoc: 80,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 80,
|
||||
vehicleSoc: 80,
|
||||
};
|
||||
|
||||
export const TargetChargePlanned = Template.bind({});
|
||||
TargetChargePlanned.args = {
|
||||
...baseState,
|
||||
mode: CHARGE_MODE.PV,
|
||||
...baseState,
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const TargetChargeActive = Template.bind({});
|
||||
TargetChargeActive.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
mode: CHARGE_MODE.PV,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const SmartChargeCostLimitActive = Template.bind({});
|
||||
SmartChargeCostLimitActive.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
smartCostLimit: 0.13,
|
||||
mode: CHARGE_MODE.PV,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
smartCostLimit: 0.13,
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const PvEnableTimer = Template.bind({});
|
||||
PvEnableTimer.args = {
|
||||
...baseState,
|
||||
pvAction: "enable",
|
||||
pvRemainingInterpolated: 32,
|
||||
...baseState,
|
||||
pvAction: "enable",
|
||||
pvRemainingInterpolated: 32,
|
||||
};
|
||||
|
||||
export const PvDisableTimer = Template.bind({});
|
||||
PvDisableTimer.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
pvAction: "disable",
|
||||
pvRemainingInterpolated: 155,
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
pvAction: "disable",
|
||||
pvRemainingInterpolated: 155,
|
||||
};
|
||||
|
||||
export const VehicleSwitch = Template.bind({});
|
||||
VehicleSwitch.args = {
|
||||
...baseState,
|
||||
vehicles: ["Blauer e-Golf", "Weißes Model 3"],
|
||||
...baseState,
|
||||
vehicles: ["Blauer e-Golf", "Weißes Model 3"],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ import type { App } from "vue";
|
|||
import settings from "./settings";
|
||||
|
||||
export function setHiddenFeatures(value: boolean) {
|
||||
settings.hiddenFeatures = value;
|
||||
settings.hiddenFeatures = value;
|
||||
}
|
||||
|
||||
export function getHiddenFeatures() {
|
||||
return settings.hiddenFeatures;
|
||||
return settings.hiddenFeatures;
|
||||
}
|
||||
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
app.config.globalProperties.$hiddenFeatures = getHiddenFeatures;
|
||||
},
|
||||
install: (app: App) => {
|
||||
app.config.globalProperties.$hiddenFeatures = getHiddenFeatures;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,36 +6,36 @@ import settings from "./settings";
|
|||
|
||||
// https://github.com/joker-x/languages.js/blob/master/languages.json
|
||||
export const LOCALES = {
|
||||
ar: ["Arabic", "العربية"],
|
||||
bg: ["Bulgarian", "Български"],
|
||||
ca: ["Catalan", "Català"],
|
||||
cs: ["Czech", "Česky"],
|
||||
da: ["Danish", "Dansk"],
|
||||
de: ["German", "Deutsch"],
|
||||
el: ["Greek", "Ελληνικά"],
|
||||
en: ["English", "English"],
|
||||
es: ["Spanish", "Español"],
|
||||
et: ["Estonian", "Eesti"],
|
||||
fi: ["Finnish", "Suomi"],
|
||||
fr: ["French", "Français"],
|
||||
hr: ["Croatian", "Hrvatski"],
|
||||
hu: ["Hungarian", "Magyar"],
|
||||
it: ["Italian", "Italiano"],
|
||||
lb: ["Luxembourgish", "Lëtzebuergesch"],
|
||||
lt: ["Lithuanian", "Lietuvių"],
|
||||
nl: ["Dutch", "Nederlands"],
|
||||
no: ["Norwegian", "Norsk"],
|
||||
pl: ["Polish", "Polski"],
|
||||
pt: ["Portuguese", "Português"],
|
||||
ro: ["Romanian", "Română"],
|
||||
ru: ["Russian", "Русский"],
|
||||
sk: ["Slovak", "Slovenčina"],
|
||||
sl: ["Slovenian", "Slovenščina"],
|
||||
sv: ["Swedish", "Svenska"],
|
||||
ta: ["Tamil", "தமிழ்"],
|
||||
tr: ["Turkish", "Türkçe"],
|
||||
uk: ["Ukrainian", "Українська"],
|
||||
"zh-Hans": ["Chinese (Simplified)", "简体中文"],
|
||||
ar: ["Arabic", "العربية"],
|
||||
bg: ["Bulgarian", "Български"],
|
||||
ca: ["Catalan", "Català"],
|
||||
cs: ["Czech", "Česky"],
|
||||
da: ["Danish", "Dansk"],
|
||||
de: ["German", "Deutsch"],
|
||||
el: ["Greek", "Ελληνικά"],
|
||||
en: ["English", "English"],
|
||||
es: ["Spanish", "Español"],
|
||||
et: ["Estonian", "Eesti"],
|
||||
fi: ["Finnish", "Suomi"],
|
||||
fr: ["French", "Français"],
|
||||
hr: ["Croatian", "Hrvatski"],
|
||||
hu: ["Hungarian", "Magyar"],
|
||||
it: ["Italian", "Italiano"],
|
||||
lb: ["Luxembourgish", "Lëtzebuergesch"],
|
||||
lt: ["Lithuanian", "Lietuvių"],
|
||||
nl: ["Dutch", "Nederlands"],
|
||||
no: ["Norwegian", "Norsk"],
|
||||
pl: ["Polish", "Polski"],
|
||||
pt: ["Portuguese", "Português"],
|
||||
ro: ["Romanian", "Română"],
|
||||
ru: ["Russian", "Русский"],
|
||||
sk: ["Slovak", "Slovenčina"],
|
||||
sl: ["Slovenian", "Slovenščina"],
|
||||
sv: ["Swedish", "Svenska"],
|
||||
ta: ["Tamil", "தமிழ்"],
|
||||
tr: ["Turkish", "Türkçe"],
|
||||
uk: ["Ukrainian", "Українська"],
|
||||
"zh-Hans": ["Chinese (Simplified)", "简体中文"],
|
||||
};
|
||||
|
||||
export const DEFAULT_LOCALE = "en";
|
||||
|
|
@ -43,71 +43,71 @@ const htmlLang = document.querySelector("html")?.getAttribute("lang");
|
|||
const DEFAULT_BROWSER_LOCALE = htmlLang?.length == 2 ? htmlLang : DEFAULT_LOCALE;
|
||||
|
||||
export function getLocalePreference() {
|
||||
return settings.locale;
|
||||
return settings.locale;
|
||||
}
|
||||
|
||||
export function removeLocalePreference(i18n: VueI18nInstance) {
|
||||
settings.locale = null;
|
||||
setI18nLanguage(i18n, DEFAULT_BROWSER_LOCALE as typeof i18n.locale);
|
||||
ensureCurrentLocaleMessages(i18n);
|
||||
settings.locale = null;
|
||||
setI18nLanguage(i18n, DEFAULT_BROWSER_LOCALE as typeof i18n.locale);
|
||||
ensureCurrentLocaleMessages(i18n);
|
||||
}
|
||||
|
||||
export function setLocalePreference(i18n: VueI18nInstance, locale: typeof i18n.locale) {
|
||||
if (!LOCALES.hasOwnProperty(locale)) {
|
||||
console.error("unknown locale", locale);
|
||||
return;
|
||||
}
|
||||
settings.locale = locale;
|
||||
setI18nLanguage(i18n, locale);
|
||||
ensureCurrentLocaleMessages(i18n);
|
||||
if (!(locale in LOCALES)) {
|
||||
console.error("unknown locale", locale);
|
||||
return;
|
||||
}
|
||||
settings.locale = locale;
|
||||
setI18nLanguage(i18n, locale);
|
||||
ensureCurrentLocaleMessages(i18n);
|
||||
}
|
||||
|
||||
function getLocale() {
|
||||
return getLocalePreference() || DEFAULT_BROWSER_LOCALE;
|
||||
return getLocalePreference() || DEFAULT_BROWSER_LOCALE;
|
||||
}
|
||||
|
||||
export default function setupI18n() {
|
||||
const i18n = createI18n({
|
||||
legacy: true,
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true,
|
||||
locale: DEFAULT_LOCALE,
|
||||
fallbackLocale: DEFAULT_LOCALE,
|
||||
messages: { "en-US": en },
|
||||
});
|
||||
setI18nLanguage(i18n.global, getLocale());
|
||||
return i18n;
|
||||
const i18n = createI18n({
|
||||
legacy: true,
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true,
|
||||
locale: DEFAULT_LOCALE,
|
||||
fallbackLocale: DEFAULT_LOCALE,
|
||||
messages: { "en-US": en },
|
||||
});
|
||||
setI18nLanguage(i18n.global, getLocale());
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export function setI18nLanguage(i18n: VueI18nInstance, locale: typeof i18n.locale) {
|
||||
i18n.locale = locale;
|
||||
document.querySelector("html")?.setAttribute("lang", locale);
|
||||
i18n.locale = locale;
|
||||
document.querySelector("html")?.setAttribute("lang", locale);
|
||||
}
|
||||
|
||||
async function loadLocaleMessages(i18n: VueI18nInstance, locale: typeof i18n.locale) {
|
||||
try {
|
||||
const response = await i18nApi.get(`${locale}.json`, {
|
||||
params: { v: window.evcc?.version },
|
||||
});
|
||||
if ("setLocaleMessage" in i18n) {
|
||||
i18n.setLocaleMessage(locale, response.data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`unable to load translation for [${locale}]`, e);
|
||||
}
|
||||
try {
|
||||
const response = await i18nApi.get(`${locale}.json`, {
|
||||
params: { v: window.evcc?.version },
|
||||
});
|
||||
if ("setLocaleMessage" in i18n) {
|
||||
i18n.setLocaleMessage(locale, response.data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`unable to load translation for [${locale}]`, e);
|
||||
}
|
||||
|
||||
return nextTick();
|
||||
return nextTick();
|
||||
}
|
||||
|
||||
export async function ensureCurrentLocaleMessages(i18n: VueI18nInstance) {
|
||||
const { locale } = i18n;
|
||||
if (!i18n.availableLocales.includes(locale)) {
|
||||
await loadLocaleMessages(i18n, locale);
|
||||
}
|
||||
const { locale } = i18n;
|
||||
if (!i18n.availableLocales.includes(locale)) {
|
||||
await loadLocaleMessages(i18n, locale);
|
||||
}
|
||||
}
|
||||
|
||||
export function docsPrefix() {
|
||||
const locale = getLocale();
|
||||
const path = locale === "de" ? "" : `/en`;
|
||||
return `https://docs.evcc.io${path}`;
|
||||
const locale = getLocale();
|
||||
const path = locale === "de" ? "" : `/en`;
|
||||
return `https://docs.evcc.io${path}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,44 @@
|
|||
export type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
|
||||
|
||||
interface BreakpointDef {
|
||||
name: Breakpoint;
|
||||
maxWidth: number;
|
||||
name: Breakpoint;
|
||||
maxWidth: number;
|
||||
}
|
||||
|
||||
const BREAKPOINTS: BreakpointDef[] = [
|
||||
{ name: "xs", maxWidth: 575 },
|
||||
{ name: "sm", maxWidth: 767 },
|
||||
{ name: "md", maxWidth: 991 },
|
||||
{ name: "lg", maxWidth: 1199 },
|
||||
{ name: "xl", maxWidth: 1399 },
|
||||
{ name: "xxl", maxWidth: Infinity },
|
||||
{ name: "xs", maxWidth: 575 },
|
||||
{ name: "sm", maxWidth: 767 },
|
||||
{ name: "md", maxWidth: 991 },
|
||||
{ name: "lg", maxWidth: 1199 },
|
||||
{ name: "xl", maxWidth: 1399 },
|
||||
{ name: "xxl", maxWidth: Infinity },
|
||||
];
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
breakpoint: "md" as Breakpoint,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateBreakpoint(): void {
|
||||
const width: number = window.innerWidth;
|
||||
const self = this as any;
|
||||
for (const bp of BREAKPOINTS) {
|
||||
if (width <= bp.maxWidth) {
|
||||
self.breakpoint = bp.name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const self = this as any;
|
||||
self.updateBreakpoint();
|
||||
window.addEventListener("resize", self.updateBreakpoint);
|
||||
},
|
||||
beforeDestroy() {
|
||||
const self = this as any;
|
||||
window.removeEventListener("resize", self.updateBreakpoint);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
breakpoint: "md" as Breakpoint,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateBreakpoint(): void {
|
||||
const width: number = window.innerWidth;
|
||||
const self = this as any;
|
||||
for (const bp of BREAKPOINTS) {
|
||||
if (width <= bp.maxWidth) {
|
||||
self.breakpoint = bp.name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const self = this as any;
|
||||
self.updateBreakpoint();
|
||||
window.addEventListener("resize", self.updateBreakpoint);
|
||||
},
|
||||
beforeDestroy() {
|
||||
const self = this as any;
|
||||
window.removeEventListener("resize", self.updateBreakpoint);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,22 +2,22 @@ import { defineComponent } from "vue";
|
|||
import type { State } from "@/types/evcc";
|
||||
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
// collect all target component properties from current instance
|
||||
collectProps(component: any, state?: State) {
|
||||
let data: Record<string, any> = {};
|
||||
for (const prop in component.props) {
|
||||
const p = prop as keyof State;
|
||||
// check in optional state
|
||||
if (state && p in state) {
|
||||
data[p] = state[p];
|
||||
}
|
||||
// check in current instance
|
||||
if (p in this) {
|
||||
data[p] = (this as Partial<State>)[p];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// collect all target component properties from current instance
|
||||
collectProps(component: any, state?: State) {
|
||||
const data: Record<string, any> = {};
|
||||
for (const prop in component.props) {
|
||||
const p = prop as keyof State;
|
||||
// check in optional state
|
||||
if (state && p in state) {
|
||||
data[p] = state[p];
|
||||
}
|
||||
// check in current instance
|
||||
if (p in this) {
|
||||
data[p] = (this as Partial<State>)[p];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,202 +11,204 @@ config.global.mocks["$i18n"] = { locale: "de-DE" };
|
|||
config.global.mocks["$t"] = (a: string) => a;
|
||||
|
||||
const fmt = mount(
|
||||
defineComponent({
|
||||
render() {},
|
||||
mixins: [formatter],
|
||||
})
|
||||
defineComponent({
|
||||
mixins: [formatter],
|
||||
render() {
|
||||
return null;
|
||||
},
|
||||
})
|
||||
).vm;
|
||||
|
||||
describe("fmtW", () => {
|
||||
test("should format with units", () => {
|
||||
expect(fmt.fmtW(0, POWER_UNIT.AUTO)).eq("0,0 kW");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.AUTO)).eq("1.200,0 kW");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.MW)).eq("0,0 MW");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.MW)).eq("1,2 MW");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.KW)).eq("0,0 kW");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.KW)).eq("1.200,0 kW");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.W)).eq("0 W");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.W)).eq("1.200.000 W");
|
||||
});
|
||||
test("should format without units", () => {
|
||||
expect(fmt.fmtW(0, POWER_UNIT.AUTO, false)).eq("0,0");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.AUTO, false)).eq("1.200,0");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.MW, false)).eq("0,0");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.MW, false)).eq("1,2");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.KW, false)).eq("0,0");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.KW, false)).eq("1.200,0");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.W, false)).eq("0");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.W, false)).eq("1.200.000");
|
||||
});
|
||||
test("should format a given number of digits", () => {
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.AUTO, true, 0)).eq("12 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.AUTO, true, 1)).eq("12,3 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.AUTO, true, 2)).eq("12,35 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.MW, true, 0)).eq("0 MW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.MW, true, 1)).eq("0,0 MW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.MW, true, 2)).eq("0,01 MW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.KW, true, 0)).eq("12 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.KW, true, 1)).eq("12,3 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.KW, true, 2)).eq("12,35 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.W, true, 0)).eq("12.345 W");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.W, true, 1)).eq("12.345,0 W");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.W, true, 2)).eq("12.345,00 W");
|
||||
});
|
||||
test("should format with units", () => {
|
||||
expect(fmt.fmtW(0, POWER_UNIT.AUTO)).eq("0,0 kW");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.AUTO)).eq("1.200,0 kW");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.MW)).eq("0,0 MW");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.MW)).eq("1,2 MW");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.KW)).eq("0,0 kW");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.KW)).eq("1.200,0 kW");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.W)).eq("0 W");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.W)).eq("1.200.000 W");
|
||||
});
|
||||
test("should format without units", () => {
|
||||
expect(fmt.fmtW(0, POWER_UNIT.AUTO, false)).eq("0,0");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.AUTO, false)).eq("1.200,0");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.MW, false)).eq("0,0");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.MW, false)).eq("1,2");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.KW, false)).eq("0,0");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.KW, false)).eq("1.200,0");
|
||||
expect(fmt.fmtW(0, POWER_UNIT.W, false)).eq("0");
|
||||
expect(fmt.fmtW(1200000, POWER_UNIT.W, false)).eq("1.200.000");
|
||||
});
|
||||
test("should format a given number of digits", () => {
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.AUTO, true, 0)).eq("12 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.AUTO, true, 1)).eq("12,3 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.AUTO, true, 2)).eq("12,35 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.MW, true, 0)).eq("0 MW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.MW, true, 1)).eq("0,0 MW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.MW, true, 2)).eq("0,01 MW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.KW, true, 0)).eq("12 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.KW, true, 1)).eq("12,3 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.KW, true, 2)).eq("12,35 kW");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.W, true, 0)).eq("12.345 W");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.W, true, 1)).eq("12.345,0 W");
|
||||
expect(fmt.fmtW(12345, POWER_UNIT.W, true, 2)).eq("12.345,00 W");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fmtWh", () => {
|
||||
test("should format with units", () => {
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.AUTO)).eq("0,0 kWh");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.AUTO)).eq("1.200,0 kWh");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.MW)).eq("0,0 MWh");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.MW)).eq("1,2 MWh");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.KW)).eq("0,0 kWh");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.KW)).eq("1.200,0 kWh");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.W)).eq("0 Wh");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.W)).eq("1.200.000 Wh");
|
||||
});
|
||||
test("should format without units", () => {
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.AUTO, false)).eq("0,0");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.AUTO, false)).eq("1.200,0");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.MW, false)).eq("0,0");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.MW, false)).eq("1,2");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.KW, false)).eq("0,0");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.KW, false)).eq("1.200,0");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.W, false)).eq("0");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.W, false)).eq("1.200.000");
|
||||
});
|
||||
test("should format a given number of digits", () => {
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.AUTO, true, 0)).eq("12 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.AUTO, true, 1)).eq("12,3 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.AUTO, true, 2)).eq("12,35 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.MW, true, 0)).eq("0 MWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.MW, true, 1)).eq("0,0 MWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.MW, true, 2)).eq("0,01 MWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.KW, true, 0)).eq("12 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.KW, true, 1)).eq("12,3 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.KW, true, 2)).eq("12,35 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.W, true, 0)).eq("12.345 Wh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.W, true, 1)).eq("12.345,0 Wh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.W, true, 2)).eq("12.345,00 Wh");
|
||||
});
|
||||
test("should format with units", () => {
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.AUTO)).eq("0,0 kWh");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.AUTO)).eq("1.200,0 kWh");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.MW)).eq("0,0 MWh");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.MW)).eq("1,2 MWh");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.KW)).eq("0,0 kWh");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.KW)).eq("1.200,0 kWh");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.W)).eq("0 Wh");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.W)).eq("1.200.000 Wh");
|
||||
});
|
||||
test("should format without units", () => {
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.AUTO, false)).eq("0,0");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.AUTO, false)).eq("1.200,0");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.MW, false)).eq("0,0");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.MW, false)).eq("1,2");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.KW, false)).eq("0,0");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.KW, false)).eq("1.200,0");
|
||||
expect(fmt.fmtWh(0, POWER_UNIT.W, false)).eq("0");
|
||||
expect(fmt.fmtWh(1200000, POWER_UNIT.W, false)).eq("1.200.000");
|
||||
});
|
||||
test("should format a given number of digits", () => {
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.AUTO, true, 0)).eq("12 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.AUTO, true, 1)).eq("12,3 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.AUTO, true, 2)).eq("12,35 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.MW, true, 0)).eq("0 MWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.MW, true, 1)).eq("0,0 MWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.MW, true, 2)).eq("0,01 MWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.KW, true, 0)).eq("12 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.KW, true, 1)).eq("12,3 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.KW, true, 2)).eq("12,35 kWh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.W, true, 0)).eq("12.345 Wh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.W, true, 1)).eq("12.345,0 Wh");
|
||||
expect(fmt.fmtWh(12345, POWER_UNIT.W, true, 2)).eq("12.345,00 Wh");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fmtPricePerKWh", () => {
|
||||
test("should format with units", () => {
|
||||
expect(fmt.fmtPricePerKWh(0.2, CURRENCY.EUR)).eq("20,0 ct/kWh");
|
||||
expect(fmt.fmtPricePerKWh(0.2, CURRENCY.EUR, true)).eq("20,0 ct");
|
||||
expect(fmt.fmtPricePerKWh(0.234, CURRENCY.USD)).eq("23,4 ¢/kWh");
|
||||
expect(fmt.fmtPricePerKWh(1234, CURRENCY.SEK)).eq("123.400,0 öre/kWh");
|
||||
expect(fmt.fmtPricePerKWh(0.2, CURRENCY.EUR, false, false)).eq("20,0");
|
||||
expect(fmt.fmtPricePerKWh(0.123, CURRENCY.CHF)).eq("12,3 rp/kWh");
|
||||
});
|
||||
test("should format with units", () => {
|
||||
expect(fmt.fmtPricePerKWh(0.2, CURRENCY.EUR)).eq("20,0 ct/kWh");
|
||||
expect(fmt.fmtPricePerKWh(0.2, CURRENCY.EUR, true)).eq("20,0 ct");
|
||||
expect(fmt.fmtPricePerKWh(0.234, CURRENCY.USD)).eq("23,4 ¢/kWh");
|
||||
expect(fmt.fmtPricePerKWh(1234, CURRENCY.SEK)).eq("123.400,0 öre/kWh");
|
||||
expect(fmt.fmtPricePerKWh(0.2, CURRENCY.EUR, false, false)).eq("20,0");
|
||||
expect(fmt.fmtPricePerKWh(0.123, CURRENCY.CHF)).eq("12,3 rp/kWh");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pricePerKWhUnit", () => {
|
||||
test("should return correct unit", () => {
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.EUR)).eq("ct/kWh");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.EUR, true)).eq("ct");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.USD)).eq("¢/kWh");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.SEK)).eq("öre/kWh");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.SEK, true)).eq("öre");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.CHF)).eq("rp/kWh");
|
||||
});
|
||||
test("should return correct unit", () => {
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.EUR)).eq("ct/kWh");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.EUR, true)).eq("ct");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.USD)).eq("¢/kWh");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.SEK)).eq("öre/kWh");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.SEK, true)).eq("öre");
|
||||
expect(fmt.pricePerKWhUnit(CURRENCY.CHF)).eq("rp/kWh");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fmtDuration", () => {
|
||||
test("should format zero duration", () => {
|
||||
expect(fmt.fmtDuration(0)).eq("—");
|
||||
expect(fmt.fmtDuration(-100)).eq("—");
|
||||
});
|
||||
test("should format seconds", () => {
|
||||
expect(fmt.fmtDuration(1)).eq("1\u202Fs");
|
||||
expect(fmt.fmtDuration(59)).eq("59\u202Fs");
|
||||
expect(fmt.fmtDuration(59, false)).eq("59");
|
||||
expect(fmt.fmtDuration(59, true, "m")).eq("0:59\u202Fm");
|
||||
expect(fmt.fmtDuration(59, true, "h")).eq("0:00\u202Fh");
|
||||
});
|
||||
test("should format minutes", () => {
|
||||
expect(fmt.fmtDuration(60)).eq("1:00\u202Fm");
|
||||
expect(fmt.fmtDuration(150)).eq("2:30\u202Fm");
|
||||
expect(fmt.fmtDuration(150, false)).eq("2:30");
|
||||
expect(fmt.fmtDuration(150, true, "h")).eq("0:02\u202Fh");
|
||||
});
|
||||
test("should format hours", () => {
|
||||
expect(fmt.fmtDuration(60 * 60)).eq("1:00\u202Fh");
|
||||
expect(fmt.fmtDuration(60 * 60 * 2.5)).eq("2:30\u202Fh");
|
||||
expect(fmt.fmtDuration(60 * 60 * 2.5, false)).eq("2:30");
|
||||
});
|
||||
test("should format internationalized", () => {
|
||||
config.global.mocks["$i18n"].locale = "ar-EG";
|
||||
expect(fmt.fmtDuration(30, false)).eq("٣٠");
|
||||
expect(fmt.fmtDuration(90, false)).eq("١:٣٠");
|
||||
expect(fmt.fmtDuration(60 * 100, false)).eq("١:٤٠");
|
||||
config.global.mocks["$i18n"].locale = "de-DE";
|
||||
});
|
||||
test("should format zero duration", () => {
|
||||
expect(fmt.fmtDuration(0)).eq("—");
|
||||
expect(fmt.fmtDuration(-100)).eq("—");
|
||||
});
|
||||
test("should format seconds", () => {
|
||||
expect(fmt.fmtDuration(1)).eq("1\u202Fs");
|
||||
expect(fmt.fmtDuration(59)).eq("59\u202Fs");
|
||||
expect(fmt.fmtDuration(59, false)).eq("59");
|
||||
expect(fmt.fmtDuration(59, true, "m")).eq("0:59\u202Fm");
|
||||
expect(fmt.fmtDuration(59, true, "h")).eq("0:00\u202Fh");
|
||||
});
|
||||
test("should format minutes", () => {
|
||||
expect(fmt.fmtDuration(60)).eq("1:00\u202Fm");
|
||||
expect(fmt.fmtDuration(150)).eq("2:30\u202Fm");
|
||||
expect(fmt.fmtDuration(150, false)).eq("2:30");
|
||||
expect(fmt.fmtDuration(150, true, "h")).eq("0:02\u202Fh");
|
||||
});
|
||||
test("should format hours", () => {
|
||||
expect(fmt.fmtDuration(60 * 60)).eq("1:00\u202Fh");
|
||||
expect(fmt.fmtDuration(60 * 60 * 2.5)).eq("2:30\u202Fh");
|
||||
expect(fmt.fmtDuration(60 * 60 * 2.5, false)).eq("2:30");
|
||||
});
|
||||
test("should format internationalized", () => {
|
||||
config.global.mocks["$i18n"].locale = "ar-EG";
|
||||
expect(fmt.fmtDuration(30, false)).eq("٣٠");
|
||||
expect(fmt.fmtDuration(90, false)).eq("١:٣٠");
|
||||
expect(fmt.fmtDuration(60 * 100, false)).eq("١:٤٠");
|
||||
config.global.mocks["$i18n"].locale = "de-DE";
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWeekdaysList", () => {
|
||||
test("should return the correct weekday-order", () => {
|
||||
expect(fmt.getWeekdaysList("long")).toEqual([
|
||||
{ name: "Montag", value: 1 },
|
||||
{ name: "Dienstag", value: 2 },
|
||||
{ name: "Mittwoch", value: 3 },
|
||||
{ name: "Donnerstag", value: 4 },
|
||||
{ name: "Freitag", value: 5 },
|
||||
{ name: "Samstag", value: 6 },
|
||||
{ name: "Sonntag", value: 0 },
|
||||
]);
|
||||
});
|
||||
test("should return the correct weekday-order", () => {
|
||||
expect(fmt.getWeekdaysList("long")).toEqual([
|
||||
{ name: "Montag", value: 1 },
|
||||
{ name: "Dienstag", value: 2 },
|
||||
{ name: "Mittwoch", value: 3 },
|
||||
{ name: "Donnerstag", value: 4 },
|
||||
{ name: "Freitag", value: 5 },
|
||||
{ name: "Samstag", value: 6 },
|
||||
{ name: "Sonntag", value: 0 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getShortenedWeekdaysLabel", () => {
|
||||
test("should format single days", () => {
|
||||
expect(fmt.getShortenedWeekdaysLabel([0])).eq("So");
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 2, 4, 6])).eq("Di, Do, Sa, So");
|
||||
expect(fmt.getShortenedWeekdaysLabel([6])).eq("Sa");
|
||||
expect(fmt.getShortenedWeekdaysLabel([3, 6])).eq("Mi, Sa");
|
||||
});
|
||||
test("should format ranges", () => {
|
||||
expect(fmt.getShortenedWeekdaysLabel([1, 2])).eq("Mo, Di");
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 1, 2, 3, 4, 5, 6])).eq("Mo – So");
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 1, 3, 4, 5])).eq("Mo, Mi – Fr, So");
|
||||
});
|
||||
test("should format single days and ranges", () => {
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 1, 3, 5, 6])).eq("Mo, Mi, Fr – So");
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 2, 3, 5, 6])).eq("Di, Mi, Fr – So");
|
||||
});
|
||||
test("should format single days", () => {
|
||||
expect(fmt.getShortenedWeekdaysLabel([0])).eq("So");
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 2, 4, 6])).eq("Di, Do, Sa, So");
|
||||
expect(fmt.getShortenedWeekdaysLabel([6])).eq("Sa");
|
||||
expect(fmt.getShortenedWeekdaysLabel([3, 6])).eq("Mi, Sa");
|
||||
});
|
||||
test("should format ranges", () => {
|
||||
expect(fmt.getShortenedWeekdaysLabel([1, 2])).eq("Mo, Di");
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 1, 2, 3, 4, 5, 6])).eq("Mo – So");
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 1, 3, 4, 5])).eq("Mo, Mi – Fr, So");
|
||||
});
|
||||
test("should format single days and ranges", () => {
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 1, 3, 5, 6])).eq("Mo, Mi, Fr – So");
|
||||
expect(fmt.getShortenedWeekdaysLabel([0, 2, 3, 5, 6])).eq("Di, Mi, Fr – So");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fmtPercentage", () => {
|
||||
test("should format percentage", () => {
|
||||
expect(fmt.fmtPercentage(0)).eq("0 %");
|
||||
expect(fmt.fmtPercentage(100)).eq("100 %");
|
||||
expect(fmt.fmtPercentage(100, 1)).eq("100,0 %");
|
||||
});
|
||||
test("should force signed", () => {
|
||||
expect(fmt.fmtPercentage(100, 0, true)).eq("+100 %");
|
||||
expect(fmt.fmtPercentage(100, 0, false)).eq("100 %");
|
||||
expect(fmt.fmtPercentage(-100, 0, true)).eq("-100 %");
|
||||
expect(fmt.fmtPercentage(-100, 0, false)).eq("-100 %");
|
||||
});
|
||||
test("should format percentage", () => {
|
||||
expect(fmt.fmtPercentage(0)).eq("0 %");
|
||||
expect(fmt.fmtPercentage(100)).eq("100 %");
|
||||
expect(fmt.fmtPercentage(100, 1)).eq("100,0 %");
|
||||
});
|
||||
test("should force signed", () => {
|
||||
expect(fmt.fmtPercentage(100, 0, true)).eq("+100 %");
|
||||
expect(fmt.fmtPercentage(100, 0, false)).eq("100 %");
|
||||
expect(fmt.fmtPercentage(-100, 0, true)).eq("-100 %");
|
||||
expect(fmt.fmtPercentage(-100, 0, false)).eq("-100 %");
|
||||
});
|
||||
});
|
||||
|
||||
const testDate = new Date(2023, 0, 15, 15, 30); // Jan 15, 2023, 15:30 (3:30 PM)
|
||||
|
||||
describe("12h/24h time format", () => {
|
||||
test("12h format", () => {
|
||||
is12hSpy.mockReturnValue(true);
|
||||
expect(fmt.fmtHourMinute(testDate)).toBe("3:30 PM");
|
||||
expect(fmt.fmtFullDateTime(testDate, false)).toBe("So., 15. Jan., 3:30 PM");
|
||||
expect(fmt.fmtWeekdayTime(testDate)).toBe("So. 3:30 PM");
|
||||
expect(fmt.fmtAbsoluteDate(testDate)).toBe("So 3:30 PM");
|
||||
});
|
||||
test("12h format", () => {
|
||||
is12hSpy.mockReturnValue(true);
|
||||
expect(fmt.fmtHourMinute(testDate)).toBe("3:30 PM");
|
||||
expect(fmt.fmtFullDateTime(testDate, false)).toBe("So., 15. Jan., 3:30 PM");
|
||||
expect(fmt.fmtWeekdayTime(testDate)).toBe("So. 3:30 PM");
|
||||
expect(fmt.fmtAbsoluteDate(testDate)).toBe("So 3:30 PM");
|
||||
});
|
||||
|
||||
test("24h format", () => {
|
||||
is12hSpy.mockReturnValue(false);
|
||||
expect(fmt.fmtHourMinute(testDate)).toBe("15:30");
|
||||
expect(fmt.fmtFullDateTime(testDate, false)).toBe("So., 15. Jan., 15:30");
|
||||
expect(fmt.fmtWeekdayTime(testDate)).toBe("So., 15:30");
|
||||
expect(fmt.fmtAbsoluteDate(testDate)).toBe("So 15:30");
|
||||
});
|
||||
test("24h format", () => {
|
||||
is12hSpy.mockReturnValue(false);
|
||||
expect(fmt.fmtHourMinute(testDate)).toBe("15:30");
|
||||
expect(fmt.fmtFullDateTime(testDate, false)).toBe("So., 15. Jan., 15:30");
|
||||
expect(fmt.fmtWeekdayTime(testDate)).toBe("So., 15:30");
|
||||
expect(fmt.fmtAbsoluteDate(testDate)).toBe("So 15:30");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,442 +3,440 @@ import { is12hFormat } from "@/units";
|
|||
import { CURRENCY } from "../types/evcc";
|
||||
|
||||
const CURRENCY_SYMBOLS: Record<CURRENCY, string> = {
|
||||
AUD: "$",
|
||||
BGN: "лв",
|
||||
BRL: "R$",
|
||||
CAD: "$",
|
||||
CHF: "Fr.",
|
||||
CNY: "¥",
|
||||
EUR: "€",
|
||||
GBP: "£",
|
||||
ILS: "₪",
|
||||
NZD: "$",
|
||||
PLN: "zł",
|
||||
USD: "$",
|
||||
DKK: "kr",
|
||||
SEK: "kr",
|
||||
AUD: "$",
|
||||
BGN: "лв",
|
||||
BRL: "R$",
|
||||
CAD: "$",
|
||||
CHF: "Fr.",
|
||||
CNY: "¥",
|
||||
EUR: "€",
|
||||
GBP: "£",
|
||||
ILS: "₪",
|
||||
NZD: "$",
|
||||
PLN: "zł",
|
||||
USD: "$",
|
||||
DKK: "kr",
|
||||
SEK: "kr",
|
||||
};
|
||||
|
||||
// list of currencies where energy price should be displayed in subunits (factor 100)
|
||||
const ENERGY_PRICE_IN_SUBUNIT: Record<CURRENCY, string> = {
|
||||
AUD: "c", // Australian cent
|
||||
BGN: "st", // Bulgarian stotinka
|
||||
BRL: "¢", // Brazilian centavo
|
||||
CAD: "¢", // Canadian cent
|
||||
CHF: "rp", // Swiss Rappen
|
||||
CNY: "f", // Chinese fen
|
||||
EUR: "ct", // Euro cent
|
||||
GBP: "p", // GB pence
|
||||
ILS: "ag", // Israeli agora
|
||||
NZD: "c", // New Zealand cent
|
||||
PLN: "gr", // Polish grosz
|
||||
USD: "¢", // US cent
|
||||
DKK: "øre", // Danish øre
|
||||
SEK: "öre", // Swedish öre
|
||||
AUD: "c", // Australian cent
|
||||
BGN: "st", // Bulgarian stotinka
|
||||
BRL: "¢", // Brazilian centavo
|
||||
CAD: "¢", // Canadian cent
|
||||
CHF: "rp", // Swiss Rappen
|
||||
CNY: "f", // Chinese fen
|
||||
EUR: "ct", // Euro cent
|
||||
GBP: "p", // GB pence
|
||||
ILS: "ag", // Israeli agora
|
||||
NZD: "c", // New Zealand cent
|
||||
PLN: "gr", // Polish grosz
|
||||
USD: "¢", // US cent
|
||||
DKK: "øre", // Danish øre
|
||||
SEK: "öre", // Swedish öre
|
||||
};
|
||||
|
||||
export enum POWER_UNIT {
|
||||
W = "W",
|
||||
KW = "kW",
|
||||
MW = "MW",
|
||||
AUTO = "",
|
||||
W = "W",
|
||||
KW = "kW",
|
||||
MW = "MW",
|
||||
AUTO = "",
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
POWER_UNIT,
|
||||
fmtLimit: 100,
|
||||
fmtDigits: 1,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
round(num: number, precision: number) {
|
||||
const base = 10 ** precision;
|
||||
return (Math.round(num * base) / base).toFixed(precision);
|
||||
},
|
||||
fmtW(watt = 0, format = POWER_UNIT.KW, withUnit = true, digits?: number) {
|
||||
let unit = format;
|
||||
let d = digits;
|
||||
if (POWER_UNIT.AUTO === unit) {
|
||||
if (watt >= 10_000_000) {
|
||||
unit = POWER_UNIT.MW;
|
||||
} else if (watt >= 1000 || 0 === watt) {
|
||||
unit = POWER_UNIT.KW;
|
||||
} else {
|
||||
unit = POWER_UNIT.W;
|
||||
}
|
||||
}
|
||||
let value = watt;
|
||||
if (POWER_UNIT.KW === unit) {
|
||||
value = watt / 1000;
|
||||
} else if (POWER_UNIT.MW === unit) {
|
||||
value = watt / 1_000_000;
|
||||
}
|
||||
if (d === undefined) {
|
||||
d =
|
||||
POWER_UNIT.KW === unit ||
|
||||
POWER_UNIT.MW === unit ||
|
||||
(POWER_UNIT.W !== unit && 0 === watt)
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
return `${new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "decimal",
|
||||
minimumFractionDigits: d,
|
||||
maximumFractionDigits: d,
|
||||
}).format(value)}${withUnit ? ` ${unit}` : ""}`;
|
||||
},
|
||||
fmtWh(watt: number, format = POWER_UNIT.KW, withUnit = true, digits?: number) {
|
||||
return this.fmtW(watt, format, withUnit, digits) + (withUnit ? "h" : "");
|
||||
},
|
||||
fmtNumber(number: number, decimals: number, unit?: string) {
|
||||
const style = unit ? "unit" : "decimal";
|
||||
return new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style,
|
||||
unit,
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(number);
|
||||
},
|
||||
fmtGrams(gramms: number, withUnit = true) {
|
||||
let unit = "gram";
|
||||
let value = gramms;
|
||||
if (gramms >= 1000) {
|
||||
unit = "kilogram";
|
||||
value = gramms / 1000;
|
||||
}
|
||||
return new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: withUnit ? "unit" : "decimal",
|
||||
unit,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
},
|
||||
fmtCo2Short(gramms = 0) {
|
||||
return `${this.fmtNumber(gramms, 0)} g`;
|
||||
},
|
||||
fmtCo2Medium(gramms = 0) {
|
||||
return `${this.fmtNumber(gramms, 0)} g/kWh`;
|
||||
},
|
||||
fmtCo2Long(gramms = 0) {
|
||||
return `${this.fmtNumber(gramms, 0)} gCO₂e/kWh`;
|
||||
},
|
||||
fmtNumberToLocale(val: number, pad = 0) {
|
||||
return val.toLocaleString(this.$i18n?.locale).padStart(pad, "0");
|
||||
},
|
||||
fmtDurationToTime(date: Date) {
|
||||
const diff = date.getTime() - new Date().getTime();
|
||||
return this.fmtDuration(diff / 1000, true, "h");
|
||||
},
|
||||
fmtDurationNs(duration = 0, withUnit = true, minUnit = "s") {
|
||||
return this.fmtDuration(duration / 1e9, withUnit, minUnit);
|
||||
},
|
||||
fmtDuration(duration = 0, withUnit = true, minUnit = "s") {
|
||||
if (duration <= 0) {
|
||||
return "—";
|
||||
}
|
||||
const roundedDuration = Math.round(duration);
|
||||
const seconds = roundedDuration % 60;
|
||||
const minutes = Math.floor(roundedDuration / 60) % 60;
|
||||
const hours = Math.floor(roundedDuration / 3600);
|
||||
let result = "";
|
||||
let unit = "";
|
||||
if (hours >= 1 || minUnit === "h") {
|
||||
result = `${this.fmtNumberToLocale(hours)}:${this.fmtNumberToLocale(minutes, 2)}`;
|
||||
unit = "h";
|
||||
} else if (minutes >= 1 || minUnit === "m") {
|
||||
result = `${this.fmtNumberToLocale(minutes)}:${this.fmtNumberToLocale(seconds, 2)}`;
|
||||
unit = "m";
|
||||
} else {
|
||||
result = `${this.fmtNumberToLocale(seconds)}`;
|
||||
unit = "s";
|
||||
}
|
||||
if (withUnit) {
|
||||
result += `\u202F${unit}`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
fmtDurationLong(seconds: number) {
|
||||
// @ts-expect-error - Intl.DurationFormat is a new API not yet in TS types, see https://github.com/microsoft/TypeScript/issues/60608
|
||||
if (!Intl.DurationFormat) {
|
||||
// old browser fallback
|
||||
return this.fmtDuration(seconds);
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
data() {
|
||||
return {
|
||||
POWER_UNIT,
|
||||
fmtLimit: 100,
|
||||
fmtDigits: 1,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
round(num: number, precision: number) {
|
||||
const base = 10 ** precision;
|
||||
return (Math.round(num * base) / base).toFixed(precision);
|
||||
},
|
||||
fmtW(watt = 0, format = POWER_UNIT.KW, withUnit = true, digits?: number) {
|
||||
let unit = format;
|
||||
let d = digits;
|
||||
if (POWER_UNIT.AUTO === unit) {
|
||||
if (watt >= 10_000_000) {
|
||||
unit = POWER_UNIT.MW;
|
||||
} else if (watt >= 1000 || 0 === watt) {
|
||||
unit = POWER_UNIT.KW;
|
||||
} else {
|
||||
unit = POWER_UNIT.W;
|
||||
}
|
||||
}
|
||||
let value = watt;
|
||||
if (POWER_UNIT.KW === unit) {
|
||||
value = watt / 1000;
|
||||
} else if (POWER_UNIT.MW === unit) {
|
||||
value = watt / 1_000_000;
|
||||
}
|
||||
if (d === undefined) {
|
||||
d =
|
||||
POWER_UNIT.KW === unit || POWER_UNIT.MW === unit || (POWER_UNIT.W !== unit && 0 === watt)
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
return `${new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "decimal",
|
||||
minimumFractionDigits: d,
|
||||
maximumFractionDigits: d,
|
||||
}).format(value)}${withUnit ? ` ${unit}` : ""}`;
|
||||
},
|
||||
fmtWh(watt: number, format = POWER_UNIT.KW, withUnit = true, digits?: number) {
|
||||
return this.fmtW(watt, format, withUnit, digits) + (withUnit ? "h" : "");
|
||||
},
|
||||
fmtNumber(number: number, decimals: number, unit?: string) {
|
||||
const style = unit ? "unit" : "decimal";
|
||||
return new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style,
|
||||
unit,
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(number);
|
||||
},
|
||||
fmtGrams(gramms: number, withUnit = true) {
|
||||
let unit = "gram";
|
||||
let value = gramms;
|
||||
if (gramms >= 1000) {
|
||||
unit = "kilogram";
|
||||
value = gramms / 1000;
|
||||
}
|
||||
return new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: withUnit ? "unit" : "decimal",
|
||||
unit,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
},
|
||||
fmtCo2Short(gramms = 0) {
|
||||
return `${this.fmtNumber(gramms, 0)} g`;
|
||||
},
|
||||
fmtCo2Medium(gramms = 0) {
|
||||
return `${this.fmtNumber(gramms, 0)} g/kWh`;
|
||||
},
|
||||
fmtCo2Long(gramms = 0) {
|
||||
return `${this.fmtNumber(gramms, 0)} gCO₂e/kWh`;
|
||||
},
|
||||
fmtNumberToLocale(val: number, pad = 0) {
|
||||
return val.toLocaleString(this.$i18n?.locale).padStart(pad, "0");
|
||||
},
|
||||
fmtDurationToTime(date: Date) {
|
||||
const diff = date.getTime() - new Date().getTime();
|
||||
return this.fmtDuration(diff / 1000, true, "h");
|
||||
},
|
||||
fmtDurationNs(duration = 0, withUnit = true, minUnit = "s") {
|
||||
return this.fmtDuration(duration / 1e9, withUnit, minUnit);
|
||||
},
|
||||
fmtDuration(duration = 0, withUnit = true, minUnit = "s") {
|
||||
if (duration <= 0) {
|
||||
return "—";
|
||||
}
|
||||
const roundedDuration = Math.round(duration);
|
||||
const seconds = roundedDuration % 60;
|
||||
const minutes = Math.floor(roundedDuration / 60) % 60;
|
||||
const hours = Math.floor(roundedDuration / 3600);
|
||||
let result = "";
|
||||
let unit = "";
|
||||
if (hours >= 1 || minUnit === "h") {
|
||||
result = `${this.fmtNumberToLocale(hours)}:${this.fmtNumberToLocale(minutes, 2)}`;
|
||||
unit = "h";
|
||||
} else if (minutes >= 1 || minUnit === "m") {
|
||||
result = `${this.fmtNumberToLocale(minutes)}:${this.fmtNumberToLocale(seconds, 2)}`;
|
||||
unit = "m";
|
||||
} else {
|
||||
result = `${this.fmtNumberToLocale(seconds)}`;
|
||||
unit = "s";
|
||||
}
|
||||
if (withUnit) {
|
||||
result += `\u202F${unit}`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
fmtDurationLong(seconds: number) {
|
||||
// @ts-expect-error - Intl.DurationFormat is a new API not yet in TS types, see https://github.com/microsoft/TypeScript/issues/60608
|
||||
if (!Intl.DurationFormat) {
|
||||
// old browser fallback
|
||||
return this.fmtDuration(seconds);
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
// @ts-expect-error - Intl.DurationFormat is a new API not yet in TS types, see https://github.com/microsoft/TypeScript/issues/60608
|
||||
const formatter = new Intl.DurationFormat(this.$i18n?.locale, { style: "long" });
|
||||
return formatter.format({ minutes, hours });
|
||||
},
|
||||
fmtDayString(date: Date) {
|
||||
const YY = `${date.getFullYear()}`;
|
||||
const MM = `${date.getMonth() + 1}`.padStart(2, "0");
|
||||
const DD = `${date.getDate()}`.padStart(2, "0");
|
||||
return `${YY}-${MM}-${DD}`;
|
||||
},
|
||||
fmtTimeString(date: Date) {
|
||||
const HH = `${date.getHours()}`.padStart(2, "0");
|
||||
const mm = `${date.getMinutes()}`.padStart(2, "0");
|
||||
return `${HH}:${mm}`;
|
||||
},
|
||||
isToday(date: Date) {
|
||||
const today = new Date();
|
||||
return today.toDateString() === date.toDateString();
|
||||
},
|
||||
isTomorrow(date: Date) {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
return tomorrow.toDateString() === date.toDateString();
|
||||
},
|
||||
weekdayPrefix(date: Date) {
|
||||
if (this.isToday(date)) {
|
||||
return "";
|
||||
}
|
||||
if (this.isTomorrow(date)) {
|
||||
try {
|
||||
const rtf = new Intl.RelativeTimeFormat(this.$i18n?.locale, {
|
||||
numeric: "auto",
|
||||
});
|
||||
return rtf.formatToParts(1, "day")[0].value;
|
||||
} catch (e) {
|
||||
console.warn("weekdayPrefix: Intl.RelativeTimeFormat not supported", e);
|
||||
return "tomorrow";
|
||||
}
|
||||
}
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: "short",
|
||||
}).format(date);
|
||||
},
|
||||
hourShort(date: Date) {
|
||||
const locale = this.$i18n?.locale;
|
||||
// special: use shorter german format
|
||||
if (locale === "de") return date.getHours();
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
hour: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
},
|
||||
weekdayShort(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: "short",
|
||||
}).format(date);
|
||||
},
|
||||
fmtAbsoluteDate(date: Date) {
|
||||
const weekday = this.weekdayPrefix(date);
|
||||
const hour = new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
// @ts-expect-error - Intl.DurationFormat is a new API not yet in TS types, see https://github.com/microsoft/TypeScript/issues/60608
|
||||
const formatter = new Intl.DurationFormat(this.$i18n?.locale, { style: "long" });
|
||||
return formatter.format({ minutes, hours });
|
||||
},
|
||||
fmtDayString(date: Date) {
|
||||
const YY = `${date.getFullYear()}`;
|
||||
const MM = `${date.getMonth() + 1}`.padStart(2, "0");
|
||||
const DD = `${date.getDate()}`.padStart(2, "0");
|
||||
return `${YY}-${MM}-${DD}`;
|
||||
},
|
||||
fmtTimeString(date: Date) {
|
||||
const HH = `${date.getHours()}`.padStart(2, "0");
|
||||
const mm = `${date.getMinutes()}`.padStart(2, "0");
|
||||
return `${HH}:${mm}`;
|
||||
},
|
||||
isToday(date: Date) {
|
||||
const today = new Date();
|
||||
return today.toDateString() === date.toDateString();
|
||||
},
|
||||
isTomorrow(date: Date) {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
return tomorrow.toDateString() === date.toDateString();
|
||||
},
|
||||
weekdayPrefix(date: Date) {
|
||||
if (this.isToday(date)) {
|
||||
return "";
|
||||
}
|
||||
if (this.isTomorrow(date)) {
|
||||
try {
|
||||
const rtf = new Intl.RelativeTimeFormat(this.$i18n?.locale, {
|
||||
numeric: "auto",
|
||||
});
|
||||
return rtf.formatToParts(1, "day")[0].value;
|
||||
} catch (e) {
|
||||
console.warn("weekdayPrefix: Intl.RelativeTimeFormat not supported", e);
|
||||
return "tomorrow";
|
||||
}
|
||||
}
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: "short",
|
||||
}).format(date);
|
||||
},
|
||||
hourShort(date: Date) {
|
||||
const locale = this.$i18n?.locale;
|
||||
// special: use shorter german format
|
||||
if (locale === "de") return date.getHours();
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
hour: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
},
|
||||
weekdayShort(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: "short",
|
||||
}).format(date);
|
||||
},
|
||||
fmtAbsoluteDate(date: Date) {
|
||||
const weekday = this.weekdayPrefix(date);
|
||||
const hour = new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
|
||||
return `${weekday} ${hour}`.trim();
|
||||
},
|
||||
fmtHourMinute(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
},
|
||||
fmtFullDateTime(date: Date, short: boolean) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: short ? undefined : "short",
|
||||
month: short ? "numeric" : "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
},
|
||||
fmtWeekdayTime(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: "short",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
},
|
||||
fmtMonthYear(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(date);
|
||||
},
|
||||
fmtMonth(date: Date, short: boolean) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
month: short ? "short" : "long",
|
||||
}).format(date);
|
||||
},
|
||||
fmtDayMonth(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: "short",
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
}).format(date);
|
||||
},
|
||||
fmtDurationUnit(value: number, unit = "second") {
|
||||
return new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "unit",
|
||||
unit,
|
||||
unitDisplay: "long",
|
||||
})
|
||||
.formatToParts(value)
|
||||
.find((part) => part.type === "unit")?.value;
|
||||
},
|
||||
fmtMoney(amout = 0, currency = CURRENCY.EUR, decimals = true, withSymbol = false) {
|
||||
const currencyDisplay = withSymbol ? "narrowSymbol" : "code";
|
||||
const digits = decimals ? undefined : 0;
|
||||
const result = new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "currency",
|
||||
currency,
|
||||
currencyDisplay,
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}).format(amout);
|
||||
return `${weekday} ${hour}`.trim();
|
||||
},
|
||||
fmtHourMinute(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
},
|
||||
fmtFullDateTime(date: Date, short: boolean) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: short ? undefined : "short",
|
||||
month: short ? "numeric" : "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
},
|
||||
fmtWeekdayTime(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: "short",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: is12hFormat(),
|
||||
}).format(date);
|
||||
},
|
||||
fmtMonthYear(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(date);
|
||||
},
|
||||
fmtMonth(date: Date, short: boolean) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
month: short ? "short" : "long",
|
||||
}).format(date);
|
||||
},
|
||||
fmtDayMonth(date: Date) {
|
||||
return new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: "short",
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
}).format(date);
|
||||
},
|
||||
fmtDurationUnit(value: number, unit = "second") {
|
||||
return new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "unit",
|
||||
unit,
|
||||
unitDisplay: "long",
|
||||
})
|
||||
.formatToParts(value)
|
||||
.find((part) => part.type === "unit")?.value;
|
||||
},
|
||||
fmtMoney(amout = 0, currency = CURRENCY.EUR, decimals = true, withSymbol = false) {
|
||||
const currencyDisplay = withSymbol ? "narrowSymbol" : "code";
|
||||
const digits = decimals ? undefined : 0;
|
||||
const result = new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "currency",
|
||||
currency,
|
||||
currencyDisplay,
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}).format(amout);
|
||||
|
||||
return withSymbol ? result : result.replace(currency, "").trim();
|
||||
},
|
||||
fmtCurrencySymbol(currency = CURRENCY.EUR) {
|
||||
return CURRENCY_SYMBOLS[currency] || currency;
|
||||
},
|
||||
fmtPricePerKWh(amout = 0, currency = CURRENCY.EUR, short = false, withUnit = true) {
|
||||
let value = amout;
|
||||
let minimumFractionDigits = 1;
|
||||
let maximumFractionDigits = 3;
|
||||
if (ENERGY_PRICE_IN_SUBUNIT[currency]) {
|
||||
value *= 100;
|
||||
minimumFractionDigits = 1;
|
||||
maximumFractionDigits = 1;
|
||||
}
|
||||
const price = new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "decimal",
|
||||
minimumFractionDigits,
|
||||
maximumFractionDigits,
|
||||
}).format(value);
|
||||
if (withUnit) {
|
||||
return `${price} ${this.pricePerKWhUnit(currency, short)}`;
|
||||
}
|
||||
return price;
|
||||
},
|
||||
timezone() {
|
||||
return Intl?.DateTimeFormat?.().resolvedOptions?.().timeZone || "UTC";
|
||||
},
|
||||
pricePerKWhUnit(currency = CURRENCY.EUR, short = false) {
|
||||
const unit = ENERGY_PRICE_IN_SUBUNIT[currency] || currency;
|
||||
return `${unit}${short ? "" : "/kWh"}`;
|
||||
},
|
||||
fmtTimeAgo(elapsed: number) {
|
||||
const units = {
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
minute: 60 * 1000,
|
||||
second: 1000,
|
||||
};
|
||||
return withSymbol ? result : result.replace(currency, "").trim();
|
||||
},
|
||||
fmtCurrencySymbol(currency = CURRENCY.EUR) {
|
||||
return CURRENCY_SYMBOLS[currency] || currency;
|
||||
},
|
||||
fmtPricePerKWh(amout = 0, currency = CURRENCY.EUR, short = false, withUnit = true) {
|
||||
let value = amout;
|
||||
let minimumFractionDigits = 1;
|
||||
let maximumFractionDigits = 3;
|
||||
if (ENERGY_PRICE_IN_SUBUNIT[currency]) {
|
||||
value *= 100;
|
||||
minimumFractionDigits = 1;
|
||||
maximumFractionDigits = 1;
|
||||
}
|
||||
const price = new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "decimal",
|
||||
minimumFractionDigits,
|
||||
maximumFractionDigits,
|
||||
}).format(value);
|
||||
if (withUnit) {
|
||||
return `${price} ${this.pricePerKWhUnit(currency, short)}`;
|
||||
}
|
||||
return price;
|
||||
},
|
||||
timezone() {
|
||||
return Intl?.DateTimeFormat?.().resolvedOptions?.().timeZone || "UTC";
|
||||
},
|
||||
pricePerKWhUnit(currency = CURRENCY.EUR, short = false) {
|
||||
const unit = ENERGY_PRICE_IN_SUBUNIT[currency] || currency;
|
||||
return `${unit}${short ? "" : "/kWh"}`;
|
||||
},
|
||||
fmtTimeAgo(elapsed: number) {
|
||||
const units = {
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
minute: 60 * 1000,
|
||||
second: 1000,
|
||||
};
|
||||
|
||||
// "Math.abs" accounts for both "past" & "future" scenarios
|
||||
for (const u in units) {
|
||||
const unitKey = u as keyof typeof units;
|
||||
if (Math.abs(elapsed) > units[unitKey] || u == "second") {
|
||||
try {
|
||||
const rtf = new Intl.RelativeTimeFormat(this.$i18n?.locale, {
|
||||
numeric: "auto",
|
||||
});
|
||||
return rtf.format(Math.round(elapsed / units[unitKey]), unitKey);
|
||||
} catch (e) {
|
||||
console.warn("fmtTimeAgo: Intl.RelativeTimeFormat not supported", e);
|
||||
return `${elapsed} ${u}s ago`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// "Math.abs" accounts for both "past" & "future" scenarios
|
||||
for (const u in units) {
|
||||
const unitKey = u as keyof typeof units;
|
||||
if (Math.abs(elapsed) > units[unitKey] || u == "second") {
|
||||
try {
|
||||
const rtf = new Intl.RelativeTimeFormat(this.$i18n?.locale, {
|
||||
numeric: "auto",
|
||||
});
|
||||
return rtf.format(Math.round(elapsed / units[unitKey]), unitKey);
|
||||
} catch (e) {
|
||||
console.warn("fmtTimeAgo: Intl.RelativeTimeFormat not supported", e);
|
||||
return `${elapsed} ${u}s ago`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
fmtSocOption(soc: number, rangePerSoc?: number, distanceUnit?: string, heating?: boolean) {
|
||||
let result = heating ? this.fmtTemperature(soc) : `${this.fmtPercentage(soc)}`;
|
||||
if (rangePerSoc && distanceUnit) {
|
||||
const range = soc * rangePerSoc;
|
||||
result += ` (${this.fmtNumber(range, 0)} ${distanceUnit})`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
fmtPercentage(value: number, digits = 0, forceSign = false) {
|
||||
const sign = forceSign && value > 0 ? "+" : "";
|
||||
return `${sign}${new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "percent",
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}).format(value / 100)}`;
|
||||
},
|
||||
hasLeadingPercentageSign() {
|
||||
return ["tr", "ar"].includes(this.$i18n?.locale);
|
||||
},
|
||||
fmtTemperature(value: number) {
|
||||
// TODO: handle fahrenheit
|
||||
return this.fmtNumber(value, 1, "celsius");
|
||||
},
|
||||
getWeekdaysList(
|
||||
weekdayFormat: Intl.DateTimeFormatOptions["weekday"]
|
||||
): { name: string; value: number }[] {
|
||||
const { format } = new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: weekdayFormat,
|
||||
});
|
||||
const mondayToSaturday = [7, 8, 9, 10, 11, 12].map((day, index) => {
|
||||
return { name: format(new Date(Date.UTC(2021, 5, day))), value: index + 1 };
|
||||
});
|
||||
const sunday = { name: format(new Date(Date.UTC(2021, 5, 6))), value: 0 };
|
||||
return [...mondayToSaturday, sunday];
|
||||
},
|
||||
getShortenedWeekdaysLabel(selectedWeekdays: number[]): string {
|
||||
if (0 === selectedWeekdays.length) {
|
||||
return "–";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
fmtSocOption(soc: number, rangePerSoc?: number, distanceUnit?: string, heating?: boolean) {
|
||||
let result = heating ? this.fmtTemperature(soc) : `${this.fmtPercentage(soc)}`;
|
||||
if (rangePerSoc && distanceUnit) {
|
||||
const range = soc * rangePerSoc;
|
||||
result += ` (${this.fmtNumber(range, 0)} ${distanceUnit})`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
fmtPercentage(value: number, digits = 0, forceSign = false) {
|
||||
const sign = forceSign && value > 0 ? "+" : "";
|
||||
return `${sign}${new Intl.NumberFormat(this.$i18n?.locale, {
|
||||
style: "percent",
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}).format(value / 100)}`;
|
||||
},
|
||||
hasLeadingPercentageSign() {
|
||||
return ["tr", "ar"].includes(this.$i18n?.locale);
|
||||
},
|
||||
fmtTemperature(value: number) {
|
||||
// TODO: handle fahrenheit
|
||||
return this.fmtNumber(value, 1, "celsius");
|
||||
},
|
||||
getWeekdaysList(
|
||||
weekdayFormat: Intl.DateTimeFormatOptions["weekday"]
|
||||
): { name: string; value: number }[] {
|
||||
const { format } = new Intl.DateTimeFormat(this.$i18n?.locale, {
|
||||
weekday: weekdayFormat,
|
||||
});
|
||||
const mondayToSaturday = [7, 8, 9, 10, 11, 12].map((day, index) => {
|
||||
return { name: format(new Date(Date.UTC(2021, 5, day))), value: index + 1 };
|
||||
});
|
||||
const sunday = { name: format(new Date(Date.UTC(2021, 5, 6))), value: 0 };
|
||||
return [...mondayToSaturday, sunday];
|
||||
},
|
||||
getShortenedWeekdaysLabel(selectedWeekdays: number[]): string {
|
||||
if (0 === selectedWeekdays.length) {
|
||||
return "–";
|
||||
}
|
||||
|
||||
const weekdays = this.getWeekdaysList("short");
|
||||
let label = "";
|
||||
const weekdays = this.getWeekdaysList("short");
|
||||
let label = "";
|
||||
|
||||
// the week in the input-parameter starts with 0 for sunday and ends with 6 for saturday
|
||||
// this algorithms works only if the week starts with 1 for monday and ends with 7 for sunday because
|
||||
// then we are able to count from 1 to 7 by incrementing the number
|
||||
// so we have to transform the input accordingly
|
||||
const selectedWeekdaysTransformed = selectedWeekdays.map(function (dayIndex) {
|
||||
return 0 === dayIndex ? 7 : dayIndex;
|
||||
});
|
||||
function getWeekdayName(dayIndex: number) {
|
||||
return weekdays.find((day) => day.value === (7 === dayIndex ? 0 : dayIndex))?.name;
|
||||
}
|
||||
// the week in the input-parameter starts with 0 for sunday and ends with 6 for saturday
|
||||
// this algorithms works only if the week starts with 1 for monday and ends with 7 for sunday because
|
||||
// then we are able to count from 1 to 7 by incrementing the number
|
||||
// so we have to transform the input accordingly
|
||||
const selectedWeekdaysTransformed = selectedWeekdays.map(function (dayIndex) {
|
||||
return 0 === dayIndex ? 7 : dayIndex;
|
||||
});
|
||||
function getWeekdayName(dayIndex: number) {
|
||||
return weekdays.find((day) => day.value === (7 === dayIndex ? 0 : dayIndex))?.name;
|
||||
}
|
||||
|
||||
const maxWeekday = Math.max(...selectedWeekdaysTransformed);
|
||||
const maxWeekday = Math.max(...selectedWeekdaysTransformed);
|
||||
|
||||
for (let weekdayRangeStart = 1; weekdayRangeStart < 8; weekdayRangeStart++) {
|
||||
if (selectedWeekdaysTransformed.includes(weekdayRangeStart)) {
|
||||
label += getWeekdayName(weekdayRangeStart);
|
||||
for (let weekdayRangeStart = 1; weekdayRangeStart < 8; weekdayRangeStart++) {
|
||||
if (selectedWeekdaysTransformed.includes(weekdayRangeStart)) {
|
||||
label += getWeekdayName(weekdayRangeStart);
|
||||
|
||||
let weekdayRangeEnd = weekdayRangeStart;
|
||||
while (selectedWeekdaysTransformed.includes(weekdayRangeEnd + 1)) {
|
||||
weekdayRangeEnd++;
|
||||
}
|
||||
let weekdayRangeEnd = weekdayRangeStart;
|
||||
while (selectedWeekdaysTransformed.includes(weekdayRangeEnd + 1)) {
|
||||
weekdayRangeEnd++;
|
||||
}
|
||||
|
||||
if (weekdayRangeEnd - weekdayRangeStart > 1) {
|
||||
// more than 2 consecutive weekdays selected
|
||||
label += " – " + getWeekdayName(weekdayRangeEnd);
|
||||
weekdayRangeStart = weekdayRangeEnd;
|
||||
if (maxWeekday !== weekdayRangeEnd) {
|
||||
label += ", ";
|
||||
}
|
||||
} else if (weekdayRangeStart !== weekdayRangeEnd) {
|
||||
// exactly 2 consecutive weekdays selected
|
||||
label += ", ";
|
||||
} else {
|
||||
// exactly 1 single day selected
|
||||
if (maxWeekday !== weekdayRangeEnd) {
|
||||
label += ", ";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return label;
|
||||
},
|
||||
},
|
||||
if (weekdayRangeEnd - weekdayRangeStart > 1) {
|
||||
// more than 2 consecutive weekdays selected
|
||||
label += " – " + getWeekdayName(weekdayRangeEnd);
|
||||
weekdayRangeStart = weekdayRangeEnd;
|
||||
if (maxWeekday !== weekdayRangeEnd) {
|
||||
label += ", ";
|
||||
}
|
||||
} else if (weekdayRangeStart !== weekdayRangeEnd) {
|
||||
// exactly 2 consecutive weekdays selected
|
||||
label += ", ";
|
||||
} else {
|
||||
// exactly 1 single day selected
|
||||
if (maxWeekday !== weekdayRangeEnd) {
|
||||
label += ", ";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return label;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,26 +2,26 @@ import { ICON_SIZE } from "@/types/evcc";
|
|||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
size: {
|
||||
type: String as PropType<ICON_SIZE>,
|
||||
validator(value: ICON_SIZE) {
|
||||
return Object.values(ICON_SIZE).includes(value);
|
||||
},
|
||||
default: ICON_SIZE.S,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
svgStyle() {
|
||||
const sizes: Record<ICON_SIZE, string> = {
|
||||
xs: "16px",
|
||||
s: "24px",
|
||||
m: "32px",
|
||||
l: "48px",
|
||||
xl: "64px",
|
||||
};
|
||||
const size = sizes[this.size];
|
||||
return { display: "block", width: size, height: size };
|
||||
},
|
||||
},
|
||||
props: {
|
||||
size: {
|
||||
type: String as PropType<ICON_SIZE>,
|
||||
validator(value: ICON_SIZE) {
|
||||
return Object.values(ICON_SIZE).includes(value);
|
||||
},
|
||||
default: ICON_SIZE.S,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
svgStyle() {
|
||||
const sizes: Record<ICON_SIZE, string> = {
|
||||
xs: "16px",
|
||||
s: "24px",
|
||||
m: "32px",
|
||||
l: "48px",
|
||||
xl: "64px",
|
||||
};
|
||||
const size = sizes[this.size];
|
||||
return { display: "block", width: size, height: size };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,22 +2,22 @@ import { reactive } from "vue";
|
|||
import api from "./api";
|
||||
|
||||
const restart = reactive({
|
||||
restartNeeded: false,
|
||||
restarting: false,
|
||||
restartNeeded: false,
|
||||
restarting: false,
|
||||
});
|
||||
|
||||
export async function performRestart() {
|
||||
try {
|
||||
await api.post("/system/shutdown");
|
||||
restart.restarting = true;
|
||||
} catch (e) {
|
||||
alert(`Unable to restart server. ${e}`);
|
||||
}
|
||||
try {
|
||||
await api.post("/system/shutdown");
|
||||
restart.restarting = true;
|
||||
} catch (e) {
|
||||
alert(`Unable to restart server. ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function restartComplete() {
|
||||
restart.restarting = false;
|
||||
restart.restartNeeded = false;
|
||||
restart.restarting = false;
|
||||
restart.restartNeeded = false;
|
||||
}
|
||||
|
||||
export default restart;
|
||||
|
|
|
|||
|
|
@ -1,102 +1,102 @@
|
|||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
type RouteLocationNormalizedGeneric,
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
type RouteLocationNormalizedGeneric,
|
||||
} from "vue-router";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import { ensureCurrentLocaleMessages } from "./i18n.ts";
|
||||
import {
|
||||
openLoginModal,
|
||||
statusUnknown,
|
||||
updateAuthStatus,
|
||||
isLoggedIn,
|
||||
isConfigured,
|
||||
openLoginModal,
|
||||
statusUnknown,
|
||||
updateAuthStatus,
|
||||
isLoggedIn,
|
||||
isConfigured,
|
||||
} from "./components/Auth/auth";
|
||||
import type { VueI18nInstance } from "vue-i18n";
|
||||
|
||||
function hideAllModals() {
|
||||
[...document.querySelectorAll(".modal.show")].forEach((modal) => {
|
||||
// skip unclosable modals
|
||||
if (modal.getAttribute("data-bs-backdrop") === "static") return;
|
||||
[...document.querySelectorAll(".modal.show")].forEach((modal) => {
|
||||
// skip unclosable modals
|
||||
if (modal.getAttribute("data-bs-backdrop") === "static") return;
|
||||
|
||||
const modalInstance = Modal.getInstance(modal);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
});
|
||||
const modalInstance = Modal.getInstance(modal);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureAuth(to: RouteLocationNormalizedGeneric) {
|
||||
await updateAuthStatus();
|
||||
if (!isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
if (!isLoggedIn() && !statusUnknown()) {
|
||||
openLoginModal(to.path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
await updateAuthStatus();
|
||||
if (!isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
if (!isLoggedIn() && !statusUnknown()) {
|
||||
openLoginModal(to.path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function setupRouter(i18n: VueI18nInstance) {
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("./views/Main.vue"),
|
||||
props: (route) => {
|
||||
const { lp } = route.query;
|
||||
return {
|
||||
selectedLoadpointIndex: lp ? parseInt(lp as string, 10) - 1 : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/config",
|
||||
component: () => import("./views/Config.vue"),
|
||||
beforeEnter: ensureAuth,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/sessions",
|
||||
component: () => import("./views/Sessions.vue"),
|
||||
props: (route) => {
|
||||
const { month, year, loadpoint, vehicle, period } = route.query;
|
||||
return {
|
||||
month: month ? parseInt(month as string, 10) : undefined,
|
||||
year: year ? parseInt(year as string, 10) : undefined,
|
||||
period,
|
||||
loadpointFilter: loadpoint,
|
||||
vehicleFilter: vehicle,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/energy",
|
||||
component: () => import("./views/Energy.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/log",
|
||||
component: () => import("./views/Log.vue"),
|
||||
beforeEnter: ensureAuth,
|
||||
props: (route) => {
|
||||
const { areas, level } = route.query;
|
||||
return {
|
||||
areas: typeof areas === "string" ? areas.split(",") : undefined,
|
||||
level: typeof level === "string" ? level : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
router.beforeEach(async () => {
|
||||
await ensureCurrentLocaleMessages(i18n);
|
||||
return true;
|
||||
});
|
||||
router.afterEach(() => {
|
||||
hideAllModals();
|
||||
});
|
||||
return router;
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("./views/Main.vue"),
|
||||
props: (route) => {
|
||||
const { lp } = route.query;
|
||||
return {
|
||||
selectedLoadpointIndex: lp ? parseInt(lp as string, 10) - 1 : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/config",
|
||||
component: () => import("./views/Config.vue"),
|
||||
beforeEnter: ensureAuth,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/sessions",
|
||||
component: () => import("./views/Sessions.vue"),
|
||||
props: (route) => {
|
||||
const { month, year, loadpoint, vehicle, period } = route.query;
|
||||
return {
|
||||
month: month ? parseInt(month as string, 10) : undefined,
|
||||
year: year ? parseInt(year as string, 10) : undefined,
|
||||
period,
|
||||
loadpointFilter: loadpoint,
|
||||
vehicleFilter: vehicle,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/energy",
|
||||
component: () => import("./views/Energy.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/log",
|
||||
component: () => import("./views/Log.vue"),
|
||||
beforeEnter: ensureAuth,
|
||||
props: (route) => {
|
||||
const { areas, level } = route.query;
|
||||
return {
|
||||
areas: typeof areas === "string" ? areas.split(",") : undefined,
|
||||
level: typeof level === "string" ? level : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
router.beforeEach(async () => {
|
||||
await ensureCurrentLocaleMessages(i18n);
|
||||
return true;
|
||||
});
|
||||
router.afterEach(() => {
|
||||
hideAllModals();
|
||||
});
|
||||
return router;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,68 +2,68 @@ import { reactive } from "vue";
|
|||
import type { State } from "./types/evcc";
|
||||
|
||||
function setProperty(obj: object, props: string[], value: any) {
|
||||
const prop = props.shift();
|
||||
// @ts-expect-error no-explicit-any
|
||||
if (!obj[prop]) {
|
||||
// @ts-expect-error no-explicit-any
|
||||
obj[prop] = {};
|
||||
}
|
||||
const prop = props.shift();
|
||||
// @ts-expect-error no-explicit-any
|
||||
if (!obj[prop]) {
|
||||
// @ts-expect-error no-explicit-any
|
||||
obj[prop] = {};
|
||||
}
|
||||
|
||||
if (!props.length) {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
// @ts-expect-error no-explicit-any
|
||||
obj[prop] = { ...obj[prop], ...value };
|
||||
} else {
|
||||
// @ts-expect-error no-explicit-any
|
||||
obj[prop] = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!props.length) {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
// @ts-expect-error no-explicit-any
|
||||
obj[prop] = { ...obj[prop], ...value };
|
||||
} else {
|
||||
// @ts-expect-error no-explicit-any
|
||||
obj[prop] = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error no-explicit-any
|
||||
setProperty(obj[prop], props, value);
|
||||
// @ts-expect-error no-explicit-any
|
||||
setProperty(obj[prop], props, value);
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
offline: false,
|
||||
// ensure array type
|
||||
loadpoints: [],
|
||||
vehicles: [],
|
||||
offline: false,
|
||||
// ensure array type
|
||||
loadpoints: [],
|
||||
vehicles: [],
|
||||
};
|
||||
|
||||
const state = reactive(initialState);
|
||||
|
||||
const store = {
|
||||
state,
|
||||
offline(value: boolean) {
|
||||
state.offline = value;
|
||||
},
|
||||
// @ts-expect-error no-explicit-any
|
||||
update(msg) {
|
||||
Object.keys(msg).forEach(function (k) {
|
||||
if (k === "log") {
|
||||
window.app.raise(msg[k]);
|
||||
} else {
|
||||
setProperty(state, k.split("."), msg[k]);
|
||||
}
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
console.log("resetting state");
|
||||
// reset to initial state
|
||||
Object.keys(initialState).forEach(function (k) {
|
||||
if (k === "offline") return;
|
||||
state,
|
||||
offline(value: boolean) {
|
||||
state.offline = value;
|
||||
},
|
||||
// @ts-expect-error no-explicit-any
|
||||
update(msg) {
|
||||
Object.keys(msg).forEach(function (k) {
|
||||
if (k === "log") {
|
||||
window.app.raise(msg[k]);
|
||||
} else {
|
||||
setProperty(state, k.split("."), msg[k]);
|
||||
}
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
console.log("resetting state");
|
||||
// reset to initial state
|
||||
Object.keys(initialState).forEach(function (k) {
|
||||
if (k === "offline") return;
|
||||
|
||||
// @ts-expect-error no-explicit-any
|
||||
if (Array.isArray(initialState[k])) {
|
||||
// @ts-expect-error no-explicit-any
|
||||
state[k] = [];
|
||||
} else {
|
||||
// @ts-expect-error no-explicit-any
|
||||
state[k] = undefined;
|
||||
}
|
||||
});
|
||||
},
|
||||
// @ts-expect-error no-explicit-any
|
||||
if (Array.isArray(initialState[k])) {
|
||||
// @ts-expect-error no-explicit-any
|
||||
state[k] = [];
|
||||
} else {
|
||||
// @ts-expect-error no-explicit-any
|
||||
state[k] = undefined;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default store;
|
||||
|
|
|
|||
|
|
@ -4,68 +4,68 @@ import { THEME } from "./types/evcc";
|
|||
const darkModeMatcher = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
export function getThemePreference(): THEME {
|
||||
const theme = settings.theme;
|
||||
if (Object.values(THEME).includes(theme)) {
|
||||
return theme;
|
||||
}
|
||||
return THEME.AUTO;
|
||||
const theme = settings.theme;
|
||||
if (Object.values(THEME).includes(theme)) {
|
||||
return theme;
|
||||
}
|
||||
return THEME.AUTO;
|
||||
}
|
||||
|
||||
export function setThemePreference(theme: THEME) {
|
||||
if (!Object.values(THEME).includes(theme)) {
|
||||
return;
|
||||
}
|
||||
settings.theme = theme;
|
||||
updateTheme();
|
||||
if (!Object.values(THEME).includes(theme)) {
|
||||
return;
|
||||
}
|
||||
settings.theme = theme;
|
||||
updateTheme();
|
||||
}
|
||||
|
||||
function setMetaThemeColor(theme: Exclude<THEME, THEME.AUTO>) {
|
||||
const themeColors = { light: "#f3f3f7", dark: "#020318" };
|
||||
const $metaThemeColor = document.querySelector("meta[name=theme-color]");
|
||||
if ($metaThemeColor) {
|
||||
$metaThemeColor.setAttribute("content", themeColors[theme]);
|
||||
}
|
||||
const themeColors = { light: "#f3f3f7", dark: "#020318" };
|
||||
const $metaThemeColor = document.querySelector("meta[name=theme-color]");
|
||||
if ($metaThemeColor) {
|
||||
$metaThemeColor.setAttribute("content", themeColors[theme]);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentTheme() {
|
||||
let theme = getThemePreference();
|
||||
if (theme === THEME.AUTO) {
|
||||
theme = darkModeMatcher?.matches ? THEME.DARK : THEME.LIGHT;
|
||||
}
|
||||
return theme;
|
||||
let theme = getThemePreference();
|
||||
if (theme === THEME.AUTO) {
|
||||
theme = darkModeMatcher?.matches ? THEME.DARK : THEME.LIGHT;
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
function updateTheme() {
|
||||
const theme = getCurrentTheme();
|
||||
const theme = getCurrentTheme();
|
||||
|
||||
// update iOS title bar color
|
||||
setMetaThemeColor(theme);
|
||||
// update iOS title bar color
|
||||
setMetaThemeColor(theme);
|
||||
|
||||
// toggle the class on html root
|
||||
const $html = document.querySelector("html");
|
||||
if ($html) {
|
||||
$html.classList.add("no-transitions");
|
||||
$html.classList.toggle("dark", theme === THEME.DARK);
|
||||
window.setTimeout(function () {
|
||||
$html.classList.remove("no-transitions");
|
||||
}, 100);
|
||||
}
|
||||
// toggle the class on html root
|
||||
const $html = document.querySelector("html");
|
||||
if ($html) {
|
||||
$html.classList.add("no-transitions");
|
||||
$html.classList.toggle("dark", theme === THEME.DARK);
|
||||
window.setTimeout(function () {
|
||||
$html.classList.remove("no-transitions");
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMetaThemeForBackdrop() {
|
||||
const $backdrop = document.querySelector("[data-bs-backdrop=true][aria-modal=true]");
|
||||
// dark if there is a backdrop, otherwise use the current theme
|
||||
const theme = $backdrop ? THEME.DARK : getCurrentTheme();
|
||||
setMetaThemeColor(theme);
|
||||
const $backdrop = document.querySelector("[data-bs-backdrop=true][aria-modal=true]");
|
||||
// dark if there is a backdrop, otherwise use the current theme
|
||||
const theme = $backdrop ? THEME.DARK : getCurrentTheme();
|
||||
setMetaThemeColor(theme);
|
||||
}
|
||||
|
||||
export function watchThemeChanges() {
|
||||
if (darkModeMatcher && darkModeMatcher.addEventListener) {
|
||||
darkModeMatcher.addEventListener("change", updateTheme);
|
||||
}
|
||||
updateTheme();
|
||||
if (darkModeMatcher && darkModeMatcher.addEventListener) {
|
||||
darkModeMatcher.addEventListener("change", updateTheme);
|
||||
}
|
||||
updateTheme();
|
||||
|
||||
// listen for modal backdrops
|
||||
document.addEventListener("shown.bs.modal", updateMetaThemeForBackdrop);
|
||||
document.addEventListener("hidden.bs.modal", updateMetaThemeForBackdrop);
|
||||
// listen for modal backdrops
|
||||
document.addEventListener("shown.bs.modal", updateMetaThemeForBackdrop);
|
||||
document.addEventListener("hidden.bs.modal", updateMetaThemeForBackdrop);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,170 +2,170 @@ import type { StaticPlan, RepeatingPlan } from "../components/ChargingPlans/type
|
|||
import type { ForecastSlot, SolarDetails } from "../components/Forecast/types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
app: any;
|
||||
evcc: {
|
||||
version: string;
|
||||
commit: string;
|
||||
customCss: string;
|
||||
};
|
||||
}
|
||||
interface Window {
|
||||
app: any;
|
||||
evcc: {
|
||||
version: string;
|
||||
commit: string;
|
||||
customCss: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface Auth {
|
||||
vehicles: VehicleLogins;
|
||||
vehicles: VehicleLogins;
|
||||
}
|
||||
|
||||
export type VehicleLogins = Record<string, { authenticated: boolean; uri: string }>;
|
||||
|
||||
export interface FatalError {
|
||||
error: any;
|
||||
class?: any;
|
||||
error: any;
|
||||
class?: any;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
offline: boolean;
|
||||
startup?: boolean;
|
||||
loadpoints: [];
|
||||
forecast?: Forecast;
|
||||
currency?: CURRENCY;
|
||||
fatal?: FatalError;
|
||||
auth?: Auth;
|
||||
vehicles: Vehicle[];
|
||||
offline: boolean;
|
||||
startup?: boolean;
|
||||
loadpoints: [];
|
||||
forecast?: Forecast;
|
||||
currency?: CURRENCY;
|
||||
fatal?: FatalError;
|
||||
auth?: Auth;
|
||||
vehicles: Vehicle[];
|
||||
}
|
||||
|
||||
export interface LoadpointCompact {
|
||||
icon: string;
|
||||
title: string;
|
||||
charging: boolean;
|
||||
soc?: number;
|
||||
power: number;
|
||||
heating?: boolean;
|
||||
chargePower: number;
|
||||
connected: boolean;
|
||||
index: number;
|
||||
vehicleName: string;
|
||||
chargerIcon?: string;
|
||||
vehicleSoc: number;
|
||||
chargerFeatureHeating: boolean;
|
||||
icon: string;
|
||||
title: string;
|
||||
charging: boolean;
|
||||
soc?: number;
|
||||
power: number;
|
||||
heating?: boolean;
|
||||
chargePower: number;
|
||||
connected: boolean;
|
||||
index: number;
|
||||
vehicleName: string;
|
||||
chargerIcon?: string;
|
||||
vehicleSoc: number;
|
||||
chargerFeatureHeating: boolean;
|
||||
}
|
||||
|
||||
export enum THEME {
|
||||
AUTO = "auto",
|
||||
LIGHT = "light",
|
||||
DARK = "dark",
|
||||
AUTO = "auto",
|
||||
LIGHT = "light",
|
||||
DARK = "dark",
|
||||
}
|
||||
|
||||
export enum CURRENCY {
|
||||
AUD = "AUD",
|
||||
BGN = "BGN",
|
||||
BRL = "BRL",
|
||||
CAD = "CAD",
|
||||
CHF = "CHF",
|
||||
CNY = "CNY",
|
||||
EUR = "EUR",
|
||||
GBP = "GBP",
|
||||
ILS = "ILS",
|
||||
NZD = "NZD",
|
||||
PLN = "PLN",
|
||||
USD = "USD",
|
||||
DKK = "DKK",
|
||||
SEK = "SEK",
|
||||
AUD = "AUD",
|
||||
BGN = "BGN",
|
||||
BRL = "BRL",
|
||||
CAD = "CAD",
|
||||
CHF = "CHF",
|
||||
CNY = "CNY",
|
||||
EUR = "EUR",
|
||||
GBP = "GBP",
|
||||
ILS = "ILS",
|
||||
NZD = "NZD",
|
||||
PLN = "PLN",
|
||||
USD = "USD",
|
||||
DKK = "DKK",
|
||||
SEK = "SEK",
|
||||
}
|
||||
|
||||
export enum ICON_SIZE {
|
||||
XS = "xs",
|
||||
S = "s",
|
||||
M = "m",
|
||||
L = "l",
|
||||
XL = "xl",
|
||||
XS = "xs",
|
||||
S = "s",
|
||||
M = "m",
|
||||
L = "l",
|
||||
XL = "xl",
|
||||
}
|
||||
|
||||
export enum CHARGE_MODE {
|
||||
OFF = "off",
|
||||
NOW = "now",
|
||||
MINPV = "minpv",
|
||||
PV = "pv",
|
||||
OFF = "off",
|
||||
NOW = "now",
|
||||
MINPV = "minpv",
|
||||
PV = "pv",
|
||||
}
|
||||
|
||||
export enum PHASES {
|
||||
AUTO = 0,
|
||||
ONE_PHASE = 1,
|
||||
TWO_PHASES = 2,
|
||||
THREE_PHASES = 3,
|
||||
AUTO = 0,
|
||||
ONE_PHASE = 1,
|
||||
TWO_PHASES = 2,
|
||||
THREE_PHASES = 3,
|
||||
}
|
||||
|
||||
export interface Sponsor {
|
||||
name: string;
|
||||
expiresAt: string;
|
||||
expiresSoon: boolean;
|
||||
name: string;
|
||||
expiresAt: string;
|
||||
expiresSoon: boolean;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
message: string;
|
||||
time: Date;
|
||||
level: string;
|
||||
lp: number;
|
||||
count: number;
|
||||
message: string;
|
||||
time: Date;
|
||||
level: string;
|
||||
lp: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface Battery {
|
||||
power: number;
|
||||
soc: number;
|
||||
controllable: boolean;
|
||||
capacity: number; // 0 when not specified
|
||||
title?: string;
|
||||
power: number;
|
||||
soc: number;
|
||||
controllable: boolean;
|
||||
capacity: number; // 0 when not specified
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface Vehicle {
|
||||
name: string;
|
||||
minSoc?: number;
|
||||
limitSoc?: number;
|
||||
plan?: StaticPlan;
|
||||
repeatingPlans: RepeatingPlan[];
|
||||
title: string;
|
||||
features?: string[];
|
||||
capacity?: number;
|
||||
icon?: string;
|
||||
name: string;
|
||||
minSoc?: number;
|
||||
limitSoc?: number;
|
||||
plan?: StaticPlan;
|
||||
repeatingPlans: RepeatingPlan[];
|
||||
title: string;
|
||||
features?: string[];
|
||||
capacity?: number;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export type Timeout = ReturnType<typeof setInterval> | null;
|
||||
|
||||
export interface Tariff {
|
||||
rates: Rate[];
|
||||
lastUpdate: Date;
|
||||
rates: Rate[];
|
||||
lastUpdate: Date;
|
||||
}
|
||||
|
||||
export interface Rate {
|
||||
start: Date;
|
||||
end: Date;
|
||||
value: number;
|
||||
start: Date;
|
||||
end: Date;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface Slot {
|
||||
day: string;
|
||||
value?: number;
|
||||
startHour: number;
|
||||
endHour: number;
|
||||
charging: boolean;
|
||||
toLate?: boolean | null;
|
||||
warning?: boolean | null;
|
||||
isTarget?: boolean | null;
|
||||
selectable?: boolean | null;
|
||||
day: string;
|
||||
value?: number;
|
||||
startHour: number;
|
||||
endHour: number;
|
||||
charging: boolean;
|
||||
toLate?: boolean | null;
|
||||
warning?: boolean | null;
|
||||
isTarget?: boolean | null;
|
||||
selectable?: boolean | null;
|
||||
}
|
||||
|
||||
export interface Forecast {
|
||||
grid?: ForecastSlot[];
|
||||
co2?: ForecastSlot[];
|
||||
solar?: SolarDetails;
|
||||
planner?: ForecastSlot[];
|
||||
grid?: ForecastSlot[];
|
||||
co2?: ForecastSlot[];
|
||||
solar?: SolarDetails;
|
||||
planner?: ForecastSlot[];
|
||||
}
|
||||
|
||||
export interface SelectOption<T> {
|
||||
name: string;
|
||||
value: T;
|
||||
count?: number;
|
||||
disabled?: boolean;
|
||||
name: string;
|
||||
value: T;
|
||||
count?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export type DeviceType = "charger" | "meter" | "vehicle";
|
||||
|
|
|
|||
14
assets/js/types/vue.d.ts
vendored
14
assets/js/types/vue.d.ts
vendored
|
|
@ -2,11 +2,11 @@
|
|||
import type { ComponentCustomProperties } from "vue";
|
||||
|
||||
declare module "vue" {
|
||||
interface ComponentCustomProperties {
|
||||
/**
|
||||
* Whether experimental UI features should be shown.
|
||||
*/
|
||||
$hiddenFeatures: () => boolean;
|
||||
$refs: { [key: string]: HTMLElement | undefined };
|
||||
}
|
||||
interface ComponentCustomProperties {
|
||||
/**
|
||||
* Whether experimental UI features should be shown.
|
||||
*/
|
||||
$hiddenFeatures: () => boolean;
|
||||
$refs: { [key: string]: HTMLElement | undefined };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,30 +2,30 @@ import { describe, expect, test } from "vitest";
|
|||
import { cleanYaml } from "./cleanYaml";
|
||||
|
||||
describe("cleanYaml", () => {
|
||||
test("removes key from single line", () => {
|
||||
const result = cleanYaml("key: value", "key");
|
||||
expect(result).toBe("value");
|
||||
});
|
||||
test("removes key from multi-line yaml", () => {
|
||||
const input = `key:\n nested: value\n another: thing`;
|
||||
const expected = `nested: value\nanother: thing`;
|
||||
const result = cleanYaml(input, "key");
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
test("keep untouched if key not found", () => {
|
||||
const result = cleanYaml("key: value", "not-found");
|
||||
expect(result).toBe("key: value");
|
||||
});
|
||||
test("trim whitespace at the end of the line", () => {
|
||||
const result = cleanYaml("key: \n - foo \n - bar ", "key");
|
||||
expect(result).toBe("- foo\n- bar");
|
||||
});
|
||||
test("should remove leading comment lines", () => {
|
||||
const result = cleanYaml("# this is\n# a comment\nkey: value", "key");
|
||||
expect(result).toBe("value");
|
||||
});
|
||||
test("should note remove leading comment lines if key is not found", () => {
|
||||
const result = cleanYaml("# this is\n# a comment\nnot-found: value", "key");
|
||||
expect(result).toBe("# this is\n# a comment\nnot-found: value");
|
||||
});
|
||||
test("removes key from single line", () => {
|
||||
const result = cleanYaml("key: value", "key");
|
||||
expect(result).toBe("value");
|
||||
});
|
||||
test("removes key from multi-line yaml", () => {
|
||||
const input = `key:\n nested: value\n another: thing`;
|
||||
const expected = `nested: value\nanother: thing`;
|
||||
const result = cleanYaml(input, "key");
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
test("keep untouched if key not found", () => {
|
||||
const result = cleanYaml("key: value", "not-found");
|
||||
expect(result).toBe("key: value");
|
||||
});
|
||||
test("trim whitespace at the end of the line", () => {
|
||||
const result = cleanYaml("key: \n - foo \n - bar ", "key");
|
||||
expect(result).toBe("- foo\n- bar");
|
||||
});
|
||||
test("should remove leading comment lines", () => {
|
||||
const result = cleanYaml("# this is\n# a comment\nkey: value", "key");
|
||||
expect(result).toBe("value");
|
||||
});
|
||||
test("should note remove leading comment lines if key is not found", () => {
|
||||
const result = cleanYaml("# this is\n# a comment\nnot-found: value", "key");
|
||||
expect(result).toBe("# this is\n# a comment\nnot-found: value");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,34 +10,34 @@
|
|||
// foo: bar
|
||||
// """
|
||||
export function cleanYaml(text: string, removeKey: string) {
|
||||
if (!removeKey) return text;
|
||||
const result: string[] = [];
|
||||
if (!removeKey) return text;
|
||||
const result: string[] = [];
|
||||
|
||||
const prefix = removeKey + ":";
|
||||
const lines = text.split("\n");
|
||||
const prefix = removeKey + ":";
|
||||
const lines = text.split("\n");
|
||||
|
||||
// remove first comment lines
|
||||
while (lines[0].startsWith("#")) lines.shift();
|
||||
// remove first comment lines
|
||||
while (lines[0].startsWith("#")) lines.shift();
|
||||
|
||||
const [firstLine, ...restLines] = lines;
|
||||
const [firstLine, ...restLines] = lines;
|
||||
|
||||
if (!firstLine.startsWith(prefix)) {
|
||||
// does not start with key, skip
|
||||
return text;
|
||||
} else {
|
||||
const first = firstLine.slice(prefix.length).trim();
|
||||
if (first) {
|
||||
result.push(first);
|
||||
}
|
||||
}
|
||||
if (!firstLine.startsWith(prefix)) {
|
||||
// does not start with key, skip
|
||||
return text;
|
||||
} else {
|
||||
const first = firstLine.slice(prefix.length).trim();
|
||||
if (first) {
|
||||
result.push(first);
|
||||
}
|
||||
}
|
||||
|
||||
if (restLines.length > 0) {
|
||||
const indentChars = restLines[0].match(/^(\s+)/)?.[0] || "";
|
||||
restLines
|
||||
.map((l) => (l.startsWith(indentChars) ? l.slice(indentChars.length) : l))
|
||||
.map((l) => l.trimEnd())
|
||||
.forEach((l) => result.push(l));
|
||||
}
|
||||
if (restLines.length > 0) {
|
||||
const indentChars = restLines[0].match(/^(\s+)/)?.[0] || "";
|
||||
restLines
|
||||
.map((l) => (l.startsWith(indentChars) ? l.slice(indentChars.length) : l))
|
||||
.map((l) => l.trimEnd())
|
||||
.forEach((l) => result.push(l));
|
||||
}
|
||||
|
||||
return result.join("\n");
|
||||
return result.join("\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import type { Rate } from "../types/evcc";
|
|||
import type { ForecastSlot } from "../components/Forecast/types";
|
||||
|
||||
function convertRate(slot: ForecastSlot): Rate {
|
||||
return {
|
||||
start: new Date(slot.start),
|
||||
end: new Date(slot.end),
|
||||
value: slot.value,
|
||||
};
|
||||
return {
|
||||
start: new Date(slot.start),
|
||||
end: new Date(slot.end),
|
||||
value: slot.value,
|
||||
};
|
||||
}
|
||||
|
||||
export default function convertRates(slots: ForecastSlot[] | null): Rate[] {
|
||||
if (!slots) return [];
|
||||
return slots.map(convertRate);
|
||||
if (!slots) return [];
|
||||
return slots.map(convertRate);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export default function <T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,64 +2,64 @@ import type { TimeseriesEntry, SolarDetails } from "../components/Forecast/types
|
|||
import deepCopy from "./deepClone";
|
||||
|
||||
export enum ForecastType {
|
||||
Solar = "solar",
|
||||
Price = "price",
|
||||
Co2 = "co2",
|
||||
Solar = "solar",
|
||||
Price = "price",
|
||||
Co2 = "co2",
|
||||
}
|
||||
|
||||
// return the date in local YYYY-MM-DD format
|
||||
function toDayString(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// return only slots that are on a given date, ignores slots that are in the past
|
||||
export function filterEntriesByDate(
|
||||
entries: TimeseriesEntry[],
|
||||
dayString: string
|
||||
entries: TimeseriesEntry[],
|
||||
dayString: string
|
||||
): TimeseriesEntry[] {
|
||||
const now = new Date();
|
||||
return entries.filter(({ ts }) => {
|
||||
const isPast = new Date(ts) < now;
|
||||
const dateMatches = toDayString(new Date(ts)) === dayString;
|
||||
return !isPast && dateMatches;
|
||||
});
|
||||
const now = new Date();
|
||||
return entries.filter(({ ts }) => {
|
||||
const isPast = new Date(ts) < now;
|
||||
const dateMatches = toDayString(new Date(ts)) === dayString;
|
||||
return !isPast && dateMatches;
|
||||
});
|
||||
}
|
||||
|
||||
// return the date in local YYYY-MM-DD format
|
||||
export function dayStringByOffset(day: number): string {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + day);
|
||||
return toDayString(date);
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + day);
|
||||
return toDayString(date);
|
||||
}
|
||||
|
||||
// return the highest slot for a given day (0 = today, 1 = tomorrow, etc.)
|
||||
export function highestSlotIndexByDay(entries: TimeseriesEntry[], day: number = 0): number {
|
||||
const dayString = dayStringByOffset(day);
|
||||
const dayEntries = filterEntriesByDate(entries, dayString);
|
||||
const sortedEntries = dayEntries.sort((a, b) => b.val - a.val);
|
||||
const highestEntry = sortedEntries[0] || {};
|
||||
return entries.findIndex((entry) => entry.ts === highestEntry.ts);
|
||||
const dayString = dayStringByOffset(day);
|
||||
const dayEntries = filterEntriesByDate(entries, dayString);
|
||||
const sortedEntries = dayEntries.sort((a, b) => b.val - a.val);
|
||||
const highestEntry = sortedEntries[0] || {};
|
||||
return entries.findIndex((entry) => entry.ts === highestEntry.ts);
|
||||
}
|
||||
|
||||
export function adjustedSolar(solar?: SolarDetails): SolarDetails | undefined {
|
||||
if (!solar?.scale) return solar;
|
||||
if (!solar?.scale) return solar;
|
||||
|
||||
const { scale } = solar;
|
||||
const result = deepCopy(solar);
|
||||
const { scale } = solar;
|
||||
const result = deepCopy(solar);
|
||||
|
||||
if (result.today) result.today.energy *= scale;
|
||||
if (result.tomorrow) result.tomorrow.energy *= scale;
|
||||
if (result.dayAfterTomorrow) result.dayAfterTomorrow.energy *= scale;
|
||||
if (result.timeseries) {
|
||||
result.timeseries.forEach((entry) => {
|
||||
entry.val *= scale;
|
||||
});
|
||||
}
|
||||
if (result.today) result.today.energy *= scale;
|
||||
if (result.tomorrow) result.tomorrow.energy *= scale;
|
||||
if (result.dayAfterTomorrow) result.dayAfterTomorrow.energy *= scale;
|
||||
if (result.timeseries) {
|
||||
result.timeseries.forEach((entry) => {
|
||||
entry.val *= scale;
|
||||
});
|
||||
}
|
||||
|
||||
result.scale = 1 / scale; // invert to allow back-adjustment
|
||||
result.scale = 1 / scale; // invert to allow back-adjustment
|
||||
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,31 +4,36 @@ import pluginVue from "eslint-plugin-vue";
|
|||
import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript";
|
||||
import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs["flat/recommended"],
|
||||
vueTsConfigs.recommended,
|
||||
skipFormattingConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
export default [
|
||||
...defineConfigWithVueTs(
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs["flat/recommended"],
|
||||
vueTsConfigs.recommended,
|
||||
skipFormattingConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
rules: {
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/attribute-hyphenation": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/no-reserved-component-names": "off",
|
||||
/*"vue/no-undef-properties": "warn",*/
|
||||
"no-param-reassign": "error",
|
||||
"vue/block-lang": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
}
|
||||
);
|
||||
rules: {
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/attribute-hyphenation": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/no-reserved-component-names": "off",
|
||||
/*"vue/no-undef-properties": "warn",*/
|
||||
"no-param-reassign": "error",
|
||||
"vue/block-lang": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
}
|
||||
).map((config) => ({
|
||||
...config,
|
||||
files: ["assets/**/*.{ts,js,vue}", "tests/**/*.ts"],
|
||||
})),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "cross-env TZ=Europe/Berlin vitest",
|
||||
"lint": "prettier assets ./**/*.{yml,yaml,sh} ./i18n/*.json --write && eslint assets/**/*.js assets/**/*.vue --fix --max-warnings=0 && npm run tsc",
|
||||
"tsc": "vue-tsc --noEmit",
|
||||
"lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:tsc",
|
||||
"lint:prettier": "prettier assets tests **/*.{yaml,sh} i18n/*.json --write",
|
||||
"lint:eslint": "eslint --fix --max-warnings=0",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"dev": "vite",
|
||||
"playwright": "playwright test --ui",
|
||||
"simulator": "vite tests/simulator",
|
||||
|
|
|
|||
|
|
@ -7,83 +7,81 @@ const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("battery meter", async () => {
|
||||
test("create, edit and remove battery meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("create, edit and remove battery meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("battery")).toHaveCount(0);
|
||||
await expect(page.getByTestId("battery")).toHaveCount(0);
|
||||
|
||||
// create #1
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
// create #1
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("Demo Battery");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
||||
await meterModal.getByLabel("Power").fill("4000");
|
||||
await meterModal.getByLabel("Charge").fill("80");
|
||||
await expect(meterModal.getByLabel("Maximum AC power of the hybrid inverter")).toHaveCount(
|
||||
0
|
||||
);
|
||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
||||
await meterModal.getByRole("link", { name: "validate" }).click();
|
||||
await expect(meterModal.getByTestId("device-tag-power")).toContainText("4.0 kW");
|
||||
await expect(meterModal.getByTestId("device-tag-soc")).toContainText("80.0%");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("battery")).toBeVisible();
|
||||
await expect(page.getByTestId("battery")).toContainText("Demo Battery");
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("Demo Battery");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
||||
await meterModal.getByLabel("Power").fill("4000");
|
||||
await meterModal.getByLabel("Charge").fill("80");
|
||||
await expect(meterModal.getByLabel("Maximum AC power of the hybrid inverter")).toHaveCount(0);
|
||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
||||
await meterModal.getByRole("link", { name: "validate" }).click();
|
||||
await expect(meterModal.getByTestId("device-tag-power")).toContainText("4.0 kW");
|
||||
await expect(meterModal.getByTestId("device-tag-soc")).toContainText("80.0%");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("battery")).toBeVisible();
|
||||
await expect(page.getByTestId("battery")).toContainText("Demo Battery");
|
||||
|
||||
// edit #1
|
||||
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByLabel("Power").fill("5000");
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
// edit #1
|
||||
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByLabel("Power").fill("5000");
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
const battery = page.getByTestId("battery");
|
||||
await expect(battery).toBeVisible();
|
||||
await expect(battery).toContainText("Demo Battery");
|
||||
await expect(battery.getByTestId("device-tag-power")).toContainText("5.0 kW");
|
||||
await expect(battery.getByTestId("device-tag-soc")).toContainText("80.0%");
|
||||
const battery = page.getByTestId("battery");
|
||||
await expect(battery).toBeVisible();
|
||||
await expect(battery).toContainText("Demo Battery");
|
||||
await expect(battery.getByTestId("device-tag-power")).toContainText("5.0 kW");
|
||||
await expect(battery.getByTestId("device-tag-soc")).toContainText("80.0%");
|
||||
|
||||
// restart and check in main ui
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("visualization").click();
|
||||
await expect(page.getByTestId("energyflow")).toContainText("Battery discharging80%5.0 kW");
|
||||
// restart and check in main ui
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("visualization").click();
|
||||
await expect(page.getByTestId("energyflow")).toContainText("Battery discharging80%5.0 kW");
|
||||
|
||||
// delete #1
|
||||
await page.goto("/#/config");
|
||||
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("battery")).toHaveCount(0);
|
||||
});
|
||||
// delete #1
|
||||
await page.goto("/#/config");
|
||||
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("battery")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("advanced fields", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("advanced fields", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("Demo Battery");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("Demo Battery");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
||||
|
||||
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
|
||||
await page.getByRole("button", { name: "Show advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Meter reading")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Hide advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
|
||||
});
|
||||
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
|
||||
await page.getByRole("button", { name: "Show advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Meter reading")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Hide advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,448 +8,438 @@ const CONFIG_ONE_LP = "config-one-lp.evcc.yaml";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
await stop();
|
||||
});
|
||||
|
||||
async function enableExperimental(page: Page) {
|
||||
await page
|
||||
.getByTestId("generalconfig-experimental")
|
||||
.getByRole("button", { name: "edit" })
|
||||
.click();
|
||||
await page.getByLabel("Experimental 🧪").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await page
|
||||
.getByTestId("generalconfig-experimental")
|
||||
.getByRole("button", { name: "edit" })
|
||||
.click();
|
||||
await page.getByLabel("Experimental 🧪").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
}
|
||||
|
||||
async function addDemoCharger(page: Page) {
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
|
||||
const modal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Manufacturer").selectOption("Demo charger");
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expectModalVisible(lpModal);
|
||||
const modal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Manufacturer").selectOption("Demo charger");
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expectModalVisible(lpModal);
|
||||
}
|
||||
|
||||
async function addDemoMeter(page: Page, power = "0") {
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Add dedicated charger meter" }).click();
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Add dedicated charger meter" }).click();
|
||||
|
||||
const modal = page.getByTestId("meter-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await modal.getByLabel("Power").fill(power);
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expectModalVisible(lpModal);
|
||||
const modal = page.getByTestId("meter-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await modal.getByLabel("Power").fill(power);
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expectModalVisible(lpModal);
|
||||
}
|
||||
|
||||
async function addVehicle(page: Page, title: string) {
|
||||
await page.getByRole("button", { name: "Add vehicle" }).click();
|
||||
const modal = page.getByTestId("vehicle-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Manufacturer").selectOption("Generic vehicle (without API)");
|
||||
await modal.getByLabel("Title").fill(title);
|
||||
await modal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await page.getByRole("button", { name: "Add vehicle" }).click();
|
||||
const modal = page.getByTestId("vehicle-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Manufacturer").selectOption("Generic vehicle (without API)");
|
||||
await modal.getByLabel("Title").fill(title);
|
||||
await modal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
}
|
||||
|
||||
async function newLoadpoint(page: Page, title: string) {
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await page.getByRole("button", { name: "Add charge point" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Title").fill(title);
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await page.getByRole("button", { name: "Add charge point" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Title").fill(title);
|
||||
}
|
||||
|
||||
test.describe("loadpoint", async () => {
|
||||
test("create, update and delete", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
test("create, update and delete", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
|
||||
|
||||
// new loadpoint
|
||||
await newLoadpoint(page, "Solar Carport");
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
// new loadpoint
|
||||
await newLoadpoint(page, "Solar Carport");
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
|
||||
// add charger
|
||||
await chargerModal.getByLabel("Manufacturer").selectOption("Demo charger");
|
||||
await chargerModal.getByLabel("Charge status").selectOption("C");
|
||||
await chargerModal.getByLabel("Power").fill("11000");
|
||||
await chargerModal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport");
|
||||
// add charger
|
||||
await chargerModal.getByLabel("Manufacturer").selectOption("Demo charger");
|
||||
await chargerModal.getByLabel("Charge status").selectOption("C");
|
||||
await chargerModal.getByLabel("Power").fill("11000");
|
||||
await chargerModal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport");
|
||||
|
||||
// create loadpoint
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("charging");
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
|
||||
// create loadpoint
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("charging");
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
|
||||
|
||||
// restart button appears
|
||||
const restartButton = await page
|
||||
.getByTestId("bottom-banner")
|
||||
.getByRole("button", { name: "Restart" });
|
||||
await expect(restartButton).toBeVisible();
|
||||
// restart button appears
|
||||
const restartButton = await page
|
||||
.getByTestId("bottom-banner")
|
||||
.getByRole("button", { name: "Restart" });
|
||||
await expect(restartButton).toBeVisible();
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
|
||||
|
||||
// update loadpoint title
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Title").fill("Solar Carport 2");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
|
||||
// update loadpoint title
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Title").fill("Solar Carport 2");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
|
||||
|
||||
// update loadpoint power
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByTestId("chargerPower-22kw").click();
|
||||
// update loadpoint power
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByTestId("chargerPower-22kw").click();
|
||||
|
||||
// update charger mode
|
||||
await expect(lpModal.getByRole("textbox", { name: "Charger" })).toHaveValue(
|
||||
"Demo charger [db:1]"
|
||||
);
|
||||
await lpModal.getByRole("textbox", { name: "Charger" }).click();
|
||||
await chargerModal.getByLabel("Charge status").selectOption("A");
|
||||
await chargerModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(chargerModal);
|
||||
// update charger mode
|
||||
await expect(lpModal.getByRole("textbox", { name: "Charger" })).toHaveValue(
|
||||
"Demo charger [db:1]"
|
||||
);
|
||||
await lpModal.getByRole("textbox", { name: "Charger" }).click();
|
||||
await chargerModal.getByLabel("Charge status").selectOption("A");
|
||||
await chargerModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(chargerModal);
|
||||
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("not connected");
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("not connected");
|
||||
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByTestId("chargerPower-22kw")).toHaveClass(/active/);
|
||||
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport 2");
|
||||
await lpModal.getByRole("button", { name: "Close" }).click();
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByTestId("chargerPower-22kw")).toHaveClass(/active/);
|
||||
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport 2");
|
||||
await lpModal.getByRole("button", { name: "Close" }).click();
|
||||
|
||||
// delete loadpoint
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
|
||||
// delete loadpoint
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
|
||||
});
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("priority", async ({ page }) => {
|
||||
await start(CONFIG_ONE_LP);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("priority", async ({ page }) => {
|
||||
await start(CONFIG_ONE_LP);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
|
||||
// loadpoint from yaml
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
|
||||
// loadpoint from yaml
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
|
||||
|
||||
// add loadpoint via UI
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
// add loadpoint via UI
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// two loadpoints
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
|
||||
await expect(page.getByTestId("loadpoint").nth(0)).toContainText("Carport");
|
||||
await expect(page.getByTestId("loadpoint").nth(1)).toContainText("Garage");
|
||||
// two loadpoints
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
|
||||
await expect(page.getByTestId("loadpoint").nth(0)).toContainText("Carport");
|
||||
await expect(page.getByTestId("loadpoint").nth(1)).toContainText("Garage");
|
||||
|
||||
// second loadpoint: increase priority
|
||||
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
|
||||
await lpModal.getByLabel("Priority").selectOption("1");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
// second loadpoint: increase priority
|
||||
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
|
||||
await lpModal.getByLabel("Priority").selectOption("1");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_ONE_LP);
|
||||
await page.reload();
|
||||
// restart
|
||||
await restart(CONFIG_ONE_LP);
|
||||
await page.reload();
|
||||
|
||||
// check priorities
|
||||
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Priority")).toHaveValue("1");
|
||||
// check priorities
|
||||
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Priority")).toHaveValue("1");
|
||||
|
||||
// change back to 0
|
||||
await lpModal.getByLabel("Priority").selectOption("0 (default)");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
// change back to 0
|
||||
await lpModal.getByLabel("Priority").selectOption("0 (default)");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_ONE_LP);
|
||||
await page.reload();
|
||||
// restart
|
||||
await restart(CONFIG_ONE_LP);
|
||||
await page.reload();
|
||||
|
||||
// check priorities
|
||||
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
|
||||
});
|
||||
// check priorities
|
||||
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
|
||||
});
|
||||
|
||||
test("vehicle", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("vehicle", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
const LP_1 = "Carport";
|
||||
const LP_2 = "Garage";
|
||||
const VEHICLE_1 = "Green Car";
|
||||
const VEHICLE_2 = "Yellow Van";
|
||||
const LP_1 = "Carport";
|
||||
const LP_2 = "Garage";
|
||||
const VEHICLE_1 = "Green Car";
|
||||
const VEHICLE_2 = "Yellow Van";
|
||||
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
|
||||
// add loadpoint > no vehicle option
|
||||
await newLoadpoint(page, LP_1);
|
||||
await addDemoCharger(page);
|
||||
await expect(lpModal.getByLabel("Default vehicle")).not.toBeVisible();
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
// add loadpoint > no vehicle option
|
||||
await newLoadpoint(page, LP_1);
|
||||
await addDemoCharger(page);
|
||||
await expect(lpModal.getByLabel("Default vehicle")).not.toBeVisible();
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// edit loadpoint
|
||||
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal).toContainText("No vehicles are configured.");
|
||||
await lpModal.getByRole("button", { name: "Close" }).click();
|
||||
// edit loadpoint
|
||||
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal).toContainText("No vehicles are configured.");
|
||||
await lpModal.getByRole("button", { name: "Close" }).click();
|
||||
|
||||
// add vehicles
|
||||
for (const title of [VEHICLE_1, VEHICLE_2]) {
|
||||
await addVehicle(page, title);
|
||||
}
|
||||
// add vehicles
|
||||
for (const title of [VEHICLE_1, VEHICLE_2]) {
|
||||
await addVehicle(page, title);
|
||||
}
|
||||
|
||||
// set vehicle as default for loadpoint 1
|
||||
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByTestId("loadpointPollMode-charging")).toHaveClass(/active/);
|
||||
await expect(
|
||||
lpModal.getByRole("checkbox", { name: "Interpolate charge level" })
|
||||
).toBeChecked();
|
||||
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_1);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
// set vehicle as default for loadpoint 1
|
||||
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByTestId("loadpointPollMode-charging")).toHaveClass(/active/);
|
||||
await expect(lpModal.getByRole("checkbox", { name: "Interpolate charge level" })).toBeChecked();
|
||||
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_1);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// add second loadpoint
|
||||
await newLoadpoint(page, LP_2);
|
||||
await addDemoCharger(page);
|
||||
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_2);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
// add second loadpoint
|
||||
await newLoadpoint(page, LP_2);
|
||||
await addDemoCharger(page);
|
||||
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_2);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
// check loadpoint default vehicles
|
||||
for (const [index, vehicle] of [VEHICLE_1, VEHICLE_2].entries()) {
|
||||
await page
|
||||
.getByTestId("loadpoint")
|
||||
.nth(index)
|
||||
.getByRole("button", { name: "edit" })
|
||||
.click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.locator("#loadpointParamVehicle option:checked")).toHaveText(
|
||||
vehicle
|
||||
);
|
||||
await lpModal.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
}
|
||||
});
|
||||
// check loadpoint default vehicles
|
||||
for (const [index, vehicle] of [VEHICLE_1, VEHICLE_2].entries()) {
|
||||
await page.getByTestId("loadpoint").nth(index).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.locator("#loadpointParamVehicle option:checked")).toHaveText(vehicle);
|
||||
await lpModal.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
}
|
||||
});
|
||||
|
||||
test("keep mode", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("keep mode", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add grid meter
|
||||
await page.getByRole("button", { name: "Add grid meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await meterModal.getByLabel("Power").fill("-1000");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
// add grid meter
|
||||
await page.getByRole("button", { name: "Add grid meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await meterModal.getByLabel("Power").fill("-1000");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
// add a loadpoint with dummy charger,
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await newLoadpoint(page, "Carport");
|
||||
await addDemoCharger(page);
|
||||
await lpModal.getByLabel("Default mode").selectOption("---");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await restart(CONFIG_EMPTY);
|
||||
// add a loadpoint with dummy charger,
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await newLoadpoint(page, "Carport");
|
||||
await addDemoCharger(page);
|
||||
await lpModal.getByLabel("Default mode").selectOption("---");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await restart(CONFIG_EMPTY);
|
||||
|
||||
// change on main ui
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("button", { name: "Off" })).toHaveClass(/active/);
|
||||
await page.getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByRole("button", { name: "Solar", exact: true })).toHaveClass(
|
||||
/active/
|
||||
);
|
||||
// change on main ui
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("button", { name: "Off" })).toHaveClass(/active/);
|
||||
await page.getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByRole("button", { name: "Solar", exact: true })).toHaveClass(/active/);
|
||||
|
||||
// change default mode in config to fast
|
||||
await page.goto("/#/config");
|
||||
// open first loadpoint
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Default mode").selectOption("Fast");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await restart(CONFIG_EMPTY);
|
||||
// change default mode in config to fast
|
||||
await page.goto("/#/config");
|
||||
// open first loadpoint
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Default mode").selectOption("Fast");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await restart(CONFIG_EMPTY);
|
||||
|
||||
// check loadpoint mode
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("button", { name: "Fast" })).toHaveClass(/active/);
|
||||
});
|
||||
// check loadpoint mode
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("button", { name: "Fast" })).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test("delete vehicle references", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("delete vehicle references", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add vehicle, add loadpoint, add charger
|
||||
await addVehicle(page, "Porsche");
|
||||
await addVehicle(page, "Tesla");
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Default vehicle").selectOption("Porsche");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
// add vehicle, add loadpoint, add charger
|
||||
await addVehicle(page, "Porsche");
|
||||
await addVehicle(page, "Tesla");
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Default vehicle").selectOption("Porsche");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// delete vehicle
|
||||
await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click();
|
||||
const vehicleModal = page.getByTestId("vehicle-modal");
|
||||
await expectModalVisible(vehicleModal);
|
||||
await vehicleModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(vehicleModal);
|
||||
// delete vehicle
|
||||
await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click();
|
||||
const vehicleModal = page.getByTestId("vehicle-modal");
|
||||
await expectModalVisible(vehicleModal);
|
||||
await vehicleModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(vehicleModal);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
// check loadpoint default vehicle
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Default vehicle")).toHaveValue("");
|
||||
});
|
||||
// check loadpoint default vehicle
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Default vehicle")).toHaveValue("");
|
||||
});
|
||||
|
||||
test("delete charger references", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("delete charger references", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add loadpoint, add charger
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
// add loadpoint, add charger
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// delete charger
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("textbox", { name: "Charger" }).click();
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(chargerModal);
|
||||
await chargerModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(chargerModal);
|
||||
// delete charger
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("textbox", { name: "Charger" }).click();
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(chargerModal);
|
||||
await chargerModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(chargerModal);
|
||||
|
||||
// restart without saving loadpoint
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
// restart without saving loadpoint
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
// check loadpoint default vehicle
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
|
||||
await expect(lpModal).toContainText("Configuring a charger is required.");
|
||||
});
|
||||
// check loadpoint default vehicle
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
|
||||
await expect(lpModal).toContainText("Configuring a charger is required.");
|
||||
});
|
||||
|
||||
test("delete meter references", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("delete meter references", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add loadpoint, add charger
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
await addDemoMeter(page, "11000");
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
|
||||
// add loadpoint, add charger
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
await addDemoMeter(page, "11000");
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
|
||||
|
||||
// delete charger
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("textbox", { name: "Meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
// delete charger
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("textbox", { name: "Meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
// restart without saving loadpoint
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
// restart without saving loadpoint
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
// check loadpoint default vehicle
|
||||
await expect(page.getByTestId("loadpoint")).not.toContainText("11.0 kW");
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
|
||||
await expect(
|
||||
lpModal.getByRole("button", { name: "Add dedicated charger meter" })
|
||||
).toBeVisible();
|
||||
});
|
||||
// check loadpoint default vehicle
|
||||
await expect(page.getByTestId("loadpoint")).not.toContainText("11.0 kW");
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
|
||||
await expect(
|
||||
lpModal.getByRole("button", { name: "Add dedicated charger meter" })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("user-defined charger", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("user-defined charger", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add loadpoint
|
||||
await newLoadpoint(page, "Carport");
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
// add loadpoint
|
||||
await newLoadpoint(page, "Carport");
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
|
||||
// add user-defined charger
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(chargerModal);
|
||||
await chargerModal.getByLabel("Manufacturer").selectOption("User-defined device");
|
||||
await page.waitForLoadState("networkidle");
|
||||
const editor = chargerModal.getByTestId("yaml-editor");
|
||||
await expect(editor).toContainText("status: # charger status [A..F]");
|
||||
// add user-defined charger
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(chargerModal);
|
||||
await chargerModal.getByLabel("Manufacturer").selectOption("User-defined device");
|
||||
await page.waitForLoadState("networkidle");
|
||||
const editor = chargerModal.getByTestId("yaml-editor");
|
||||
await expect(editor).toContainText("status: # charger status [A..F]");
|
||||
|
||||
await editorClear(editor, 10);
|
||||
await editorPaste(
|
||||
editor,
|
||||
page,
|
||||
`status:
|
||||
await editorClear(editor, 10);
|
||||
await editorPaste(
|
||||
editor,
|
||||
page,
|
||||
`status:
|
||||
source: const
|
||||
value: 'C'
|
||||
enabled:
|
||||
|
|
@ -464,49 +454,47 @@ maxcurrent:
|
|||
power:
|
||||
source: const
|
||||
value: 11000`
|
||||
);
|
||||
);
|
||||
|
||||
const restResult = chargerModal.getByTestId("test-result");
|
||||
await expect(restResult).toContainText("Status: unknown");
|
||||
await restResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(restResult).toContainText("Status: successful");
|
||||
await expect(restResult).toContainText(["Status", "charging"].join(""));
|
||||
await expect(restResult).toContainText(["Enabled", "yes"].join(""));
|
||||
await expect(restResult).toContainText(["Power", "11.0 kW"].join(""));
|
||||
const restResult = chargerModal.getByTestId("test-result");
|
||||
await expect(restResult).toContainText("Status: unknown");
|
||||
await restResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(restResult).toContainText("Status: successful");
|
||||
await expect(restResult).toContainText(["Status", "charging"].join(""));
|
||||
await expect(restResult).toContainText(["Enabled", "yes"].join(""));
|
||||
await expect(restResult).toContainText(["Power", "11.0 kW"].join(""));
|
||||
|
||||
// create
|
||||
await chargerModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(chargerModal);
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
// create
|
||||
await chargerModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(chargerModal);
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
|
||||
|
||||
// restart evcc
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
// restart evcc
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
const lpEntry = page.getByTestId("loadpoint");
|
||||
await expect(lpEntry).toHaveCount(1);
|
||||
await expect(lpEntry).toContainText("Carport");
|
||||
await expect(lpEntry).toContainText(["Status", "charging"].join(""));
|
||||
await expect(lpEntry).toContainText(["Enabled", "yes"].join(""));
|
||||
await expect(lpEntry).toContainText(["Power", "11.0 kW"].join(""));
|
||||
const lpEntry = page.getByTestId("loadpoint");
|
||||
await expect(lpEntry).toHaveCount(1);
|
||||
await expect(lpEntry).toContainText("Carport");
|
||||
await expect(lpEntry).toContainText(["Status", "charging"].join(""));
|
||||
await expect(lpEntry).toContainText(["Enabled", "yes"].join(""));
|
||||
await expect(lpEntry).toContainText(["Power", "11.0 kW"].join(""));
|
||||
|
||||
await lpEntry.getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpEntry.getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
|
||||
await expect(lpModal.getByLabel("Charger").first()).toHaveValue(
|
||||
"User-defined device [db:1]"
|
||||
);
|
||||
await lpModal.getByLabel("Charger").first().click();
|
||||
await expectModalVisible(chargerModal);
|
||||
await expect(lpModal.getByLabel("Charger").first()).toHaveValue("User-defined device [db:1]");
|
||||
await lpModal.getByLabel("Charger").first().click();
|
||||
await expectModalVisible(chargerModal);
|
||||
|
||||
await expect(chargerModal.getByLabel("Manufacturer")).toHaveValue("User-defined device");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(editor).toContainText("value: 'C'");
|
||||
await expect(editor).toContainText("value: 11000");
|
||||
});
|
||||
await expect(chargerModal.getByLabel("Manufacturer")).toHaveValue("User-defined device");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(editor).toContainText("value: 'C'");
|
||||
await expect(editor).toContainText("value: 11000");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,38 +7,38 @@ const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
await stop();
|
||||
});
|
||||
|
||||
const SELECT_ALL = "ControlOrMeta+KeyA";
|
||||
|
||||
async function goToConfig(page: Page) {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
}
|
||||
|
||||
test.describe("messaging", async () => {
|
||||
test("save a comment", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
test("save a comment", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
|
||||
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
|
||||
const modal = await page.getByTestId("messaging-modal");
|
||||
await expectModalVisible(modal);
|
||||
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
|
||||
const modal = await page.getByTestId("messaging-modal");
|
||||
await expectModalVisible(modal);
|
||||
|
||||
await modal.locator(".monaco-editor .view-line").nth(0).click();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await page.keyboard.press(SELECT_ALL, { delay: 10 });
|
||||
await page.keyboard.press("Backspace", { delay: 10 });
|
||||
}
|
||||
await page.keyboard.type("# hello world");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await modal.locator(".monaco-editor .view-line").nth(0).click();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await page.keyboard.press(SELECT_ALL, { delay: 10 });
|
||||
await page.keyboard.press("Backspace", { delay: 10 });
|
||||
}
|
||||
await page.keyboard.type("# hello world");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
|
||||
page.reload();
|
||||
page.reload();
|
||||
|
||||
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(modal);
|
||||
await expect(modal).toContainText("# hello world");
|
||||
});
|
||||
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(modal);
|
||||
await expect(modal).toContainText("# hello world");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,10 +30,7 @@ test.describe("mqtt", async () => {
|
|||
test.beforeAll(async () => {
|
||||
mqttReachable = await isMqttReachable(VALID_BROKER, VALID_USERNAME, VALID_PASSWORD);
|
||||
});
|
||||
test.skip(
|
||||
!mqttReachable,
|
||||
`MQTT broker ${VALID_BROKER} is not reachable, skipping tests`
|
||||
);
|
||||
test.skip(!mqttReachable, `MQTT broker ${VALID_BROKER} is not reachable, skipping tests`);
|
||||
|
||||
test("mqtt not configured", async ({ page }) => {
|
||||
await expect(page.getByTestId("mqtt")).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -8,109 +8,107 @@ const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
|||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("pv meter", async () => {
|
||||
test("create, edit and remove pv meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
test("create, edit and remove pv meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
|
||||
// create #1
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
// create #1
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("PV North");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await meterModal.getByLabel("Power optional").fill("5000");
|
||||
await page.getByRole("button", { name: "Show advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Minimum charge")).not.toBeVisible(); // battery usage only
|
||||
await expect(
|
||||
meterModal.getByLabel("Maximum AC power of the hybrid inverter")
|
||||
).toBeVisible(); // pv usage only
|
||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
||||
await meterModal.getByRole("link", { name: "validate" }).click();
|
||||
await expect(meterModal.getByTestId("device-tag-power")).toContainText("5.0 kW");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toContainText("PV North");
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("PV North");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await meterModal.getByLabel("Power optional").fill("5000");
|
||||
await page.getByRole("button", { name: "Show advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Minimum charge")).not.toBeVisible(); // battery usage only
|
||||
await expect(meterModal.getByLabel("Maximum AC power of the hybrid inverter")).toBeVisible(); // pv usage only
|
||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
||||
await meterModal.getByRole("link", { name: "validate" }).click();
|
||||
await expect(meterModal.getByTestId("device-tag-power")).toContainText("5.0 kW");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toContainText("PV North");
|
||||
|
||||
// edit #1
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByLabel("Power optional").fill("6000");
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
// edit #1
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByLabel("Power optional").fill("6000");
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
const pv = page.getByTestId("pv");
|
||||
await expect(pv).toBeVisible();
|
||||
await expect(pv).toContainText("PV North");
|
||||
await expect(pv.getByTestId("device-tag-power")).toContainText("6.0 kW");
|
||||
const pv = page.getByTestId("pv");
|
||||
await expect(pv).toBeVisible();
|
||||
await expect(pv).toContainText("PV North");
|
||||
await expect(pv.getByTestId("device-tag-power")).toContainText("6.0 kW");
|
||||
|
||||
// restart and check in main ui
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("visualization").click();
|
||||
await expect(page.getByTestId("energyflow")).toContainText("Production6.0 kW");
|
||||
// restart and check in main ui
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("visualization").click();
|
||||
await expect(page.getByTestId("energyflow")).toContainText("Production6.0 kW");
|
||||
|
||||
// delete #1
|
||||
await page.goto("/#/config");
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
// delete #1
|
||||
await page.goto("/#/config");
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
|
||||
// restart and check again
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
});
|
||||
// restart and check again
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("remove broken pv meter", async ({ page }) => {
|
||||
// setup test data for mock openems api
|
||||
await startSimulator();
|
||||
test("remove broken pv meter", async ({ page }) => {
|
||||
// setup test data for mock openems api
|
||||
await startSimulator();
|
||||
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// create meter
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("North Roof");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("shelly-1pm");
|
||||
await meterModal.getByLabel("IP address or hostname").fill(simulatorHost());
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toContainText("North Roof");
|
||||
// create meter
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("North Roof");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("shelly-1pm");
|
||||
await meterModal.getByLabel("IP address or hostname").fill(simulatorHost());
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toContainText("North Roof");
|
||||
|
||||
// break meter
|
||||
await stopSimulator();
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
// break meter
|
||||
await stopSimulator();
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
|
||||
// remove meter
|
||||
await expect(page.getByTestId("fatal-error")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toBeVisible();
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
// remove meter
|
||||
await expect(page.getByTestId("fatal-error")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toBeVisible();
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
|
||||
// restart and check again
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
||||
});
|
||||
// restart and check again
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import {
|
||||
enableExperimental,
|
||||
expectModalHidden,
|
||||
expectModalVisible,
|
||||
editorClear,
|
||||
editorPaste,
|
||||
enableExperimental,
|
||||
expectModalHidden,
|
||||
expectModalVisible,
|
||||
editorClear,
|
||||
editorPaste,
|
||||
} from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
|
|
@ -15,92 +15,92 @@ test.use({ baseURL: baseUrl() });
|
|||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
await stop();
|
||||
});
|
||||
|
||||
async function goToConfig(page: Page) {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
}
|
||||
|
||||
test.describe("tariffs", async () => {
|
||||
test("tariffs not configured", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
test("tariffs not configured", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
|
||||
await expect(page.getByTestId("tariffs")).not.toBeVisible();
|
||||
await expect(page.getByTestId("add-tariffs")).toBeVisible();
|
||||
});
|
||||
await expect(page.getByTestId("tariffs")).not.toBeVisible();
|
||||
await expect(page.getByTestId("add-tariffs")).toBeVisible();
|
||||
});
|
||||
|
||||
test("tariffs via ui", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
test("tariffs via ui", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
|
||||
await page.getByTestId("add-tariffs").click();
|
||||
const modal = await page.getByTestId("tariffs-modal");
|
||||
await expectModalVisible(modal);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.getByTestId("add-tariffs").click();
|
||||
const modal = await page.getByTestId("tariffs-modal");
|
||||
await expectModalVisible(modal);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// default content
|
||||
const editor = modal.getByTestId("yaml-editor");
|
||||
await expect(editor).toContainText("#currency: EUR");
|
||||
// default content
|
||||
const editor = modal.getByTestId("yaml-editor");
|
||||
await expect(editor).toContainText("#currency: EUR");
|
||||
|
||||
// clear and enter invalid yaml
|
||||
await editorClear(editor);
|
||||
await editorPaste(editor, page, "foo: bar");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(modal.getByTestId("error")).toContainText("invalid keys: foo");
|
||||
// clear and enter invalid yaml
|
||||
await editorClear(editor);
|
||||
await editorPaste(editor, page, "foo: bar");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(modal.getByTestId("error")).toContainText("invalid keys: foo");
|
||||
|
||||
// clear and enter valid yaml
|
||||
await editorClear(editor);
|
||||
await editorPaste(
|
||||
editor,
|
||||
page,
|
||||
`currency: CHF
|
||||
// clear and enter valid yaml
|
||||
await editorClear(editor);
|
||||
await editorPaste(
|
||||
editor,
|
||||
page,
|
||||
`currency: CHF
|
||||
grid:
|
||||
type: fixed
|
||||
price: 0.123`
|
||||
);
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(modal.getByTestId("error")).not.toBeVisible();
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(modal.getByTestId("error")).not.toBeVisible();
|
||||
|
||||
// modal closes
|
||||
await expectModalHidden(modal);
|
||||
// modal closes
|
||||
await expectModalHidden(modal);
|
||||
|
||||
// restart button appears
|
||||
const restartButton = await page
|
||||
.getByTestId("bottom-banner")
|
||||
.getByRole("button", { name: "Restart" });
|
||||
await expect(restartButton).toBeVisible();
|
||||
// restart button appears
|
||||
const restartButton = await page
|
||||
.getByTestId("bottom-banner")
|
||||
.getByRole("button", { name: "Restart" });
|
||||
await expect(restartButton).toBeVisible();
|
||||
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
|
||||
// restart done
|
||||
await expect(restartButton).not.toBeVisible();
|
||||
// restart done
|
||||
await expect(restartButton).not.toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("tariffs")).toContainText(
|
||||
["Currency", "CHF", "Grid price", "12.3 rp"].join("")
|
||||
);
|
||||
});
|
||||
await expect(page.getByTestId("tariffs")).toContainText(
|
||||
["Currency", "CHF", "Grid price", "12.3 rp"].join("")
|
||||
);
|
||||
});
|
||||
|
||||
test("tariffs from evcc.yaml", async ({ page }) => {
|
||||
await start(CONFIG_WITH_TARIFFS);
|
||||
await goToConfig(page);
|
||||
test("tariffs from evcc.yaml", async ({ page }) => {
|
||||
await start(CONFIG_WITH_TARIFFS);
|
||||
await goToConfig(page);
|
||||
|
||||
await expect(page.getByTestId("tariffs")).toBeVisible();
|
||||
await expect(page.getByTestId("tariffs")).toContainText(
|
||||
[
|
||||
"Tariffs",
|
||||
"Currency",
|
||||
"SEK",
|
||||
"Grid price",
|
||||
"30.0 öre",
|
||||
"Feed-in price",
|
||||
"-10.0 öre",
|
||||
"Grid CO₂",
|
||||
"300 g",
|
||||
].join("")
|
||||
);
|
||||
});
|
||||
await expect(page.getByTestId("tariffs")).toBeVisible();
|
||||
await expect(page.getByTestId("tariffs")).toContainText(
|
||||
[
|
||||
"Tariffs",
|
||||
"Currency",
|
||||
"SEK",
|
||||
"Grid price",
|
||||
"30.0 öre",
|
||||
"Feed-in price",
|
||||
"-10.0 öre",
|
||||
"Grid CO₂",
|
||||
"300 g",
|
||||
].join("")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
176
tests/evcc.ts
176
tests/evcc.ts
|
|
@ -9,132 +9,132 @@ import { Transform } from "stream";
|
|||
const BINARY = "./evcc";
|
||||
|
||||
function workerPort() {
|
||||
const index = Number(process.env["TEST_WORKER_INDEX"] ?? 0);
|
||||
return 11000 + index;
|
||||
const index = Number(process.env["TEST_WORKER_INDEX"] ?? 0);
|
||||
return 11000 + index;
|
||||
}
|
||||
|
||||
function logPrefix() {
|
||||
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
|
||||
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
|
||||
}
|
||||
|
||||
function createSteamLog() {
|
||||
return new Transform({
|
||||
transform(chunk: Buffer, _, callback) {
|
||||
const lines = chunk.toString().split("\n");
|
||||
lines.forEach((line: string) => {
|
||||
if (line.trim()) log(line);
|
||||
});
|
||||
callback();
|
||||
},
|
||||
});
|
||||
return new Transform({
|
||||
transform(chunk: Buffer, _, callback) {
|
||||
const lines = chunk.toString().split("\n");
|
||||
lines.forEach((line: string) => {
|
||||
if (line.trim()) log(line);
|
||||
});
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function log(...args: any[]) {
|
||||
console.log(logPrefix(), ...args);
|
||||
console.log(logPrefix(), ...args);
|
||||
}
|
||||
|
||||
export function baseUrl() {
|
||||
return `http://localhost:${workerPort()}`;
|
||||
return `http://localhost:${workerPort()}`;
|
||||
}
|
||||
|
||||
function dbPath() {
|
||||
const file = `evcc-${workerPort()}.db`;
|
||||
return path.join(os.tmpdir(), file);
|
||||
const file = `evcc-${workerPort()}.db`;
|
||||
return path.join(os.tmpdir(), file);
|
||||
}
|
||||
|
||||
export async function start(
|
||||
config: string,
|
||||
sqlDumps?: string | null,
|
||||
flags: string | string[] = "--disable-auth"
|
||||
config: string,
|
||||
sqlDumps?: string | null,
|
||||
flags: string | string[] = "--disable-auth"
|
||||
) {
|
||||
await _clean();
|
||||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
}
|
||||
return await _start(config, flags);
|
||||
await _clean();
|
||||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
}
|
||||
return await _start(config, flags);
|
||||
}
|
||||
|
||||
export async function stop(instance?: ChildProcess) {
|
||||
await _stop(instance);
|
||||
await _clean();
|
||||
await _stop(instance);
|
||||
await _clean();
|
||||
}
|
||||
|
||||
export async function restart(config: string, flags = "--disable-auth") {
|
||||
await _stop();
|
||||
await _start(config, flags);
|
||||
await _stop();
|
||||
await _start(config, flags);
|
||||
}
|
||||
|
||||
export async function cleanRestart(config: string, sqlDumps: string) {
|
||||
await _stop();
|
||||
await _clean();
|
||||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
}
|
||||
await _start(config);
|
||||
await _stop();
|
||||
await _clean();
|
||||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
}
|
||||
await _start(config);
|
||||
}
|
||||
|
||||
async function _restoreDatabase(sqlDumps: string) {
|
||||
const dumps = Array.isArray(sqlDumps) ? sqlDumps : [sqlDumps];
|
||||
for (const dump of dumps) {
|
||||
log("loading database", dbPath(), dump);
|
||||
execSync(`sqlite3 ${dbPath()} < tests/${dump}`);
|
||||
}
|
||||
const dumps = Array.isArray(sqlDumps) ? sqlDumps : [sqlDumps];
|
||||
for (const dump of dumps) {
|
||||
log("loading database", dbPath(), dump);
|
||||
execSync(`sqlite3 ${dbPath()} < tests/${dump}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function _start(config: string, flags: string | string[] = []) {
|
||||
const configFile = config.includes("/") ? config : `tests/${config}`;
|
||||
const port = workerPort();
|
||||
log(`wait until port ${port} is available`);
|
||||
// wait for port to be available
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
const additionalFlags = typeof flags === "string" ? [flags] : flags;
|
||||
additionalFlags.push("--log", "debug,httpd:trace");
|
||||
log("starting evcc", { config, port, additionalFlags });
|
||||
const instance = spawn(BINARY, ["--config", configFile, ...additionalFlags], {
|
||||
env: { EVCC_NETWORK_PORT: port.toString(), EVCC_DATABASE_DSN: dbPath() },
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const steamLog = createSteamLog();
|
||||
instance.stdout.pipe(steamLog);
|
||||
instance.stderr.pipe(steamLog);
|
||||
instance.on("exit", (code) => {
|
||||
log("evcc terminated", { code, port, config });
|
||||
steamLog.end();
|
||||
});
|
||||
await waitOn({ resources: [baseUrl()], log: true });
|
||||
return instance;
|
||||
const configFile = config.includes("/") ? config : `tests/${config}`;
|
||||
const port = workerPort();
|
||||
log(`wait until port ${port} is available`);
|
||||
// wait for port to be available
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
const additionalFlags = typeof flags === "string" ? [flags] : flags;
|
||||
additionalFlags.push("--log", "debug,httpd:trace");
|
||||
log("starting evcc", { config, port, additionalFlags });
|
||||
const instance = spawn(BINARY, ["--config", configFile, ...additionalFlags], {
|
||||
env: { EVCC_NETWORK_PORT: port.toString(), EVCC_DATABASE_DSN: dbPath() },
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const steamLog = createSteamLog();
|
||||
instance.stdout.pipe(steamLog);
|
||||
instance.stderr.pipe(steamLog);
|
||||
instance.on("exit", (code) => {
|
||||
log("evcc terminated", { code, port, config });
|
||||
steamLog.end();
|
||||
});
|
||||
await waitOn({ resources: [baseUrl()], log: true });
|
||||
return instance;
|
||||
}
|
||||
|
||||
async function _stop(instance?: ChildProcess) {
|
||||
const port = workerPort();
|
||||
if (instance) {
|
||||
log("shutting down evcc hard", { port });
|
||||
instance.kill("SIGKILL");
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
log("evcc is down", { port });
|
||||
return;
|
||||
}
|
||||
// check if auth is required
|
||||
const res = await axios.get(`${baseUrl()}/api/auth/status`);
|
||||
log("auth status", res.status, res.statusText, res.data);
|
||||
let cookie;
|
||||
// login required
|
||||
if (!res.data) {
|
||||
const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" });
|
||||
log("login", res.status, res.statusText);
|
||||
cookie = res.headers["set-cookie"];
|
||||
}
|
||||
log("shutting down evcc", { port });
|
||||
await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } });
|
||||
log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
log("evcc is down", { port });
|
||||
const port = workerPort();
|
||||
if (instance) {
|
||||
log("shutting down evcc hard", { port });
|
||||
instance.kill("SIGKILL");
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
log("evcc is down", { port });
|
||||
return;
|
||||
}
|
||||
// check if auth is required
|
||||
const res = await axios.get(`${baseUrl()}/api/auth/status`);
|
||||
log("auth status", res.status, res.statusText, res.data);
|
||||
let cookie;
|
||||
// login required
|
||||
if (!res.data) {
|
||||
const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" });
|
||||
log("login", res.status, res.statusText);
|
||||
cookie = res.headers["set-cookie"];
|
||||
}
|
||||
log("shutting down evcc", { port });
|
||||
await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } });
|
||||
log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
log("evcc is down", { port });
|
||||
}
|
||||
|
||||
async function _clean() {
|
||||
const db = dbPath();
|
||||
if (fs.existsSync(db)) {
|
||||
log("delete database", db);
|
||||
fs.unlinkSync(db);
|
||||
}
|
||||
const db = dbPath();
|
||||
if (fs.existsSync(db)) {
|
||||
log("delete database", db);
|
||||
fs.unlinkSync(db);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
import mqtt from "mqtt";
|
||||
|
||||
export async function isMqttReachable(
|
||||
broker: string,
|
||||
username: string,
|
||||
password: string
|
||||
broker: string,
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const client = mqtt.connect(`mqtt://${broker}`, {
|
||||
connectTimeout: 2000,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
try {
|
||||
const client = mqtt.connect(`mqtt://${broker}`, {
|
||||
connectTimeout: 2000,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.once("connect", () => resolve());
|
||||
client.once("error", (err) => reject(err));
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.once("connect", () => resolve());
|
||||
client.once("error", (err) => reject(err));
|
||||
});
|
||||
|
||||
client.end();
|
||||
return true; // connection successful
|
||||
} catch {
|
||||
return false; // connection failed
|
||||
}
|
||||
client.end();
|
||||
return true; // connection successful
|
||||
} catch {
|
||||
return false; // connection failed
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,254 +7,252 @@ const mobile = devices["iPhone 12 Mini"].viewport;
|
|||
const desktop = devices["Desktop Chrome"].viewport;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("basics.evcc.yaml", "sessions.sql");
|
||||
await start("basics.evcc.yaml", "sessions.sql");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
await stop();
|
||||
});
|
||||
|
||||
async function selectLoadpointFilter(page: Page, value: string) {
|
||||
await page
|
||||
.getByTestId("filter-loadpoint")
|
||||
.locator("visible=true")
|
||||
.getByRole("combobox")
|
||||
.selectOption(value);
|
||||
await page
|
||||
.getByTestId("filter-loadpoint")
|
||||
.locator("visible=true")
|
||||
.getByRole("combobox")
|
||||
.selectOption(value);
|
||||
}
|
||||
|
||||
async function selectVehicleFilter(page: Page, value: string) {
|
||||
await page
|
||||
.getByTestId("filter-vehicle")
|
||||
.locator("visible=true")
|
||||
.getByRole("combobox")
|
||||
.selectOption(value);
|
||||
await page
|
||||
.getByTestId("filter-vehicle")
|
||||
.locator("visible=true")
|
||||
.getByRole("combobox")
|
||||
.selectOption(value);
|
||||
}
|
||||
|
||||
test.describe("basics", async () => {
|
||||
test("navigation to sessions", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const topNavigationButton = await page.getByTestId("topnavigation-button");
|
||||
await topNavigationButton.click();
|
||||
await page
|
||||
.getByTestId("topnavigation-dropdown")
|
||||
.getByRole("link", { name: "Charging sessions" })
|
||||
.click();
|
||||
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
||||
});
|
||||
test("month without data", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(0);
|
||||
});
|
||||
test("month with data", async ({ page }) => {
|
||||
await page.setViewportSize(desktop);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("May");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
|
||||
await expect(page.getByRole("table")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(9);
|
||||
test("navigation to sessions", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const topNavigationButton = await page.getByTestId("topnavigation-button");
|
||||
await topNavigationButton.click();
|
||||
await page
|
||||
.getByTestId("topnavigation-dropdown")
|
||||
.getByRole("link", { name: "Charging sessions" })
|
||||
.click();
|
||||
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
||||
});
|
||||
test("month without data", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(0);
|
||||
});
|
||||
test("month with data", async ({ page }) => {
|
||||
await page.setViewportSize(desktop);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("May");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
|
||||
await expect(page.getByRole("table")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(9);
|
||||
|
||||
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
|
||||
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
|
||||
await expect(page.getByTestId("sessions-foot-price")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-price")).toHaveText("5.50");
|
||||
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
|
||||
await expect(page.getByTestId("sessions-foot-price")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-price")).toHaveText("5.50");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toHaveText("27.5");
|
||||
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toHaveText("27.5");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-chargeDuration")).toContainText(
|
||||
"Durationh:mm"
|
||||
);
|
||||
await expect(page.getByTestId("sessions-foot-chargeDuration")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-chargeDuration")).toHaveText("1:30");
|
||||
await expect(page.getByTestId("sessions-head-chargeDuration")).toContainText("Durationh:mm");
|
||||
await expect(page.getByTestId("sessions-foot-chargeDuration")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-chargeDuration")).toHaveText("1:30");
|
||||
|
||||
await page
|
||||
.getByTestId("sessions-head-chargeDuration")
|
||||
.getByRole("combobox")
|
||||
.selectOption("⌀ Power");
|
||||
await expect(page.getByTestId("sessions-head-avgPower")).toContainText("⌀ PowerkW");
|
||||
await expect(page.getByTestId("sessions-foot-avgPower")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-avgPower")).toHaveText("10.0");
|
||||
await page
|
||||
.getByTestId("sessions-head-chargeDuration")
|
||||
.getByRole("combobox")
|
||||
.selectOption("⌀ Power");
|
||||
await expect(page.getByTestId("sessions-head-avgPower")).toContainText("⌀ PowerkW");
|
||||
await expect(page.getByTestId("sessions-foot-avgPower")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-avgPower")).toHaveText("10.0");
|
||||
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("mobile basics", async () => {
|
||||
test("column select", async ({ page }) => {
|
||||
await page.setViewportSize(mobile);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
test("column select", async ({ page }) => {
|
||||
await page.setViewportSize(mobile);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(5);
|
||||
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(5);
|
||||
|
||||
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
|
||||
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
|
||||
|
||||
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
|
||||
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
|
||||
|
||||
await page.getByTestId("sessions-head-solar").getByRole("combobox").selectOption("Cost");
|
||||
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
|
||||
await expect(page.getByTestId("sessions-foot-price")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-price")).toHaveText("5.50");
|
||||
await page.getByTestId("sessions-head-solar").getByRole("combobox").selectOption("Cost");
|
||||
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
|
||||
await expect(page.getByTestId("sessions-foot-price")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-price")).toHaveText("5.50");
|
||||
|
||||
await page.getByTestId("sessions-head-price").getByRole("combobox").selectOption("⌀ Price");
|
||||
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toHaveText("27.5");
|
||||
});
|
||||
await page.getByTestId("sessions-head-price").getByRole("combobox").selectOption("⌀ Price");
|
||||
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toHaveText("27.5");
|
||||
});
|
||||
|
||||
test("keep selection when paging", async ({ page }) => {
|
||||
await page.setViewportSize(mobile);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
test("keep selection when paging", async ({ page }) => {
|
||||
await page.setViewportSize(mobile);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
|
||||
await page.getByTestId("navigate-next-year-month").click();
|
||||
await page.getByTestId("navigate-prev-year-month").click();
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
});
|
||||
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
|
||||
await page.getByTestId("navigate-next-year-month").click();
|
||||
await page.getByTestId("navigate-prev-year-month").click();
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("paging", async () => {
|
||||
test("prev/next links", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
|
||||
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
|
||||
});
|
||||
test("next month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByTestId("navigate-next-month").click();
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("June");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
|
||||
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
|
||||
});
|
||||
test("prev month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await page.getByTestId("navigate-prev-month").click();
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("May");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
});
|
||||
test("prev/next links", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
|
||||
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
|
||||
});
|
||||
test("next month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByTestId("navigate-next-month").click();
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("June");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
|
||||
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
|
||||
});
|
||||
test("prev month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await page.getByTestId("navigate-prev-month").click();
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("May");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
});
|
||||
});
|
||||
|
||||
for (const [name, viewport] of Object.entries({ desktop, mobile })) {
|
||||
test.describe(`filter on ${name}`, async () => {
|
||||
test("by vehicle", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
test.describe(`filter on ${name}`, async () => {
|
||||
test("by vehicle", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
|
||||
await selectVehicleFilter(page, "weißes Model 3 (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/weißes Model 3/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/weißes Model 3/);
|
||||
await selectVehicleFilter(page, "weißes Model 3 (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/weißes Model 3/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/weißes Model 3/);
|
||||
|
||||
await selectVehicleFilter(page, "all vehicles (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
await selectVehicleFilter(page, "all vehicles (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("by loadpoint", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
test("by loadpoint", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(2)).toHaveText(/Carport/);
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(2)).toHaveText(/Carport/);
|
||||
|
||||
await selectLoadpointFilter(page, "Garage (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Garage/);
|
||||
await selectLoadpointFilter(page, "Garage (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Garage/);
|
||||
|
||||
await selectLoadpointFilter(page, "all charging points (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
await selectLoadpointFilter(page, "all charging points (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("by vehicle and loadpoint", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
test("by vehicle and loadpoint", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await selectVehicleFilter(page, "weißes Model 3 (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/weißes Model 3/);
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await selectVehicleFilter(page, "weißes Model 3 (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/weißes Model 3/);
|
||||
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
});
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
});
|
||||
|
||||
test("by vehicle and loadpoint disabled options", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
test("by vehicle and loadpoint disabled options", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
const option = page
|
||||
.getByTestId("filter-loadpoint")
|
||||
.locator("visible=true")
|
||||
.locator("option[value=Garage]");
|
||||
await expect(option).toHaveAttribute("disabled", "");
|
||||
await expect(option).toHaveText("Garage (0)");
|
||||
});
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
const option = page
|
||||
.getByTestId("filter-loadpoint")
|
||||
.locator("visible=true")
|
||||
.locator("option[value=Garage]");
|
||||
await expect(option).toHaveAttribute("disabled", "");
|
||||
await expect(option).toHaveText("Garage (0)");
|
||||
});
|
||||
|
||||
test("keep filter when paging", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
test("keep filter when paging", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
if (name === "mobile") {
|
||||
await page.getByTestId("navigate-next-year-month").click();
|
||||
await page.getByTestId("navigate-prev-year-month").click();
|
||||
} else {
|
||||
await page.getByTestId("navigate-next-month").click();
|
||||
await page.getByTestId("navigate-prev-month").click();
|
||||
}
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
if (name === "mobile") {
|
||||
await page.getByTestId("navigate-next-year-month").click();
|
||||
await page.getByTestId("navigate-prev-year-month").click();
|
||||
} else {
|
||||
await page.getByTestId("navigate-next-month").click();
|
||||
await page.getByTestId("navigate-prev-month").click();
|
||||
}
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("columns desktop", async () => {
|
||||
test("show vehicle column if multiple different exist", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("vehicle")).toBeVisible();
|
||||
});
|
||||
test("show loadpoint column if multiple different exist", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("loadpoint")).toBeVisible();
|
||||
});
|
||||
test("show co2 column it has values", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=3");
|
||||
await expect(page.getByTestId("sessions-head-co2")).toBeVisible();
|
||||
});
|
||||
test("hide co2 column if it doesnt have values", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-head-co2")).toHaveCount(0);
|
||||
});
|
||||
test("show vehicle column if multiple different exist", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("vehicle")).toBeVisible();
|
||||
});
|
||||
test("show loadpoint column if multiple different exist", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("loadpoint")).toBeVisible();
|
||||
});
|
||||
test("show co2 column it has values", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=3");
|
||||
await expect(page.getByTestId("sessions-head-co2")).toBeVisible();
|
||||
});
|
||||
test("hide co2 column if it doesnt have values", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-head-co2")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
const date = new Date();
|
||||
|
|
@ -263,81 +261,82 @@ const MONTH = date.getMonth() + 1;
|
|||
const MONTH_YEAR = new Intl.DateTimeFormat("en", { month: "long", year: "numeric" }).format(date);
|
||||
|
||||
test.describe("csv export", async () => {
|
||||
test("total export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?period=total");
|
||||
await expect(page.getByRole("link", { name: "Download total CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en"
|
||||
);
|
||||
});
|
||||
test("year export", async ({ page }) => {
|
||||
// fixed year
|
||||
await page.goto("/#/sessions?period=year&year=2023");
|
||||
await expect(page.getByRole("link", { name: "Download 2023 CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en&year=2023"
|
||||
);
|
||||
test("total export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?period=total");
|
||||
await expect(page.getByRole("link", { name: "Download total CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en"
|
||||
);
|
||||
});
|
||||
test("year export", async ({ page }) => {
|
||||
// fixed year
|
||||
await page.goto("/#/sessions?period=year&year=2023");
|
||||
await expect(page.getByRole("link", { name: "Download 2023 CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en&year=2023"
|
||||
);
|
||||
|
||||
// current year
|
||||
await page.goto(`/#/sessions?period=year`);
|
||||
await expect(page.getByRole("link", { name: `Download ${YEAR} CSV` })).toHaveAttribute(
|
||||
"href",
|
||||
`./api/sessions?format=csv&lang=en&year=${YEAR}`
|
||||
);
|
||||
});
|
||||
test("monthly export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?&year=2023&month=5");
|
||||
await expect(page.getByRole("link", { name: "Download May 2023 CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en&year=2023&month=5"
|
||||
);
|
||||
// current year
|
||||
await page.goto(`/#/sessions?period=year`);
|
||||
await expect(page.getByRole("link", { name: `Download ${YEAR} CSV` })).toHaveAttribute(
|
||||
"href",
|
||||
`./api/sessions?format=csv&lang=en&year=${YEAR}`
|
||||
);
|
||||
});
|
||||
test("monthly export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?&year=2023&month=5");
|
||||
await expect(page.getByRole("link", { name: "Download May 2023 CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en&year=2023&month=5"
|
||||
);
|
||||
|
||||
// current month
|
||||
await page.goto(`/#/sessions`);
|
||||
await expect(
|
||||
page.getByRole("link", { name: `Download ${MONTH_YEAR} CSV` })
|
||||
).toHaveAttribute("href", `./api/sessions?format=csv&lang=en&year=${YEAR}&month=${MONTH}`);
|
||||
});
|
||||
// current month
|
||||
await page.goto(`/#/sessions`);
|
||||
await expect(page.getByRole("link", { name: `Download ${MONTH_YEAR} CSV` })).toHaveAttribute(
|
||||
"href",
|
||||
`./api/sessions?format=csv&lang=en&year=${YEAR}&month=${MONTH}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("session details", async () => {
|
||||
test("show session details (session 5)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
test("show session details (session 5)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByTestId("session-details").getByRole("heading", { name: "Charging Session" })
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Garage");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("weißes Model 3");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Thu, May 4, 22:00", "Fri, May 5, 06:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("5.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("0.0% (0.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.50 € 50.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-odometer")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-meter")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-delete")).toContainText("Delete");
|
||||
});
|
||||
await expect(
|
||||
page.getByTestId("session-details").getByRole("heading", { name: "Charging Session" })
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Garage");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("weißes Model 3");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Thu, May 4, 22:00", "Fri, May 5, 06:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("5.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("0.0% (0.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.50 € 50.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-odometer")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-meter")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-delete")).toContainText("Delete");
|
||||
});
|
||||
|
||||
test("show session details with CO2 data (session 1)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=3");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
test("show session details with CO2 data (session 1)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=3");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Carport");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("blauer e-Golf");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Wed, Mar 1, 07:00", "Tue, May 2, 12:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("10.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("100.0% (10.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.00 € 20.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toContainText("300 g/kWh");
|
||||
});
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Carport");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("blauer e-Golf");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Wed, Mar 1, 07:00", "Tue, May 2, 12:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("10.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("100.0% (10.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.00 € 20.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toContainText("300 g/kWh");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,71 +7,71 @@ import { spawn } from "child_process";
|
|||
import { Transform } from "stream";
|
||||
|
||||
function workerPort() {
|
||||
const index = parseInt(process.env["TEST_WORKER_INDEX"] ?? "-1");
|
||||
return 12000 + index;
|
||||
const index = parseInt(process.env["TEST_WORKER_INDEX"] ?? "-1");
|
||||
return 12000 + index;
|
||||
}
|
||||
|
||||
function logPrefix() {
|
||||
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
|
||||
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
|
||||
}
|
||||
|
||||
function createSteamLog() {
|
||||
return new Transform({
|
||||
transform(chunk: Buffer, _, callback) {
|
||||
const lines = chunk.toString().split("\n");
|
||||
lines.forEach((line) => {
|
||||
if (line.trim()) log(line);
|
||||
});
|
||||
callback();
|
||||
},
|
||||
});
|
||||
return new Transform({
|
||||
transform(chunk: Buffer, _, callback) {
|
||||
const lines = chunk.toString().split("\n");
|
||||
lines.forEach((line) => {
|
||||
if (line.trim()) log(line);
|
||||
});
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function log(...args: any[]) {
|
||||
console.log(logPrefix(), ...args);
|
||||
console.log(logPrefix(), ...args);
|
||||
}
|
||||
|
||||
export function simulatorHost() {
|
||||
return `localhost:${workerPort()}`;
|
||||
return `localhost:${workerPort()}`;
|
||||
}
|
||||
|
||||
export function simulatorUrl() {
|
||||
return `http://${simulatorHost()}`;
|
||||
return `http://${simulatorHost()}`;
|
||||
}
|
||||
|
||||
export function simulatorConfig() {
|
||||
const input = "./tests/simulator.evcc.yaml";
|
||||
const content = fs.readFileSync(input, "utf8");
|
||||
const result = content.replace(/localhost:7072/g, simulatorHost());
|
||||
const resultName = "simulator.evcc.generated.yaml";
|
||||
const resultPath = path.join(os.tmpdir(), resultName);
|
||||
fs.writeFileSync(resultPath, result);
|
||||
return resultPath;
|
||||
const input = "./tests/simulator.evcc.yaml";
|
||||
const content = fs.readFileSync(input, "utf8");
|
||||
const result = content.replace(/localhost:7072/g, simulatorHost());
|
||||
const resultName = "simulator.evcc.generated.yaml";
|
||||
const resultPath = path.join(os.tmpdir(), resultName);
|
||||
fs.writeFileSync(resultPath, result);
|
||||
return resultPath;
|
||||
}
|
||||
|
||||
export async function startSimulator() {
|
||||
const port = workerPort();
|
||||
log("starting simulator", { port });
|
||||
log(`wait until port ${port} is available`);
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
const port = workerPort();
|
||||
log("starting simulator", { port });
|
||||
log(`wait until port ${port} is available`);
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
|
||||
const instance = spawn("npm", ["run", "simulator", "--", "--port", port.toString()]);
|
||||
const instance = spawn("npm", ["run", "simulator", "--", "--port", port.toString()]);
|
||||
|
||||
const steamLog = createSteamLog();
|
||||
instance.stdout.pipe(steamLog);
|
||||
instance.stderr.pipe(steamLog);
|
||||
instance.on("exit", (code) => {
|
||||
log("simulator terminated", { code, port });
|
||||
steamLog.end();
|
||||
});
|
||||
const steamLog = createSteamLog();
|
||||
instance.stdout.pipe(steamLog);
|
||||
instance.stderr.pipe(steamLog);
|
||||
instance.on("exit", (code) => {
|
||||
log("simulator terminated", { code, port });
|
||||
steamLog.end();
|
||||
});
|
||||
|
||||
await waitOn({ resources: [`${simulatorUrl()}/api/state`], log: true });
|
||||
await waitOn({ resources: [`${simulatorUrl()}/api/state`], log: true });
|
||||
}
|
||||
|
||||
export async function stopSimulator() {
|
||||
const port = workerPort();
|
||||
log("shutting down simulator", { port });
|
||||
await axios.post(`${simulatorUrl()}/api/shutdown`);
|
||||
log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:localhost:${port}`], reverse: true });
|
||||
const port = workerPort();
|
||||
log("shutting down simulator", { port });
|
||||
await axios.post(`${simulatorUrl()}/api/shutdown`);
|
||||
log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:localhost:${port}`], reverse: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,127 +3,127 @@ import type { Connect, ViteDevServer } from "vite";
|
|||
import type { ServerResponse } from "http";
|
||||
|
||||
let state = {
|
||||
site: {
|
||||
grid: { power: 0 },
|
||||
pv: { power: 0, energy: 0 },
|
||||
battery: { power: 0, soc: 0 },
|
||||
},
|
||||
loadpoints: [{ power: 0, energy: 0, enabled: false, status: "A" }],
|
||||
vehicles: [{ soc: 0, range: 0 }],
|
||||
site: {
|
||||
grid: { power: 0 },
|
||||
pv: { power: 0, energy: 0 },
|
||||
battery: { power: 0, soc: 0 },
|
||||
},
|
||||
loadpoints: [{ power: 0, energy: 0, enabled: false, status: "A" }],
|
||||
vehicles: [{ soc: 0, range: 0 }],
|
||||
};
|
||||
|
||||
const loggingMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
_: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
req: Connect.IncomingMessage,
|
||||
_: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
console.log(`[simulator] ${req.method} ${req.url}`);
|
||||
next();
|
||||
console.log(`[simulator] ${req.method} ${req.url}`);
|
||||
next();
|
||||
};
|
||||
|
||||
const stateApiMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
if (req.method === "POST" && req.originalUrl === "/api/state") {
|
||||
console.log("[simulator] POST /api/state");
|
||||
// @ts-expect-error Property 'body' does not exist on type 'IncomingMessage'
|
||||
state = req.body;
|
||||
res.end();
|
||||
} else if (req.method === "POST" && req.originalUrl === "/api/shutdown") {
|
||||
console.log("[simulator] POST /api/shutdown");
|
||||
res.end();
|
||||
process.exit();
|
||||
} else if (req.originalUrl === "/api/state") {
|
||||
res.end(JSON.stringify(state));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
if (req.method === "POST" && req.originalUrl === "/api/state") {
|
||||
console.log("[simulator] POST /api/state");
|
||||
// @ts-expect-error Property 'body' does not exist on type 'IncomingMessage'
|
||||
state = req.body;
|
||||
res.end();
|
||||
} else if (req.method === "POST" && req.originalUrl === "/api/shutdown") {
|
||||
console.log("[simulator] POST /api/shutdown");
|
||||
res.end();
|
||||
process.exit();
|
||||
} else if (req.originalUrl === "/api/state") {
|
||||
res.end(JSON.stringify(state));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const openemsMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
const endpoints = {
|
||||
"/rest/channel/_sum/GridActivePower": { value: state.site.grid.power },
|
||||
"/rest/channel/_sum/ProductionActivePower": { value: state.site.pv.power },
|
||||
"/rest/channel/_sum/EssDischargePower": { value: state.site.battery.power },
|
||||
"/rest/channel/_sum/EssSoc": { value: state.site.battery.soc },
|
||||
};
|
||||
const endpoint = endpoints[req.originalUrl as keyof typeof endpoints];
|
||||
if (req.method === "GET" && endpoint) {
|
||||
console.log("[simulator] GET", req.originalUrl);
|
||||
res.end(JSON.stringify(endpoint));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
const endpoints = {
|
||||
"/rest/channel/_sum/GridActivePower": { value: state.site.grid.power },
|
||||
"/rest/channel/_sum/ProductionActivePower": { value: state.site.pv.power },
|
||||
"/rest/channel/_sum/EssDischargePower": { value: state.site.battery.power },
|
||||
"/rest/channel/_sum/EssSoc": { value: state.site.battery.soc },
|
||||
};
|
||||
const endpoint = endpoints[req.originalUrl as keyof typeof endpoints];
|
||||
if (req.method === "GET" && endpoint) {
|
||||
console.log("[simulator] GET", req.originalUrl);
|
||||
res.end(JSON.stringify(endpoint));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const teslaloggerMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
if (req.method === "GET" && req.originalUrl && req.originalUrl.startsWith("/currentjson/")) {
|
||||
const id = parseInt(req.originalUrl.split("/")[2]);
|
||||
const vehicle = state.vehicles[id - 1];
|
||||
if (!vehicle) {
|
||||
res.statusCode = 404;
|
||||
res.end(JSON.stringify({ error: "Vehicle not found" }));
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
battery_level: vehicle.soc,
|
||||
battery_range_km: vehicle.range,
|
||||
plugged_in: true,
|
||||
charging: false,
|
||||
odometer: 10000,
|
||||
is_preconditioning: false,
|
||||
charge_current_request: 10,
|
||||
};
|
||||
res.end(JSON.stringify(data));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
if (req.method === "GET" && req.originalUrl && req.originalUrl.startsWith("/currentjson/")) {
|
||||
const id = parseInt(req.originalUrl.split("/")[2]);
|
||||
const vehicle = state.vehicles[id - 1];
|
||||
if (!vehicle) {
|
||||
res.statusCode = 404;
|
||||
res.end(JSON.stringify({ error: "Vehicle not found" }));
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
battery_level: vehicle.soc,
|
||||
battery_range_km: vehicle.range,
|
||||
plugged_in: true,
|
||||
charging: false,
|
||||
odometer: 10000,
|
||||
is_preconditioning: false,
|
||||
charge_current_request: 10,
|
||||
};
|
||||
res.end(JSON.stringify(data));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const shellyMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
// simulate a shelly gen2 switch device api. implement power and energy
|
||||
if (req.originalUrl === "/shelly") {
|
||||
res.end(JSON.stringify({ gen: 2 }));
|
||||
} else if (req.originalUrl === "/rpc/Shelly.ListMethods") {
|
||||
res.end(JSON.stringify({ methods: ["Switch.GetStatus"] }));
|
||||
} else if (req.originalUrl === "/rpc/Switch.GetStatus?id=0") {
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
apower: state.site.pv.power,
|
||||
aenergy: { total: state.site.pv.energy },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
// simulate a shelly gen2 switch device api. implement power and energy
|
||||
if (req.originalUrl === "/shelly") {
|
||||
res.end(JSON.stringify({ gen: 2 }));
|
||||
} else if (req.originalUrl === "/rpc/Shelly.ListMethods") {
|
||||
res.end(JSON.stringify({ methods: ["Switch.GetStatus"] }));
|
||||
} else if (req.originalUrl === "/rpc/Switch.GetStatus?id=0") {
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
apower: state.site.pv.power,
|
||||
aenergy: { total: state.site.pv.energy },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
export default () => ({
|
||||
name: "api",
|
||||
enforce: "pre",
|
||||
configureServer(server: ViteDevServer) {
|
||||
console.log("[simulator] configured");
|
||||
return () => {
|
||||
server.middlewares.use(loggingMiddleware);
|
||||
server.middlewares.use(bodyParser.json());
|
||||
server.middlewares.use(stateApiMiddleware);
|
||||
server.middlewares.use(openemsMiddleware);
|
||||
server.middlewares.use(teslaloggerMiddleware);
|
||||
server.middlewares.use(shellyMiddleware);
|
||||
};
|
||||
},
|
||||
name: "api",
|
||||
enforce: "pre",
|
||||
configureServer(server: ViteDevServer) {
|
||||
console.log("[simulator] configured");
|
||||
return () => {
|
||||
server.middlewares.use(loggingMiddleware);
|
||||
server.middlewares.use(bodyParser.json());
|
||||
server.middlewares.use(stateApiMiddleware);
|
||||
server.middlewares.use(openemsMiddleware);
|
||||
server.middlewares.use(teslaloggerMiddleware);
|
||||
server.middlewares.use(shellyMiddleware);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>evcc Simulator</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>evcc Simulator</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,10 +3,10 @@ import vue from "@vitejs/plugin-vue";
|
|||
import api from "./api";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
// @ts-expect-error the in api() declared 'enforce' property is not supported in TypeScript but in JavaScript
|
||||
api(),
|
||||
],
|
||||
server: { port: 7072, host: true },
|
||||
plugins: [
|
||||
vue(),
|
||||
// @ts-expect-error the in api() declared 'enforce' property is not supported in TypeScript but in JavaScript
|
||||
api(),
|
||||
],
|
||||
server: { port: 7072, host: true },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
import { expect, type Page, type Locator } from "@playwright/test";
|
||||
|
||||
export async function enableExperimental(page: Page): Promise<void> {
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await page.getByTestId("topnavigation-settings").click();
|
||||
await page.getByLabel("Experimental 🧪").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expect(page.locator(".modal-backdrop")).not.toBeVisible();
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await page.getByTestId("topnavigation-settings").click();
|
||||
await page.getByLabel("Experimental 🧪").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expect(page.locator(".modal-backdrop")).not.toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectModalVisible(modal: Locator): Promise<void> {
|
||||
await expect(modal).toBeVisible();
|
||||
await expect(modal).toHaveAttribute("aria-hidden", "false");
|
||||
await expect(modal).toBeVisible();
|
||||
await expect(modal).toHaveAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
||||
export async function expectModalHidden(modal: Locator): Promise<void> {
|
||||
await expect(modal).not.toBeVisible();
|
||||
await expect(modal).toHaveAttribute("aria-hidden", "true");
|
||||
await expect(modal).not.toBeVisible();
|
||||
await expect(modal).toHaveAttribute("aria-hidden", "true");
|
||||
}
|
||||
|
||||
export async function editorClear(editor: Locator, iterations = 6): Promise<void> {
|
||||
await editor.locator(".view-line").nth(0).click();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await editor.page().keyboard.press("ControlOrMeta+KeyA", { delay: 10 });
|
||||
await editor.page().keyboard.press("Backspace", { delay: 10 });
|
||||
}
|
||||
await editor.locator(".view-line").nth(0).click();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await editor.page().keyboard.press("ControlOrMeta+KeyA", { delay: 10 });
|
||||
await editor.page().keyboard.press("Backspace", { delay: 10 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function editorPaste(editor: Locator, page: Page, text: string): Promise<void> {
|
||||
await editor.locator(".view-line").nth(0).click();
|
||||
await page.evaluate((text) => navigator.clipboard.writeText(text), text);
|
||||
await page.keyboard.press("ControlOrMeta+KeyV", { delay: 50 });
|
||||
await editor.locator(".view-line").nth(0).click();
|
||||
await page.evaluate((text) => navigator.clipboard.writeText(text), text);
|
||||
await page.keyboard.press("ControlOrMeta+KeyV", { delay: 50 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue