diff --git a/tools/build-debian-package.sh b/tools/build-debian-package.sh index 15da1c5b623..594c7ad505a 100755 --- a/tools/build-debian-package.sh +++ b/tools/build-debian-package.sh @@ -4,12 +4,14 @@ set -e +DEBIAN_UI_LOCATION=tools/debian/usr/share/openems/www +DEBIAN_EDGE_LOCATION=tools/debian/usr/lib/openems/ + main() { initialize_environment print_header check_dependencies common_update_version_in_code - common_build_edge_and_ui_in_parallel prepare_deb_template build_deb create_version_file @@ -25,23 +27,8 @@ initialize_environment() { # Include commons source $SCRIPT_DIR/common.sh common_initialize_environment - - # Build detailed SNAPSHOT name - if [[ "$VERSION" == *"-SNAPSHOT" ]]; then - # Replace unwanted characters with '.', compliant with Debian version - # Ref: https://unix.stackexchange.com/a/23673 - VERSION_DEV_BRANCH="$(git branch --show-current)" - VERSION_DEV_COMMIT="" - if [[ $(git diff --stat) != '' ]]; then - VERSION_DEV_COMMIT="dirty" - else - VERSION_DEV_COMMIT="$(git rev-parse --short HEAD)" - fi - VERSION_DEV_BUILD_TIME=$(date "+%Y%m%d.%H%M") - # Compliant with https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-version - VERSION_STRING="$(echo $VERSION_DEV_BRANCH | tr -cs 'a-zA-Z0-9\n' '.').${VERSION_DEV_BUILD_TIME}.${VERSION_DEV_COMMIT}" - VERSION="${VERSION/-SNAPSHOT/"-${VERSION_STRING}"}" - fi + common_build_snapshot_version + DEB_FILE="${PACKAGE_NAME}.deb" VERSION_FILE="${PACKAGE_NAME}.version" } @@ -66,13 +53,24 @@ prepare_deb_template() { sed --in-place "s/^\(Version: \).*$/\1$VERSION/" tools/debian/DEBIAN/control echo "## Add OpenEMS Edge" - mkdir -p tools/debian/usr/lib/openems/ - cp io.openems.edge.application/generated/distributions/executable/EdgeApp.jar tools/debian/usr/lib/openems/openems.jar + if [ -f "$DEBIAN_EDGE_LOCATION/openems.jar" ]; then + echo "openems.jar exists. Skipping common_build_edge." + else + mkdir -p "$DEBIAN_EDGE_LOCATION" + echo "openems.jar does not exist. Building common_build_edge." + common_build_edge + cp build/openems-edge.jar $DEBIAN_EDGE_LOCATION/openems.jar + fi echo "## Add OpenEMS UI" - rm -Rf tools/debian/usr/share/openems/www/* - mkdir -p tools/debian/usr/share/openems/www - cp -R ui/target/* tools/debian/usr/share/openems/www + if [ -d "$DEBIAN_UI_LOCATION" ] && [ "$(ls -A $DEBIAN_UI_LOCATION)" ] ; then + echo "openems.ui exists. Skipping common_build_ui." + else + mkdir -p "$DEBIAN_UI_LOCATION" + echo "openems.ui does not exist. Building common_build_ui." + common_build_ui + cp -R ui/target/* "$DEBIAN_UI_LOCATION" + fi } build_deb() { diff --git a/tools/common.sh b/tools/common.sh index 48c8883b8e8..3b1d74d0c41 100644 --- a/tools/common.sh +++ b/tools/common.sh @@ -19,6 +19,28 @@ common_initialize_environment() { VERSION_STRING=$(echo $VERSION | cut -s -d'-' -f2) } +common_build_snapshot_version() { + if [[ "$VERSION" == *"-SNAPSHOT" ]]; then + echo "1" + # Replace unwanted characters with '.', compliant with Debian version + # Ref: https://unix.stackexchange.com/a/23673 + VERSION_DEV_BRANCH="$(git branch --show-current)" + echo "2" + VERSION_DEV_COMMIT="" + if [[ $(git diff --stat) != '' ]]; then + VERSION_DEV_COMMIT="dirty" + else + VERSION_DEV_COMMIT="$(git rev-parse --short HEAD)" + fi + echo "3" + VERSION_DEV_BUILD_TIME=$(date "+%Y%m%d.%H%M") + # Compliant with https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-version + echo "4" + VERSION_STRING="$(echo $VERSION_DEV_BRANCH | tr -cs 'a-zA-Z0-9\n' '.').${VERSION_DEV_BUILD_TIME}.${VERSION_DEV_COMMIT}" + VERSION="${VERSION/-SNAPSHOT/"-${VERSION_STRING}"}" + fi +} + # Inserts the version number into the Code common_update_version_in_code() { echo "# Update version in Code" @@ -51,7 +73,7 @@ common_build_edge_and_ui_in_parallel() { # Build OpenEMS Edge common_build_edge() { echo "# Build OpenEMS Edge" - ./gradlew --build-cache build buildEdge resolve.EdgeApp resolve.BackendApp + ./gradlew $@ --build-cache build buildEdge resolve.EdgeApp resolve.BackendApp git diff --exit-code io.openems.edge.application/EdgeApp.bndrun io.openems.backend.application/BackendApp.bndrun } diff --git a/ui/src/app/edge/history/abstracthistorychart.ts b/ui/src/app/edge/history/abstracthistorychart.ts index 713ca4245df..8f35391e8d8 100644 --- a/ui/src/app/edge/history/abstracthistorychart.ts +++ b/ui/src/app/edge/history/abstracthistorychart.ts @@ -7,11 +7,12 @@ import { QueryHistoricTimeseriesDataRequest } from "src/app/shared/jsonrpc/reque import { QueryHistoricTimeseriesEnergyPerPeriodRequest } from 'src/app/shared/jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest'; import { QueryHistoricTimeseriesDataResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesDataResponse"; import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse'; +import { HistoryUtils } from 'src/app/shared/service/utils'; import { ChannelAddress, Edge, EdgeConfig, Service, Utils } from "src/app/shared/shared"; +import { DateUtils } from 'src/app/shared/utils/date/dateutils'; +import { DateTimeUtils } from 'src/app/shared/utils/datetime/datetime-utils'; import { calculateResolution, ChartOptions, DEFAULT_TIME_CHART_OPTIONS, EMPTY_DATASET, Resolution, TooltipItem } from './shared'; -import { HistoryUtils } from 'src/app/shared/service/utils'; -import { DateUtils } from 'src/app/shared/utils/dateutils/dateutils'; // NOTE: Auto-refresh of widgets is currently disabled to reduce server load export abstract class AbstractHistoryChart { @@ -104,7 +105,7 @@ export abstract class AbstractHistoryChart { this.service.stopSpinner(this.spinnerId); this.initializeChart(); } - return response; + return DateTimeUtils.normalizeTimestamps(resolution.unit, response); }); return result; @@ -145,7 +146,7 @@ export abstract class AbstractHistoryChart { this.service.stopSpinner(this.spinnerId); this.initializeChart(); } - return response; + return DateTimeUtils.normalizeTimestamps(resolution.unit, response); }); return response; } diff --git a/ui/src/app/edge/history/abstracthistorywidget.ts b/ui/src/app/edge/history/abstracthistorywidget.ts index 1ba071dae67..9bfcd0a7af9 100644 --- a/ui/src/app/edge/history/abstracthistorywidget.ts +++ b/ui/src/app/edge/history/abstracthistorywidget.ts @@ -3,7 +3,7 @@ import { QueryHistoricTimeseriesDataRequest } from 'src/app/shared/jsonrpc/reque import { QueryHistoricTimeseriesDataResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesDataResponse'; import { ChannelAddress, Edge, EdgeConfig, Service } from 'src/app/shared/shared'; import { calculateResolution } from './shared'; -import { DateUtils } from 'src/app/shared/utils/dateutils/dateutils'; +import { DateUtils } from 'src/app/shared/utils/date/dateutils'; // NOTE: Auto-refresh of widgets is currently disabled to reduce server load export abstract class AbstractHistoryWidget { diff --git a/ui/src/app/edge/history/common/consumption/chart/channels.spec.ts b/ui/src/app/edge/history/common/consumption/chart/channels.spec.ts index c0bd6115522..259ad7753a5 100644 --- a/ui/src/app/edge/history/common/consumption/chart/channels.spec.ts +++ b/ui/src/app/edge/history/common/consumption/chart/channels.spec.ts @@ -18,7 +18,7 @@ export namespace History { "_sum/ConsumptionActiveEnergyL2": 30996, "_sum/ConsumptionActiveEnergyL3": 30996, "evcs0/ActiveConsumptionEnergy": 13720, - "meter0/ActiveConsumptionEnergy": 15930, + "meter0/ActiveProductionEnergy": 15930, }, }), dataChannelWithValues: new QueryHistoricTimeseriesDataResponse("0", { @@ -42,7 +42,7 @@ export namespace History { data: { "_sum/ConsumptionActiveEnergy": 354079, "evcs0/ActiveConsumptionEnergy": 157027, - "meter0/ActiveConsumptionEnergy": 100000, + "meter0/ActiveProductionEnergy": 100000, }, }), dataChannelWithValues: new QueryHistoricTimeseriesDataResponse("0", { @@ -62,7 +62,7 @@ export namespace History { data: { '_sum/ConsumptionActiveEnergy': 1033427, "evcs0/ActiveConsumptionEnergy": 328451, - "meter0/ActiveConsumptionEnergy": 21649, + "meter0/ActiveProductionEnergy": 21649, }, }), energyPerPeriodChannelWithValues: @@ -70,7 +70,7 @@ export namespace History { data: { '_sum/ConsumptionActiveEnergy': [1784.3478512581187, 955.1978135997077, 1604.9176251387696, 1821.5474663613152, 1204.011627189301, 1037.408900359932, 1287.891020875591, 1183.057735422114, 1027.8784377276404, 1412.783196754379, 737.2379614126091, 730.6143394985477, 1727.4568452231197, 2045.4827463867603, 1289.9867373938441, 1866.5731598778827, 896.448259060122, 1271.0485678635469, 1489.7323302960576, 1367.9696489029907, 2375.6092366846033, 2368.729102836224, 2686.60326649514, 1597.0390753045413, 972.6650191463931, 1774.233690168573, 2953.838257099637, 917.3087792235759, 1172.9800154902882, null, null], "evcs0/ActiveConsumptionEnergy": [598.058461158158, 0, 607.3861225965935, 891.3491768679577, 269.6236843407865, 0, 306.41009901340226, 220.51407209843148, 50.15525733301707, 356.42246970726825, 0, 0, 880.0942036863182, 977.4033026217928, 210.23831546417276, 787.0712558876392, 0, 165.50743075023163, 513.2605942604259, 149.06825174512016, 883.6100586172083, 1203.117101530366, 1467.2314708234808, 553.6230686820822, 0, 714.8486366912176, 1836.7765179313803, 0, 300.374916784946, null, null], - "meter0/ActiveConsumptionEnergy": [15.53700680772126, 13.97856617670663, 16.22420643945345, 15.425155003970989, 16.41557086346929, 15.280936198647838, 15.028196655704793, 15.146427851947054, 15.567356482244767, 17.404197969735606, 17.635152684968116, 14.140394156739468, 16.107488806188936, 16.75001277671301, 15.512752420609466, 14.951999780788457, 13.733457057782298, 16.868671206682027, 14.770978996449593, 16.47697124898351, 16.772182319685665, 16.288562161254703, 15.417362341926745, 15.892028990939403, 13.811584043067414, 14.81782772305683, 14.165639305307824, 16.223347257543285, 14.495672387672808, null, null], + "meter0/ActiveProductionEnergy": [15.53700680772126, 13.97856617670663, 16.22420643945345, 15.425155003970989, 16.41557086346929, 15.280936198647838, 15.028196655704793, 15.146427851947054, 15.567356482244767, 17.404197969735606, 17.635152684968116, 14.140394156739468, 16.107488806188936, 16.75001277671301, 15.512752420609466, 14.951999780788457, 13.733457057782298, 16.868671206682027, 14.770978996449593, 16.47697124898351, 16.772182319685665, 16.288562161254703, 15.417362341926745, 15.892028990939403, 13.811584043067414, 14.81782772305683, 14.165639305307824, 16.223347257543285, 14.495672387672808, null, null], }, timestamps: ["2023-05-31T22:00:00Z", "2023-06-01T22:00:00Z", "2023-06-02T22:00:00Z", "2023-06-03T22:00:00Z", "2023-06-04T22:00:00Z", "2023-06-05T22:00:00Z", "2023-06-06T22:00:00Z", "2023-06-07T22:00:00Z", "2023-06-08T22:00:00Z", "2023-06-09T22:00:00Z", "2023-06-10T22:00:00Z", "2023-06-11T22:00:00Z", "2023-06-12T22:00:00Z", "2023-06-13T22:00:00Z", "2023-06-14T22:00:00Z", "2023-06-15T22:00:00Z", "2023-06-16T22:00:00Z", "2023-06-17T22:00:00Z", "2023-06-18T22:00:00Z", "2023-06-19T22:00:00Z", "2023-06-20T22:00:00Z", "2023-06-21T22:00:00Z", "2023-06-22T22:00:00Z", "2023-06-23T22:00:00Z", "2023-06-24T22:00:00Z", "2023-06-25T22:00:00Z", "2023-06-26T22:00:00Z", "2023-06-27T22:00:00Z", "2023-06-28T22:00:00Z", "2023-06-29T22:00:00Z"], }), @@ -86,7 +86,7 @@ export namespace History { "_sum/ConsumptionActiveEnergyL2": 4954551, "_sum/ConsumptionActiveEnergyL3": 4954551, 'evcs0/ActiveConsumptionEnergy': 2071139, - 'meter0/ActiveConsumptionEnergy': 1908650, + 'meter0/ActiveProductionEnergy': 1908650, }, }), energyPerPeriodChannelWithValues: @@ -97,7 +97,7 @@ export namespace History { '_sum/ConsumptionActiveEnergyL2': [560980.3333333334, 611576.3333333334, 601740.6666666666, 644714.6666666666, 585859, 503784, 768996, 676644.3333333334, 0, 0, 0, 0], '_sum/ConsumptionActiveEnergyL3': [560980.3333333334, 611576.3333333334, 601740.6666666666, 644714.6666666666, 585859, 503784, 768996, 676644.3333333334, 0, 0, 0, 0], 'evcs0/ActiveConsumptionEnergy': [69104, 131703, 25773, 51085, 169943, 332522, 748189, 540740, 0, 0, 0, 0], - 'meter0/ActiveConsumptionEnergy': [338070, 312380, 298930, 317700, 200210, 151160, 145880, 144280, 0, 0, 0, 0], + 'meter0/ActiveProductionEnergy': [338070, 312380, 298930, 317700, 200210, 151160, 145880, 144280, 0, 0, 0, 0], }, timestamps: ["2022-12-31T23:00:00Z", "2023-01-31T23:00:00Z", "2023-02-28T23:00:00Z", "2023-03-31T22:00:00Z", "2023-04-30T22:00:00Z", "2023-05-31T22:00:00Z", "2023-06-30T22:00:00Z", "2023-07-31T22:00:00Z", "2023-08-31T22:00:00Z", "2023-09-30T22:00:00Z", "2023-10-31T23:00:00Z", "2023-11-30T23:00:00Z"], }), diff --git a/ui/src/app/edge/history/common/consumption/chart/chart.spec.ts b/ui/src/app/edge/history/common/consumption/chart/chart.spec.ts index e38ba109db9..68d01e1c094 100644 --- a/ui/src/app/edge/history/common/consumption/chart/chart.spec.ts +++ b/ui/src/app/edge/history/common/consumption/chart/chart.spec.ts @@ -1,9 +1,9 @@ import { ChartConfig, DummyConfig } from "src/app/shared/edge/edgeconfig.spec"; import { sharedSetup, TestContext } from "../../../../../shared/test/utils.spec"; +import { DATA, LABELS } from "../../energy/chart/chart.constants.spec"; import { History } from "./channels.spec"; import { expectView } from "./chart.constants.spec"; -import { DATA, LABELS } from "../../energy/chart/chart.constants.spec"; describe('History Consumption', () => { const defaultEMS = DummyConfig.from( @@ -12,8 +12,8 @@ describe('History Consumption', () => { ); let TEST_CONTEXT: TestContext; - beforeEach(() => - TEST_CONTEXT = sharedSetup(), + beforeEach(async () => + TEST_CONTEXT = await sharedSetup(), ); it('#getChartData()', () => { diff --git a/ui/src/app/edge/history/common/energy/chart/chart.spec.ts b/ui/src/app/edge/history/common/energy/chart/chart.spec.ts index 62a2b264b05..e46dcb5c3a3 100644 --- a/ui/src/app/edge/history/common/energy/chart/chart.spec.ts +++ b/ui/src/app/edge/history/common/energy/chart/chart.spec.ts @@ -12,8 +12,8 @@ describe('History EnergyMonitor', () => { ); let TEST_CONTEXT: TestContext; - beforeEach(() => - TEST_CONTEXT = sharedSetup(), + beforeEach(async () => + TEST_CONTEXT = await sharedSetup(), ); it('getChartData()', () => { diff --git a/ui/src/app/edge/history/common/grid/chart/chart.spec.ts b/ui/src/app/edge/history/common/grid/chart/chart.spec.ts index 71c46867002..644eb2d84c2 100644 --- a/ui/src/app/edge/history/common/grid/chart/chart.spec.ts +++ b/ui/src/app/edge/history/common/grid/chart/chart.spec.ts @@ -12,8 +12,8 @@ describe('History Grid', () => { ); let TEST_CONTEXT: TestContext; - beforeEach(() => - TEST_CONTEXT = sharedSetup(), + beforeEach(async () => + TEST_CONTEXT = await sharedSetup(), ); it('#getChartData()', () => { diff --git a/ui/src/app/edge/history/historydataservice.ts b/ui/src/app/edge/history/historydataservice.ts index cfd0a2bedf2..0de2e34ddca 100644 --- a/ui/src/app/edge/history/historydataservice.ts +++ b/ui/src/app/edge/history/historydataservice.ts @@ -3,7 +3,7 @@ import { Inject, Injectable } from "@angular/core"; import { DataService } from "../../shared/genericComponents/shared/dataservice"; import { QueryHistoricTimeseriesEnergyResponse } from "../../shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; import { ChannelAddress, Edge } from "../../shared/shared"; -import { DateUtils } from "src/app/shared/utils/dateutils/dateutils"; +import { DateUtils } from "src/app/shared/utils/date/dateutils"; import { QueryHistoricTimeseriesEnergyRequest } from "src/app/shared/jsonrpc/request/queryHistoricTimeseriesEnergyRequest"; import { Websocket } from "src/app/shared/service/websocket"; import { Service } from "src/app/shared/service/service"; diff --git a/ui/src/app/edge/history/shared.ts b/ui/src/app/edge/history/shared.ts index 7e0ba40932e..d3f96907fc4 100644 --- a/ui/src/app/edge/history/shared.ts +++ b/ui/src/app/edge/history/shared.ts @@ -287,57 +287,59 @@ export function calculateActiveTimeOverPeriod(channel: ChannelAddress, queryResu * @param toDate the To-Date * @returns resolution and timeformat */ -export function calculateResolution(service: Service, fromDate: Date, toDate: Date): { resolution: Resolution, timeFormat: 'day' | 'month' | 'hour' } { +export function calculateResolution(service: Service, fromDate: Date, toDate: Date): { resolution: Resolution, timeFormat: 'day' | 'month' | 'hour' | 'year' } { let days = Math.abs(differenceInDays(toDate, fromDate)); - let resolution: { resolution: Resolution, timeFormat: 'day' | 'month' | 'hour' }; + let resolution: { resolution: Resolution, timeFormat: 'day' | 'month' | 'hour' | 'year' }; if (days <= 1) { - resolution = { resolution: { value: 5, unit: Unit.MINUTES }, timeFormat: 'hour' }; // 5 Minutes + resolution = { resolution: { value: 5, unit: ChronoUnit.Type.MINUTES }, timeFormat: 'hour' }; // 5 Minutes } else if (days == 2) { if (service.isSmartphoneResolution) { - resolution = { resolution: { value: 1, unit: Unit.DAYS }, timeFormat: 'hour' }; // 1 Day + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.DAYS }, timeFormat: 'hour' }; // 1 Day } else { - resolution = { resolution: { value: 10, unit: Unit.MINUTES }, timeFormat: 'hour' }; // 1 Hour + resolution = { resolution: { value: 10, unit: ChronoUnit.Type.MINUTES }, timeFormat: 'hour' }; // 1 Hour } } else if (days <= 4) { if (service.isSmartphoneResolution) { - resolution = { resolution: { value: 1, unit: Unit.DAYS }, timeFormat: 'day' }; // 1 Day + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.DAYS }, timeFormat: 'day' }; // 1 Day } else { - resolution = { resolution: { value: 1, unit: Unit.HOURS }, timeFormat: 'hour' }; // 1 Hour + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.HOURS }, timeFormat: 'hour' }; // 1 Hour } } else if (days <= 6) { // >> show Hours - resolution = { resolution: { value: 1, unit: Unit.HOURS }, timeFormat: 'day' }; // 1 Day + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.HOURS }, timeFormat: 'day' }; // 1 Day } else if (days <= 31 && service.isSmartphoneResolution) { // Smartphone-View: show 31 days in daily view - resolution = { resolution: { value: 1, unit: Unit.DAYS }, timeFormat: 'day' }; // 1 Day + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.DAYS }, timeFormat: 'day' }; // 1 Day } else if (days <= 90) { - resolution = { resolution: { value: 1, unit: Unit.DAYS }, timeFormat: 'day' }; // 1 Day + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.DAYS }, timeFormat: 'day' }; // 1 Day } else if (days <= 144) { // >> show Days if (service.isSmartphoneResolution == true) { - resolution = { resolution: { value: 1, unit: Unit.MONTHS }, timeFormat: 'month' }; // 1 Month + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.MONTHS }, timeFormat: 'month' }; // 1 Month } else { - resolution = { resolution: { value: 1, unit: Unit.DAYS }, timeFormat: 'day' }; // 1 Day + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.DAYS }, timeFormat: 'day' }; // 1 Day } + } else if (days <= 365) { + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.MONTHS }, timeFormat: 'month' }; // 1 Day } else { - // >> show Months - resolution = { resolution: { value: 1, unit: Unit.MONTHS }, timeFormat: 'month' }; // 1 Month + // >> show Years + resolution = { resolution: { value: 1, unit: ChronoUnit.Type.YEARS }, timeFormat: 'year' }; // 1 Month } return resolution; } /** * Returns true if Chart Label should be visible. Defaults to true. - * - * Compares only the first part of the label string - without a value or unit. - * + * + * Compares only the first part of the label string - without a value or ChronoUnit.Type. + * * @param label the Chart label * @param orElse the default, in case no value was stored yet in Session-Storage * @returns true for visible labels; hidden otherwise @@ -368,15 +370,32 @@ export function setLabelVisible(label: string, visible: boolean | null): void { export type Resolution = { value: number, - unit: Unit + unit: ChronoUnit.Type } -export enum Unit { - SECONDS = "Seconds", - MINUTES = "Minutes", - HOURS = "Hours", - DAYS = "Days", - MONTHS = "Months", +export namespace ChronoUnit { + + export enum Type { + SECONDS = "Seconds", + MINUTES = "Minutes", + HOURS = "Hours", + DAYS = "Days", + MONTHS = "Months", + YEARS = "Years" + } + + /** + * Evaluates whether "ChronoUnit 1" is equal or a bigger period than "ChronoUnit 2". + * + * @param unit1 the ChronoUnit 1 + * @param unit2 the ChronoUnit 2 + * @return true if "ChronoUnit 1" is equal or a bigger period than "ChronoUnit 2" + */ + export function isAtLeast(unit1: Type, unit2: Type) { + const currentUnit = Object.values(Type).indexOf(unit1); + const unitToCompareTo = Object.values(Type).indexOf(unit2); + return currentUnit >= unitToCompareTo; + } } export type ChartData = { diff --git a/ui/src/app/edge/history/timeofusetariffdischarge/chart.component.ts b/ui/src/app/edge/history/timeofusetariffdischarge/chart.component.ts index f994386329e..5ff3c8152de 100644 --- a/ui/src/app/edge/history/timeofusetariffdischarge/chart.component.ts +++ b/ui/src/app/edge/history/timeofusetariffdischarge/chart.component.ts @@ -5,11 +5,11 @@ import { TranslateService } from '@ngx-translate/core'; import { differenceInDays } from 'date-fns'; import { DefaultTypes } from 'src/app/shared/service/defaulttypes'; +import { TimeOfUseTariffUtils } from 'src/app/shared/service/utils'; import { QueryHistoricTimeseriesDataResponse } from '../../../shared/jsonrpc/response/queryHistoricTimeseriesDataResponse'; import { ChannelAddress, Currency, Edge, EdgeConfig, Service } from '../../../shared/shared'; import { AbstractHistoryChart } from '../abstracthistorychart'; -import { Data, TooltipItem, Unit } from '../shared'; -import { TimeOfUseTariffUtils } from 'src/app/shared/service/utils'; +import { ChronoUnit, Data, TooltipItem } from '../shared'; // TODO rename folder; remove 'Discharge' @Component({ @@ -57,7 +57,7 @@ export class TimeOfUseTariffDischargeChartComponent extends AbstractHistoryChart this.colors = []; this.loading = true; - this.queryHistoricTimeseriesData(this.period.from, this.period.to, { value: 15, unit: Unit.MINUTES }).then(response => { + this.queryHistoricTimeseriesData(this.period.from, this.period.to, { value: 15, unit: ChronoUnit.Type.MINUTES }).then(response => { this.service.getConfig().then(config => { let result = (response as QueryHistoricTimeseriesDataResponse).result; diff --git a/ui/src/app/edge/live/Controller/Ess/GridOptimizedCharge/modal/predictionChart.ts b/ui/src/app/edge/live/Controller/Ess/GridOptimizedCharge/modal/predictionChart.ts index 8be72da193b..91794e2725c 100644 --- a/ui/src/app/edge/live/Controller/Ess/GridOptimizedCharge/modal/predictionChart.ts +++ b/ui/src/app/edge/live/Controller/Ess/GridOptimizedCharge/modal/predictionChart.ts @@ -3,7 +3,7 @@ import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Data } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { AbstractHistoryChart } from 'src/app/edge/history/abstracthistorychart'; -import { ChartOptions, DEFAULT_TIME_CHART_OPTIONS, TooltipItem, Unit } from 'src/app/edge/history/shared'; +import { ChartOptions, ChronoUnit, DEFAULT_TIME_CHART_OPTIONS, TooltipItem } from 'src/app/edge/history/shared'; import { DefaultTypes } from 'src/app/shared/service/defaulttypes'; import { ChannelAddress, Edge, EdgeConfig, Service, Utils } from 'src/app/shared/shared'; @@ -49,7 +49,7 @@ export class PredictionChartComponent extends AbstractHistoryChart implements On this.loading = true; this.colors = []; - this.queryHistoricTimeseriesData(PredictionChartComponent.DEFAULT_PERIOD.from, PredictionChartComponent.DEFAULT_PERIOD.to, { unit: Unit.MINUTES, value: 5 }).then(response => { + this.queryHistoricTimeseriesData(PredictionChartComponent.DEFAULT_PERIOD.from, PredictionChartComponent.DEFAULT_PERIOD.to, { unit: ChronoUnit.Type.MINUTES, value: 5 }).then(response => { let result = response.result; let datasets = []; diff --git a/ui/src/app/edge/live/common/autarchy/modal/modal.spec.ts b/ui/src/app/edge/live/common/autarchy/modal/modal.spec.ts index 11487c15960..22049048563 100644 --- a/ui/src/app/edge/live/common/autarchy/modal/modal.spec.ts +++ b/ui/src/app/edge/live/common/autarchy/modal/modal.spec.ts @@ -9,13 +9,12 @@ export const VIEW_CONTEXT: OeFormlyViewTester.Context = ({}); export function expectView(testContext: TestContext, viewContext: OeFormlyViewTester.Context, view: OeFormlyViewTester.View): void { const generatedView = OeFormlyViewTester.apply(ModalComponent.generateView(testContext.translate), viewContext); - expect(generatedView).toEqual(view); }; describe('Autarchy - Modal', () => { let TEST_CONTEXT: TestContext; - beforeEach(() => TEST_CONTEXT = sharedSetup()); + beforeEach(async () => TEST_CONTEXT = await sharedSetup()); it('generateView()', () => { { diff --git a/ui/src/app/edge/live/common/consumption/modal/modal.spec.ts b/ui/src/app/edge/live/common/consumption/modal/modal.spec.ts index 726d839f24b..a93aed2185b 100644 --- a/ui/src/app/edge/live/common/consumption/modal/modal.spec.ts +++ b/ui/src/app/edge/live/common/consumption/modal/modal.spec.ts @@ -7,7 +7,7 @@ import { expectView } from "./modal.constants.spec"; describe('Consumption - Modal', () => { let TEST_CONTEXT; - beforeEach(() => TEST_CONTEXT = sharedSetup()); + beforeEach(async () => TEST_CONTEXT = await sharedSetup()); it('generateView()', () => { diff --git a/ui/src/app/edge/live/common/grid/modal/modal.spec.ts b/ui/src/app/edge/live/common/grid/modal/modal.spec.ts index 77b4aad58b8..3372107e288 100644 --- a/ui/src/app/edge/live/common/grid/modal/modal.spec.ts +++ b/ui/src/app/edge/live/common/grid/modal/modal.spec.ts @@ -19,7 +19,7 @@ const VIEW_CONTEXT = (properties?: {}): OeFormlyViewTester.Context => ({ describe('Grid - Modal', () => { let TEST_CONTEXT: TestContext; - beforeEach(() => TEST_CONTEXT = sharedSetup()); + beforeEach(async () => TEST_CONTEXT = await sharedSetup()); it('generateView()', () => { { diff --git a/ui/src/app/edge/live/common/selfconsumption/modal/modal.spec.ts b/ui/src/app/edge/live/common/selfconsumption/modal/modal.spec.ts index 2ae12ca5b54..fb9447c958c 100644 --- a/ui/src/app/edge/live/common/selfconsumption/modal/modal.spec.ts +++ b/ui/src/app/edge/live/common/selfconsumption/modal/modal.spec.ts @@ -15,7 +15,7 @@ export function expectView(testContext: TestContext, viewContext: OeFormlyViewTe describe('SelfConsumption - Modal', () => { let TEST_CONTEXT: TestContext; - beforeEach(() => TEST_CONTEXT = sharedSetup()); + beforeEach(async () => TEST_CONTEXT = await sharedSetup()); it('generateView()', () => { { diff --git a/ui/src/app/edge/live/common/storage/storage.component.ts b/ui/src/app/edge/live/common/storage/storage.component.ts index 2267cd848e7..a6041ff980e 100644 --- a/ui/src/app/edge/live/common/storage/storage.component.ts +++ b/ui/src/app/edge/live/common/storage/storage.component.ts @@ -2,7 +2,7 @@ import { formatNumber } from '@angular/common'; import { Component } from '@angular/core'; import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget'; import { CurrentData } from "src/app/shared/shared"; -import { DateUtils } from 'src/app/shared/utils/dateutils/dateutils'; +import { DateUtils } from 'src/app/shared/utils/date/dateutils'; import { ChannelAddress, EdgeConfig, Utils } from '../../../../shared/shared'; import { StorageModalComponent } from './modal/modal.component'; diff --git a/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts b/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts index 514ca93ba01..8394f2f93fe 100644 --- a/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts +++ b/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts @@ -8,20 +8,20 @@ import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from 'src/app/shared/j import { DefaultTypes } from 'src/app/shared/service/defaulttypes'; import { v4 as uuidv4 } from 'uuid'; -import { startOfMonth } from 'date-fns'; -import { calculateResolution, ChartOptions, DEFAULT_TIME_CHART_OPTIONS, DEFAULT_TIME_CHART_OPTIONS_WITHOUT_PREDEFINED_Y_AXIS, isLabelVisible, setLabelVisible, TooltipItem, Unit } from '../../../edge/history/shared'; +import { calculateResolution, ChartOptions, ChronoUnit, DEFAULT_TIME_CHART_OPTIONS, DEFAULT_TIME_CHART_OPTIONS_WITHOUT_PREDEFINED_Y_AXIS, isLabelVisible, setLabelVisible, TooltipItem } from '../../../edge/history/shared'; import { JsonrpcResponseError } from '../../jsonrpc/base'; import { QueryHistoricTimeseriesDataRequest } from '../../jsonrpc/request/queryHistoricTimeseriesDataRequest'; import { QueryHistoricTimeseriesEnergyPerPeriodRequest } from '../../jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest'; import { QueryHistoricTimeseriesEnergyRequest } from '../../jsonrpc/request/queryHistoricTimeseriesEnergyRequest'; import { QueryHistoricTimeseriesDataResponse } from '../../jsonrpc/response/queryHistoricTimeseriesDataResponse'; import { QueryHistoricTimeseriesEnergyResponse } from '../../jsonrpc/response/queryHistoricTimeseriesEnergyResponse'; +import { FormatSecondsToDurationPipe } from '../../pipe/formatSecondsToDuration/formatSecondsToDuration.pipe'; import { ChartAxis, HistoryUtils, YAxisTitle } from '../../service/utils'; import { ChannelAddress, Edge, EdgeConfig, Service, Utils } from "../../shared"; -import { DateUtils } from '../../utils/dateutils/dateutils'; -import { FormatSecondsToDurationPipe } from '../../pipe/formatSecondsToDuration/formatSecondsToDuration.pipe'; import { Language } from '../../type/language'; -import { TimeUtils } from '../../utils/timeutils/timeutils'; +import { DateUtils } from '../../utils/date/dateutils'; +import { DateTimeUtils } from '../../utils/datetime/datetime-utils'; +import { TimeUtils } from '../../utils/time/timeutils'; import { Converter } from '../shared/converter'; // NOTE: Auto-refresh of widgets is currently disabled to reduce server load @@ -270,7 +270,7 @@ export abstract class AbstractHistoryChart implements OnInit { let unit = calculateResolution(this.service, this.service.historyPeriod.value.from, this.service.historyPeriod.value.to).resolution.unit; // Show Barchart if resolution is days or months - if (unit == Unit.DAYS || unit == Unit.MONTHS) { + if (ChronoUnit.isAtLeast(unit, ChronoUnit.Type.DAYS)) { Promise.all([ this.queryHistoricTimeseriesEnergyPerPeriod(this.service.historyPeriod.value.from, this.service.historyPeriod.value.to), this.queryHistoricTimeseriesEnergy(this.service.historyPeriod.value.from, this.service.historyPeriod.value.to), @@ -279,9 +279,7 @@ export abstract class AbstractHistoryChart implements OnInit { this.chartObject = this.getChartData(); // TODO after chartjs migration, look for config - if (unit === Unit.MONTHS) { - energyPeriodResponse.result.timestamps[0] = startOfMonth(DateUtils.stringToDate(energyPeriodResponse.result.timestamps[0]))?.toString() ?? energyPeriodResponse.result.timestamps[0]; - } + energyPeriodResponse = DateTimeUtils.normalizeTimestamps(unit, energyPeriodResponse); let displayValues = AbstractHistoryChart.fillChart(this.chartType, this.chartObject, energyPeriodResponse, energyResponse); this.datasets = displayValues.datasets; @@ -297,6 +295,7 @@ export abstract class AbstractHistoryChart implements OnInit { case DefaultTypes.PeriodString.CUSTOM: { barWidthPercentage = 0.7; categoryGapPercentage = 0.4; + break; } case DefaultTypes.PeriodString.MONTH: { if (this.service.isSmartphoneResolution == true) { @@ -306,8 +305,10 @@ export abstract class AbstractHistoryChart implements OnInit { barWidthPercentage = 0.9; categoryGapPercentage = 0.8; } + break; } - case DefaultTypes.PeriodString.YEAR: { + case DefaultTypes.PeriodString.YEAR: + case DefaultTypes.PeriodString.TOTAL: { if (this.service.isSmartphoneResolution == true) { barWidthPercentage = 1; categoryGapPercentage = 0.6; @@ -315,6 +316,7 @@ export abstract class AbstractHistoryChart implements OnInit { barWidthPercentage = 0.8; categoryGapPercentage = 0.8; } + break; } } this.datasets.forEach(element => { @@ -330,6 +332,8 @@ export abstract class AbstractHistoryChart implements OnInit { this.queryHistoricTimeseriesEnergy(this.service.historyPeriod.value.from, this.service.historyPeriod.value.to), ]) .then(([dataResponse, energyResponse]) => { + + dataResponse = DateTimeUtils.normalizeTimestamps(unit, dataResponse); this.chartType = 'line'; this.chartObject = this.getChartData(); let displayValues = AbstractHistoryChart.fillChart(this.chartType, this.chartObject, dataResponse, energyResponse); @@ -407,7 +411,7 @@ export abstract class AbstractHistoryChart implements OnInit { this.service.getConfig().then(async () => { let channelAddresses = (await this.getChannelAddresses()).energyChannels.filter(element => element != null); - let request = new QueryHistoricTimeseriesEnergyPerPeriodRequest(DateUtils.maxDate(fromDate, this.edge?.firstSetupProtocol), toDate, channelAddresses, resolution); + let request = new QueryHistoricTimeseriesEnergyPerPeriodRequest(DateUtils.maxDate(fromDate, edge?.firstSetupProtocol), toDate, channelAddresses, resolution); if (channelAddresses.length > 0) { edge.sendRequest(this.service.websocket, request).then(response => { @@ -459,7 +463,6 @@ export abstract class AbstractHistoryChart implements OnInit { let result: Promise = new Promise((resolve, reject) => { this.service.getCurrentEdge().then(edge => { this.service.getConfig().then(async () => { - let channelAddresses = (await this.getChannelAddresses()).energyChannels?.filter(element => element != null) ?? []; let request = new QueryHistoricTimeseriesEnergyRequest(DateUtils.maxDate(fromDate, edge?.firstSetupProtocol), toDate, channelAddresses); if (channelAddresses.length > 0) { @@ -497,17 +500,16 @@ export abstract class AbstractHistoryChart implements OnInit { */ protected static toTooltipTitle(fromDate: Date, toDate: Date, date: Date, service: Service): string { let unit = calculateResolution(service, fromDate, toDate).resolution.unit; - if (unit == Unit.MONTHS) { - // Yearly view - return date.toLocaleDateString('default', { month: 'long' }); - } else if (unit == Unit.DAYS) { - // Monthly view - return date.toLocaleDateString('default', { day: '2-digit', month: 'long' }); - - } else { - // Default - return date.toLocaleString('default', { day: '2-digit', month: '2-digit', year: '2-digit' }) + ' ' + date.toLocaleTimeString('default', { hour12: false, hour: '2-digit', minute: '2-digit' }); + switch (unit) { + case ChronoUnit.Type.YEARS: + return date.toLocaleDateString('default', { year: 'numeric' }); + case ChronoUnit.Type.MONTHS: + return date.toLocaleDateString('default', { month: 'long' }); + case ChronoUnit.Type.DAYS: + return date.toLocaleDateString('default', { day: '2-digit', month: 'long' }); + default: + return date.toLocaleString('default', { day: '2-digit', month: '2-digit', year: '2-digit' }) + ' ' + date.toLocaleTimeString('default', { hour12: false, hour: '2-digit', minute: '2-digit' }); } } @@ -515,9 +517,7 @@ export abstract class AbstractHistoryChart implements OnInit { translate: TranslateService, legendOptions: { label: string, strokeThroughHidingStyle: boolean }[], channelData: { data: { [name: string]: number[] } }, locale: string): ChartOptions { let tooltipsLabel: string | null = null; - let options = Utils.deepCopy(Utils.deepCopy(DEFAULT_TIME_CHART_OPTIONS_WITHOUT_PREDEFINED_Y_AXIS)); - chartObject.yAxes.forEach((element) => { switch (element.unit) { @@ -675,7 +675,7 @@ export abstract class AbstractHistoryChart implements OnInit { )).reduce((a, e) => a + parseFloat(e.yLabel), 0); if (afterTitle) { - return afterTitle + ": " + formatNumber(totalValue, 'de', chartObject.tooltip.formatNumber) + ' ' + tooltipsLabel; + return afterTitle + ": " + formatNumber(totalValue, 'de', chartObject.tooltip.formatNumber) + ' ' + tooltipsLabel ?? AbstractHistoryChart.getToolTipsAfterTitleLabel(YAxisTitle.ENERGY, chartType, totalValue, translate); } return null; diff --git a/ui/src/app/shared/genericComponents/shared/converter.ts b/ui/src/app/shared/genericComponents/shared/converter.ts index e8542cd16f6..93447afc438 100644 --- a/ui/src/app/shared/genericComponents/shared/converter.ts +++ b/ui/src/app/shared/genericComponents/shared/converter.ts @@ -2,7 +2,7 @@ import { formatNumber } from "@angular/common"; import { TranslateService } from "@ngx-translate/core"; import { CurrentData, EdgeConfig, Utils } from "../../shared"; -import { TimeUtils } from "../../utils/timeutils/timeutils"; +import { TimeUtils } from "../../utils/time/timeutils"; import { Formatter } from "./formatter"; export type Converter = (value: number | string | null) => string; diff --git a/ui/src/app/shared/genericComponents/shared/testing/tester.ts b/ui/src/app/shared/genericComponents/shared/testing/tester.ts index 44d388539c8..8eeca21ed61 100644 --- a/ui/src/app/shared/genericComponents/shared/testing/tester.ts +++ b/ui/src/app/shared/genericComponents/shared/testing/tester.ts @@ -230,7 +230,7 @@ export class OeChartTester { testContext.service.historyPeriod.next({ from: new Date(channelData.result.timestamps[0] ?? 0), to: new Date(channelData.result.timestamps.reverse()[0] ?? 0), - getText: () => testContext.service.historyPeriod.value.getText(testContext.translate), + getText: () => testContext.service.historyPeriod.value.getText(testContext.translate, testContext.service), }); // Fill Data diff --git a/ui/src/app/shared/pickdate/pickdate.component.html b/ui/src/app/shared/pickdate/pickdate.component.html index 8164d823a3b..ea2c96f7009 100644 --- a/ui/src/app/shared/pickdate/pickdate.component.html +++ b/ui/src/app/shared/pickdate/pickdate.component.html @@ -3,7 +3,7 @@ - {{service.historyPeriod.value.getText(translate)}} + {{service.historyPeriod.value.getText(translate, service)}} diff --git a/ui/src/app/shared/pickdate/pickdate.component.spec.ts b/ui/src/app/shared/pickdate/pickdate.component.spec.ts index f85f0fb4c25..9bbeefd21b3 100644 --- a/ui/src/app/shared/pickdate/pickdate.component.spec.ts +++ b/ui/src/app/shared/pickdate/pickdate.component.spec.ts @@ -1,6 +1,7 @@ import { endOfMonth, endOfWeek, endOfYear, startOfDay, startOfMonth, startOfWeek, startOfYear, subDays, subMonths, subWeeks, subYears } from "date-fns"; + import { DefaultTypes } from "../service/defaulttypes"; -import { TestContext, sharedSetup } from "../test/utils.spec"; +import { sharedSetup, TestContext } from "../test/utils.spec"; import { PickDateComponent } from "./pickdate.component"; export function expectPreviousPeriod(testContext: TestContext, firstSetupProtocol: Date, expectToBe: boolean): void { @@ -14,8 +15,8 @@ export function expectNextPeriod(testContext: TestContext, expectToBe: boolean): describe('Pickdate', () => { let TEST_CONTEXT: TestContext; - beforeEach(() => - TEST_CONTEXT = sharedSetup(), + beforeEach(async () => + TEST_CONTEXT = await sharedSetup(), ); it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Day-View: firstSetupProtocol = today', () => { @@ -129,4 +130,13 @@ describe('Pickdate', () => { expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, true); expectNextPeriod(TEST_CONTEXT, false); }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Total-View', () => { + const firstSetupProtocol = startOfYear(subYears(new Date(), 2)); + TEST_CONTEXT.service.historyPeriod.next(new DefaultTypes.HistoryPeriod(firstSetupProtocol, new Date())); + TEST_CONTEXT.service.periodString = DefaultTypes.PeriodString.TOTAL; + + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, false); + expectNextPeriod(TEST_CONTEXT, false); + }); }); diff --git a/ui/src/app/shared/pickdate/pickdate.component.ts b/ui/src/app/shared/pickdate/pickdate.component.ts index 2f4403ea9c7..64b15636731 100644 --- a/ui/src/app/shared/pickdate/pickdate.component.ts +++ b/ui/src/app/shared/pickdate/pickdate.component.ts @@ -6,6 +6,7 @@ import { addDays, addWeeks, endOfWeek, isFuture, subDays, subWeeks } from 'date- import { DefaultTypes } from '../service/defaulttypes'; import { Edge, Service } from '../shared'; import { PickDatePopoverComponent } from './popover/popover.component'; +import { DateUtils } from '../utils/date/dateutils'; @Component({ selector: 'pickdate', @@ -50,6 +51,7 @@ export class PickDateComponent implements OnInit, OnDestroy { public checkArrowAutomaticForwarding() { this.isBackArrowAllowed = PickDateComponent.isPreviousPeriodAllowed(this.service, this.edge?.firstSetupProtocol); this.isForwardArrowAllowed = PickDateComponent.isNextPeriodAllowed(this.service); + switch (this.service.periodString) { case DefaultTypes.PeriodString.DAY: { if (isFuture(addDays(this.service.historyPeriod.value.from, 1))) { @@ -106,6 +108,11 @@ export class PickDateComponent implements OnInit, OnDestroy { this.disableArrow = false; } } + case DefaultTypes.PeriodString.TOTAL: { + this.disableArrow = true; + break; + } + case DefaultTypes.PeriodString.CUSTOM: { let dateDistance = Math.floor(Math.abs(this.service.historyPeriod.value.from - this.service.historyPeriod.value.to) / (1000 * 60 * 60 * 24)); dateDistance == 0 ? dateDistance = 1 : dateDistance = dateDistance; @@ -193,6 +200,12 @@ export class PickDateComponent implements OnInit, OnDestroy { } break; } + + case DefaultTypes.PeriodString.TOTAL: { + this.setDateRange(new DefaultTypes.HistoryPeriod(this.edge?.firstSetupProtocol ?? DateUtils.stringToDate('03.11.2022 16:04:37'), endOfYear(addYears(this.service.historyPeriod.value.to, 1)))); + this.disableArrow = true; + break; + } case DefaultTypes.PeriodString.CUSTOM: { let dateDistance = Math.floor(Math.abs(this.service.historyPeriod.value.from - this.service.historyPeriod.value.to) / (1000 * 60 * 60 * 24)); dateDistance == 0 ? dateDistance = 1 : dateDistance = dateDistance; @@ -257,7 +270,7 @@ export class PickDateComponent implements OnInit, OnDestroy { private forwardToNextDayWhenReached() { this.changePeriodTimeout = setTimeout(() => { this.setDateRange(new DefaultTypes.HistoryPeriod(new Date(), new Date())); - this.service.historyPeriod.value?.getText(this.translate); + this.service.historyPeriod.value?.getText(this.translate, this.service); }, this.millisecondsUntilnextPeriod()); } @@ -267,7 +280,7 @@ export class PickDateComponent implements OnInit, OnDestroy { private forwardToNextWeekWhenReached() { this.changePeriodTimeout = setTimeout(() => { this.setDateRange(new DefaultTypes.HistoryPeriod(new Date(), endOfWeek(new Date(), { weekStartsOn: 1 }))); - this.service.historyPeriod.value?.getText(this.translate); + this.service.historyPeriod.value?.getText(this.translate, this.service); }, this.millisecondsUntilnextPeriod()); } @@ -279,7 +292,7 @@ export class PickDateComponent implements OnInit, OnDestroy { if (this.millisecondsUntilnextPeriod() < 2147483647) { this.changePeriodTimeout = setTimeout(() => { this.setDateRange(new DefaultTypes.HistoryPeriod(new Date(), endOfMonth(new Date()))); - this.service.historyPeriod.value?.getText(this.translate); + this.service.historyPeriod.value?.getText(this.translate, this.service); }, this.millisecondsUntilnextPeriod()); } } @@ -291,7 +304,7 @@ export class PickDateComponent implements OnInit, OnDestroy { if (this.millisecondsUntilnextPeriod() < 2147483647) { this.changePeriodTimeout = setTimeout(() => { this.setDateRange(new DefaultTypes.HistoryPeriod(new Date(), endOfYear(new Date()))); - this.service.historyPeriod.value?.getText(this.translate); + this.service.historyPeriod.value?.getText(this.translate, this.service); }, this.millisecondsUntilnextPeriod()); } } @@ -365,6 +378,8 @@ export class PickDateComponent implements OnInit, OnDestroy { return isBefore(firstSetupProtocol, endOfMonth(subWeeks(service.historyPeriod.value.from, 1))); case DefaultTypes.PeriodString.YEAR: return isBefore(firstSetupProtocol, endOfYear(subWeeks(service.historyPeriod.value.from, 1))); + case DefaultTypes.PeriodString.TOTAL: + return false; case DefaultTypes.PeriodString.CUSTOM: var timeRange: number = differenceInDays(service.historyPeriod.value.to, service.historyPeriod.value.from); return isBefore(startOfDay(firstSetupProtocol), startOfDay(subDays(service.historyPeriod.value.from, timeRange))); @@ -388,6 +403,8 @@ export class PickDateComponent implements OnInit, OnDestroy { return isAfter(new Date(), startOfMonth(addMonths(service.historyPeriod.value.to, 1))); case DefaultTypes.PeriodString.YEAR: return isAfter(new Date(), startOfYear(addYears(service.historyPeriod.value.to, 1))); + case DefaultTypes.PeriodString.TOTAL: + return false; case DefaultTypes.PeriodString.CUSTOM: var timeRange: number = differenceInDays(service.historyPeriod.value.to, service.historyPeriod.value.from); return isAfter(startOfDay(new Date()), addDays(service.historyPeriod.value.to, timeRange)); diff --git a/ui/src/app/shared/pickdate/popover/popover.component.html b/ui/src/app/shared/pickdate/popover/popover.component.html index aadbebb757a..b68f55acbc7 100644 --- a/ui/src/app/shared/pickdate/popover/popover.component.html +++ b/ui/src/app/shared/pickdate/popover/popover.component.html @@ -1,30 +1,24 @@ - - - Edge.History.day - - - - - Edge.History.week - - - - - Edge.History.month - - - - - - - Edge.History.year - - + + + + Edge.History.day + + + Edge.History.week + + + Edge.History.month + + + Edge.History.year + + + Edge.History.TOTAL + + + @@ -34,4 +28,4 @@ \ No newline at end of file + angular-mydatepicker name="mydate" [options]="myDpOptions" /> diff --git a/ui/src/app/shared/pickdate/popover/popover.component.ts b/ui/src/app/shared/pickdate/popover/popover.component.ts index d22f2fd1c67..c8e48ace95d 100644 --- a/ui/src/app/shared/pickdate/popover/popover.component.ts +++ b/ui/src/app/shared/pickdate/popover/popover.component.ts @@ -7,7 +7,7 @@ import { addDays, endOfWeek, endOfYear, getDate, getMonth, getYear, startOfWeek, import { Edge } from '../../edge/edge'; import { DefaultTypes } from '../../service/defaulttypes'; -import { Service, Utils } from '../../shared'; +import { EdgePermission, Service, Utils } from '../../shared'; @Component({ selector: 'pickdatepopover', @@ -24,6 +24,7 @@ export class PickDatePopoverComponent implements OnInit { public locale: string = 'de'; public showCustomDate: boolean = false; + protected periods: string[] = []; protected myDpOptions: IAngularMyDpOptions = { stylesData: { selector: 'dp1', @@ -57,6 +58,9 @@ export class PickDatePopoverComponent implements OnInit { // Restrict user to pick date before ibn-date this.myDpOptions.disableUntil = { day: Utils.subtractSafely(getDate(this.edge?.firstSetupProtocol), 1) ?? 1, month: Utils.addSafely(getMonth(this.edge?.firstSetupProtocol), 1) ?? 1, year: this.edge?.firstSetupProtocol?.getFullYear() ?? 2013 }, this.locale = this.translate.getBrowserLang(); + + // Filter out custom due to different on click event + this.periods = EdgePermission.getAllowedHistoryPeriods(this.edge).filter(period => period !== DefaultTypes.PeriodString.CUSTOM); } /** @@ -92,6 +96,12 @@ export class PickDatePopoverComponent implements OnInit { this.popoverCtrl.dismiss(); break; } + case DefaultTypes.PeriodString.TOTAL: { + this.setDateRange(new DefaultTypes.HistoryPeriod(this.edge?.firstSetupProtocol, endOfYear(this.TODAY))); + this.service.periodString = period; + this.popoverCtrl.dismiss(); + break; + } } } diff --git a/ui/src/app/shared/service/defaulttypes.ts b/ui/src/app/shared/service/defaulttypes.ts index b456baa7551..7b22d865498 100644 --- a/ui/src/app/shared/service/defaulttypes.ts +++ b/ui/src/app/shared/service/defaulttypes.ts @@ -1,7 +1,8 @@ import { TranslateService } from '@ngx-translate/core'; import { endOfMonth, endOfYear, format, getDay, getMonth, getYear, isSameDay, isSameMonth, isSameYear, startOfMonth, startOfYear, subDays } from 'date-fns'; + import { QueryHistoricTimeseriesEnergyResponse } from '../jsonrpc/response/queryHistoricTimeseriesEnergyResponse'; -import { ChannelAddress } from '../shared'; +import { ChannelAddress, Service } from '../shared'; export module DefaultTypes { @@ -91,7 +92,7 @@ export module DefaultTypes { params?: string[] } - export enum PeriodString { DAY = 'day', WEEK = 'week', MONTH = 'month', YEAR = 'year', CUSTOM = 'custom' }; + export enum PeriodString { DAY = 'day', WEEK = 'week', MONTH = 'month', YEAR = 'year', TOTAL = 'total', CUSTOM = 'custom' }; export namespace History { @@ -151,7 +152,12 @@ export module DefaultTypes { public to: Date = new Date(), ) { } - public getText(translate: TranslateService): string { + public getText(translate: TranslateService, service: Service): string { + + if (service.periodString === DefaultTypes.PeriodString.TOTAL) { + return translate.instant('Edge.History.TOTAL'); + } + if (isSameDay(this.from, this.to)) { if (isSameDay(this.from, new Date())) { // Selected TODAY @@ -167,7 +173,6 @@ export module DefaultTypes { value: format(this.from, translate.instant('General.dateFormat')), }); } - } else if (isSameMonth(this.from, this.to) && isSameDay(this.from, startOfMonth(this.from)) && isSameDay(this.to, endOfMonth(this.to))) { // Selected one month return HistoryPeriod.getTranslatedMonthString(translate, this.from) + " " + getYear(this.from); @@ -175,11 +180,12 @@ export module DefaultTypes { // Selected one year else if (isSameYear(this.from, this.to) && isSameDay(this.from, startOfYear(this.from)) && isSameDay(this.to, endOfYear(this.to))) { return getYear(this.from).toString(); + } - } else { + else { return translate.instant( 'General.periodFromTo', { - value1: format(this.from, translate.instant('General.dateFormatShort')), + value1: format(this.from, translate.instant('General.dateFormat')), value2: format(this.to, translate.instant('General.dateFormat')), }); } diff --git a/ui/src/app/shared/service/service.ts b/ui/src/app/shared/service/service.ts index 7d4a9c34d5e..fc01ff93497 100644 --- a/ui/src/app/shared/service/service.ts +++ b/ui/src/app/shared/service/service.ts @@ -25,7 +25,7 @@ import { Role } from '../type/role'; import { AbstractService } from './abstractservice'; import { DefaultTypes } from './defaulttypes'; import { Websocket } from './websocket'; -import { DateUtils } from '../utils/dateutils/dateutils'; +import { DateUtils } from '../utils/date/dateutils'; @Injectable() export class Service extends AbstractService { diff --git a/ui/src/app/shared/service/utils.spec.ts b/ui/src/app/shared/service/utils.spec.ts index 5eec8804f58..816379415c1 100644 --- a/ui/src/app/shared/service/utils.spec.ts +++ b/ui/src/app/shared/service/utils.spec.ts @@ -1,6 +1,6 @@ import { Utils } from "./utils"; -fdescribe('Utils', () => { +describe('Utils', () => { it('#subtractSafely', () => { expect(Utils.subtractSafely(null, null)).toEqual(null); diff --git a/ui/src/app/shared/shared.spec.ts b/ui/src/app/shared/shared.spec.ts new file mode 100644 index 00000000000..8fd29c0ed4f --- /dev/null +++ b/ui/src/app/shared/shared.spec.ts @@ -0,0 +1,17 @@ +import { SumState } from "../index/shared/sumState"; +import { Edge, EdgePermission } from "./shared"; +import { Role } from "./type/role"; + +describe('EdgePermission', () => { + + const edge = new Edge("", "", "", "", Role.ADMIN, true, new Date(), SumState.OK, null); + + it('#getAllowedHistoryPeriods - no first ibn date', () => { + expect(EdgePermission.getAllowedHistoryPeriods(edge)).toEqual(['day', 'week', 'month', 'year', 'custom']); + }); + + const edgeWithFirstIbnDate = new Edge("", "", "", "", Role.ADMIN, true, new Date(), SumState.OK, new Date()); + it('#getAllowedHistoryPeriods - first ibn date', () => { + expect(EdgePermission.getAllowedHistoryPeriods(edgeWithFirstIbnDate)).toEqual(['day', 'week', 'month', 'year', 'total', 'custom']); + }); +}); diff --git a/ui/src/app/shared/shared.ts b/ui/src/app/shared/shared.ts index 4ea97179094..ab6c1b4fd0c 100644 --- a/ui/src/app/shared/shared.ts +++ b/ui/src/app/shared/shared.ts @@ -11,6 +11,7 @@ export { SystemLog } from "./type/systemlog"; export { Widget, WidgetFactory, WidgetNature, Widgets } from "./type/widget"; import { User } from "./jsonrpc/shared"; +import { DefaultTypes } from "./service/defaulttypes"; import { Role } from "./type/role"; import { addIcons } from 'ionicons'; @@ -25,6 +26,28 @@ addIcons({ 'oe-storage': 'assets/img/icon/storage.svg', }); +export class EdgePermission { + + /** + * Gets the allowed history periods for this edge, used in {@link PickDatePopoverComponent} + * + * @param edge the edge + * @returns the list of allowed periods for this edge + */ + public static getAllowedHistoryPeriods(edge: Edge) { + return Object.values(DefaultTypes.PeriodString).reduce((arr, el) => { + + // hide total, if no first ibn date + if (el === DefaultTypes.PeriodString.TOTAL && edge?.firstSetupProtocol === null) { + return arr; + } + + arr.push(el); + return arr; + }, []); + } +} + export class UserPermission { public static isUserAllowedToSeeOverview(user: User): boolean { diff --git a/ui/src/app/shared/test/utils.spec.ts b/ui/src/app/shared/test/utils.spec.ts index f3637097db8..261e23a6836 100644 --- a/ui/src/app/shared/test/utils.spec.ts +++ b/ui/src/app/shared/test/utils.spec.ts @@ -1,9 +1,12 @@ import { registerLocaleData } from "@angular/common"; -import localeDe from '@angular/common/locales/de'; +import localDE from '@angular/common/locales/de'; import localeDeExtra from '@angular/common/locales/extra/de'; +import { LOCALE_ID } from "@angular/core"; import { TestBed } from "@angular/core/testing"; import { FORMLY_CONFIG } from "@ngx-formly/core"; import { TranslateLoader, TranslateModule, TranslateService } from "@ngx-translate/core"; +import { setDefaultOptions } from 'date-fns'; +import { de } from "date-fns/locale"; import { Service } from "../shared"; import { registerTranslateExtension } from "../translate.extension"; @@ -11,18 +14,25 @@ import { Language, MyTranslateLoader } from "../type/language"; export type TestContext = { translate: TranslateService, service: Service }; -export function sharedSetup(): TestContext { - TestBed.configureTestingModule({ +export async function sharedSetup(): Promise { + await TestBed.configureTestingModule({ imports: [ - TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: MyTranslateLoader }, defaultLanguage: Language.DEFAULT.key }), + TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: MyTranslateLoader }, defaultLanguage: Language.DEFAULT.key, useDefaultLang: false }), ], providers: [ TranslateService, { provide: FORMLY_CONFIG, multi: true, useFactory: registerTranslateExtension, deps: [TranslateService] }, + { provide: LOCALE_ID, useValue: Language.DEFAULT.key }, Service, ], - }).compileComponents(); - registerLocaleData(localeDe, 'de', localeDeExtra); + }).compileComponents().then(() => { + const translateService = TestBed.inject(TranslateService); + translateService.addLangs(['en', 'de']); + translateService.use('de'); + setDefaultOptions({ locale: de }); + registerLocaleData(localDE, 'de', localeDeExtra); + }); + return { translate: TestBed.inject(TranslateService), service: TestBed.inject(Service), diff --git a/ui/src/app/shared/utils/dateutils/dateutils.spec.ts b/ui/src/app/shared/utils/date/dateutils.spec.ts similarity index 100% rename from ui/src/app/shared/utils/dateutils/dateutils.spec.ts rename to ui/src/app/shared/utils/date/dateutils.spec.ts diff --git a/ui/src/app/shared/utils/dateutils/dateutils.ts b/ui/src/app/shared/utils/date/dateutils.ts similarity index 100% rename from ui/src/app/shared/utils/dateutils/dateutils.ts rename to ui/src/app/shared/utils/date/dateutils.ts diff --git a/ui/src/app/shared/utils/datetime/datetime-utils.ts b/ui/src/app/shared/utils/datetime/datetime-utils.ts new file mode 100644 index 00000000000..c196dc28c99 --- /dev/null +++ b/ui/src/app/shared/utils/datetime/datetime-utils.ts @@ -0,0 +1,55 @@ +import { format, startOfMonth, startOfYear } from "date-fns"; +import { de } from "date-fns/locale"; +import { ChronoUnit } from "src/app/edge/history/shared"; + +import { QueryHistoricTimeseriesDataResponse } from "../../jsonrpc/response/queryHistoricTimeseriesDataResponse"; +import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from "../../jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse"; +import { DateUtils } from "../date/dateutils"; + +export class DateTimeUtils { + + /** + * Normalizes timestamps depending on chosen period + * + * @param unit the Chronounit + * @param energyPerPeriodResponse the timeseries data + * @returns the adjusted timestamps + */ + public static normalizeTimestamps(unit: ChronoUnit.Type, energyPerPeriodResponse: QueryHistoricTimeseriesDataResponse | QueryHistoricTimeseriesEnergyPerPeriodResponse): QueryHistoricTimeseriesDataResponse | QueryHistoricTimeseriesEnergyPerPeriodResponse { + + switch (unit) { + case ChronoUnit.Type.MONTHS: + + // Change first timestamp to start of month + const formattedDate = startOfMonth(DateUtils.stringToDate(energyPerPeriodResponse.result.timestamps[0])); + energyPerPeriodResponse.result.timestamps[0] = format(formattedDate, 'yyyy-MM-dd HH:mm:ss', { locale: de })?.toString() ?? energyPerPeriodResponse.result.timestamps[0]; + + // show 12 stacks, even if no data and timestamps + let newTimestamps: string[] = []; + const firstTimestamp = DateUtils.stringToDate(energyPerPeriodResponse.result.timestamps[0]); + + if (firstTimestamp.getMonth() !== 0) { + for (let i = 0; i <= (firstTimestamp.getMonth() - 1); i++) { + newTimestamps.push(new Date(firstTimestamp.getFullYear(), i).toString()); + + for (let channel of Object.keys(energyPerPeriodResponse.result.data)) { + energyPerPeriodResponse.result.data[channel.toString()]?.unshift(null); + } + } + } + + energyPerPeriodResponse.result.timestamps = newTimestamps.concat(energyPerPeriodResponse.result.timestamps); + break; + + case ChronoUnit.Type.YEARS: + + // Change dates to be first day of year + const formattedDates = energyPerPeriodResponse.result.timestamps.map((timestamp) => + startOfYear(DateUtils.stringToDate(timestamp))); + energyPerPeriodResponse.result.timestamps = formattedDates.map(date => format(date, 'yyyy-MM-dd HH:mm:ss', { locale: de })?.toString()); + break; + } + + return energyPerPeriodResponse; + } +} diff --git a/ui/src/app/shared/utils/timeutils/timeutils.spec.ts b/ui/src/app/shared/utils/time/timeutils.spec.ts similarity index 92% rename from ui/src/app/shared/utils/timeutils/timeutils.spec.ts rename to ui/src/app/shared/utils/time/timeutils.spec.ts index d5331f0c160..03abf88d4f3 100644 --- a/ui/src/app/shared/utils/timeutils/timeutils.spec.ts +++ b/ui/src/app/shared/utils/time/timeutils.spec.ts @@ -1,6 +1,6 @@ import { TimeUtils } from "./timeutils"; -fdescribe('TimeUtils', () => { +describe('TimeUtils', () => { it('#formatSecondsToDuration', () => { expect(TimeUtils.formatSecondsToDuration(12000, 'de')).toEqual("3h 20m"); expect(TimeUtils.formatSecondsToDuration(null, 'de')).toEqual(null); diff --git a/ui/src/app/shared/utils/timeutils/timeutils.ts b/ui/src/app/shared/utils/time/timeutils.ts similarity index 100% rename from ui/src/app/shared/utils/timeutils/timeutils.ts rename to ui/src/app/shared/utils/time/timeutils.ts diff --git a/ui/src/assets/i18n/de.json b/ui/src/assets/i18n/de.json index 437f1781fc8..259eb29042f 100644 --- a/ui/src/assets/i18n/de.json +++ b/ui/src/assets/i18n/de.json @@ -279,6 +279,7 @@ "today": "Heute", "week": "Woche", "year": "Jahr", + "TOTAL": "Gesamt", "yesterday": "Gestern", "sun": "So", "mon": "Mo", @@ -461,7 +462,6 @@ "currentName": "Aktueller Name", "currentValue": "Aktueller Wert", "dateFormat": "dd.MM.yyyy", - "dateFormatShort": "dd.MM", "digitalInputs": "Digitaleingänge", "numberOfComponents": "Anzahl der Komponenten", "directConsumption": "Direktverbrauch", diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 3da5fffe9c5..fb8f2816028 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -280,6 +280,7 @@ "today": "Today", "week": "Week", "year": "Year", + "TOTAL": "Total", "yesterday": "Yesterday", "sun": "Sun", "mon": "Mon",