From 9191c7e1ab823518a2d252032b60f9bfc57b1032 Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Sun, 1 Oct 2023 22:14:35 +0200 Subject: [PATCH 01/27] Start development of version 2023.11.0-SNAPSHOT --- io.openems.common/src/io/openems/common/OpenemsConstants.java | 4 ++-- ui/package-lock.json | 4 ++-- ui/package.json | 2 +- ui/src/app/changelog/view/component/changelog.constants.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/io.openems.common/src/io/openems/common/OpenemsConstants.java b/io.openems.common/src/io/openems/common/OpenemsConstants.java index 36707104b65..dc9c9939415 100644 --- a/io.openems.common/src/io/openems/common/OpenemsConstants.java +++ b/io.openems.common/src/io/openems/common/OpenemsConstants.java @@ -29,7 +29,7 @@ public class OpenemsConstants { *

* This is the month of the release. */ - public static final short VERSION_MINOR = 10; + public static final short VERSION_MINOR = 11; /** * The patch version of OpenEMS. @@ -43,7 +43,7 @@ public class OpenemsConstants { /** * The additional version string. */ - public static final String VERSION_STRING = ""; + public static final String VERSION_STRING = "SNAPSHOT"; /** * The complete version as a SemanticVersion. diff --git a/ui/package-lock.json b/ui/package-lock.json index ace21d75648..2890aa9c62a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "openems-ui", - "version": "2023.10.0", + "version": "2023.11.0-SNAPSHOT", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openems-ui", - "version": "2023.10.0", + "version": "2023.11.0-SNAPSHOT", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "~15.2.9", diff --git a/ui/package.json b/ui/package.json index 360509fab4c..be8b14c1dbd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "openems-ui", - "version": "2023.10.0", + "version": "2023.11.0-SNAPSHOT", "license": "AGPL-3.0", "private": true, "dependencies": { diff --git a/ui/src/app/changelog/view/component/changelog.constants.ts b/ui/src/app/changelog/view/component/changelog.constants.ts index 3da44d9c92f..b41d1331209 100644 --- a/ui/src/app/changelog/view/component/changelog.constants.ts +++ b/ui/src/app/changelog/view/component/changelog.constants.ts @@ -2,7 +2,7 @@ import { Role } from "src/app/shared/type/role"; export class Changelog { - public static readonly UI_VERSION = "2023.10.0"; + public static readonly UI_VERSION = "2023.11.0-SNAPSHOT"; public static product(...products: Product[]) { return products.map(product => Changelog.link(product.name, product.url)).join(", ") + '. '; From 886fab3626e7e877ee2f3d36004505abd912d911 Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Sun, 1 Oct 2023 23:05:18 +0200 Subject: [PATCH 02/27] UI: fix bugs + cleanup --- .../common/production/chart/totalChart.ts | 1 - ui/src/app/edge/settings/app/app.module.ts | 2 +- ui/src/app/index/index.component.html | 175 -------------- ui/src/app/index/index.component.ts | 214 ------------------ .../index/overview/overview.component.html | 5 +- ui/src/app/shared/edge/edge.ts | 2 +- .../flat/abstract-flat-widget.ts | 2 + 7 files changed, 7 insertions(+), 394 deletions(-) delete mode 100644 ui/src/app/index/index.component.html delete mode 100644 ui/src/app/index/index.component.ts diff --git a/ui/src/app/edge/history/common/production/chart/totalChart.ts b/ui/src/app/edge/history/common/production/chart/totalChart.ts index 27a40a0db8a..ab92d3e5dd5 100644 --- a/ui/src/app/edge/history/common/production/chart/totalChart.ts +++ b/ui/src/app/edge/history/common/production/chart/totalChart.ts @@ -160,7 +160,6 @@ export class TotalChartComponent extends AbstractHistoryChart { }; return chartObject; - } public override getChartHeight(): number { diff --git a/ui/src/app/edge/settings/app/app.module.ts b/ui/src/app/edge/settings/app/app.module.ts index f403dc871b8..4272ed6e191 100644 --- a/ui/src/app/edge/settings/app/app.module.ts +++ b/ui/src/app/edge/settings/app/app.module.ts @@ -77,4 +77,4 @@ export function registerTranslateExtension(translate: TranslateService) { { provide: FORMLY_CONFIG, multi: true, useFactory: registerTranslateExtension, deps: [TranslateService] } ] }) -export class AppModule { } +export class AppModule { } \ No newline at end of file diff --git a/ui/src/app/index/index.component.html b/ui/src/app/index/index.component.html deleted file mode 100644 index 2d0eb30b756..00000000000 --- a/ui/src/app/index/index.component.html +++ /dev/null @@ -1,175 +0,0 @@ -

- - - - - - - - - - - - - - - - Login.title - - - - -
-
- - - Login.preamble - - - - Login.passwordLabel - - - - - - - - - -
-
-
- - - -
- - E-Mail / Login.user - - - - - - Login.passwordLabel - - - - - - - - - - - Login - - -
- -
-
-
-
-
-
- - - - - - - - - - - - - - - - Index.NO_EDGE_AVAILABLE - - - - - - - Leider wurde noch kein - {{environment.edgeShortName}} mit Ihrem Account verknüpft. - - - -

Nach dem Ihr {{environment.edgeShortName}} durch einen Installateur in Betrieb genommen - wurde, sehen Sie es an dieser Stelle. -

-
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - -

{{ edge.comment }}

-

- Index.deviceOffline - - Index.OFFLINE_SINCE - {{edge.lastmessage | date:'dd.MM.yyyy HH:mm'}} - -

-
- - -

ID: {{ edge.id }}

-

- Index.type {{ edge.producttype }} -

-
-

- Index.loggedInAs {{ edge.getRoleString() }}. -

-
- -
-
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file diff --git a/ui/src/app/index/index.component.ts b/ui/src/app/index/index.component.ts deleted file mode 100644 index af1a506c889..00000000000 --- a/ui/src/app/index/index.component.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormGroup } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; -import { InfiniteScrollCustomEvent } from '@ionic/angular'; -import { TranslateService } from '@ngx-translate/core'; -import { Subject } from 'rxjs'; -import { filter, take } from 'rxjs/operators'; -import { environment } from 'src/environments'; - -import { AuthenticateWithPasswordRequest } from '../shared/jsonrpc/request/authenticateWithPasswordRequest'; -import { Edge, Service, Utils, Websocket } from '../shared/shared'; -import { Role } from '../shared/type/role'; -import { ChosenFilter } from './filter/filter.component'; - -@Component({ - selector: 'index', - templateUrl: './index.component.html' -}) -export class IndexComponent implements OnInit, OnDestroy { - - public environment = environment; - - /** - * True, if there is no access to any Edge. - */ - public noEdges: boolean = false; - - /** - * True, if the logged in user is allowed to install - * new edges. - */ - public loggedInUserCanInstall: boolean = false; - - public form: FormGroup; - public filteredEdges: Edge[] = []; - - - private stopOnDestroy: Subject = new Subject(); - private page = 0; - private query: string | null = null; - - /** Limits edges in pagination response */ - private readonly limit: number = 20; - /** True, if all available edges for this user had been retrieved */ - private limitReached: boolean = false; - - protected formIsDisabled: boolean = false; - protected onlyOneEdgeAvailable: boolean = false; - protected spinnerId: string = 'index'; - protected loading: boolean = false; - protected searchParams: Map = new Map(); - - constructor( - public service: Service, - private translate: TranslateService, - public websocket: Websocket, - public utils: Utils, - private router: Router, - private route: ActivatedRoute - ) { } - - ngOnInit() { - this.page = 0; - this.filteredEdges = []; - this.limitReached = false; - this.service.metadata.pipe(filter(metadata => !!metadata), take(1)).subscribe(() => { - this.init(); - }); - } - - async ionViewWillEnter() { - - // Execute Login-Request if url path matches 'demo' - if (this.route.snapshot.routeConfig.path == 'demo') { - - // Wait for Websocket - await new Promise((resolve) => setTimeout(() => { - if (this.websocket.status == 'waiting for credentials') { - let lang = this.route.snapshot.queryParamMap.get('lang') ?? null; - if (lang) { - localStorage.DEMO_LANGUAGE = lang; - } - resolve( - this.websocket - .login(new AuthenticateWithPasswordRequest({ username: 'admin', password: 'admin' }))); - } - }, 2000)).then(() => { this.service.setCurrentComponent('', this.route); }); - } else { - localStorage.removeItem('DEMO_LANGUAGE'); - this.service.setCurrentComponent('', this.route); - } - } - - /** - * Search on change, triggered by searchbar input-event. - * - * @param event from template passed event - */ - protected searchOnChange(searchParams?: Map) { - - if (searchParams) { - this.searchParams = searchParams; - } - - this.filteredEdges = []; - this.page = 0; - this.limitReached = false; - - this.loadNextPage().then((edges) => { - this.filteredEdges = edges; - this.page++; - }); - } - - /** - * Login to OpenEMS Edge or Backend. - * - * @param param data provided in login form - */ - public doLogin(param: { username?: string, password: string }) { - this.query = ""; - this.limitReached = false; - - // Prevent that user submits via keyevent 'enter' multiple times - if (this.formIsDisabled) { - return; - } - - this.formIsDisabled = true; - this.websocket.login(new AuthenticateWithPasswordRequest(param)) - .finally(() => { - - // Unclean - this.ngOnInit(); - this.formIsDisabled = false; - }); - } - - private init() { - this.loadNextPage().then((edges) => { - - this.service.metadata - .pipe( - filter(metadata => !!metadata), - take(1) - ) - .subscribe(metadata => { - - let edgeIds = Object.keys(metadata.edges); - this.onlyOneEdgeAvailable = edgeIds.length <= 1; - this.noEdges = edgeIds.length === 0; - this.loggedInUserCanInstall = environment.backend === 'OpenEMS Backend' && Role.isAtLeast(metadata.user.globalRole, "installer"); - - // Forward directly to device page, if - // - Direct local access to Edge - // - No installer (i.e. guest or owner) and access to only one Edge - if ((!this.loggedInUserCanInstall && !metadata.user.hasMultipleEdges)) { - let edge = metadata.edges[edgeIds[0]]; - this.router.navigate(['/device', edge.id]); - return; - } - this.filteredEdges = edges; - }); - }); - } - - /** - * Updates available edges on scroll-event - * - * @param infiniteScroll the InfiniteScrollCustomEvent - */ - doInfinite(infiniteScroll: InfiniteScrollCustomEvent) { - setTimeout(() => { - this.page++; - this.loadNextPage().then((edges) => { - this.filteredEdges.push(...edges); - infiniteScroll.target.complete(); - }).catch(() => { - infiniteScroll.target.complete(); - }); - }, 200); - } - - ngOnDestroy() { - this.stopOnDestroy.next(); - this.stopOnDestroy.complete(); - } - - loadNextPage(): Promise { - this.loading = true; - return new Promise((resolve, reject) => { - if (this.limitReached) { - resolve([]); - return; - } - - let searchParamsObj = {}; - if (this.searchParams) { - for (const [key, value] of this.searchParams) { - searchParamsObj[key] = value; - } - } - - this.service.getEdges(this.page, this.query, this.limit, searchParamsObj) - .then((edges) => { - this.limitReached = edges.length < this.limit; - resolve(edges); - }).catch((err) => { - reject(err); - }); - }).finally(() => - this.loading = false); - } -} \ No newline at end of file diff --git a/ui/src/app/index/overview/overview.component.html b/ui/src/app/index/overview/overview.component.html index 37fe4287cba..e290bef0023 100644 --- a/ui/src/app/index/overview/overview.component.html +++ b/ui/src/app/index/overview/overview.component.html @@ -40,7 +40,7 @@ - + @@ -120,7 +120,8 @@

{{ edge.comment }}

- Index.ADD_EDGE diff --git a/ui/src/app/shared/edge/edge.ts b/ui/src/app/shared/edge/edge.ts index e3583257331..a762533993e 100644 --- a/ui/src/app/shared/edge/edge.ts +++ b/ui/src/app/shared/edge/edge.ts @@ -1,7 +1,7 @@ import { compareVersions } from 'compare-versions'; import { BehaviorSubject, Subject } from 'rxjs'; -import { SumState } from 'src/app/index/shared/sumState'; +import { SumState } from 'src/app/index/shared/sumState'; import { JsonrpcRequest, JsonrpcResponseSuccess } from '../jsonrpc/base'; import { CurrentDataNotification } from '../jsonrpc/notification/currentDataNotification'; import { EdgeConfigNotification } from '../jsonrpc/notification/edgeConfigNotification'; diff --git a/ui/src/app/shared/genericComponents/flat/abstract-flat-widget.ts b/ui/src/app/shared/genericComponents/flat/abstract-flat-widget.ts index 395616680d6..feef520084b 100644 --- a/ui/src/app/shared/genericComponents/flat/abstract-flat-widget.ts +++ b/ui/src/app/shared/genericComponents/flat/abstract-flat-widget.ts @@ -8,11 +8,13 @@ import { ChannelAddress, CurrentData, Edge, EdgeConfig, Service, Utils, Websocke import { v4 as uuidv4 } from 'uuid'; import { DataService } from "../shared/dataservice"; +import { Converter } from "../shared/converter"; @Directive() export abstract class AbstractFlatWidget implements OnInit, OnDestroy { public readonly Utils = Utils; + public readonly Converter = Converter; @Input() protected componentId: string; From 6e8987186c6d2ac6482fade215d3f8392ff1acf9 Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Fri, 6 Oct 2023 09:30:53 +0200 Subject: [PATCH 03/27] Update to Gradle 8.4 --- .gradle-wrapper/gradle-wrapper.jar | Bin 63375 -> 63721 bytes .gradle-wrapper/gradle-wrapper.properties | 2 +- gradlew | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gradle-wrapper/gradle-wrapper.jar b/.gradle-wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 28216 zcmZ6yQ*@x+6TO*^ZQHip9ox2TJ8x{;wr$&H$LgqKv*-KI%$l`+bAK-CVxOv0&)z5g z2JHL}tl@+Jd?b>@B>9{`5um}}z@(_WbP841wh56Q*(#D!%+_WFn zxTW!hkY%qR9|LgnC$UfeVp69yjV8RF>YD%YeVEatr**mzN7 z%~mf;`MId9ttnTP(NBpBu_T!aR9RPfUey|B+hCTWWUp*Wy%dWP;fVVjO?KDc*VJ^iSto8gEBp#a5qRnMR zR-GrMr4};1AUK^Wl4El^I$-(Vox98wN~VNm(oL!Se73~FCH0%|9`4hgXt)VkY;&YA zxyNzaSx28JDZ@IjQQ-r%=U60hdM!;;Y1B&M`-jR5wo|dL0PfRJBs={0-i#sk@ffUT z&!L4AR}OfxIMF;CysW-jf@GxJRaJf6F$^KwJk-s_L0t?_fJ4k67RHAk3M+heW>EqQ>mh(Ebmt5gvhew5D{oe# zo`>K30R3ukH;X#Wq!&s zh<7!d$VmuwoQfFr&7EXB^fHQhPSUeX-@m@70<^Z-3rtpi;hOA_$6iw7N*XT>pwkm9^O|F` zV$|!O7HK<&%rdLqo6c5A>AL}T)rY)mCX9IQZdUUafh2CzC~-ixktzMIU(ZZ}?tK;b zJk9Wwx!+Ej!fTgInh8by&<<;Q+>(gN(w-wO{3c($ua2PiC10N6MH6zHuCrIMQL^<_ zJbok&IZ1f&2hF8#E}+@2;m7z@mRJbXJZAMDrA>>?YCn~dS;HOKzymOhHng2>Vqt^| zqR71FIPY1`Y_tsTs>9k)&f%JOVl9oUZ$3ufI0`kM#_d@%1~~NYRSbgq>`8HS@YCTP zN1lIW7odKxwcu71yGi#68$K_+c ziEt@@hyTm6*U^3V^=kEYm`?AR*^&DQz$%CV6-c-87CA>z6cAI!Vqdi|Jtw*PVTC)3 zlYI4yE!rS)gHla|DYjQ~Vea(In8~mqeIn7W;5?2$4lJ;wAqMcLS|AcWwN%&FK2(WL zCB@UE7+TPVkEN#q8zY_zi3x8BE+TsYo3s#nfJ3DnuABb|!28j#;A;27g+x)xLTX7; zFdUA=o26z`apjP!WJaK>P+gP2ijuSvm!WBq{8a4#OJrB?Ug=K7+zHCo#~{om5nhEs z9#&+qk>(sVESM`sJSaE)ybL7yTB^J;zDIu1m$&l!OE#yxvjF6c{p&|oM!+4^|7sVv zEAcZqfZP}eW}<;f4=Lg1u0_*M-Zd@kKx|7%JfW;#kT}yRVY^C5IX^Mr^9vW0=G!6T zF&u}?lsA7r)qVcE`SrY(kG$-uK` zy|vn}D^GBxhP+f%Y;>yBFh0^0Q5|u_)gQylO808C5xO_%+ih8?+Yv@4|M?vYB7is!1y@n%8fZ?IL%a@%Qe;9q@IC)BmfjA?Nu*COkU$PP%XoE%%B7dd0rf;*AuGIs%d zOMi)Jd9Gk%3W)sXCM{Upg&JbSh^G5j%l!y8;nw*n+WIK}OM-wt=d*R0>_L9r1Z`Z+ zc;l>^^y#C*RBicDoGdG^c-*Zr{)PYO-TL>cc2ra#H9P@ml{LnWdB+Cg@@z`F$Cg+) zG%M(!=}+i3o``uvsP4UI;}edQyyqZbhpD_!BTz{O#yrq`+%` zc`uT~qNjFFBRixfq)^)E7CBxi+tN7qW>|BPwlr(li({kN6O$wSLd~@Z?I;>xiv*V4 zNVM-0H#h?4NaQa%3c&yC zig%>pq3m7pKFUN(2zW>A1lJ+WSZAKAGYMiK8&pp)v01^a<6B_rE*}s1p0O(4zakbSt3e((EqbeC`uF1H|A;Kp%N@+b0~5;x6Sji?IUl||MmI_F~I2l;HWrhBF@A~cyW>#?3TOhsOX~T z(J+~?l^huJf-@6)ffBq5{}E(V#{dT0S-bwmxJdBun@ag@6#pTiE9Ezrr2eTc4o@dX z7^#jNNu1QkkCv-BX}AEd5UzX2tqN~X2OVPl&L0Ji(PJ5Iy^nx?^D%V!wnX-q2I;-) z60eT5kXD5n4_=;$XA%1n?+VR-OduZ$j7f}>l5G`pHDp*bY%p$(?FY8OO;Quk$1iAZ zsH$={((`g1fW)?#-qm}Z7ooqMF{7%3NJzC`sqBIK+w16yQ{=>80lt}l2ilW=>G0*7 zeU>_{?`68NS8DJ>H1#HgY!!{EG)+Cvvb{7~_tlQnzU!^l+JP7RmY4hKA zbNYsg5Imd)jj?9-HRiDIvpga&yhaS2y6}aAS?|gA9y$}Z2w%N?Hi;14$6Qt9Fc(zl zSClM66;E1hxh^>PDv1XMq3yzJ#jIQ2n+?hwjw)8hFcXDQ$PiWf{s&^_>jbGGeg0{e zx4b5kIhB2gIgyS27y+;DfV`%)h1F!WTP!76o?^QsSBR~nBXnz|IYr*$k${m-u>9Mj z>09A!u0*q9wSQ>0WDmmm6hKju+`dxYkybvA=1jG|1`G$ikS^okbnAN=Wz*xojmwWtY zZq{@FnLJg|h&Ci78w-ZXi=9I>WkRlD1d>c0=b9iXFguf*jq8UF(aM^HPO6~l!aXXi zc4bhK;mEsobxUit``hThf!0qvU3#~h%+C7bA-UJ%beFlm%?79KFM=Q2ALm>*ejo)1 zN33ZFKX8=zsg25G0Ab*X= zdcI5{@`irEC^Vn3q59Jucz{N6{KZY%y!;&|6(=B*Qp4*X@6+qsstjw|K^Wnh^m zw8Uv>6;*bKq>4?Gx3QFDLt`0UxmmN7Xiq<$s>g!~1}N!FL8j3aRyuwusB^Rr5ctV|o-cP?J#Un1>4_;4aB&7@B;k zdZy2^x1cZ-*IQTd25OC9?`_p0K$U0DHZIt8<7E+h=)E^Rp0gzu`UVffNxwLzG zX*D_UAl34>+%*J+r|O0;FZ>F4(Wc?6+cR=BtS-N0cj2Yp2q1d6l?d$Iytr<#v-_FO z?eHZv2-Ip;7yMv=O)FL_oCZRJQZX}2v%EkS681es?4j-kL}8;X|j8CJgydxjyLn~K)YXxg3=u&4MoB$FGPl~zhg3Z zt9ULN>|(KD1PZU)Y&rZfmS<5B={#}jsn5pr0NC%Kj3BZIDQ?<^F6!SqVMmILZ*Rg9 zh;>0;5a)j%SOPWU-3a2Uio^ISC|#-S@d({=CDa}9snC0(l2PSpUg_lNxPwJt^@lHE zzsH2EZ{#WTf~S~FR+S{&bn+>G!R`)dK>!wpyCXVYKkn$H26^H}y?Pi92!6C`>d|xr z04#wV>t1@WEpp8Z4ox^;Kfbf?SOf8A+gRb-FV zo*K})Vl88rX(Cy{n7WTpuH!!Cg7%u|7ebCsC3o@cBYL-WRS+Ei#Eqz-Kus=L zHm{IVReCv-q^w<(1uL|t!n?OI9^C>u04UcQmT0+f^tju& z)>4-ifqvfZeaFYITS2-g=cs6(oOxE+d0EAHd3=(PzjT#uzKm@ zgrDe|sc}|ch_f*s3u~u-E>%w54`pHmYs8;Y6D8+zZv{~2!v$2Rn;zl9<~J?1z{;(A z@UoM9-m`u#g!u`Iq<$7d5R2hKH24np5$k`9nQM%%90Hu&6MGS8YIgT?UIB{>&e~~QN=3Dxs}jp=o+ZtT+@i3B z08fM@&s=^0OlDN8C7NrIV)tHN@k(btrvS=hU;f^XtyY9ut0iGguY>N^z5G-_QRcbC zY1in&LcJK1Gy{kQR-+*eQxf|JW=##h%gG)PkfBE#!`!l9VMx=a#}oEB`ankvFMAzGI$+YZtR5 z1#tsKLDn{?6SAY-0$IOK4t{yC)-@xeTjmW*n{|re;5Zj0I?(*cntWv<9!m=Xzc)thU&Kd>|ZN$$^G_#)x z2%^6f(ME|_JBHgD=EEJIc0R()U=&0+!(7cWHJKxMo1=D#X9X^ zrn{#b5-y<<3@jpQxz(mDBys9EFS5&gC%No+d9<9`I(p|yOCN8U|MWIe?<88JU1}F$ z65mW}YpxpK(06$&)134EYp_b9?A<36n^XgK?+NsqIxAAw_@(Tp-w?v6(>YT23bWyZ zk~QuSf%CmhEgzU-si-Le?l zi<Y8De#UBk7GH}6lp7u4ZWWW(HWvk6HGK98r>$Lhc4g>ap&DIbg26pN+IKTkJ zj5m%j@9m+o$P$$I!#9sR5R0^V@L^NNGv^d6!c6ZN5bxwax7k%OpKLd_i@oS9R%8#E zOguV^hwbW1dDkx{my`)5g+*i`=fWpHXS6_nmBZR1B?{kB6?K=0PvDypQp`g_ZXmio zBbJ}pvNMlcCGE?=PM>)|nvl5CgjfTi#%PTW40+-&gMw{NEtnF+S~(9qEfgfDG^6G4 z%$l!(mS|w3m6R10{XU%-Ur0t>CjI)`_R)dXqz;6O(d3<7PL>M_R%b8%6DaTC^J;#i1tIdy>{u!xr>XSQX51%i%eA(F-EG&?U3Y(n$kgTebw z*5Ia#73$3pSKF2>3>E&PR7fw#DEU;bDP7H_=iDgSbb#c^bgLQP$1EJqp!V1){_wra zF59?uP;Z@lTi7ryb657UZjutvVVOkT6$~??*6|%Rc<>G0dh(q_OVcx$60m@FQA&sL zfT*O1>pj?j0>2}h+`SRQ%DG!)|FBZo@t$e_g0-S3r>OdqMG>pIeoj+aK^9mNx16!O z7_Y)>4;X8X_QdIEDmGS_z)Zut1ZLLs+{!kZ!>rS_()wo@HKglQ?U-lq6Q26_Rs?#N z)9_e6|54ab35x_OYoog1O$J@^GOgyFR-BQ#au9KSFL3Ku3489qnI6QaKc`JoyDPg^ zDi3~ zFkumPkT5n=3>cI$4y%}(Ae_H+!eb+hL;0W01;%>Oq(0LM7ssp8>O+%V zmDC^L*Fu(}l%Hx*h_ZlbpuhcNVU~)(u3aW~F4l`abNHXu3G!^0jg}1t0wVPvqviVl z*4n&FOdwTl$9Y*C{d+BqOpJPzJ5pqch&V)B+BgSX+A^mM=Ffbslck)9h)zaqElW|< zaiVEi?-|}Ls9(^o<1${kiaD?DOCUBc1Hqg$t(*zUGLFyu_2$jzb$j*Rzwak55Sb3D zBQOlKj)KDu?6F4rqoOEyb=8zc+9NUu8(MTSv6hmf)&w1EUDX6k zGk)E41#Er(#H*^f+!#Vwq1tp~5Jy;xy)BC*M!Oj+eyvuV*3I>G#x6sjNiwB|OZN8e zVIIX=qcZHZj-ZHpGn!_dijxQ5_EF#^i>2B)OK;Sy-yZo$XVzt_j9q-YZSzV?Evk`6 zC$NlaWbZuB)tebCI0f&_rmIw7^GY_1hNtO%zBgBo2-wfycBB z*db(hOg4Om(MRI;=R3R|BOH9z#LTn%#zCSy?Qf!75wuqvVD=eiaCi7r+H5i;9$?zr zyrOR5UhmUEienla;e|Z~zNvROs1xkD`qDKJW_?BGV+Sla;(8$2nW%OS%ret|12;a; z`E{Z#hS)NP5PF$|Ib`}Rv&68%SpPEY{~l=$!$)u*edKO&Lc}y!b&0L0^rp4s%dR#p z&Rb0lAa!89w%6_piY4(I@-_px7>I)K?vD>PO6o&HRX)65xFFC@m1IrI+!QDQ%A{a# zmbl4N{^INwcVhl<1YIW2ERZ#wL3d6g*(vTMETNjPZ5Dw40)3-NdH2n?7Nh+W=A#IV zR8ny_^+GY|#y{SwBT2Yu;d*mFqm>x@DMuwPv#=^Z3b7?G!HP{rQWuX(0hQs6<0%Tf zH6%>VCi5&)-@gLCq!dOCUITlfZFq@J2-eBXEpGiaPsz|N(}t+~!V!agF$|5<%u)YX z0`N<4D`wP>I_3S1LL%z=*o`9$hB_7V#%Yq4Q~rTp<&_YN{g|gU9i(1B_d7l}iL6Zj z-<#a0p5CAQ&F2b+?uXUv#vk+p0=i(Xqbm7R;1_TukEVny;PKIT)s&(PE~Qc3$Q8 z{{+A?Mw{8ajV#H_*i98t&3Qtt5V(x0G8PMp$VJ5>HqoymH+V3RRQXLKocae7bawv$ z`JLyE?M8K>eOH`+aFX=tS_INlAhueE#lj|qEp*GvJLZt|wee$As&+4;0i-1=(S<8g$m3Xb=#BWA0>4=j}1$3D)zaX}Q=oUvOk^ z*G8i{bP{R$f13(&Bv@%4!0}n~d|tu=4$8T7p~mgvKI_8zACF<}1^ z2T!5zg82qwbK-BTWdGH#74|81kL~SQYYrjQ$I2ygzB)uvzS!zyH@kIbvnHcMZ&U$h zq+N1$CZR5Y2qw(GxEM~)!j$edV-jfeN`L)8uvMwk7gw&i;sjR=9}`q>qB;toio7ZJ z;57Za)8J~a)%KinL+9}ShCi>x8hLFcKK94Ew2zwm>sf=WmwJu5!=CvcEMU%wSWcDY{lffr`Ln!Vqu*WB* zm|=gzA%I%wGdVshI$arMJQ*i1FBvfIIxcK?A|vEFs}|1mtY0ERL%Sg*HC&n?!hgiIDq|(#Y)g^T%xRON`#>J+>-SyaWjZJ#@}e8@R;yVcl)vqza?DVx4(E%~O$55{&N zT{2{U;6Y@lG5sg#RM|zLWsf&$9N)6ORZp{rCCAYJIlkI}9_WLpLn|}+b}1IN-Cuz7 ze(Ao9VI*_Wa7V>iyWl>Pe`x1A-zQc2*tLF-w`QUfmv(O5PK<=ZoWR-;gMko_-RA9F z6ERTL6?g*aZkeyS!)4qACG4KV$_#|Ti@ba6!rT1w3amqq9yP}9m1hV$-~9)!hdS<@ zeIWE`dsZg*#2YN;?ZJx;d6rtWudEpbNy9qH+7#Idck6NN2)~$>A|)8W{w5ATfDn^p zrkpo-Ft13BWQ#RlSm97m=}<_U{m?I7ZT*b?p5Yw^?qD%r;u96}`y1p5q8s>CBzb0< z9Yw8l1oLhiP|iF7m3ShOabR`)#w_g%KJ80S+Jee;g`Bi2w;d&Ef5hpPGr?ej?@?in z$+JzNK!N1SYh~M5&#c*Vac+leQN%Wfdw|hY*?CB1`S8dmVer9}RbmWlg`?mWRg-)| zAhh`uWNth_@elmkDC-$xJD&5Fhd<&ky!b?%N*@sfd@>i!!MR{oSpex+KiL0j*K?W) z4*WmucKqiVu>OCKD~>A^AXP=rVaX8PU!DdX&Lx0#=hJwC6B}=J2PcLSRZe!oJZN+D zTED*HJ8`{wvt0(%3_rZIe(CyVblz{zJ}bPW#u_=_wNkl;x&mu{Bw+ zHKu~yN`slvxNvTQ*SQpvx0vKA-Z*$O8ob_+^?LI4!Dz=#ReaG6;8M1N06Fv%b87jH z+)BJ$Uvk0^nbuW}2^EFv;ilA8Z5+$!?0#CEOOec?WMsi3H}Hlh*N`96xq^?}t+n!= zvyd6n;GI!|mX|la=NIbK({<)6IljR};&OBfmBiH;49R6^dP0gKS*D$lF;sKX_VfeVlea2Qyc&L^)p8C zgNS|b8Uo9DzwhC(vVPW3+dGS&-V{dt%WY%BfrEklVMAnbNYKb3bJMd0*y6d!?+lJ` zZ20^QvpPDgXOo5xG0%*-xUUNIri#IvhXS?mk7k1lbRY)+rUasnarW-lk0U%jNLzn% z*QBY5#(V`3Ta6#dsRh_*sT-8!c6F@mZp|t0h!2+tSx*_}41whAjUG@QLb94;Um2bR zcsW%39m?x5CVdXHTRF<&FlIt3f?4Q&hBmTeSu~6a=TZjeQb#O#BW9`C{gGR?TnUF< zTbe9(bsJ;20&PefJqcfM|Erf9&5@pDUhxo^UOWRhF8l2>sOE9;N>BvkXI|V`R1gqa zS`ZM*|5rzl$puo-fR&-nYU+0!!};VqQ#KkEiYba##FZyZV8)16E(G(4`~bK6JzDMuJ)vrJ`JvjUZ&7PE{@R+(v8qop6hX>Zql zN%WhroL_|=H{CBeF7pD@9`kmBgA zeSC`r*~jk4O$2q93WFvgdwft4XhI2j7TuV-`o^qUMpO?bfG(NxfR#+oagb#A@0IM6RYV$cSzvH=jYYHm^E2ky!Yg z;J3EoqNPuCR(a%Uq|t({W+_um%W5&6`ka8$ilj^S($F0X*Vm{fSHpKo8vbXdxw|S+ zBS&wt3{IF`-5HYW62(IfGenbS{{~z9#gEESBE;;kL~OnuV&cw?83V=C?1Kgq#=Cv) zTMbbRFu}Knl4TFi9pC?AHX~h74l`fcBbZ53h?^aTWn3f}zwsx~tsCk6f;P zu&HY5B_812M#a5$B4Eq&;Fc3U=^1^{Zm|c?xncA)Q&yq?<->-oJKf*)Qs*obH+2x(FnH|-x(lQb`R5Gdl?o!$nCx`d<3|6ed7R3raL>;n7=qV4|byO!fh5x{2#Vtq7Z0D+qio4lT zZtn~8C9PmHYw1`~*xzKHu02^SWG?I?(k(4=fz*>Ymd$>U+QAU-qN zClRs5z}Z&%9MUWZW$JT{S8Z=+bI??tHG;snJWo$H^+& zUNV$D&)zckKt*O$0hwAu9522A{34ez&5Mr61!_7-37jyZwKz=e@8~y6NCZ?yv?h&~ z;O7*xraDDhV79j90vUoLd#^G$lBk}3FThNgTWpDQR?JTc6#pY5h07ZBUGbebfCf-#PPfMIelyFl*xiiV+z<%58 zfOFgaKz_9w>IJpXJB^zPK(;wy4FhM`q_)Gn9%l^f|G9BR7HnlACCTXo0aGm@s(30Aqqu%!C zu=BD^+qu+L+c{O&Zjz&EHp#|}udvwCzlK|grM+h)>GIfH?2$nRuus5)iTBo*tJd;` z@@O=aib<`dV=~$<|Dn-@tb-aWUX-?7l0vx3#Sm0TnaVQcw?p5q>0G^SK6y2Tyq9*B zwoT%p?VP@CIl0rZo^&%IkhWbd`t+=mui19oeJ`-4sAZ@;IyTSt*+pu-^;o^%@oZ3D-?IU6-_yavDEcK3xqhA;t&txcIA7Lpf(m5p5b3-cSM zzxkM?Qw~IiFzp6T+m(ed>g}kuEngzy=hEN3UpC{@K}NvgBg0F6ZR*|S63w4@H`|EK zbobi^WwJmyPCJYTDC2KQ?v?X+C}X?7;%-zFLrHq~1tdQkfZMvyg(L}Ynk-&SdM{Oo zHXCPKXKu1Sf|^#-cH6dNiF<4hb}gvkqnP!Ky?Si=w?^qdiJMBR2~_A`$u$B?Q4B@q zGQ=ZYEhcDODOH(TqCDcy3YqxXhe*yqVFiKZ#Ut09D$Lg_V>Iplw)Y7(A)%k&BnThg0n6dv?&X8j#*hafajC7Z=HEJI3)^OAw&F;{~^Y zq+Vq4H6h1GTCfRJ^synHxe^VI{T@^Iu2ABOU_8+7()wBYX`?a>!zPl~Tp~lmT4s6m zS!=UZUxBD}oob`p+w^oP9mTLo_hGr>Uz|4j733cYy!S58UucX(*8P{4tNEJ_3_d#e zpWr}m=kE^>#sn6+=ifksiN)<2pn;d}9h0&rm{2^(h}v^2Q)YM@*U`ghE`TAuOPBQi zq%LMOyUVSGoFiUN;N@;slp~cvl5BE+05_i7K8~rPRyxLbVb~SuvZXpbD>_75_3J}Z z&AlK5SZF_DbJ*;_sH5Nep`U?H0l9kh1r4|~wZW8G33FSfb2v8v8-$UIzYI=alOa#J zbTtOz=ol7sN#XXeuJ(#tH{ zRjBq2r!@tEi){HTj3x|iFJbo%iruQ=6v&DAkW12o60mUVsbkJG>Mv&<^p>0~hUX># z!kuy60#ZSSeQB|ewqlJ&a^CyNOn7uNUAzu0Y_`V@>%6kf&60I;Q+P>~ za$iUy6P8UTgB3d|UA2|qH~S%r6K5;ySM`(U^#9oR(OU`$1E8oXf2a2*JEGYGVf&cR zE{=3SPw~Uo*83OYx2N9vSGO9UYfG2by&tlbXZYzuw{Ld1?lZSu6INZ4eFxt2&;!16 z-dfJy(XuJrOaPqP#$evbf(g~NNq6k}7nEe7>8x3`<%4wDb?_p@jS3A3;jC*LCi4=B zG_+zb)E)9Ek@?=}^T+2-yq+o$BkZylg!hJibRn)U!Zj0?BrvfV?>nfk>BCadh8K({ zEp5gWwj#F^U)ZD3;am5GO}RnhP^BNZPXS-=oc^}0hutWW_t*&s+s*6@73OZD8f;9U z*RDgj-%t-nbu}PW^4KZm>x?y~>gAiq7(+3rjvBKJej@m?(5Z)QaP9<9!$}=zw1myy z-p#s2{t*b3wMe!KGUpXr?%IY?j(X}8py|4sH$0R_Px3~s^dRlWOFoZMF(8MFtm3!c z5}fy!oh(F=pw-G7iPGllNl(x-vy>(i>a4B76GKVarn-lpUDbuYT-&^oU z<}-6qO-a1cx`Q=MP{1M?p2x4yMm|oGQ)($ zjq!wIrfG%WBmT3@uV+b(@t%$P$%MDJy9XOvVI7{0y{}ffn!r-)wxvA^yBAucD|OHE z^iOEy{v4n4m4(L9hbsypf5Zny((kaUAa&`^u$d0+Os)e^>ePMVF!DUO>e{F z{k2%oVQ}-q5mBQMmP7il&BS_>#}GAlIvArt-u!m_gEPh#dwz96gJI>v)R|(rTa>$eL1bgJ0%k?(9B22W?pKIl4Jg~Nmz z8XfqPUPnT9wp!Nqmb86!!hdVpKB-0UHT*rKhH%la=coFZ>F{!;XHQfGIH?e!(trd$ zwK=?;#WRz|F?d9Q(VxHOfByE$c7|tgKw*aiM9kOz^Sk3Q4GIo7)h9X;$EC54iar3|MN{zd%afpw5w%VeU+5Z*&v( zKE!zed9qHQM$jCr+<}>6q5nQTb$>FO1JsWkt5jE_o$e8};a8nInzIdBDwkPYPi~&D zb9&lML^jKp)Uxs`N@~}Qe2E%U3EJ&ds=2dR)%w>xJLAAKw)S4I)d?*9t>BldVm(hr zHR6$#P82}d=O^m>p+P^;Z$$Dv@de}zwJWQK_m2~;;EXewN z2BCeYmQUDbO6su=>uX{KCD>T}=}zlLHDd0__&?%N{o+`F`0^fR(AxJDCl~jGIWo5? ze92r^DAe+qtH;u*_Tx-r{9p|tatXyj5CQ-jtv}#{8rF@SjhqVc>F_6Tn;)6n6;$h- z!|HU6)_V=hwlrtS^(|8?`{(DuyjF&bw*h+-8<6B?hBGh~)ALVWFB9_&XFy|NEfg6E za^1eeIe&B{NbUpKA9L34MqcDR$)dFb-zL!U7GR$=SeScuUh_wxNT5}3cJ58l=%(Jn z-rBT1vgO;*7kA3uv^QekntXOnkEGkMKlz|;(`f3Ax>`-)&$!~SZEx&dOAWrVttb0> zvh6QTyeIZQpZoy+5ARAwxW-LZwLnh(Ws2M^qDz2=prk!IDD)pE#rcnu3ML!b;3r2q zPyu%TrK*wr+n989;<2WqNl8l!+5!Ydn8t9?g0eEu*>hHIoqY7B4jVl>?P1=lZ{f(3 zUROu{DYF_s*brO70dS zl0ut8DZ&a*m8HIdNVI6zag_0dRG4GdN&r-y+~Kf@-G?xRJYR;}4ujJ~cK7+rrH`iB z+Zs$!hH{L%GNzokv_7&_%*4aK2a-c0>Z0_fTCz=IdPTm(ev}Hb|MI`7MpKu#>%!RT zGOb|#BLw-?X-BAK+N*UEkaITY(bk1srnEBHN0d z&I;Z)o}v&~(i-WU9lx}pR*>9uyWHiNhLN6Wk&Qv1>PNJpjA)e1IPF>^==Mq{^kq)jyWrOeTwu>=5YaU_P0AsAr8k=$ zH$EAcZu%hpV9l3Kf0$tpiao4EAV5HB;F9kOag&*Iox6mQH(o|Qbrtr2AA=h~9xwSdLLZ%y*>x!`>`{N{p@S5P zO)8giI0iU=Oie+P8D8e6NmW%{UFw%@Qyq!zl-88UPM^)ixCT*b61_Yg&otyQbkyZ` z<)vuFZK)-yHFTcERO+0cZH}mAK1xdXZAtpoqGGh_0~wK@t$pEYQVz z#6e%6dbg5tl^B8egc=QYo2%R$ZK;BpY%?jY;B`jo`@Htl71vD`;QGcra7=JLLD``7 zte&w}^+yPSTz6>$Tb>f5-JmxIet}50g;DX~f@4&m`K&J%uezgHpazF@813MF=I0K# zwZMQ!N2TFM6P*dqG#jfk&690L3;!75jc%<~g_ims{lPl536&Iqfu>X&EiHF52AM2&|KTUo zuzLyuZ<989r#NL(!cnRx*~oRM&HFnJ9Y%*pISgAxDl;6m%KUcK3v^mXJL#;YWMFz1 z-`HX8`;%UP`^3V=%imqqkg&mmVR@}`RZXLxbeteKFT=5O@;SA>m3s8t+soac=O-qe zyFbg)Fuv6(F6q;awd0e-F@5raumN$c;zC%~n0Ve2NbLtK-K;fG>U34lK6M^kmF2G& zk)+CXHCGJV+R`TaJTDUII#W!$1n|UPNV-@O7D~Fz@>`R_ReWW7RxOA$q>%^ycxMJ{ zLya|cLJt1{jB}#Dmv>5Amjm9yYkc2}!AC;SsYi8?8D_P_j=IC8pE1`VHx7x9&Y7UbCs-fNix$IE)f& z%*I|(DN7W-`;E?;@=zqLbyD}lxSixcliB3HZ@vw-QAo^%`||vsb3-uf$oM7rKjjQ! z%UMFO54nTku*E^iB#-cWEu6NC;DLCj&j^^$5UEdT{OFEj3#K6C$*Tbr{HF)c_Jna} z{{fb&LgA&I(B&i1y_gF?-bpC5s_4bR_7$qQg+$?(H#-03hJ+SCJJDreP^ThC9v|+Y zL7xYW4J)3$g8cX4O`&Md0LpRdCtisn(qdhtr4P#I6Y3L;<-h;i^-Lak#BEluXaz-J zc-7zd!~p@3=L7*EPB!wwOlGV`0-!u~Rxt!mt@yS4aoUc^r&NVy@#p^{^N@45iQwB( zZD`3;6K~D8{Yr}=r($U~Lm#3IRmQc{BCvuBEn#r4$Sj4B{;$qbpT%CTt*?1Mg=ux+ zrF!2xpO+n{>&$;VFHxtvZ%ZbkEvkIeGNZaw@!nqSo|U;=XTDv*uP0PJ!0}7sgW`((})@6D|;$_@JOtNV?UQinTx ztIFKH;{TG~f)b}LZiwDij1ISs;XQmOizh}ZyF2<>!valh>%$~o`Bbj+=@OcRe!LQ{ zao&|tAHAxRSQBKF@f~w801}d?7t+nstsoQ9eJEkygv|7-@#Z^fF4NPknecHhp?`k5 zb9s$SLH7Lm-P65OFu(odEmY4VQJ>T)l6R%p zt7oi3TAoe`M*3QKk1rjtA%oHKnr=3A%1$+qP}nwvCBx=fw7jZDW#& zHL<8*T@Mb*)MG`MPC(T3( zzWE>nM5Vr;lnDjO5Q!V*&kXVrCqE7v;q5S=3hb2ym<356yjKczdIU~QCf=dndN0Ul zTn`g{G({HN-fBP9_`GollfMB3&UPEdUwMBXobdq$wlQy{_|puf6l?z9-dn{(MMl1t>#!4^PHQI=tS9oW1h>2^zPK8$$1QZm<7w zE?^uWHKk+7gOix!LS-B<7_sJ{s6SifWWT<))*iUNGBVA0Y+tq6nOp_-sp<0A3YmXcOt$_R|N!Dpy$8Tl&!JK4!$X+Rv=N{;O^eH`e(TxB0T7Ey@=`!}*?MXO7ij4(cC6BffqHIw#0fzIOcp zV`&|l+1VBo`6B{`Y|~4?83OWVI;{pV;K?wFp@Qr)Mha=Q!eF_ zql$279;UB4mF6P7ZNmc!=#00h?5aI=EvV{n17v0aBLaDVu*>qsO@+yA%^diVx&fq4 z7FFVyGA`vw%gSl5@Rvh;zEI)J_a=lF#uF~|yq=!~_RQ1eNsLpOjr%J+0w!WZ99?@4 zRUo^DPwc~EF;uMpWNl-dUky+-v_$;?m-4`M-_WSJ)?lG_M=unHpaddzRwf#jB1Y76 zf$zMl4c#)w#Ak2lVN*P$?3KALZ$?1Imtup;J;nQn3XY2iH&0m|CFME;;kiwRk*Rtu zPO&R99xaa>T^kK#KVOF667{h4L_q#cy}v4Kd6|7KxUzEc#-0a2y6G%wRB{W| z`DMLFX{dseQ=02*$FgEh#o(Z)UxEMJH%(N|#@#7h1MhVWz! z{ak$Kg90_`mq?;TKB(JFo*Z#$4kW?A0?a>S^Zik)5Ek3_o6@QDV_B@xFPRT>Jt63v z#9*dw|5?~c!ahmoHNIN773Vb~_Ku~%)0N8Z&BzD9FA1>Brd@}NkugZ^Ep`{cznY+$ z%EeAZ>SM&HKFWE0nVt#zSvHl4eXf82F<4#qsB0T3HHd`}!U}NYxALu%XNax>dRi$j z{|rT36BA4}F(ZL$iro%h;c1YX8l9FH6nc^r12c`qJ%bLnaQsx{ZWpa`^}g>isl1g zP;_fFXphQc!Tu8|CcfULKs347U5jEwryPV$y6>RAWB!^Y*dSMqYd@EW@B$aGT*!T* z7)o@o9rOW4_gb+5X+JxI=#ip8R_%S80k8SW9|BX0Mk*I;Z_PwZG813N- zHbUGm(7C8w1NSZB>kG+un`?ctG9ygwtgW54XTnhFBL4U#jCfH>FWd+*Qgu^+7Ik`5 zH1QILxLZ)j5e7Q;VdYBF*Rx{qU8d`d>l(GiZTz^$7uC5Zk7)~QM@48k?bGbhx!Whj zKJ3;gX>!o-MLwe0$Fb?Lu1j{6whN`00%o$kFu(4pi|3MJH=%HHO{~#P#T-(&aKnB< zrWIM8a72XR#v_^?G2|m!*Zo2UjG#qm^|705mj1S=uE!hzZy^)UAq$JKXw8kJm&{tz zaL`*wXiZ^5nV2iL6B5rU`XpiMuGt&rm|MGXvhXSAAm7iJp5*!2}6rEiTKfDF#SJm5pZi6uDl)Hw5wqjheZIM&S6Yz`R}%7Pi*j?SUB zs%f-Hp1u=x_H%~_4bsYG3gw3hLaoJ9sl65Rqt|G0z~{0c7Ya7Hj)iF&%+V}E@Ovc& z_(zJjEXC(pGj9X)~rpsbY+w;T?^&b)D_ zFclEt83QqG>rmA%@%183yfvlyKede_-+60fa`U6VWQiAddCu=K zg=SoKEkpTaxPFCzm76Z34$J^fZF%CR`aK$?0hF~|*Vgc3FI$v$(7z?p zjen`&!$VhVlseS9!#Q4^+DO&?iWTQ}&cJSoF{GgGs@eEUBv@=xb8WQ}>49g;>degb zw7AjB=EG}|c9ECb75z!runjX|SA#HEZL0igt2;BJ6PfQu?};YuCVFY$vM>OmX4;3j zkRf~tyldY*9Z*>hPQS!Nkkj)$X67qBs%?d0ZJ`o&5xQ&Ip%I0p$9+ok zr%pnEbk9MC_?PBU*PllR0WlI^9H2GWl2{lKeZ**|GWD{3kW+@xc=#;2Sp#xy1P7vBw!rp(x~(G;ODqCAiC(A7kY4-Js!=t_6!t zM96+;YwCG1RIG^KMD%_P6>fyooYx0_;7EHu-h|01zGQZ*C5%@bEiK&`L-Xtx!52|L zF9|Dcq@KE2v^>mPgRP>SJ4q34r1!~6E^*6NUjWK?L?FU-?bTV*J#SgtTyQJxV!z1^ z=?XgjzKPxAViu9bAr2*wRlJ;#^YWN?#`&Z#8t2olG~PMbB-D%wbX0Db7z$(cd5y#* z5y$+XPQ;wE_zEA$gNs)OFI9}H@oq|wSCM|yuBcAS$@GFg!oFP4i?{R$B_554HjJ*B z`2}!rV1sMJ@Y?I^dx=l?(`g#kXS;oJCQb~eEHBR{(8@e&nLY-A((cE(t1rrN zm=HWf>#8(*IWUp_N9j`|0@bN8lUZ9!S)kkuPNgd77RF}m0X{~h(q%F)^)XTYK{Wbx z{sV2-kN0$ZY0_*+Bm zl55$t3`?zTVI6BOy!lNbCNf%F#1}l=rl#DkEB`ZX5aTuW5kqw?D>{lZu6ygiqcwOQ zE*m0Db$-;-gOaWjN3%|7W4z7St3)gRjJ;R%`|+j6ib@s7r8%ZldCrI4#7pf@Rw)47 z8{70U)E#Da@X43CV=VeHq{-AZJwBdyM;)bbJUr6f?=dGjYMk7M4iWmS&Zh@uvLMA9tsyBdMlkQwrm41CFa)p9eB3-#H z?h|txb4$vWJ=rVsY^`8jMNk|KN)5;df-$-K`q!goZx|i9J?CN`4r;JSge$Ae7h(9R zlVZ&42`HCDYrtdu2tD*2UemJ+#jvA4fe}QYGHA~1l^`!^sRTj&{ z|#4F)+%Y6_z=e+^ss17tLZ!#Uutbq1{W-^8m+Nb>uV^=CsAFgo5(M;_!O1Hm{atl3I-N>kDXv{2KE1 zyAW1C=G~lKv1yFNjiCj(+q+|WL8X73=45tc3tY`Xvw#^Dk$b)rur@!2bgC;KD3J^ID zG~T7G7$BLYNn3~GxC1O)uQapRl|&obXFf@n#34FXK-e?XkK$h!#djuE7S>mqPLtqZ z*Dmz;%#o4C!DH<)*(bKOTZs=pOs4~D+Y`{fUKw=;L!C->h6;hKZIK9yM>hSUTaapOtgn6Y zUr0)4q#usk#t%=<%^F;wPxlY+buu5jBcWQq)KJCZk+Ew1LgyHdNmCIsy|Slj+Ll;v z$qGn#>hLoFfGI-Jj-qY4^BMhb>AhLeqxh6`iNLq|7dc*K8((y8r zs^(cPW>x_Qp$MoVOKg_Pv)vj>DIHufIf=X{$8Y}*$`<09GZ6$|!Kp2v(4xSYhKx>k z1Kx}l&j;00Y(HAvwt2MF+`LzX$d8mDwg>OEuP8-| zZoYLdOg>C{VX1q;?bD+pT*Oa^+7;&pgKuuqQ8y_myutFC(np zj48I}aRV+jtfk$>O&3vZ9r23NJt_94rxRKrfv2d-eZ2ZzvHqB5O^kL{+q^G{t_6#% zeo-?5JTLm*j%T85U`#eo28rUOtyub~pa*!`jWxH8epQ`8QuMKglT3nQ`ivlJN8LHM z0W;&Vk=CzB1?rtgSM3YK(9*_9@p4GP9kM1Ig@8h{cwc?nwS?-hLKtog7T6;FpeaE@ zQ9*pu9uPR1aJY0*kNOaNh-)FlE54^ksVD%|!l5I@lo3S~JjiLN4APbO_Oi2u>V@w0 zGg#%-BZv=lSm z06?zxL%4AzSn$W(_mk~HvJoAz7aEu@4A(d5iXTCQ4d@@!t02~*Vp(xcc}D|Z;FEZb zq-Vwzu$<;{JkR4pAWe()hw~vekzhM%!};?P)%?0jiZ5U;_{6%9O%E8BzIvIS2%1L{ zATR#R#w-##M&&!kRp9fQqQHeAk{do8rvpg#fD{>rwKJ2h_aY>|A?+Pw@)3fx zWc#`Mg2si`URmQGksFEXPe`*ol*orX)+V8Eno)m1=Va#vx7FIxMYq1TDO53r>kN=3 zB&WSS7*$Wug8E9~ybpoQWFjs!X9{Olhm*_>&eVhwVU+M_i^FHQyj)gVC%*PwUsm7h zlmE3icMMXez8aj4Uej}~;Sqt@QQu~b#!z76`J6S6q@|$3GEXPt%6}?7CJ<)n=-;UMiS0-)lp@hEd;A=(J>5nrC$F0wycd;J*UVVf+A4*rv?bhOr%L zx;&>^tM|H0S~kC`Qi%o1269k4BKv*-~Ovy@|sg~O>oTk7AdWR-jt>XAVaV1yM({;bW7~c4Fx<=L8(lPu0K`~^k zP(3R=N~7&YS@x?+39JUR3>~cprCU|AtQ=7L=Uk&FX%^O%8w@X~b=TX}duLQd5U^U;)cl4m3@{4 zkuz^_&g;|WWbSz;$6`lEQ3?Bz=-P0o>#b4!6Ea81u;%&C=+H-xZcdLrnj$VCSk+xI zPSr_Dm2!N8>0RJ1GoPATro2z`?cJHW-1q#+a|$oP40?d@Yzcik*ofkOUQ5$NJ*=%P zK%WKheP-Edk(O^0<~z~wQC1O2=t>mQc9PqeUFsv0O||`4?d)NsIzM9|Lcm@*C8QFD zE92qZMf&fw8GdUs$+8k07WdKqdEtIseNX}Dh44zc9v|oqA8gEP$LwJ%@WjSbsay5W%R?173^hLb2{`BOgV(k75`JR|e7U4|~L+mJ71xtz^|yj6N3 zKI$4hwADr`Esk*A&YWlEeUo;}ilTI?=CdCD*^Eq5eIrC|OIEpl!tk~mRqq?W1MxO= zT-SX&)w2eJ!3|hzPbJY>KKw9{-f#}zvA{2mr@0p4ZU9kAxWU&av&W7Lk z_y=En#~H{N@J2F5+Q;kt6uv?=KD_!dfHU;N=P4q}DaKnU%qg5T%qjAkQ0s#UdD~oi z+v*e&l{w-X91DOmAWzy&Fp#M8XOzqc^|~+4C}|Q{ZG&sO)v95L4j{4MRAgnd_{o8( z-nScjhYn;{uaSpWzpGhv>!?}|AAUYRmjq4DI=fZm)l6?uvkfM&E^`6R!!=}Q)cuxz z*i;8|(kUS9WkdIE_3JM>T-U~0hO8LYI&GankCIhh_zv~DwoiRY#PXWkzcKUI7#8DHu=(ozVr z=i}8TB-1-B#+IwiN|`2CULcZHNEJh!Ju)!txHW4UwLFzOjmgXu8GlAhb?%d2;qM;! z{SG;0IKL+=EXzp;g$%oGs+yXZa;cPYG;AE4^C(}*i+&5W%m=tj*1=`Q_IQ~KOXM@g zh&9LGHrv+&B?vkfs<2e`@VvAz7E|RXO7+wfrX^O4dFgivBT9voC_V{AsK%{$Slj0|Cp3j9aSbF58I#jRL*ABYnEJ*gK!3GYv6?2a4$L2mDIA>!D9y1ZJ z-PdVox@E$9YidVU#Rhl+>2}e*B?fo}$o4d0ZQc|HGzBPkWvApaN6_7Wdv#`9yLD5E zO67O<8PVA2Gh$0Q-XFOrD0#mN-^5gfp(E=wIt^n8BLF~l6w?9XHP`_tf^L>!) zC8B){UAkss?o2A?W8PT70{V?9-w<=qw)(aq@A**Z4|vkFhC3JTIVOs2!;L;z>oV zX9Utkz}N*H?VA-lpVN+$(7a=ka>8)N28yoeqX^Jt(*Tv$C;ml6yfDN2fFfU@Gxp`% zI#1$T0o5T_QmvaZ7R=7+`{`=iWO%z~d;APB{;n2wbB*LrGOys(Wey+;gYSGuV{Ml! zOS(gc;f)sI_l~A^$CI{pPQDG#xyhhD?6mj}PS2lU{5SKCYtI)SzBK6$gc(lY4IHUf z4jlmd%bR1Z`=_zAfIWtN9>H{_MfB-JA%VDWDA%mnEu^A%iC3A4WCNRt2Qb_sFERIt z*$DB83-;me{`VINKS+nrz2>o$x5BRwN1sB>k1B3x;z#EaXgX=`sck5KW$&^ofFul= zLP+n4I8an1-wbrefi8w>5*)A=MravTd$w0s91g#l`tsvc7N#2a>uGtC(QO zpoDD%&4$RrxXaq`#@G!K6{{p}%VN%h3t2~et-S%oxO6M#g0Q@Rg$%zu0>mf(L7oBt zDGRK}O@s$pPMtdEg1lVqsvt(5c{{ge#li!Y!necl%bBlHAO$b_V!Isit|JI(LdaQF zA|6RB3A`QrBfUY4sQFt7V(&M_0SRD4S&C}S!Hfv?Pq0h#djQIg2M`y_ zQesg4c^DMN5E4np@bI=_ev8xDcE^0w(o0q~a6xOzL%X3TBh} zam(7^Km>WD7mJiolv}c4n|=B<@qj#rjssux2^-!ddxx>66mt#klHjU*pI>|rPLVTk-OVxlPO=%sq@V`D4YP(Rq&x0 z0v%Zd_r^7*rMT}X76=opBG0m^rpSjFMFiPh%iAJzi4`{p!!SD}T6tzEC(f)`1)*hx z0{~Q1m-yW|{h`o1fezEX8EP^JnrAq%8}9kmtf)9H%U;DT&W2nva}6ma#j@7KLGi~& zkY2g|{Nf$u#ZRGOe9vi6|1qNYMG$|Y@DV7~hNl$|>_SI`|;@ZpB z)Yq&{gsAUtY}=1LkG+5RdmpzRFU*w%pHPB0#j2vTquLh}wdH6AY9zY##9$KuGAPd2 z>PF;yErH!iLuZr(Blr}lyYXmPJ5f>GvN}=Z78E|*fUT*5lI|O#kM3}tf0 zbFRIHCg)nrXojcfY8D%Gt0b7kl~&4IO2Jkg)F}{@@LMJWp0wcSHqquOz>Mir%-6Fu zv0k?=kb`ZNd?zN^`HwZl8uy%L)X5&kz=Nlx*CXONUVMaK=L=K`lh%cbpO?3vU$b5F zoIa@9#GHDysjaP^Nc@G%$P${vJ1?J)AuDx@xO~z&W@~AA+f6owoVl;7K@Q5?QXM|J z19}9Sa;3v!L`rdhL)S$kU@>JJC#LFDc1?q`9>3J80gt`S4l2N7zc8pJ{&^=u?3}M~ zgsnNg&p*#MmqCBEj&gZxYAMrJB8|0`bFOYQbtuWqy4y4Aysad|Oxlwt=p8a4U0Q*% zwLw~z_f@XVR(5)W%ETf#ZL7!*4~=B5)mEFygD|R!mKsdRO|7I4z-^Epdl*qY)MjV1 zI0qdc7Bn2MXvC|RJeTJE{mkH9FD0{@EsZ^_7KvINcah2o^@bAFxV-YfUOx5-4$@7G zlQCdT=QHhwWvG&+G2Pl9%u=N2Ntcl>P5 z1E`>-CJ6Uhhf{6~(1G4nkAsboN{d8d6Z=LAxnwLy3K=j3{)f!x$_6g{C)RqEa`G%Z zjsJ|P>TQE{u2b$Y>7ZqyHk<20t>nUK- z;wQ_VP1v@I)07Hw6gH=O|UjlM7b=-Xxv+vWN0S)A15A(e4L z_mkd8P+uzT0d@#3xZC|+lK#pgpQ{&fcTb=;ab0*KkttdhZ%LHMdsMi>W-UHw?=ifz z`=bmu=$2YtS;?~DOdT?oawEzParzc-al;4VdURsa#cOzhGaJSStoA#`Z2Q_%m4!$g zb@;Ev7|Md;E>E0+gHha*PmF=m+LUF{A22 z2L&?6;rw+Q=e7Mzgn$XYa;=0v1(k*)@S21}q_}PSC|Ub69NJfhb%696>^IGkZ5}7I zOtc#>+&_K7l5g@O-)~Ce{_N1ADo<)yfiZ@WsnVoF7O0RF_GlyPL89lbOpWgdJrw5g zo~Gh00!BDFiI!6GM~ufBSKv{{zN6pnq2+Ph+q{D10x#So?Nm)=;oH~lLZ;57mVmMN z&-%7yUTb=4y$g2E7d)Gw5N2(fi*a`3(a;yUM16lmRy~`#^@Xw zW#jp)D3~YC2dZlI`~ z7qW~=huPW8cIp`zV@I|bI;XKs6lz&QYnfvcK6Iet}7TPqK4(mv?v3g~ndHVx`L*`GOOUA9Oi*X1kLkkytv zDE;V6{}`x$P}AGq(Sx?>nQU<^^k}o|0i>)5)_X*)^wfLMgZcL?2=sB+axUb_n?t^b z5e}iqUY2W8%h^CJ<%h8N!$}SniMU|(s?*@k6m!7ev_n1`ysU*N;*>YoI}JoZ8b%26 z_Q6JBHBfSZ{}I%2g|iq09rwb6kBAjd)*aJLEiknx@+TZlPk_S<)(o4E@vZed1=xN{ zwdPaOFD;576X;htV>?`<9{SV7!hspd^u;O_vn{!z1*_c2YH$KMrEi?wCK<3IiAa>N zmL+PkhB4W7%v8Zz1f~j^Vy&hMx5^n?Y_#>7t=5_g6}w`}GRGyh6PptQtq6 ze;~To_HiD(!7&W!F|?vN2+BGPx!Mmv*_U&yg{azxN87nTx9%DlMDDleJM+O-5gyM4 zQ`6}3u8@lHMdGCZiagMci%bx{S`q;Ivt7(Eb*WWDiz{GDGiMAWlB3Xw06$RDh~1Q= z5Efz{my%J~We_=4Iw;_Z-P? zo|y&16$jm$bNsStJM~WhXRID6Hcyb8?Lt-a;u`(tqyjUCEjvq<)V(6}+~D zbGD8iwr$_&i=cIW`#$~Cc;FSDJF$Z+&eUy>NJ?*WsI!rdyp8)Q`L| z(x0O&O04-Jl)Qscb{B>nVK99nYYS+FOA~WS`4^)c7inYX;212%OaKtOC}k(r(cn4> z`X;bBhNsFHxPVnFo7zSTSG;%ca3-W^x4z-Vy)SZe1;$PHZ>fdJe-W{)5zkD#j( z%mO6tB9NArhn#?xUVyZ!-WmVaEsdOB0<&OD6Usv_;%In>nZDFks552Ek(d}_Qa|UH zbF_iFQHLSnbH3+@Tt-A*eZ1V0n{%$F80B6h=5I>jlVV~wK$s{V12rkNw&R)a1#pR8 z%lZM1e$k7^5dmKS%i;3HBurkNuEj!D@;&CUK^gkDUT@ec^1#6Zyl>C@fe`<e1f=9shLYzW(7eF^jtF~B`agPh%;%V3GeZCCm^+68dYofH{?!QsCVe``MgKo1 z6~R9uO#ckuDe)J`c|l6>ALX6R&%3hw%r*)C145Gi3$l_T`g=$JNb&pwl#%-cl6|W3 zKmo^oqX4ll@xX8mfusgBK>bTPFe-~rlMJZx1px?si~=0~^vYQScP}l$h-`tfR~BG5 zcEGP!0$`-}z{@L1FungY1i(N$T%heW3c)`Fsefj*bOt&)i2(DDP=L=aCm z0p|lTfdsAue@M&@Z zzuwY;^@IZZL&$-DK25I7&t5{H%$*1rRo1782`spi17j=%vKBA{@$TusZi<1T4_H8h zdm@7WN4Wt3A^Yz|eYT~+>m{Ec0$|fU8<k~{XdsT@Xx;Se`3gMKYLNpE|Wq{rB@`RXuCYxyBgl z><%p92CU(j0Q~gDra$G3KpD{EZeUQZBHl%z6J<&bf!0?3ajZ)Xo&2Z2)ZjvNlVVH4 zA0mH9Yd}0y*7T$NE-Th$&M|mRwGA8f``7f$FQ+~pJ~qF=udjOyVWM<$c2Z3xvHCE| z5%Q766A7Vf7kKAwtZWh({9$|~Zb@?QJLQltDf|SUF>KpeEnC5j=>;HZCC;ASZX)X! zs@%!SMp$1fgc(SkVTOiMiZ|4 z5jHQL1+#xl5IU+B z6H#S>cAV^J_19u!WRL+*$Hm3M`|;R)I!_uSJe_tz@%^bS4mz=?gzMzk;X=)s-(-V7 zgWfrw!_gx8LZKe}!1UA%TGK6FM0d?AwuQAa`q74=`3%MDSPTHc^1m(4I;=!W$vnt> zGJ$M{zf#m1X1TIh#>;4V%x}Yg@JglLQHu9GyiGW~6BgmI6L%XOo~(_08hU^g6Yf;N2|X_dj6K;D8&9t0{p%lPCJP$?BYe>z z<1D`Nuc^95(GVaDu0E$TYJN(8ja~T|>j{(z#UUiQa=ITnO_b>ibW5=1gUXPo` zzh2wLK<+&!nXf!ZeQW3M3sX`n5edG}g`Cs%`H#TGI_u*IId`T7r6kYg7O&+?xNxB% z3|OhB{Xiu@EM04RbY9LFTuvw^xuP`l+7dE9{UMA2T@_%D1ZUXe-m9%HN-y#a8lM6F@&_ZPxMV8lEOia670ShaHsp1a=mL+Ti*p9DT48nWVl*TWE>a#m&x|)f^OFr zqqreScC}o{i3#;wiWm(oU1I(8GmCl7lDJ3kdbX~({nYHiDXRBlkJphO51Ku?iX87JRU^YGBHCrydn4*4YhczR9Nz7~sIA+IgYF`h~6ZAji%Tqp2MsCx0_bE0> zvAv4JkHR4*i7a}jx$w{JH)_`MXZ$QnDs*aj%5c~kXmYKIF#2B2+ZL^8xI_&q66kt0v7lFvQ^T~kcQUa)|oFNh>dGRbZWn$ zHInpr6%DTg;ZpvN{LXgN(|_~#Y4!D*&ghxhQSi&hDu@LY$guGhJ3~XMS3_7<|$Hyir zfk89c-k5)AK^H!bo(gmfL@_cJswK3D?3rNFO5%YHm3FvJ$uH>QN5g`$L{?v zyHIrfHD55Fs0Z1uDN$ebaA0XZj{_|;FQh;}uIlWrvSbbB~ zi`G}R8oRPpx3wypk7s!0rc%?Oy{V+vJTszq#@TL3@6!W8s%N<RpP?gS`!f@4AxMZbGib$tfc2}#W%7sVn z%2FP2F<^k8QX+Dt+zQ8&+sF*RG80m(>-iPsup%FyfCIVHdJ%)@(9|lBQ=ul$<-S!3NM zK43(ntb$6&5dkru$Qci9-SHmWAUA6I)sGQr2-3-@l~1)1w=4*e@ zAq$TupiyE-lvZP#ZCEe0%=Xy9`0qBaT;B*`tD>X=`{&RCWkHqZnnOfPE%T1Nk4L+P z`%hyPV(c4;K~AVU9DB3pEytRk;H72V2Egx_{gD@y_9Qi1Bh6apGUQ?ZPM#q3x{%Q; zykDqC#_k)=JLCO3rfWo|hE%k78M#%T9vyWwM>Ft6oB?WhtEF4PPiR(_{)^1N(c2X1 z>&E70n2$XV)5@MO!2X9w`dBwPUK!icIQ3>kbCIqrYXp*Wqs>1i=f}mGYcbj}G{7Dy zAg7V&k6-ZDh@3M~pcpY(oOHk08b%aT^!jadPefl$)N95VB{%6Agsj_EE7Vn zsn&8&A}v&jjcV?O&XqXA&QVH31xWAhO}I+q2RD--2RF|uKa|id&JbL0ka&F#F?Szu z$9K{~#q+cdoZye+XW&1LoU_((8(Hl(HU>T07)k{78Al8~kjOrCkiQ+lAFLqGL#q{n zi0Ah}E<#v2V-@Ak{UMu-oVWQBP5y@X-v)5&aEmGj3IYjo0}cWrnPP%LkP;*dnF2<` z1bk{&=v6{g6+x5A_L~f#7qE<&?*?Bkok&k} zcN7pXYom~I`P@#n-EMetKLhWM>4I==aWXgNj76Ae_*bUM(D--_*i|@HSX3;exk~6l zDaDGkdCjHUdV-C$&!x3`2=gDqc>f4Q0<5p`>nC$0TB`Yn=B(aS0TFSS&k|ez!Y`(U z^P(LKO8D%3sL1NP|Ik2IUv-JL;$Odqz#6*qbF@T8BjKAo6WE|Vg>{4N{A1ASQ{Hl; zzJRwB;$Ot(8=YejI&K@@DI_4dXwFj2vF%YI7Vt8<$oe5)Z&zYZoDh$Vy=vb51Gwo2 zMx`20<#u)-<0XVD<}GC%&=SOM^()^!u6piF5=`EW7T{wHc-(!M*ADQ2Y)gFU@vmcT zGfn4|3RVNBnzw_}l_glVD^HK4aQHf%jc^AOBu=qwFIu>1Z5EL}!S_Aj3DuAMr^zv` z1iaqEj;VJ1-emAPVOJh%m(cJzfZ-(BpEydBZQ@2K&}p)SC8_Z^OJQQ2e`>xsSvEmk zHkEJUUlbQiUu%5G&UuXQ>YUpql2PnF#iYGV}A1iLX0^|}&^0i>drOvAE76fd%*kVw zX-Nv3lNzX}%wvC0EWp_QG8V^)z9ywPRUfT72mduX7%+yjjsvbPF5x_gvH}h!wf{?H zTt^`APUsf@8xl#Xr@hKo4wrX7#c0>hV{d2oX7~O2;_Dg7N)Tcp!Ubo#K|vC|KfS>~ zlBUHKD7ySZGA9-Sl^dBm!%J+!3@SFnh_i0i9t%tE!+{>G^8;>p<}oOicjMzsT6(f# z%o^M;vqMXgj4<^M?<2h(pgLsy$m1f6{(~gHsTFLR#QRt}DCx4}W*yxxkCg8vSu!g->6+C0q;cyzN>^2A?5w~WyH6<7?cq0019=-7~0nNf2?ZnPI7UBUo2X#NKq9DZi(W3B0P-)!sXICls6_)zo zdgYO=8L#aSg}Ql*DAfF?rZyNI#O-7{C7UQLxf!q0o^ip-{+8LR_Lwg{>3;K7W`QvP zgPmJCJG#T{+n&M2|JcN9xm8Dlvo`lL{=tOt)`I6cA~rvkM0lP)?fi}>SE(}9)R%j* zX&c=8!E%I%3$F2xav7H+p#FZrNNqcKs3`20eHOu!u&p$gL9pIM`B1lgSz(+tPJo8m zD$ES&*vqw}12^}MeSElOx4;`=hCYfmU?^mk(+uVA75dj)NmaN1((uNaoafgHPAMzX zF|`|mmvTE7RA~{s-@ZJcD3edKh}a}L#D1=>F1x-WgK^r$K*0|N z*z{tJ!f7BpB&|baka7eZm+?xG7iR4y>Ow?a3w%pK=C{_To@#Bi$N5TFDPNUMXI1sp zn#Qd9^5mAhmKvuI*Ud)h_+)ecfz#z~AOzDv(7VrAlWq-I4slDNx=)5CCS9Wt{yCBny z#;S_r&)WnQg3xfsUaI)dGj? z@H{H^c92>dNv;UtL-{EKhd(w!gZZy%5psUBWx;jsoARh25EB%%i^2 z#nnCv!IaG$oSkbGH|VDX4{#jRnt3a;KfD&2S0%29zZZqg8Im%|b2-HvilV!uq*!g@ zEODVd^d_Cx+-!_EYd_pz0sCA}xQ=AKtnRHY`%f5s4I|`SSO&s%0xOw|sblvzuelZm zj1`{OTQ%0GT|00`-uyNUXyrRkuF^fDs*5GP2^K>09B>(<+prqh;-vSVHIpOk0WilS zoTlcky}U}?24E$^xGVU9$%!({Irkz+OOYZ<n%HBptG>=$c;rjV14YBBe%*DsL+45wzFIEma4SXR|AGy;;9Yxzy;w2NYTu2WO#| zr3o^ruf%=Q1I5!8d)R3ei^+X4OFzp|aK&_5OyKve53x(Em$69~A;js0j?Z2w;$nz@ z9AKnIWhm1in)P{O02~L?;o>q~>+0TP?`Z^tX{yfDZ7A%x1uH@WNXFt@~{mW}CUBduKaZ{-&j7k9XW?KXp7 zTRIf~@YmhgSmTZ-A7b@Ctga|3$2R$EmA{_*ZjhMP3I*Qj>84xlJCMN>&zaw8nd1C|}Y!i{;(DhwG3aHmzL9Q^pd&Pf2(VbirC@PKuF~A+EXi8f`@g1z~b&+`y zTx?ZOpZpM8-u1JNQWmjN6Ji-eUMD)JsEKes4PS514ecrLC_3hs{e-dwu!pR}Vkmzb zNj#h*(|y10A85Yy<*aH+QtueV27Md3+?^zTkp1uAtQPojP?B=ZDgziOEgPece_P@0 ztYP5L{;Zc5--K%lhK9B+dODXSr=^TCteKyw+BR z?GaB1ROf)&i^1mg8Rp^D5G0&K)O54bMG$PtxpZ@bd1u{p_;1RxhLzfe-B4>PApzxw z7iKx%w-W`e4f5+8%Z0N{F=T{&$!C{>N9W>l*A_8Cj2h2Kd;>t@`C#CN9_96%h1f>=)L6v09Cmluf&8dZe&(31MBhp=EM;G&&IS)pT+P^yaLR3Aj7SFg zx6$|yDI-ot=psOl3FFqwfMRk_{z)di_ut5VCA+7a(i{D^xb$IBWNI4EvG`!W zbux^*!(}@jXAZAIa}b@PM7#Mv^apggmNQ8&u7g;GMUXJU#gTuSE3L1E3&R7eaqT31}tObr!fms}D< zk8B0U_2_g5)>upemHAbOdX5?WR+HmA*Zu6)RiR9Zh@a0(uFJ24r-=IR1&OB?(``L` z@JLi4`-Ar>7LXRJl`2gzXB*ZWbYkd$h;X`}3Rj)XQ zAMd!IFC-9F_!K5Znz?|XJXZNnIR}kx3v8skhevzA_~LZGh2x}x!ScF0-K#-7rCU~~ zmYIHe&CZ-Exm?`2YK>)&WjCL$(JZrVIi5zn@8d7RcFqd}TY%~W7h#Ns?6Gs@ObmCZ z;Fl9|Rw|lO9y2;_(GTWdB-PSCnQLXpy5TGv>Y;Jex}kyl`H(r)Uls+8EaV&95fd3j z*tv!O_!o9%;*ebo2O8#kq}#+LVlT0%i4b2&(V?b2Z^aRPNIQPYp<8vtqU2ja1vsb= zzQi)C{9ByrBXPP%tQ4roSxQEk;(sHI5*XnOPY(U*XX;~RP@Oo`gg%`gbwl4^N2R4*d7&#i6agknUz&v6k!GgWH z#7<@l1&9y|V+#C17Pa5pKVFd^d(wuW$VtO!Fh3nI=XNb{@)-E}?-edcB9+3NnXE9s z|Bac>R51iZV+d516jOp;M%s-pj*3*1+h1cu4aJUh4ab*L9@u*1!byg(ND!gsgMu8c zt+K)6tNq)z-?#Y8a1XDU+vRw5RyTPyLGyAWpFq;>ca#%v;F&GeRs9}6O{`_Vwu>a6FN={o#)u-E1Wi~x4(^x zS$?FDBxdkT*p!D=V=jmArQd{~{fL;J@g^O57uL~-;~~21%pc4!0Wn|@r4I165%mUs z>51VcB?A2xi+Q45;z^#se4f}Qy6{=0bUHn;oY5v5@%G!i`#5eBlR1*3Dg9*OTv6+M%@_3bKR*{SqOA z6bcYxUBkjcnpuGT;bg;feCxZuO(01$N_A@_4UVed4?;A>-OT{qB2y@1Wo2pA_iAam zB?JIpkj#-*0oXy6DVb|YqAHoCasp02i1Q!JX0uoMg(q7lv z?a%#xop0B(_4HQ7{#h7B^dtCU*Ze;4pFO&*!^~QF`K6DtUm?q&-BC^2z ze^wj%m!;=c=`<#-s76bOc46s+sxUMSN#cJRWmV=%;;935PE*Ha@(#nDQE&H_>vz`jQ?qT6W;0)JIz|F->;Oo;DS&&4{skDh?BqJ6A1VS^f`po2UVT4bo z!rDqhLE(S)S-Sz>wy`qoC;?>a`4yl8KkTv9n%9Qp#qiy^;X%!&`kXzqiPFb#=%|YD zd=*5}9f1BjZwoqL%R!@em~200;Q=Q$`$9Kx6-C4t#j*DKm7)1KMqr#ZC*A?|Nx8$X zX_IXqDm}lyOEp}?P7;M9mu3ZNq>-6mzikFv=WG_;&V4MVDvjcuaA5R_Gzvhz^b3^c ze!7H*$$=jjdMxgE3dNa@S;Xd&Pm<^bm_J3Ewq?u{F3c4m6PutNr z@~LsvkBst-*nC_D%xr=cFb_PLZFtMaI#q4drjJ;xUNOx)|5jR{aG`IBgk;50Tf-#K(u+^81DSJcS8sk~@+(8yQjpemR)cu*+-Q7S%l@hIHA(s{@i zkO*&Bo;tH^q@sak>IV|~J9%+y9>?Dl4ENkgdPCffYP0zF9b$R1gs1LH z8|FqP4c@D4dhByM*WA@%S`%efa`^?bi#PCKx&7A3@igY<{F@9-lIdO$7FuxGaX+v= z&^jV%erq`k4V~Q45jQP&D0=?7r$J{C-3<$~g0#*imBs!>{9j&c;K%SGQf9?v0sjt# zlW}C1&_#@C%iw4{shhFnc-!2h(X*D5~|36vc)0+fY`^!yhGrvESYUjKft@ z7CvAd=Ou3$X3UHvvP(==D~Hwz4c6?g^v1QMs5l`BOL|DR*N;&UW*p1)=#lhzQl;BP zcEWd`f}CPSy8723iY6$}sAZuDHRTt_PPtq5j7_)qFC53UM7SdpVy4kPAd72$$q)7j z{iqgScZ1?`1?z#|>7tlZP>5{h3reBEZ!jFU^NfExxh5vXr|O&U($DDwgaUdG~qA36Crxh1TwmnUc-TN(rA6x3tl6m2jvIo0qAJM^V}!ymq( zmSkl*O2jY$^5W1pzsuNntU-NI~R50T|8fP2Ajab$pD~S3AE0CTF%M zXCXw12dJkfNH;^NQHF3aIb=a`!G}o|lXJ``n9(dLMYk(LJSs=mYC}9|YRlSeAvl6m z&h0K#?W)@ZYx^{fwx0dvv}zqNbl&)$=j1JuW1>FIu6dq+-T0sA0VjN3hJs&@CLnCb zmG~`(fYSM$)xVdRcwhg5eK7(@|ANE%7wMDRJ@yZSVIkK$O2M_lLo@;&?xKA)f?*eS ztZ`?4tas-Sq+rS-vq*Cv3cYb^7n_4M7EOM`#g%R?0ax_!x?(xkUek&slXDjRxY%1+ zLW`s%!^w5?)OeehAiim91z30V1F-s76FRe1!0eaqzFLABdZ-%4-rYHi$fQkePG-z7 zYZMax`bd4Ts^YSFQ~V~YL`r40{4$G{;<^gOGKNJVr35eL60B-XvF@z8Y!qcFZ#r#+ z(LRUboh5A#tJsxmgqCI1lf1!PvQCv&<>Y3kHcfLct5gc@YHqb>?n&CK>?4FB zpi{AnWusba#^5t;if^Tqz5plN+{&t$QfjDErp_ldZsA&Y{$DY!MZtqdr*Qg(DxHU+ zj)=)As!ru}xNDNu`RWm^0wX3i$9@Bj0V?c>sii!#rGykeHq82X@u2fX^2FbGVRqyM zaSk1Z%ocKFHoGAfHhj3T(2ShVC~zO(>HN{d4*ZZ2u|1MZZ}{nGN|@bJ^5QVKqjHjB z`z|D9h67rX7rq_?eFf5t#nEA2Q%bLv=3I3Lm8 z&7q&p!#5v@05MdH!5P{)O}4ley=Gm&W3I^_9)bb0lMXdp#&Ed}am2%l3@g#L2HBo9 z3*!cpY9Xa_i1T$YQ&CCFTeJpjEg91CpOOREvL@FF8rJ&zR7?P8LjOy-l+IoQKqTq_FWW(XbgJ_0ZuCP62qIg+oW1|m7OUL-dQIV_$HNpdQde1nsndQV+ znjniOCzZjU6Ze6`)NwB2=;O&;<`O95OY&6?QJ~((jcY9W#d% z*OFqT{zZR{d_Wr%nWUq}r#7HlHE9uYEM_Q3PNjG*haxIY8f3b<-xrpp%N>-Y_HvF{ zj4{)nUO3i(mXoCL$@U5~FHL6DjddH$$|8G+0HwjbUL-Fd4aFU0 ziiglWQ!?t3s^a6tUhqUkVT_fAbdQf0&zZGmwYpTH(3e`VZ`4o3pOiy$^kFVLnswyr z{)w6aC7Qdv;t+AD@~>~k5ssC_t%{>YQ-b%97L$O&eCRG{!+sxdr;Kq+9xlPjBViAB zi?l{-+spym0#|$6T4YHse^NUoH+RcjaUKH3SDPV)xbW9(mMUaYD8c>K%cK*3aMd%% zEhbA-n{(>?_=CQTNPJ9rPUlokwh=w1U|w`PmmOQ`zXTw?kz1C@A}EN4O?#%i0uoiL@5-dMp6++qi)*2x@sOkrM`Rh1x73yb75TNx&OFSFA;} zY1&L|5QjfYWQY)#Adv-5a8NT8al8HtS4~?~7uYWlEW;_aqBI-P(dl`eeIQUoxXYB2 zXicO==u>FnxyIR3xuY}2Vo*^3&A`IDhv?KqF|e9I+?4Td`McVZJ*w3ZqaklvV=v~z zawv$mxPdIN}_w>feJLX(DN#CZMmuH&z`TbHfQVz~E4L({LU`o-XRU2xGm>4+jiun0!`525&!$i#1e6tE`U>|E>#Q!GltK=N2&G)8yz@^T_@#$Gap^J z))%Z+Er_uIJ+qGw(05Y0A8{?7J@nX5REm49-<|2qfz|HOuV%S%EN*gCNOT;i8}>_@ zECBJ}gfKCKFK^@5o6xjp>?5#sAki^x#_X4hMv4>NTcnO(35K5d?3(b;QQH$s+Em&S z9q~=cC#8JMoNFZ2e&rQ-cCXhQpQ^~&zpfOcUa4aJb`xZ@XI1IoL;KR(MAnXq6%O^K zCZIBUZ#nka+Wg3I@9mI>4qs;$%hL$kL3jX%&r0I>kzY1{9ja4|@eVT2?+B;pu)`m| z49Mr!aAB2->>Ec;w#AXz^iYcw+taq3icH@#D-FZ)DFG3eS|PDa`u(?6{|K}+BPX8E zJt_@1#}Gy(BKS#^mMTIe8DicgLQxTXRr1-WV^VfDBa?OJxO@j^<^d#J*zNoyy8)o4 zu<$7;0ZdFH{wp6EyfpuWls(mq;^9Gba`KEom8l;IyJkA^_}K&pgJ#;X{G2Ov26TBp zi^3LF?d?yJ^&!m2Wv30!KjoqxI$Z5GznYL-x^WE5+?s=j+>%{&uAhx_SnhKzNQK0> zAF$jntxxcF?H|Fa4F#}e_JWjRy(IwC%4iJ(ay47~Xe|?U&85D{g@wCGlA6!2cAkaR zitFt~@B23`{BBxqeGs(m9me_;<*;_8cg&xZp`Un zb?)-YhBc9J;5g*+1;WDHl+D8YLT)OSWP9U1pk^Ut-_k9otE;<0HO|#4t{JfHf)Lci zg~jCS{QGd7o5LMvid6wuM`dh5?J}J7EHfq0bT>v;Y3Es3d^)T*%S~46)jLcF!y(I=8sLBBro3@_^ROR znNEG5Oa*t2ptmX&X%mq(xe_2?H#a<6B~~~uj9C_`2%+lrmV|R=2au>d>DrEE7Y!a+ zwITjvF=-2(5@Qc3-??l;_VL~`cM!%Iu04peeAeCLpvPruH*x^3ZX4{RB0qbJZld$9 z_eDT>K6A#r%SWzaD7@q<*w)hdx!-USsQw^}vAKxkKXjVU#_CAj76XwU)%3BONvWPf z6EBZ>A+;4A0oP_NVWoz>8W~(!IGjxx>%U|E@;cWk+~XyUDSXz7PFQoA4OVRa>ME}U zzc~t98#!%Z{GFe)j0oWWVQ(oW48kj~sLJT2_rQz%Bd7U|`Q^>h{?=Z_>GZ2h>^=b7 z##`^?!LyG+nA7hUqaXmH<-)X$0QJWQR_DDY&Fi+Z8NzZfe6u4(V7P4D;01Tf&Zlut z0d~|*P){O9P2Uw+7pW(qJkz^IVwxV(%)SU5Y;`NtkNex>$-w^R_{MQtYH))6-AbJ$ z!(P94!sax5SNVgy36Vt08D#7SeD&4nZNz~pPY{X+MP%YQUKlWa!W)(pvU4AOehim4 zTtVxVHNO+O*nO;$&(~i7W#&m%k7b6pvgG2i~R=eKMD`7b=rRn9~%59w<@$%1*SWpP^%?bXerpY2DO%${w?JteBWwJAWm! zsPH?1#!p%Jyb>tc4c#`BFQ!xc7R*Sjm?~a*@-byt^m&Y$+MWgW1){mZ+ql zu4lNAAi=>n#(FLgN6C0BP;Wh~?h$lCn(`#uJ5i{TQ*my_WvqA8`ip)b!^J#^y!s4;QX4`F0C=38UMSYx?fI~1`WNa;ZTj)?O{ z$k^8^@kfe#fy#CUon?hDil$fDZ1GDHtHiC^vA?`{+iZ>oakvyd0X1IXnzbv!pL{NX< z1VREE_pLFd&{eHR>&g=iKD>p{e@pB;DTt9U6h=6&{1?zNcHz_6-XA#72^Ouk3XcNqusnb+X1vcB3r_o zPuU|6Z8U*HYS5a~UJY*UQ0+2Z#~e>SqFQ4yIj|;maD_Th1bC5{nIQ!9ruS*x=SfUb zkqYh4!oBhZg&v9UsA+fQg;3M~V@1o8WCA!8-xdgcBFJn{XqP+dQKpaVv*?gt028Jz~~escDay5(iNj7EK{TDK}}3Ln6}LdGz9nst;&Z z8-i|mgbQNSK{0Qhcz~9RaYxQ{u~a&B8UJ~ViuB+8a6>xazZONYMc=|ow7c5{WBB$* z?C|Fi{6uD)(0pX`ulor3IDVol7R%*ql?5m&r6eLK&cs*cq^mGGFeWtc#SKbx8jI3v zusce~TFpzFCP?(H8QQ^lTG_uz*Ma5=rwL88YVdyo9hp+`r+Jwudt9H!`Bf?S9I_R=WQDAvmUl!Uj+lTT(osusoB^`0q@)cgNtk3Az1c zF1{rgTdT)0xH;7MNFtNM<{iHSTf7rHIDa@8j$tKank45JHUyFgUMjak zwT?Y{7@hu{+{=9oMgKFvR{WBSS``<#eq#MN;^JaRuZWRC8Ozz1`J_1fgxcwrHoM-;t$w!alwNy;C;jw&xSD|h`-QZg4!8}tg z!;hR;EI=t*SG2r2>4;0Qty3g3AQ(#(Ch6SK+TXwSglJX_A85<$CEYF-{~J}fg-=d3t?1>syx z*JaKOOqHjX`w=yrJgt#EQuJJNPQBF>ND<@zM+rMl=)wIJ4uE?`vgzz^qI|>Cz4g)` z?Yy{!x$+A0`J!1op)P*Xo`Nf0w9I97oI`BBm(FF4R4bp^AE9ZE=~I7A=T~bvyw!!8 zR8eOZrXmuNmje>d2uSM3sBW+(1=%~oC_@3GceKojdL~jU6I@Q0^9+J zG0ksA?7y(Sf&Rle*05Y0pME8SEKD7?Ag2CaC=x>WI>(Nt{DIVuStyi1PzJCYMIZOc zL(Fb^vn1zRB+N;o#la`owLp~7L{iOW*PS6cgH(suEB!W?wp@EAs_t6*_Qoqyzi_$n zH2eC4ckMQ<=H7@aPglaZCpi0h3%^`CIKGW*^3Q+vu>IB~$2s1UDGy4`I0kxXFp}8m z)dK&SsZc2a&QgHh|0}_lVWqDflPY7N&_J{>Opx|r+sQ-QimF!Gltzr7v8E4Nc(Uc9 zK5Fg5kte^{9yqa%vFU{sk&`<%oy>FwoUmF2e!RUQ4AAD8CymyGiekdd=&;@x58gxR zl-w;O7lkH=vJMZpRhIY+Ceo*8!&m-umST=oFGX#=1_I?yy?QVbEo*S!_^n+TYW>UP zvkW#(yfqO#w(RWs(4gz>%>T$(glY2M?%EMbi1w!v6kEjD7ye!v^sPV)qs)L6`yHmI z%UXk8?e`Jn$NFeEEv)XVI-s#-r(9#JB`c7II<{5iq+GGQ+C&%;Ve;Zi&(YwNozGnNhTF68iv*ywu?MfEka)$l4-o|Y+giU^}duk$J zF_l23z)m(iVmuLE?UU^&>Cv{Z$|Ka6AsGXU>kn(kCxz}#a*UMrml?O+Zg`}Hoq@|8 zb~U`x_p>XuB$MP*Su2%)_M-yk>EqRElrhK;?_s>N*F>3~RaH;q zcC(Z2Pa`b>(;O7Px&xWAdl~*a!{}+h}?f?I`{dSoLG}zJ@&U&C5hyQ+!CgKci@w=rDi34W*_KhSFE{EihuCUZmrLL z3iTwj++&Y|u!W^ijqnt~xup9e!JtiyT3|ZEwbQskrgVq_pk6Y3&`)SSktHm%$#6Gl8Gf78(nthd*4k-&5>K*Q4EiE zg?5_%o!VE4da~^E%+U3LEX>N2-%kC_^}5s7+s(5O2>yVV$41ODJS5I9lUw*u5{!4| z8e{SBkY-p(jTMv3B)1-b&nSkx-b^0Hih0mDc@P2vEK_wcGzOk=bzg^nynC89Zyau> zh)qs5Jh%mRQWw%W9ElaSOye@RG8st=V}`l`eFk>LXt@@1n#KL1D2srZfu_Oav?@?R zDN`}zt{C(plghz2u>TB}ozbK&YwESkETMa?DUsoGvkTfl<`9{Te_nas+F2n>3&LlS4mc*htNr~^i3~3NqE(TVVVfM1Ma~_eIeSfFI75Re}2Y>+Ed$P+^xA^Gg+Ft$#wX3Hkrd7!P4by#ru$l zx!y9v(;b!j7?Aa>R~$Wc`v^V%B|dv<{}3SD90(xX9D+d**}gy%*}a5y3XNL93a;Nm z^r_#bMbzH`aS=`~YQ}zxF%LXjTvo@fYnzlb-m$qmox1(X`8D$019ch?j0SDubT}r;*iBQI06^U{F&3CK{LGBnYm)$vpw{KW)X zh{u*qaQsH^__HiJtx`y9A6hc_(d(r9@Eg;GamFzyECdv|dqT2*P;@y&2}ehjiIoQHVMj zIk`8W>2#Ll$?}S6{$5Wluq{2qN($m{pw(O(ey*;;-6NgrHpiJqR9cR`-m9`*sW(g0 zFuu+>E-Bo#rT41T5q`>oJQ3bI@j}S?n=j!6NNsI++L&v@k~yMg_V33l^g<&lRPt4c zZWi^zh_$~jUp_y*-}$Q!2p)cp6=`PxWM^Z!!kCPBF1tOn0^dlkr!0%973tzODptsopDYsZBgHB^b?5fHv-QMi-E zUzqWi^JdEo?r0*+Ed18m;)l-fq?~)A3=DdX-yyXvj?;%E2Ts}a&RUC1x`|bWBTuLR z#iGRJgqf9!5*txdox~+6K{u7ycs3>2r&ohjGy;9W>pU^=D;#Y@+BwMegFS#aZwwhS zX#_`qfLRq=1oGr`Rd#8ME#ihHo`@wlpE=4X$_ynV z5aR!@y&?d$x-kCgtE)mMv-gxKQ06294T#d@<`z<@;$o=enc(u;@Y)v1J>hGm6vTlWQSZDb6svJn(mC?gX z;w3=TxqoA%nPI%!&~T{X?jWB)&$L{Ok2GhW_=%i=e-?7*_OOA;P?=Axom$X}PtAm%p+#-3jIjU6cwsCMQ6dub!A6gc1fypG0~DjtnRGdiTc?-Y$UvhS^NsKCFPs z$@me^WvK|^;%h;MXVe?gPF0N z?fU{H?>qkc4G#1Fsp>3%;)u3&4THP8LvVL@_uvxTo!}N2+xjoqEAu|GaRZ3S*u)8K`bnzKOgKa862W#|sM2Q0hn3Uq(C z7{7lVSDFZyOBmrQpvLD}g@x<*x%3?Zc1S4cT+GIe95=G~>l5Aqy2cQ$p0HF=_n#97vv{Xsl z_2dJ(%qCcxw3dRGAGwYO--`BYey*EqI45c$>gz+W3huI!;iiUn#%7$aLb*9v3G&xolLap0>4GK z@j$GN*WvycKkw6JW7nLG9*(YC!9V3pH6s3o+0WsC5syk!7ej!bs5H$TI*cO+opCL; zzCse^fGk@H7edh&Ga)+vWG(O;l5oTHd+;~O%yOp$DNMvEe)n{GqlsZF*}3*idhI@H z^AH)%brK|*YW%HJHIqwy_XQc)pFl2+798xPHadUXWnG?ika7k;D=7gqlcwA_ub1@r zdFXP{&kVdn6=Yb6V?(mKIn=oDDt!3wukB|!QTpk+m>RSWW8jL$coczP|1B{yHrNKF z^^gU8&4Gg*t3q46&q?UAOD5l8gRk0fT)6u}1;K|=$TaGkADb4W%%Fm#B!JSe*6@0m zpd!Oa6M~gx^ccA}6$wB_EC)_P?#Fajk@;0(*ySY??B_9LxE-b&ZYfw;fGNaEZ?W9Z z@cIeS2-4sy<~}w%Lbfxy?1aFx_`y|x*|`v7T6qp9jju@|DVb(7?CH!eG*5Gy&l+8h zRbM^8F!tpT5oH7_gW>9GoIpm};Yf!1O{25~qK{^yWgpO~+jaA%S(nwyE0EdwL!30c zKldt?xJ0aM&=1ycCR-5a38i5O*0PK$+gT3P>!y1@WKHxy>~~O27sP(<)ig}wRNBRr z%aKHq$VG*rl$FywL80@QG^{g$)G(eHOk>J}B_@)*1Pdw21lI-z;E;-&jIZWa_0rpSSA7mp= zY4%6fSDnyAb5@>5=Tji(VLG&@QJBH2*IT9d#Z0;Q1}$-PDQPDU=b^MOJ-_5unLk?& zJZi>Qg3o#87MvE77KLnnubDpISzVT$FGU~oW?sqGR>)#s1~C4_i_tCZz~R{`G{gU{ zE$-s^yxBhQl6sEv)_Qo3lC-ZDfTii0Zc2yEfn()i7M1a+7BB|f{1XW1VWwf3P^+de z<&}b!6y9Xr(kUtJ5k~uysJ}ev!@ZJgTX43?N(3|OzqhI_ zsE`L~Z(%4Bo2itEVg!ZfoN{oLg?~rEvg_D~ERcyBo#J#Sl8d<@Xys_0V6>-ceP)`5dl2>|jwH~b+=fqshaPwn^QIdTGV^Ti z8BzI7>A~8Nw6PZUN=A6is)VG6;#e}?*nJ}5PPBsTSPCo{pUH1sUePRlAORuxUGTL; zKEk~Tq9QxSdq&rcb2q7smlm$PdEqm_b)ERpIu%W>VLYrJ7aua2XM*1h2BvVi7cSXjq-L*w5-) zq9A6ft4bIGNCMU02vz_tSz-F^eHzfm>oq1zs4eB@ z@mighTiklDogFW5lyrl{W9cm1P0|dWwlOGh#Ja$N$km}-j? zY``YYW?#ckjy5RzMFrfp_H13V40I@GOpetB-1a9QVGpY6k-=rTjyBAN>)HrTAXhx? zjs+{5lV)GZRr2S&0QY?3JgpBZBe52ll7*daQZZ++teaus3k5iw5W=xmxQO%El^)7a`2Q7ALgm-8h!U^Y(ne^KbVI#U}z#)(&OI zJDMZDDt*AHcv3>&{(4=K_-i*KDFP6MMhTKL1F6)&UtMqCUz!7YI1}H)F1sD+?HsvM zwnbTk?(?UESMwaPnd@-|!F3FkpxHG`X_-S6%)#&Q8Y130A{gi2agh>GlFZi|_=nIj zwOXpd3C|nC_-6?4odNmsLdj^GmJ30Dm3 zp^Rl(mgvZ7rg?OPuqj8wp}kBq5<%s(y*A39AfzGg1#VM{I=3eH zr#^4k3i-u(AteXe|4|m>-P1 zBXT7m&IZ-{Z`Ubnyz&hjqacZm48@VyU>ux?>kb!B8u`*$ z6tcI(Z7o)f{5l1?jg>WYf1To^3 z-<_=Hk8jxi0(ZX&7?QJDyYNQ#(tSnb(7qlF+`@y0 zGG6G;Wc?tFFKF@juW~+#NK9N0>>e|@;?1~G6^qJ%ucLp^)ph}|*{{=dgk_%K=1}uw z1yk2-(#`kOv*gNxB5=4sc1PG1MXV;pYlZU0#XlnFvM&dZmD^_C%RR9Rwzz!R@(o#^ z=+} zr7EYu@;hHinSeF0V{y^VS_`oB3u!ar0?;%DO@ZA~5#pvo<3+5q7lQov3dG(!cl(yT?b(xcB+F_-Ld` zm66hh_Bn0T?$LPQU z{0+si%bDJMog9=Z86uvtvJ#wP9>-<@Hv-={&B;l}tM8!u__j-Xf#2KA)XS_#9;<=1OL|`w zg{mpfY;ju3s^xvMcEcN6EJj35M--uDj)8VE zyH~>{jkyBn+K>r{rG;rBb1SYHD*{O|i>(6MIJi^k!p#!|E5f^#*dRw;?j7LyG*I&~ zC!S!yeWH7M1JHiqalYa&v7bn@H|TP{rCu&~7tP3qkg?Y)*Zm4k%i<|wqoC_Yfl(4WW|6uE z1IoaVykI1l6mgiCB;j-@SYWd^ILaF8@*D1UUPx>^3V$OR|F)Ub9mQ@0TKKHO3SztkrL_O9a;xo~2 zlCE0m`)9ZXfw}{QXWHLn<&o^T$s&mTEI9mcC9^#kg6rhIpwb#~8{qp}-QHG}Mw5ni zIZ|iJGmHHg-XrGK2bsQLw&}_*syR+Ee7^<@-EtE&tjmfTcE}xt56B4WX_1~RfCnQ$3*fB;!?xeos|dU_fV?S1>I_e5iuA8g zp@Hcs)BHLeXt!xJHCZ;RJCKc4`R(*$NjQnCq4O-XuE^}^bxi(QRYrclRHsz3puDKu zen8iKi?)cpKXIuDpE2-LNycrIr8<0Co1($PtV3So;5T?5W3tjsBaVtM&lDXWi<;=xuTdL#5h;7fAWS}>n zliW&C-J|?)fwu(b5K7nAgCl2JIri-qLuphbM=~#o^*Un*u z4?aO(8`voaX8h1Vz?(8-Db{BR2FG9^)695+rSPsSI+Fd}nO}~4!7{v;?j0}}tyjn$ zxz;m=LNVt%%eS^*N#m{d(KI#P_voO;g3;Uq`GV@jC%)` z{s5K^NVk%P&ogIrM{Y~TGjp@_#6s0;*<0-|?NaSPNd#d4>P2()x)kY>pJGSo_ntZx zC;?TOy^^8@I4P?_Rmwb0H_U0f6#5hQjxRZ6HW>hyYJ49a9*kN>mX2d`!{0s~Rv9&p zU+JDV*$ipn)K9ARQ|X1!V7_D~2P8KS?ym->l`-%x>@Ip{UxE^~Bt992U6)9E8*J!5 zA&+|jtFqLhzVLP$Y}L4ar-VQ&8RxK$x>0fEC++wSY5bB|{3k-)MMhe)W>7}Uq%aGy z4YsBwaQ{XE-xPzn_kqJG$+ht*gCA;S4B;T7GC2v#A?-#fLtVF4@oSfgmTc9WU_9}~ z$E1k>@D)v@&GjGJCH6gfj|qwuw+v4&%Ir0AAoqA&@S0?kY;rWcGp{_oSEH0dj_@G8 zhvsXwo#9Vj(7Nh*1Mp-yB42@A)2S{z5Hc_I>ISQ|^73E#Ii zDV+JdPl>)k39i$JNrAf_uRm@H1l<_1v%D1^XGS!xYk3<xs<)1$j0{6LQ zVMvWe#~e27`Wg6h506iG<%}!Z=5gnvVS2d3(pQ-dzhqUrlYoOq0Uzw!Cl&^LJgawM zMi}_*ZQxwho1t$?%Y8L8zvbH*;(Gg(`0H)L9PT!drU=SMrv!D81RxJJY8U}%*5trkJ(cV#X{ zR0s%~zpsi&$8do_qIn!)b7rcs9hf2cx_Yc3gnFhCTzP~PzGA7CC>$oiJDFUF2|2xt0UNN=D}EKk*CbYB`l@Q|utEPBoL zH8<&klmS{1(FXF)r$GI|)+w&C{+GM1+_MjVu z5ZQN#0Q~-hrKk6geOFA>>V%fk2yx4j#~5L29^D9O%i|s>IhYM_%AUD#wKd>omKUVV+)3u}*B-W$n09lTz9b+CG_3LKuZe5%M{7}00v zmW6EEE)TqCH{@j2YsB44u7*G46BTrGGIQwet}L<{4ohw@VfbEbWQE2XTTw=;sfZYM zSb_g+N$nh02^-hpVkmZ*Qt@@c781^U^;_#?I4%(8@y9Jd`YcDC+j52F0NdPXA{D!I ztes^veALZ(+PS(SWw$rQ30s4uagJNEMiZOL!>C1jG7;YLnk!PrTCKiCv6|hoIAJ_8ic?D`fKpOrtVOfH zB+W^({5z{CP3#z+U}mZkT4w-~6-&8Z9SPW&Y52j!2QOCr+dA(zdhf7NvB6J(er#Ul zh<)PW-g5wVH;!l?yJOC*BUSAsCC+n81K}14rp#4KXzjKL0l}=yy8No$*L-};fC-VFURL?clu+XR7EJEll&uXnW1^x;X#RVt`pGOIrWl)r(CzIRGxcu?=y!2HJ;XZd9~s6t$n<} zpTb`#`<(nv8LMggUEB9VZH%Y^eHZBxgW;aIhhUO8*0VVSuPWPu3-|pLdbIEvL_m1Y zl=X!c9xuD%#?Rf)v+F&~Q-v=mYD8}QzF6r4B+6X)wET)4N`q1wMrydoTD`!a{S7xs zG~1J$?YF#u-TUa+8^xbk1?HV)J@%4FE;^t6vP5|X4Vi6p5F4bo0QE7pDgwHfQ^EDI zoejKcw!T7FR^#95IeP347u%2o^joH>1BdZanlo`wmqP{jHtbf~$F)0H(`@6%;x-sz z_FO)(WD0J#;|K}3o8sk26Bh#grrA5yad0zD*5t{$(kFZdWv?iR9bi_;p# zUURB8U3pfDyE{eJ)?Kg^;I^nV?`xVb7lPTUf~&7wr1@9m`WVu1;=nlV!gC&>K+ZsO z_Sj8b~rcPhN}w>rfhab6|WO%{Og{!~n->G3Tr2}7_s zyIQH2U@5UL^Xud#e3$Ht_kmpT0j_T&wD%A9<{pTXq-Sk)knt<(~InierO=! z2p`()B!L$UCcaa=5mbrcsL4Vs7M`-q7^R%epvuJ^1oYi+z~zsU_uv zU!W}l-V*VwsYk8mmq(M+mjQ9C5px7Q_>qC%Xe&o8gF29C4+twG?0)iPx;!JYZny5D zL9~mY-*1Xq$lSoG2et3{#84@DQUsoADj1^$F8bd*V83}|Ct%1x_|>0cgQUpt+^+Zy z^eJBPFfh_HPz?oz1SU1`anCg=B|?*(DX{-QFrP#XfA-)1bf9rFO3xu-xjUz6cjMM} z0wM`z#ayC-exoCqHg`8kC+>eS$Pw7m7+yq+?nfM8st$qy_9DR_v{Q~TzI-N$ zP_qtp(mHb8?P_-M!H%TL(?XclnIIAq_vPiE6VWSN%Al-LTYKNK(xX(;d$~^zR7)St zXG`s7UlcBu-W}Vhl&}3c2RJ%o!`~j+FZ_SJ0Dt&xJgkd6?}ng3+Tcb@btw$yLU!p( zKpIhPH)Fm6`Dny@4S)LNMlQl#!eTh5e8zT8{us-vs2gZbxlU@8~ zLS%I3$0H|3uRN*fL`UA{G8AOawo5XhsAH@?Ywqr^)eq0vTGxkt)w?A~-3&9g`;bK#`3Z}oCI2V%~u zFJfM*I$obtt5n76{CiwK+A7eEB$bxi+KePI0~GY{ELJp=_erUf)L`D-s~nu8TH4WF z!+tT>0}WZWl8H^-b;iVQI_{vR*HIyLZe=^*3hUpU=)Op$e;})AWNvA#w0;m{nwegh zCvuCbxNmBb^=ukkfxRxmAumA|E+H%}Erros!LU|ho}SCy)0iu1)E8`q4l}f~xAVoC zEmq?yrj2OEfb=-)V4vYKqq_=S;c}v**I#T}1d@JY&W$a|$O0Ej?+tW_d)`+{?xT+9 z*E$j7*0u29y}Cv^M$8o;GgGk{SCZ0B;&XtE$Z@2yJKp1B z7-L*%jVdg(HbvH|amZ@UHk6@QWiXmd$Bq=+@!Z`@4X;tEk1p#$-ZlT3WJlLxlv0@O zUh#K>x|WFkj6s75ZaC|3N*+_Fklbp+0S;)Q*i(IpW|vr|d#DpvvEeBW%o-yoE=Kd+ zG~QnG>yWT*nfE+0$G!n57ulC*tXmn{F&y-5MB zSk5qX!e#K&lJTOd#PbFhE7`MfEB%ZI+_{*k9z&MnFoq16zIzF zOGLGQy6=pTy^0JrJAvV0+Lh4lF!1B@;>FerM>sm(6%>K!;0_1NwyXvFxgEr6Y7@iG zkH|5;*ldf}(D8j6cgFql*t~}Cle)TFxH7Uh9lM2@>;$5%>`tjyNZOzTo3C_^QFfmm zsTF~#RCPhX@!*ZR{1kzyHYegpHIX~yy{*qq`n?CbciClsXJxoIH5+MMR zIoEfXA!Dk|Dn1;wJmL%l0;+tKT&XMlE~!5=`;^JKzy}Ii6QrPJtyhyIYh~@#`^BQu zg1eXA6j&+DI-KJqCEQ+@)+4=erSjzVx>$!P zmmu=QyfY|7tcyQ1Wa)^0qh#@=pXO~lM4#?7ymc*HHN0gg1PU6sXB?{F{fZ>tDCI)C z4zr7MADYos=+X77kKlU1oR6l=g4CKte=b#ElHKZeT~3lB?)`o-C`a){PK( z9=)f${WLYSlnz52WHUn84}xC{p`N8XM^fnK)Sc47j|Ybfg(WvSFy+`6O*N<~P}OCz z5vql7vwT8P0phdPxrY%F9txWi;hY!3h-@1ms}`gL;$dDEYS1C^=18y^01@}@cE??W z3^qO!#tfk4#~vc8*9gTi($t6YZ<*krfy%-CjWlZJH)$(fjLhqejz+`#hSE{`JW-X7 z`>xsT{ptp`H`>cx`Y}4zH~l=d0f;CdUB??jN26J6;DXXNKkdg~ww7mvg7$Yg&GQ<% ze)k{3i2AAc60B&A-|y)Fiyto;>(TA&mjrB1w+Vj}|(ZfOGKn(V>no5cP;4~?a|MM9qai$5$YH}In)H_N|kJ%wEE zdx$Z6Fc7ko*OZyo|CG!w&B?BIv=@OJI>X*t!GUulJ9dnILly;;_GbzLJoz@!^eyTP z3FJ6(Fmdx-3yB*J!WKSFbNv27JBI|e?BPdEz|QNBeLkBXBJuZxY^0Y|Imm3u@`1iG z`~1gsxuzr*Sya zJh;m-lFd&fn=g^uzqV+wix*k~8f!T zn3ir71+XJq3a*|ATML^!$z&d9uh&(qV~yQRUJXAQSBDwbpX|E&S8!O65W-Z+>9)&z zGMbzw&w;!+q_q|G&ugeXvj@*#c7abnsgu&v1r4nWX-*X5c47i`^q;+i-j&%PL5+I^ zjT(Ca(EpQqY5vF(`frjLkz+&XzZp03j;)~oqr4A7IQb0oR}&o+aAHOLSLF3Qz~=T{ ztx)Jax6J=;#X-v)pe;Ho5FsZKNaPfq_&;)*74P8SJ1G3W)O%SRw8#yDJf{bNPHBk$ z(LVeKTI2f*y`7R1|DzoD4|FQ{7s3_B0Og;f6aUqZdmpmpJz9hFAMi-{9b^Sfp5YSz z73g}0yx*aJ=d~mD4yh9VRYZCR+TODbaQxHDtmNM-OgN_?{*Oe?uXo7)eK|_>ABaxo zFLZIvLj3>ra^Bag{(;Qo-yurSrwcX!i~(rtf)Z5wZem)zo4NoVYmnfj6#&r|Bw!~9 zV!K8M_3j~qo-a`WzwAJWS3&?3d(h<-5yX8zN~@GT(#HRJE;r&|R8PTpVB zD4!67cZ3cKy(0uH7l88bxQPD=xcT2f-^=2lfkM#boeF@j93*xxO8k%K_&?n5ig%6} z)Oybbz#aNK%-cN=p#R5TlXUF;SNMUB_@C9pf0~z${1?RfJMp;(LcsYH=<>k;@HP+n syvPdje?%w#=c($S<~7S8@>K@hkBTtwU;THn!}mQ03j*TT&VOqE4-{M+YybcN diff --git a/.gradle-wrapper/gradle-wrapper.properties b/.gradle-wrapper/gradle-wrapper.properties index ac72c34e8ac..3fa8f862f75 100644 --- a/.gradle-wrapper/gradle-wrapper.properties +++ b/.gradle-wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fff4e06f1f1..ae98cab62dd 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum From f8008d7a73610be7199cbc90e7b6ec9a3340d938 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 10:38:44 +0200 Subject: [PATCH 04/27] Bump com.squareup.okio:okio-jvm from 3.5.0 to 3.6.0 in /cnf (#2380) * Bump com.squareup.okio:okio-jvm from 3.5.0 to 3.6.0 in /cnf Bumps [com.squareup.okio:okio-jvm](https://github.com/square/okio) from 3.5.0 to 3.6.0. - [Changelog](https://github.com/square/okio/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/okio/compare/parent-3.5.0...parent-3.6.0) --- updated-dependencies: - dependency-name: com.squareup.okio:okio-jvm dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index dbf155618c2..b74f14bb382 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -60,7 +60,7 @@ com.squareup.okio okio-jvm - 3.5.0 + 3.6.0 diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index e2325ed4328..38e9b491697 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -64,7 +64,7 @@ com.google.gson;version='[2.10.1,2.10.2)',\ com.google.guava;version='[32.1.2,32.1.3)',\ com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ - com.squareup.okio;version='[3.5.0,3.5.1)',\ + com.squareup.okio;version='[3.6.0,3.6.1)',\ com.zaxxer.HikariCP;version='[5.0.1,5.0.2)',\ io.openems.backend.alerting;version=snapshot,\ io.openems.backend.application;version=snapshot,\ diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index c123ec702ad..e03ebc769e2 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -187,7 +187,7 @@ com.google.gson;version='[2.10.1,2.10.2)',\ com.google.guava;version='[32.1.2,32.1.3)',\ com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ - com.squareup.okio;version='[3.5.0,3.5.1)',\ + com.squareup.okio;version='[3.6.0,3.6.1)',\ com.sun.jna;version='[5.13.0,5.13.1)',\ io.openems.common;version=snapshot,\ io.openems.edge.application;version=snapshot,\ From 59f949ab8f89ab214b6ea6e2c4f8b608ebe1ed88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 11:48:40 +0200 Subject: [PATCH 05/27] Bump org.apache.felix:org.apache.felix.http.jetty from 5.1.0 to 5.1.2 in /cnf (#2379) * Bump org.apache.felix:org.apache.felix.http.jetty in /cnf Bumps org.apache.felix:org.apache.felix.http.jetty from 5.1.0 to 5.1.2. --- updated-dependencies: - dependency-name: org.apache.felix:org.apache.felix.http.jetty dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index b74f14bb382..257fe9873b7 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -208,7 +208,7 @@ org.apache.felix org.apache.felix.http.jetty - 5.1.0 + 5.1.2 diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index 38e9b491697..30285fad6c9 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -106,7 +106,7 @@ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ org.apache.felix.fileinstall;version='[3.7.4,3.7.5)',\ - org.apache.felix.http.jetty;version='[5.1.0,5.1.1)',\ + org.apache.felix.http.jetty;version='[5.1.2,5.1.3)',\ org.apache.felix.http.servlet-api;version='[2.1.0,2.1.1)',\ org.apache.felix.inventory;version='[2.0.0,2.0.1)',\ org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index e03ebc769e2..b8b21809c54 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -374,7 +374,7 @@ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ org.apache.felix.fileinstall;version='[3.7.4,3.7.5)',\ - org.apache.felix.http.jetty;version='[5.1.0,5.1.1)',\ + org.apache.felix.http.jetty;version='[5.1.2,5.1.3)',\ org.apache.felix.http.servlet-api;version='[2.1.0,2.1.1)',\ org.apache.felix.inventory;version='[2.0.0,2.0.1)',\ org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ From 493f6b659ba91a11e961a4ab43884cb3b5ac1b84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 18:28:34 +0200 Subject: [PATCH 06/27] Bump org.apache.felix:org.apache.felix.http.servlet-api from 2.1.0 to 3.0.0 in /cnf (#2378) * Bump org.apache.felix:org.apache.felix.http.servlet-api in /cnf Bumps org.apache.felix:org.apache.felix.http.servlet-api from 2.1.0 to 3.0.0. --- updated-dependencies: - dependency-name: org.apache.felix:org.apache.felix.http.servlet-api dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index 257fe9873b7..c315bec7e40 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -214,7 +214,7 @@ org.apache.felix org.apache.felix.http.servlet-api - 2.1.0 + 3.0.0 diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index 30285fad6c9..bfc7aa61d90 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -107,7 +107,7 @@ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ org.apache.felix.fileinstall;version='[3.7.4,3.7.5)',\ org.apache.felix.http.jetty;version='[5.1.2,5.1.3)',\ - org.apache.felix.http.servlet-api;version='[2.1.0,2.1.1)',\ + org.apache.felix.http.servlet-api;version='[3.0.0,3.0.1)',\ org.apache.felix.inventory;version='[2.0.0,2.0.1)',\ org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ org.apache.felix.scr;version='[2.2.6,2.2.7)',\ diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index b8b21809c54..c0847a84d7c 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -375,7 +375,7 @@ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ org.apache.felix.fileinstall;version='[3.7.4,3.7.5)',\ org.apache.felix.http.jetty;version='[5.1.2,5.1.3)',\ - org.apache.felix.http.servlet-api;version='[2.1.0,2.1.1)',\ + org.apache.felix.http.servlet-api;version='[3.0.0,3.0.1)',\ org.apache.felix.inventory;version='[2.0.0,2.0.1)',\ org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ org.apache.felix.scr;version='[2.2.6,2.2.7)',\ From 6a2807606f269c2b60253e602a3b8c8cd75b4039 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 21:28:21 +0200 Subject: [PATCH 07/27] Bump io.reactivex.rxjava3:rxjava from 3.1.7 to 3.1.8 in /cnf (#2377) * Bump io.reactivex.rxjava3:rxjava from 3.1.7 to 3.1.8 in /cnf Bumps [io.reactivex.rxjava3:rxjava](https://github.com/ReactiveX/RxJava) from 3.1.7 to 3.1.8. - [Release notes](https://github.com/ReactiveX/RxJava/releases) - [Commits](https://github.com/ReactiveX/RxJava/compare/v3.1.7...v3.1.8) --- updated-dependencies: - dependency-name: io.reactivex.rxjava3:rxjava dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index c315bec7e40..8e5fad22100 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -154,7 +154,7 @@ io.reactivex.rxjava3 rxjava - 3.1.7 + 3.1.8 diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index bfc7aa61d90..2f9caa07eec 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -99,7 +99,7 @@ io.openems.wrapper.retrofit-converter-gson;version=snapshot,\ io.openems.wrapper.retrofit-converter-scalars;version=snapshot,\ io.openems.wrapper.retrofit2;version=snapshot,\ - io.reactivex.rxjava3.rxjava;version='[3.1.7,3.1.8)',\ + io.reactivex.rxjava3.rxjava;version='[3.1.8,3.1.9)',\ org.apache.commons.commons-csv;version='[1.10.0,1.10.1)',\ org.apache.commons.commons-fileupload;version='[1.5.0,1.5.1)',\ org.apache.commons.commons-io;version='[2.13.0,2.13.1)',\ diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index c0847a84d7c..6403935f898 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -364,7 +364,7 @@ io.openems.wrapper.retrofit-converter-scalars;version=snapshot,\ io.openems.wrapper.retrofit2;version=snapshot,\ io.openems.wrapper.sdnotify;version=snapshot,\ - io.reactivex.rxjava3.rxjava;version='[3.1.7,3.1.8)',\ + io.reactivex.rxjava3.rxjava;version='[3.1.8,3.1.9)',\ javax.jmdns;version='[3.4.1,3.4.2)',\ javax.xml.soap-api;version='[1.4.0,1.4.1)',\ org.apache.commons.commons-csv;version='[1.10.0,1.10.1)',\ From 1debce6912b7d95c4dbaeb7ed7f4ee9c19934928 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 21:46:41 +0200 Subject: [PATCH 08/27] Bump org.checkerframework:checker-qual from 3.38.0 to 3.39.0 in /cnf (#2376) * Bump org.checkerframework:checker-qual from 3.38.0 to 3.39.0 in /cnf Bumps [org.checkerframework:checker-qual](https://github.com/typetools/checker-framework) from 3.38.0 to 3.39.0. - [Release notes](https://github.com/typetools/checker-framework/releases) - [Changelog](https://github.com/typetools/checker-framework/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/typetools/checker-framework/compare/checker-framework-3.38.0...checker-framework-3.39.0) --- updated-dependencies: - dependency-name: org.checkerframework:checker-qual dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index 8e5fad22100..18a00ac5706 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -261,7 +261,7 @@ org.checkerframework checker-qual - 3.38.0 + 3.39.0 org.dhatim diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index 2f9caa07eec..41427858c30 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -60,7 +60,7 @@ -runbundles: \ Java-WebSocket;version='[1.5.4,1.5.5)',\ - checker-qual;version='[3.38.0,3.38.1)',\ + checker-qual;version='[3.39.0,3.39.1)',\ com.google.gson;version='[2.10.1,2.10.2)',\ com.google.guava;version='[32.1.2,32.1.3)',\ com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ From d51471a43f6a4c0594c1352fc336ce5a767bd855 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 22:12:09 +0200 Subject: [PATCH 09/27] Bump commons-io:commons-io from 2.13.0 to 2.14.0 in /cnf (#2375) * Bump commons-io:commons-io from 2.13.0 to 2.14.0 in /cnf Bumps commons-io:commons-io from 2.13.0 to 2.14.0. --- updated-dependencies: - dependency-name: commons-io:commons-io dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index 18a00ac5706..9caeff5c5d1 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -109,7 +109,7 @@ commons-io commons-io - 2.13.0 + 2.14.0 diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index 41427858c30..399e4e4a549 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -102,7 +102,7 @@ io.reactivex.rxjava3.rxjava;version='[3.1.8,3.1.9)',\ org.apache.commons.commons-csv;version='[1.10.0,1.10.1)',\ org.apache.commons.commons-fileupload;version='[1.5.0,1.5.1)',\ - org.apache.commons.commons-io;version='[2.13.0,2.13.1)',\ + org.apache.commons.commons-io;version='[2.14.0,2.14.1)',\ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ org.apache.felix.fileinstall;version='[3.7.4,3.7.5)',\ diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 6403935f898..914372435ef 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -369,7 +369,7 @@ javax.xml.soap-api;version='[1.4.0,1.4.1)',\ org.apache.commons.commons-csv;version='[1.10.0,1.10.1)',\ org.apache.commons.commons-fileupload;version='[1.5.0,1.5.1)',\ - org.apache.commons.commons-io;version='[2.13.0,2.13.1)',\ + org.apache.commons.commons-io;version='[2.14.0,2.14.1)',\ org.apache.commons.math3;version='[3.6.1,3.6.2)',\ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ From c5847c57b1792b3c7688a38e167b929154b92252 Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Fri, 6 Oct 2023 23:03:43 +0200 Subject: [PATCH 10/27] Update to bndtools version 7.0.0 https://github.com/bndtools/bnd/wiki/Changes-in-7.0.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 39c57047bad..e60a30e7f9f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -bnd_version=6.4.0 +bnd_version=7.0.0 bnd_snapshots=https://bndtools.jfrog.io/bndtools/libs-snapshot-local bnd_releases=https://bndtools.jfrog.io/bndtools/libs-release-local From 8d671fb03510a0608b680224979e45d20f8454ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 09:22:27 +0200 Subject: [PATCH 11/27] Bump org.apache.felix:org.apache.felix.webconsole from 4.9.4 to 4.9.6 in /cnf (#2383) * Bump org.apache.felix:org.apache.felix.webconsole in /cnf Bumps org.apache.felix:org.apache.felix.webconsole from 4.9.4 to 4.9.6. --- updated-dependencies: - dependency-name: org.apache.felix:org.apache.felix.webconsole dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index 9caeff5c5d1..fa3a6832908 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -242,7 +242,7 @@ org.apache.felix org.apache.felix.webconsole - 4.9.4 + 4.9.6 diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index 399e4e4a549..dd819f40734 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -111,7 +111,7 @@ org.apache.felix.inventory;version='[2.0.0,2.0.1)',\ org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ org.apache.felix.scr;version='[2.2.6,2.2.7)',\ - org.apache.felix.webconsole;version='[4.9.4,4.9.5)',\ + org.apache.felix.webconsole;version='[4.9.6,4.9.7)',\ org.apache.felix.webconsole.plugins.ds;version='[2.3.0,2.3.1)',\ org.jetbrains.kotlin.osgi-bundle;version='[1.9.10,1.9.11)',\ org.jsr-305;version='[3.0.2,3.0.3)',\ diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 914372435ef..c8a7f8084ae 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -379,7 +379,7 @@ org.apache.felix.inventory;version='[2.0.0,2.0.1)',\ org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ org.apache.felix.scr;version='[2.2.6,2.2.7)',\ - org.apache.felix.webconsole;version='[4.9.4,4.9.5)',\ + org.apache.felix.webconsole;version='[4.9.6,4.9.7)',\ org.apache.felix.webconsole.plugins.ds;version='[2.3.0,2.3.1)',\ org.eclipse.jetty.client;version='[9.4.28,9.4.29)',\ org.eclipse.jetty.http;version='[9.4.28,9.4.29)',\ From 8119632ae9d2c06dfed55fce185f4da534fdd6e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 10:24:52 +0200 Subject: [PATCH 12/27] Bump de.bytefish:pgbulkinsert from 8.1.1 to 8.1.2 in /cnf (#2382) * Bump de.bytefish:pgbulkinsert from 8.1.1 to 8.1.2 in /cnf Bumps [de.bytefish:pgbulkinsert](https://github.com/bytefish/PgBulkInsert) from 8.1.1 to 8.1.2. - [Release notes](https://github.com/bytefish/PgBulkInsert/releases) - [Changelog](https://github.com/PgBulkInsert/PgBulkInsert/blob/master/CHANGELOG.md) - [Commits](https://github.com/bytefish/PgBulkInsert/commits) --- updated-dependencies: - dependency-name: de.bytefish:pgbulkinsert dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update bndrun + wrapper * Update pom --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 9 +-------- io.openems.backend.application/BackendApp.bndrun | 1 - io.openems.wrapper/pgbulkinsert.bnd | 4 ++-- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index fa3a6832908..ad4135fc31f 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -117,7 +117,7 @@ de.bytefish pgbulkinsert - 8.1.1 + 8.1.2 @@ -256,13 +256,6 @@ org.apache.servicemix.bundles.junit 4.13.2_1 - - - - org.checkerframework - checker-qual - 3.39.0 - org.dhatim fastexcel diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index dd819f40734..4bfc10b98fd 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -60,7 +60,6 @@ -runbundles: \ Java-WebSocket;version='[1.5.4,1.5.5)',\ - checker-qual;version='[3.39.0,3.39.1)',\ com.google.gson;version='[2.10.1,2.10.2)',\ com.google.guava;version='[32.1.2,32.1.3)',\ com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ diff --git a/io.openems.wrapper/pgbulkinsert.bnd b/io.openems.wrapper/pgbulkinsert.bnd index b6c0ef9c831..8ba26754878 100644 --- a/io.openems.wrapper/pgbulkinsert.bnd +++ b/io.openems.wrapper/pgbulkinsert.bnd @@ -1,10 +1,10 @@ Bundle-Name: pgbulkinsert Bundle-DocURL: https://github.com/PgBulkInsert/PgBulkInsert Bundle-License: https://opensource.org/licenses/MIT -Bundle-Version: 8.1.1 +Bundle-Version: 8.1.2 Include-Resource: \ - @pgbulkinsert-8.1.1.jar,\ + @pgbulkinsert-8.1.2.jar,\ -dsannotations: * From b8ddb8e79b4d05019ab65113bac9195574f5900b Mon Sep 17 00:00:00 2001 From: Sagar Date: Tue, 10 Oct 2023 21:05:06 +0200 Subject: [PATCH 13/27] Time of Use Tariff controller: refactor + charge from grid (#2238) This new implementation replaces the previous controller called TimeOfUseTariffDischarge. **BREAKING CHANGE:** Be aware, that this PR introduces a new Factory-ID `Controller.Ess.Time-Of-Use-Tariff'` for the Controller. Old configurations will have to be updated manually! - Added Jsonrpc request handler to show future data in UI. - Modified Awattar provider implementation to handle querying for a price every hour instead of once a day. - Added appropriate channels for better debugging. - Added suitable test cases. README: = ESS Time-of-Use Tariff This Controller optimizes the performance of an energy storage system (ESS) in conjunction with a Time-Of-Use (ToU) Tariff provider. The primary aim of the controller is to optimize the economic utilization of energy stored within the battery, primarily for self-consumption. To achieve this goal, the controller employs a rolling approach, where it continuously fetches predictions from the prediction service. It takes into account the current state of the battery's capacity, recalculates its operations, and generates a detailed schedule for the Energy Storage System (ESS) for the upcoming 24-hour period, in 15-minute intervals. This iterative and dynamic approach ensures that the ESS operates efficiently by staying synchronized with real-time forecasts and the evolving state of the battery, thereby maximizing cost-effectiveness and self-consumption of stored energy. == Schedule Calculation Modes The schedule calculation process varies depending on the local market conditions. To accommodate this variability, two distinct modes have been introduced to facilitate flexible operation: CHARGE_CONSUMPTION:: This mode is well-suited for markets that permit charging the battery from the grid. In this mode, the controller utilizes production and consumption forecasts along with day-ahead electricity prices to calculate the optimal time periods for charging the battery from the grid. This approach ensures efficient utilization of resources while taking advantage of cost-effective grid charging opportunities. DELAY_DISCHARGE:: This mode is tailored for markets where grid charging is restricted or discouraged. In such scenarios, optimal time periods are determined based on forecasts and day-ahead pricing information. The controller then schedules the Energy Storage System (ESS) to limit or delay discharging during these specific periods. This strategic approach aims to optimize economic performance by avoiding costly grid interactions when grid charging is not feasible or economical. These two operation modes provide the necessary flexibility to adapt to varying market conditions, allowing for efficient energy management and cost savings based on the specific requirements of your local energy market. --------- Co-authored-by: Stefan Feilmeier <3515268+sfeilmeier@users.noreply.github.com> Co-authored-by: Michael Grill <59126309+michaelgrill@users.noreply.github.com> --- io.openems.edge.application/EdgeApp.bndrun | 4 +- .../readme.adoc | 23 - .../discharge/BoundarySpace.java | 125 ---- ...rollerEssTimeOfUseTariffDischargeImpl.java | 583 ------------------ .../discharge/DelayDischargeRiskLevel.java | 46 -- .../ess/timeofusetariff/discharge/Mode.java | 5 - .../discharge/StateMachine.java | 34 - ...erEssTimeOfUseTariffDischargeImplTest.java | 351 ----------- .../.classpath | 0 .../.gitignore | 0 .../.project | 2 +- .../org.eclipse.core.resources.prefs | 0 .../bnd.bnd | 6 +- .../readme.adoc | 24 + .../ess/timeofusetariff}/Config.java | 30 +- .../ess/timeofusetariff/ControlMode.java | 12 + .../controller/ess/timeofusetariff/Mode.java | 5 + .../ess/timeofusetariff/RiskLevel.java | 26 + .../ess/timeofusetariff/Schedule.java | 471 ++++++++++++++ .../ess/timeofusetariff/ScheduleUtils.java | 130 ++++ .../ess/timeofusetariff/StateMachine.java | 37 ++ .../TimeOfUseTariffController.java | 151 ++--- .../TimeOfUseTariffControllerImpl.java | 409 ++++++++++++ .../jsonrpc/GetScheduleRequest.java | 49 ++ .../jsonrpc/GetScheduleResponse.java | 48 ++ .../test/.gitignore | 0 .../ess/timeofusetariff}/MyConfig.java | 45 +- .../TimeOfUseTariffControllerTest.java | 477 ++++++++++++++ .../app/timeofusetariff/AwattarHourly.java | 97 +-- .../timeofusetariff/StromdaoCorrently.java | 118 ++-- .../edge/app/timeofusetariff/Tibber.java | 55 +- .../app/timeofusetariff/TimeOfUseProps.java | 55 ++ .../core/appmanager/translation_de.properties | 2 +- .../test/DummyTimeOfUseTariffProvider.java | 21 +- .../awattar/TimeOfUseTariffAwattarImpl.java | 13 +- 35 files changed, 2005 insertions(+), 1449 deletions(-) delete mode 100644 io.openems.edge.controller.ess.timeofusetariff.discharge/readme.adoc delete mode 100644 io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/BoundarySpace.java delete mode 100644 io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischargeImpl.java delete mode 100644 io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/DelayDischargeRiskLevel.java delete mode 100644 io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/Mode.java delete mode 100644 io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/StateMachine.java delete mode 100644 io.openems.edge.controller.ess.timeofusetariff.discharge/test/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischargeImplTest.java rename {io.openems.edge.controller.ess.timeofusetariff.discharge => io.openems.edge.controller.ess.timeofusetariff}/.classpath (100%) rename {io.openems.edge.controller.ess.timeofusetariff.discharge => io.openems.edge.controller.ess.timeofusetariff}/.gitignore (100%) rename {io.openems.edge.controller.ess.timeofusetariff.discharge => io.openems.edge.controller.ess.timeofusetariff}/.project (87%) rename {io.openems.edge.controller.ess.timeofusetariff.discharge => io.openems.edge.controller.ess.timeofusetariff}/.settings/org.eclipse.core.resources.prefs (100%) rename {io.openems.edge.controller.ess.timeofusetariff.discharge => io.openems.edge.controller.ess.timeofusetariff}/bnd.bnd (79%) create mode 100644 io.openems.edge.controller.ess.timeofusetariff/readme.adoc rename {io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge => io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff}/Config.java (57%) create mode 100644 io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/ControlMode.java create mode 100644 io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Mode.java create mode 100644 io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/RiskLevel.java create mode 100644 io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Schedule.java create mode 100644 io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/ScheduleUtils.java create mode 100644 io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/StateMachine.java rename io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischarge.java => io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffController.java (54%) create mode 100644 io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImpl.java create mode 100644 io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleRequest.java create mode 100644 io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponse.java rename {io.openems.edge.controller.ess.timeofusetariff.discharge => io.openems.edge.controller.ess.timeofusetariff}/test/.gitignore (100%) rename {io.openems.edge.controller.ess.timeofusetariff.discharge/test/io/openems/edge/controller/ess/timeofusetariff/discharge => io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff}/MyConfig.java (63%) create mode 100644 io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerTest.java create mode 100644 io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/TimeOfUseProps.java diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index c8a7f8084ae..c13f1d2e43d 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -83,7 +83,7 @@ bnd.identity;id='io.openems.edge.controller.ess.reactivepowervoltagecharacteristic',\ bnd.identity;id='io.openems.edge.controller.ess.selltogridlimit',\ bnd.identity;id='io.openems.edge.controller.ess.standby',\ - bnd.identity;id='io.openems.edge.controller.ess.timeofusetariff.discharge',\ + bnd.identity;id='io.openems.edge.controller.ess.timeofusetariff',\ bnd.identity;id='io.openems.edge.controller.evcs',\ bnd.identity;id='io.openems.edge.controller.evcs.fixactivepower',\ bnd.identity;id='io.openems.edge.controller.generic.jsonlogic',\ @@ -239,7 +239,7 @@ io.openems.edge.controller.ess.reactivepowervoltagecharacteristic;version=snapshot,\ io.openems.edge.controller.ess.selltogridlimit;version=snapshot,\ io.openems.edge.controller.ess.standby;version=snapshot,\ - io.openems.edge.controller.ess.timeofusetariff.discharge;version=snapshot,\ + io.openems.edge.controller.ess.timeofusetariff;version=snapshot,\ io.openems.edge.controller.evcs;version=snapshot,\ io.openems.edge.controller.evcs.fixactivepower;version=snapshot,\ io.openems.edge.controller.generic.jsonlogic;version=snapshot,\ diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/readme.adoc b/io.openems.edge.controller.ess.timeofusetariff.discharge/readme.adoc deleted file mode 100644 index a6b589d4dad..00000000000 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/readme.adoc +++ /dev/null @@ -1,23 +0,0 @@ -= ESS Time-of-Use Tariff Discharge - -This Controller optimizes the behaviour of an energy storage system in combination with a Time-Of-Use (ToU) Tariff. - -The general estimation is, that during the day a local PV system can provide enough energy to supply the consumption. This Controller than evaluates the predicted consumed energy during the night (using production and consumption predictors) and tries to move Buy-From-Grid periods to cheap hours. - -This implementation works for AC, DC and Hybrid ESS. It is currently limited to the ToU tariff by aWATTar. - -1. At 14:00, controller collects the Hourly prices from the aWATTar API and also the predicions of production and consumption from prediction service in OpenEMS. - -2. It calculates the boundary hours to differentiate day and night, so that the controller can get defined set of hours to work on. - -3. During the start of the boundary hours, based on current State-of-Charge and hourly prices already calculated, it determines the set of cheapest hours. - -4. During those set of hours (Cheap Hours), the discharging is blocked. consumption is covered from the grid. - -This Controller applies to the legislation of Germany. - -TODO - -Time of Use (ToU) pricing API service is currently under implementation in OpenEMS. This service will eventually act as generalised service for all the variable pricing providers. - -Once the ToU service is implemented, the test cases can be uncommented and tested with custom hourly prices. Currently only way to test it is by using the actual API provided by Awattar. diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/BoundarySpace.java b/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/BoundarySpace.java deleted file mode 100644 index 985e1688e93..00000000000 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/BoundarySpace.java +++ /dev/null @@ -1,125 +0,0 @@ -package io.openems.edge.controller.ess.timeofusetariff.discharge; - -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.util.Map.Entry; -import java.util.TreeMap; - -class BoundarySpace { - final ZonedDateTime proLessThanCon; // Sunset - final ZonedDateTime proMoreThanCon; // Sunrise - - /** - * Factory method for {@link BoundarySpace}: calculates the boundary space - * within which the schedule logic works. - * - * @param startQuarterHour {@link ZonedDateTime} start time of the prediction - * @param productionMap predictions for production - * @param consumptionMap predictions for consumption - * @param maxStartHour fallback Morning-Hour from {@link Config} - * @param maxEndHour fallback Evening-Hour from {@link Config} - * @param bufferMinutes Number of minutes, sunrise to be adjusted. - * @return the {@link BoundarySpace} - */ - public static BoundarySpace from(ZonedDateTime startQuarterHour, TreeMap productionMap, - TreeMap consumptionMap, int maxStartHour, int maxEndHour, int bufferMinutes) { - - ZonedDateTime proLessThanCon = null; - ZonedDateTime proMoreThanCon = null; - - for (Entry entry : consumptionMap.entrySet()) { - var production = productionMap.get(entry.getKey()); - var consumption = entry.getValue(); - - if (production != null && consumption != null) { - - final ZonedDateTime start; - if (isBeforeMidnight(startQuarterHour.getHour())) { - // Last hour of the day when Production < Consumption. - if (production > consumption // - && entry.getKey().getDayOfYear() == startQuarterHour.getDayOfYear() - && entry.getKey().getHour() >= 14) { - proLessThanCon = entry.getKey(); // Sunset - } - - start = startQuarterHour.plusDays(1); - - } else { - start = startQuarterHour; - } - - // First hour of the day when production > consumption - if (production > consumption // - && entry.getKey().getDayOfYear() == start.getDayOfYear() // - && proMoreThanCon == null // - && entry.getKey().getHour() <= 10) { - proMoreThanCon = entry.getKey(); // Sunrise - } - } - } - - // if there is no production available, 'proLessThanCon' and 'proMoreThanCon' - // are not calculated. - if (proLessThanCon == null) { - // Sunset - final ZonedDateTime start; - if (isBeforeMidnight(startQuarterHour.getHour())) { - start = startQuarterHour; - } else { - start = startQuarterHour.minusDays(1); - } - proLessThanCon = start.truncatedTo(ChronoUnit.DAYS) // - .plusHours(maxEndHour); - } - if (proMoreThanCon == null) { - // Sunrise - final ZonedDateTime start; - if (isBeforeMidnight(startQuarterHour.getHour())) { - start = startQuarterHour.plusDays(1); - } else { - start = startQuarterHour; - } - proMoreThanCon = start.truncatedTo(ChronoUnit.DAYS) // - .plusHours(maxStartHour); - } - - // adjust sunrise according to the buffer minutes. - proMoreThanCon.minusMinutes(bufferMinutes); - - return new BoundarySpace(proLessThanCon, proMoreThanCon); - } - - private BoundarySpace(ZonedDateTime proLessThanCon, ZonedDateTime proMoreThanCon) { - this.proLessThanCon = proLessThanCon; - this.proMoreThanCon = proMoreThanCon; - } - - /** - * Is the given date between the boundaries?. - * - * @param now the given date - * @return true if it is within the boundaries - */ - public boolean isWithinBoundary(ZonedDateTime now) { - if (now.isBefore(this.proLessThanCon)) { - return false; - } - if (now.isAfter(this.proMoreThanCon)) { - return false; - } - return true; - } - - /** - * Is the given hour before or after the midnight?. - * - * @param hour the given hour - * @return true if it is before the midnight. - */ - public static boolean isBeforeMidnight(int hour) { - if (hour >= 10 && hour <= 23) { - return true; - } - return false; - } -} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischargeImpl.java b/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischargeImpl.java deleted file mode 100644 index 5f4e48e1a9c..00000000000 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischargeImpl.java +++ /dev/null @@ -1,583 +0,0 @@ -package io.openems.edge.controller.ess.timeofusetariff.discharge; - -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.concurrent.CopyOnWriteArrayList; - -import org.osgi.service.cm.ConfigurationAdmin; -import org.osgi.service.component.ComponentContext; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.ConfigurationPolicy; -import org.osgi.service.component.annotations.Deactivate; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; -import org.osgi.service.component.annotations.ReferencePolicyOption; -import org.osgi.service.metatype.annotations.Designate; - -import io.openems.common.exceptions.InvalidValueException; -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.types.ChannelAddress; -import io.openems.edge.common.component.AbstractOpenemsComponent; -import io.openems.edge.common.component.ComponentManager; -import io.openems.edge.common.component.OpenemsComponent; -import io.openems.edge.common.type.TypeUtils; -import io.openems.edge.controller.api.Controller; -import io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve; -import io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischarge; -import io.openems.edge.ess.api.HybridEss; -import io.openems.edge.ess.api.ManagedSymmetricEss; -import io.openems.edge.predictor.api.manager.PredictorManager; -import io.openems.edge.predictor.api.oneday.Prediction24Hours; -import io.openems.edge.timedata.api.Timedata; -import io.openems.edge.timedata.api.TimedataProvider; -import io.openems.edge.timedata.api.utils.CalculateActiveTime; -import io.openems.edge.timeofusetariff.api.TimeOfUsePrices; -import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; -import io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils; - -@Designate(ocd = Config.class, factory = true) -@Component(// - name = "Controller.Ess.Time-Of-Use-Tariff.Discharge", // - immediate = true, // - configurationPolicy = ConfigurationPolicy.REQUIRE // -) -public class ControllerEssTimeOfUseTariffDischargeImpl extends AbstractOpenemsComponent - implements Controller, OpenemsComponent, TimedataProvider, ControllerEssTimeOfUseTariffDischarge { - - private static final ChannelAddress SUM_PRODUCTION = new ChannelAddress("_sum", "ProductionActivePower"); - private static final ChannelAddress SUM_CONSUMPTION = new ChannelAddress("_sum", "ConsumptionActivePower"); - - /** Delayed Time is aggregated also after restart of OpenEMS. */ - private final CalculateActiveTime calculateDelayedTime = new CalculateActiveTime(this, - ControllerEssTimeOfUseTariffDischarge.ChannelId.DELAYED_TIME); - - @Reference - private ConfigurationAdmin cm; - - @Reference - private ComponentManager componentManager; - - @Reference - private PredictorManager predictorManager; - - @Reference - private TimeOfUseTariff timeOfUseTariff; - - @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) - private volatile Timedata timedata = null; - - @Reference(policy = ReferencePolicy.DYNAMIC, // - policyOption = ReferencePolicyOption.GREEDY, // - cardinality = ReferenceCardinality.MULTIPLE, // - target = "(&(enabled=true)(isReserveSocEnabled=true))") - private volatile List ctrlEmergencyCapacityReserves = new CopyOnWriteArrayList<>(); - - @Reference(policy = ReferencePolicy.DYNAMIC, // - policyOption = ReferencePolicyOption.GREEDY, // - cardinality = ReferenceCardinality.MULTIPLE, // - target = "(enabled=true)") - private volatile List ctrlLimitTotalDischarges = new CopyOnWriteArrayList<>(); - - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) - private ManagedSymmetricEss ess; - - private Config config = null; - private BoundarySpace boundarySpace = null; - private final TreeMap consumptionMap = new TreeMap<>(); - private final TreeMap productionMap = new TreeMap<>(); - private List targetPeriods = new ArrayList<>(); - private final TreeMap quarterlyPricesMap = new TreeMap<>(); - private TreeMap socWithoutLogic = new TreeMap<>(); - private ZonedDateTime lastAccessedTime = null; - private ZonedDateTime lastUpdatePriceTime = null; - - public ControllerEssTimeOfUseTariffDischargeImpl() { - super(// - OpenemsComponent.ChannelId.values(), // - Controller.ChannelId.values(), // - ControllerEssTimeOfUseTariffDischarge.ChannelId.values() // - ); - } - - @Activate - private void activate(ComponentContext context, Config config) { - super.activate(context, config.id(), config.alias(), config.enabled()); - this.config = config; - - // update filter for 'ess' - if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", config.ess_id())) { - return; - } - } - - @Override - @Deactivate - protected void deactivate() { - super.deactivate(); - } - - @Override - public void run() throws OpenemsNamedException { - - // Current Date Time rounded off to 15 minutes. - var now = TimeOfUseTariffUtils.getNowRoundedDownToMinutes(this.componentManager.getClock(), 15); - - // Prices contains the price values and the time it is retrieved. - var prices = this.timeOfUseTariff.getPrices(); - this.calculateBoundarySpace(now, prices); - - // Mode given from the configuration. - switch (this.config.mode()) { - - case AUTOMATIC: - this.modeAutomatic(now); - break; - case OFF: - this.modeOff(); - break; - } - - this.updateVisualizationChannels(now); - } - - /** - * calculates the boundary space for the activation of the controller. - * - * @param now Current Date Time rounded off to 15 minutes. - * @param prices TimeOfUsePrices object, containing prices and the time it - * retrieved. - */ - private void calculateBoundarySpace(ZonedDateTime now, TimeOfUsePrices prices) { - - /* - * Every day, Prices are updated in API at a certain hour. we update the - * predictions and the prices during those hour. - * - * Gets the prices and predictions when the controller is restarted or // - * re-enabled in any time. - */ - if (!prices.isEmpty() - && (this.lastUpdatePriceTime == null || this.lastUpdatePriceTime.isBefore(prices.getUpdateTime()))) { - // gets the prices, predictions and calculates the boundary space. - this.getBoundarySpace(now, prices); - - // update lastUpdateTimestamp - this.lastUpdatePriceTime = prices.getUpdateTime(); - } else { - // update the channel - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.QUATERLY_PRICES_TAKEN).setNextValue(false); - } - } - - /** - * Calculate the Target Periods every 15 minutes within the boundary period. - * - * @param now Current Date Time rounded off to 15 minutes. - * @throws InvalidValueException on error - */ - private void calculateTargetPeriodsWithinBoundarySpace(ZonedDateTime now) throws InvalidValueException { - // Initializing with Default values. - this._setTargetHoursCalculated(false); - - // if the boundary space are calculated, start scheduling only during boundary - // space. - if (this.boundarySpace != null && this.boundarySpace.isWithinBoundary(now)) { - - // Runs every 15 minutes. - if (this.lastAccessedTime == null || now.isAfter(this.lastAccessedTime)) { - - var availableEnergy = this.getAvailableEnergy(now); - var remainingEnergy = this.getRemainingCapacity(availableEnergy, this.productionMap, - this.consumptionMap, now, this.boundarySpace); - - // Resetting - this.targetPeriods.clear(); - - // list of periods calculation. - if (remainingEnergy > 0) { - // Initiating the calculation - this.targetPeriods = this.calculateTargetPeriods(this.consumptionMap, this.quarterlyPricesMap, - remainingEnergy, this.boundarySpace); - this._setTargetHoursCalculated(true); - } - - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.TARGET_HOURS) - .setNextValue(this.targetPeriods.size()); - - this.lastAccessedTime = now; - } - } else { - this._setStateMachine(StateMachine.STANDBY); - } - } - - /** - * Returns the available energy in the battery which is usable for consumption - * after adjusting the minimum SoC capacity. - * - * @param now Current Date Time rounded off to 15 minutes. - * @return available energy in Watt-milliseconds[Wmsec]. - * @throws InvalidValueException on error - */ - private long getAvailableEnergy(ZonedDateTime now) throws InvalidValueException { - - final int netCapacity = this.ess.getCapacity().getOrError(); - final int soc = this.ess.getSoc().getOrError(); - - // Usable capacity based on minimum SoC from Limit total discharge and emergency - // reserve controllers. - var limitSoc = 0; - for (ControllerEssLimitTotalDischarge ctrl : this.ctrlLimitTotalDischarges) { - limitSoc = TypeUtils.max(limitSoc, ctrl.getMinSoc().get()); - } - for (ControllerEssEmergencyCapacityReserve ctrl : this.ctrlEmergencyCapacityReserves) { - limitSoc = TypeUtils.max(limitSoc, ctrl.getActualReserveSoc().get()); - } - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.MIN_SOC).setNextValue(limitSoc); - - // Calculating available energy and usable energy [Wmsec] in the battery. - var availableEnergy = (long) ((double) netCapacity /* [Wh] */ * 3600 /* [Wsec] */ * 1000 /* [Wmsec] */ - / 100 * soc /* [current SoC] */); - - // Value is divided by 3600 * 1000 to convert from [Wmsec] to [Wh]. - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.AVAILABLE_CAPACITY) - .setNextValue(availableEnergy / 3600000); - - var limitEnergy = (long) ((double) netCapacity /* [Wh] */ * 3600 /* [Wsec] */ * 1000 /* [Wmsec] */ - / 100 * limitSoc /* [current SoC] */); - - availableEnergy = Math.max(0, availableEnergy - limitEnergy); - - // Value is divided by 3600 * 1000 to convert from [Wmsec] to [Wh]. - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.USABLE_CAPACITY) - .setNextValue(availableEnergy / 3600000); - - // To estimate the soc curve when controller logic is not applied - if (now.equals(this.boundarySpace.proLessThanCon)) { - this.socWithoutLogic = this.generateSocCurveWithoutLogic(netCapacity, availableEnergy, limitEnergy, - this.consumptionMap, soc, now, this.boundarySpace); - } - - return availableEnergy; - } - - /** - * This method calculates the boundary space within the prediction hours. - * - * @param now current time. - * @param prices TimeOfUsePrices object, containing prices and the time it - * retrieved. - */ - private void getBoundarySpace(ZonedDateTime now, TimeOfUsePrices prices) { - - // Predictions as Integer array in 15 minute intervals. - final var predictionProduction = this.predictorManager.get24HoursPrediction(SUM_PRODUCTION) // - .getValues(); - final var predictionConsumption = this.predictorManager.get24HoursPrediction(SUM_CONSUMPTION) // - .getValues(); - - // Prices as Float array in 15 minute intervals. - final var quarterlyPrices = prices.getValues(); - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.QUATERLY_PRICES_TAKEN).setNextValue(true); - - // Converts the given 15 minute integer array to a TreeMap values. - this.convertDataStructure(predictionProduction, predictionConsumption, now, quarterlyPrices); - - if (this.quarterlyPricesMap.isEmpty()) { - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.QUATERLY_PRICES_TAKEN).setNextValue(false); - } - - // Buffer minutes to adjust sunrise based on the risk level. - var bufferMinutes = this.config.delayDischargeRiskLevel().bufferMinutes; - - // calculates the boundary space, within which the controller needs to work. - this.boundarySpace = BoundarySpace.from(now, this.productionMap, this.consumptionMap, - this.config.maxStartHour(), this.config.maxEndHour(), bufferMinutes); - - // Update Channels - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.PRO_MORE_THAN_CON_SET) - .setNextValue(this.boundarySpace.proMoreThanCon.getHour()); - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.PRO_MORE_THAN_CON_ACTUAL) - .setNextValue(this.boundarySpace.proMoreThanCon.plusMinutes(bufferMinutes).getHour()); - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.PRO_LESS_THAN_CON) - .setNextValue(this.boundarySpace.proLessThanCon.getHour()); - } - - /** - * This method returns the map of 15 minutes soc curve values when no controller - * logic is applied. - * - * @param netCapacity Net Capacity of the battery. - * @param availableEnergy available energy in the battery. - * @param limitEnergy energy restricted to used based on min soc. - * @param consumptionMap map of predicted consumption values. - * @param soc current SoC of the battery. - * @param now current time. - * @param boundarySpace the {@link BoundarySpace} - * - * @return {@link TreeMap} with {@link ZonedDateTime} as key and SoC as value. - */ - private TreeMap generateSocCurveWithoutLogic(int netCapacity, long availableEnergy, - long limitEnergy, TreeMap consumptionMap, int soc, ZonedDateTime now, - BoundarySpace boundarySpace) { - - var socWithoutLogic = new TreeMap(); - - // current values. - socWithoutLogic.put(now, soc); - - for (Entry entry : consumptionMap.subMap(now, boundarySpace.proMoreThanCon) - .entrySet()) { - - long duration = 15 * 60 * 1000; - var currentConsumptionEnergy = entry.getValue() * duration; - - if (availableEnergy > limitEnergy) { - availableEnergy -= currentConsumptionEnergy; - } - - var calculatedSoc = availableEnergy // - / (netCapacity * 3600. /* [Wsec] */ * 1000 /* [Wmsec] */) // - * 100 /* [SoC] */; - - if (calculatedSoc > 100) { - soc = 100; - } else if (calculatedSoc < 0) { - soc = 0; - } else { - soc = (int) Math.round(calculatedSoc); - } - - socWithoutLogic.put(entry.getKey().plusMinutes(15), soc); - } - - return socWithoutLogic; - } - - /** - * Apply the actual logic of avoiding to discharge the battery during target - * periods. - * - * @param now Current Date Time rounded off to 15 minutes. - * @throws OpenemsNamedException on error - */ - private void modeAutomatic(ZonedDateTime now) throws OpenemsNamedException { - - this.calculateTargetPeriodsWithinBoundarySpace(now); - - this._setTargetHoursIsEmpty(this.targetPeriods.isEmpty()); - - var currentQuarterHour = TimeOfUseTariffUtils.getNowRoundedDownToMinutes(this.componentManager.getClock(), 15) // - .withZoneSameInstant(ZoneId.systemDefault()); - - if (this.boundarySpace != null && this.boundarySpace.isWithinBoundary(now)) { - if (this.targetPeriods.contains(currentQuarterHour)) { - // set result - final Integer chargeLimit; - if (this.ess instanceof HybridEss) { - // DC or Hybrid system: limit AC export power to DC production power - var e = (HybridEss) this.ess; - chargeLimit = TypeUtils.subtract(this.ess.getActivePower().get(), e.getDcDischargePower().get()); - - } else { - // AC system - chargeLimit = 0; - } - this.ess.setActivePowerLessOrEquals(chargeLimit); - this._setDelayed(true); - this._setStateMachine(StateMachine.DELAYED); - } else { - this._setDelayed(false); - this._setStateMachine(StateMachine.ALLOWS_DISCHARGE); - } - } - } - - /** - * Apply the mode OFF logic. - */ - private void modeOff() { - // Do Nothing - this._setTargetHoursCalculated(false); - this._setTargetHoursIsEmpty(true); - this._setDelayed(false); - this._setStateMachine(StateMachine.STANDBY); - } - - /** - * This is only to visualize data for better debugging. - * - * @param now Current Date Time rounded off to 15 minutes. - */ - private void updateVisualizationChannels(ZonedDateTime now) { - // Update time counter with 'Delayed' of this run. - this.calculateDelayedTime.update(this.getDelayedChannel().getNextValue().orElse(false)); - - // Storing quarterly prices in channel for visualization in Grafana and UI. - if (!this.quarterlyPricesMap.isEmpty()) { - for (Entry entry : this.quarterlyPricesMap.entrySet()) { - if (now.isEqual(entry.getKey())) { - this._setQuarterlyPrices(entry.getValue()); - } - } - } - - // Storing Production and Consumption in channel for visualization in Grafana. - if (!this.productionMap.isEmpty()) { - for (Entry entry : this.productionMap.entrySet()) { - if (now.isEqual(entry.getKey())) { - this._setPredictedProduction(entry.getValue()); - this._setPredictedConsumption(this.consumptionMap.get(entry.getKey())); - } - } - } - - Integer predictedSocWithoutLogic = null; - if (!this.socWithoutLogic.isEmpty()) { - if (this.boundarySpace.isWithinBoundary(now)) { - for (Entry entry : this.socWithoutLogic.entrySet()) { - if (now.isEqual(entry.getKey())) { - predictedSocWithoutLogic = entry.getValue(); - } - } - } - } else { - this.socWithoutLogic.clear(); - } - this._setPredictedSocWithoutLogic(predictedSocWithoutLogic); - } - - /** - * This method converts the 15 minute integer array values to a {@link TreeMap} - * format for ease in later calculations. - * - * @param productionValues list of 96 production values predicted, comprising - * for next 24 hours. - * @param consumptionValues list of 96 consumption values predicted, comprising - * for next 24 hours. - * @param startHour start hour of the predictions. - * @param quarterlyPrices list of 96 quarterly electricity prices, comprising - * for next 24 hours. - */ - private void convertDataStructure(Integer[] productionValues, Integer[] consumptionValues, ZonedDateTime startHour, - Float[] quarterlyPrices) { - this.productionMap.clear(); - this.consumptionMap.clear(); - this.quarterlyPricesMap.clear(); - - for (var i = 0; i < Prediction24Hours.NUMBER_OF_VALUES; i++) { - var production = productionValues[i]; - var consumption = consumptionValues[i]; - var price = quarterlyPrices[i]; - var time = startHour.plusMinutes(i * 15); - - if (production != null) { - this.productionMap.put(time, production); - } - - if (consumption != null) { - this.consumptionMap.put(time, consumption); - } - - if (price != null) { - this.quarterlyPricesMap.put(time, price); - } - } - } - - /** - * This Method Returns the remaining Capacity that needs to be consumed from the - * Grid. - * - * @param availableEnergy Amount of energy available in the ess based on SoC. - * @param productionMap predicted production data along with time in - * {@link TreeMap} format. - * @param consumptionMap predicted consumption data along with time in - * {@link TreeMap} format. - * @param now Current Date Time rounded off to 15 minutes. - * @param boundarySpace the {@link BoundarySpace} - * @return remainingCapacity Amount of energy that should be covered from grid - * for consumption in night. - */ - private long getRemainingCapacity(long availableEnergy, TreeMap productionMap, - TreeMap consumptionMap, ZonedDateTime now, BoundarySpace boundarySpace) { - - var consumptionEnergy = 0L; - var remainingEnergy = 0L; - - for (Entry entry : consumptionMap // - .subMap(now, boundarySpace.proMoreThanCon) // - .entrySet()) { - - long duration = 15 * 60 * 1000; - var currentConsumptionEnergy = entry.getValue() * duration; - var currentProductionEnergy = productionMap.get(entry.getKey()) * duration; - - consumptionEnergy = consumptionEnergy + currentConsumptionEnergy - Math.max(0, currentProductionEnergy); - } - - // remaining amount of energy that should be covered from grid. - remainingEnergy = Math.max(0, consumptionEnergy - availableEnergy); - - // Update Channels - // Values are divided by 3600 * 1000 to convert from [Wmsec] to [Wh]. - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.TOTAL_CONSUMPTION) - .setNextValue(consumptionEnergy / 3600000); - this.channel(ControllerEssTimeOfUseTariffDischarge.ChannelId.REMAINING_CONSUMPTION) - .setNextValue(remainingEnergy / 3600000); - - return remainingEnergy; - } - - /** - * This method returns the list of periods, during which ESS is avoided for - * consumption. - * - * @param consumptionMap predicted consumption data along with time in - * {@link TreeMap} format. - * @param quarterlyPrices {@link TreeMap} consisting of hourly electricity - * prices along with time. - * @param remainingEnergy Amount of energy that should be covered from grid for - * consumption in night. - * @param boundarySpace the {@link BoundarySpace} - * @return {@link List} list of target periods to avoid charging/discharging of - * the battery. - */ - private List calculateTargetPeriods(TreeMap consumptionMap, - TreeMap quarterlyPrices, long remainingEnergy, BoundarySpace boundarySpace) { - - List targetHours = new ArrayList<>(); - var currentQuarterHour = TimeOfUseTariffUtils.getNowRoundedDownToMinutes(this.componentManager.getClock(), 15) // - .withZoneSameInstant(ZoneId.systemDefault()); - - List> priceList = new ArrayList<>(quarterlyPrices // - .subMap(currentQuarterHour, boundarySpace.proMoreThanCon) // - .entrySet()); - priceList.sort(Entry.comparingByValue()); - long duration = 15 * 60 * 1000; - - for (Entry entry : priceList) { - targetHours.add(entry.getKey()); - - remainingEnergy = remainingEnergy - consumptionMap.get(entry.getKey()) * duration; - - // checks if we have sufficient capacity. - if (remainingEnergy <= 0) { - break; - } - } - - return targetHours; - } - - @Override - public Timedata getTimedata() { - return this.timedata; - } -} diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/DelayDischargeRiskLevel.java b/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/DelayDischargeRiskLevel.java deleted file mode 100644 index 87138f9622d..00000000000 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/DelayDischargeRiskLevel.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.openems.edge.controller.ess.timeofusetariff.discharge; - -/** - * The Risk Level is describing the risk propensity and effects on the SoC curve - * during the night. - */ -public enum DelayDischargeRiskLevel { - - /** - * Less dependent on predictions. The state of charge will most likely be at - * minimum SoC level before there is more production than consumption, but might - * end up buying from grid during high price hour for consumption. - */ - LOW(60), // - - /** - * Moderately dependent on predictions. The state of charge will likely be at - * minimum SoC level before there is more production than consumption. It is - * still possible that the storage might be empty and end up buying from grid - * during the high price hour. - */ - MEDIUM(30), // - - /** - * Complete dependency on Predictions. The state of charge will likely be at - * minimum SoC level before there is more production than consumption, but very - * often certain percentage SoC will remain in the battery which goes unused for - * the night consumption. - */ - HIGH(0); - - public final int bufferMinutes; - - private DelayDischargeRiskLevel(int bufferMinutes) { - this.bufferMinutes = bufferMinutes; - } - - /** - * Get buffer minutes. - * - * @return buffer minutes - */ - public int getBufferMinutes() { - return this.bufferMinutes; - } -} diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/Mode.java b/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/Mode.java deleted file mode 100644 index b68bd6e631e..00000000000 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/Mode.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.openems.edge.controller.ess.timeofusetariff.discharge; - -public enum Mode { - OFF, AUTOMATIC; -} diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/StateMachine.java b/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/StateMachine.java deleted file mode 100644 index 7e389d0d70e..00000000000 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/StateMachine.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.openems.edge.controller.ess.timeofusetariff.discharge; - -import io.openems.common.types.OptionsEnum; - -public enum StateMachine implements OptionsEnum { - - DELAYED(0, "Delayed"), // - ALLOWS_DISCHARGE(1, "No active limitation, discharge is allowed"), // - STANDBY(2, "Outside controller time limits"); // - - private final int value; - private final String name; - - private StateMachine(int value, String name) { - this.value = value; - this.name = name; - } - - @Override - public int getValue() { - return this.value; - } - - @Override - public String getName() { - return this.name; - } - - @Override - public OptionsEnum getUndefined() { - return STANDBY; - } - -} diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/test/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischargeImplTest.java b/io.openems.edge.controller.ess.timeofusetariff.discharge/test/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischargeImplTest.java deleted file mode 100644 index 362cc08babf..00000000000 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/test/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischargeImplTest.java +++ /dev/null @@ -1,351 +0,0 @@ -package io.openems.edge.controller.ess.timeofusetariff.discharge; - -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; - -import org.junit.Test; - -import io.openems.common.types.ChannelAddress; -import io.openems.edge.common.test.AbstractComponentTest.TestCase; -import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; -import io.openems.edge.controller.test.ControllerTest; -import io.openems.edge.ess.test.DummyManagedSymmetricEss; -import io.openems.edge.predictor.api.test.DummyPrediction24Hours; -import io.openems.edge.predictor.api.test.DummyPredictor24Hours; -import io.openems.edge.predictor.api.test.DummyPredictorManager; -import io.openems.edge.timeofusetariff.test.DummyTimeOfUseTariffProvider; - -public class ControllerEssTimeOfUseTariffDischargeImplTest { - - // Ids - private static final String CTRL_ID = "ctrlEssTimeOfUseTariffDischarge0"; - private static final String PREDICTOR_ID = "predictor0"; - private static final String ESS_ID = "ess0"; - - // Ess channels - private static final ChannelAddress ESS_CAPACITY = new ChannelAddress(ESS_ID, "Capacity"); - private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); - - // Controller Channels - private static final ChannelAddress TOTAL_CONSUMPTION = new ChannelAddress(CTRL_ID, "TotalConsumption"); - private static final ChannelAddress REMAINING_CONSUMPTION = new ChannelAddress(CTRL_ID, "RemainingConsumption"); - private static final ChannelAddress AVAILABLE_CAPACITY = new ChannelAddress(CTRL_ID, "AvailableCapacity"); - private static final ChannelAddress USABLE_CAPACITY = new ChannelAddress(CTRL_ID, "UsableCapacity"); - private static final ChannelAddress QUATERLY_PRICES_TAKEN = new ChannelAddress(CTRL_ID, "QuaterlyPricesTaken"); - private static final ChannelAddress TARGET_HOURS_CALCULATED = new ChannelAddress(CTRL_ID, "TargetHoursCalculated"); - private static final ChannelAddress TARGET_HOURS_IS_EMPTY = new ChannelAddress(CTRL_ID, "TargetHoursIsEmpty"); - private static final ChannelAddress TARGET_HOURS = new ChannelAddress(CTRL_ID, "TargetHours"); - private static final ChannelAddress STATE_MACHINE = new ChannelAddress(CTRL_ID, "StateMachine"); - - /* - * Default Prediction values - */ - private static final Integer[] DEFAULT_PRODUCTION_PREDICTION = { - /* 00:00-03:450 */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // - /* 04:00-07:45 */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 297, 610, // - /* 08:00-11:45 */ - 913, 1399, 1838, 2261, 2662, 3052, 3405, 3708, 4011, 4270, 4458, 4630, 4794, 4908, 4963, 4960, // - /* 12:00-15:45 */ - 4973, 4940, 4859, 4807, 4698, 4530, 4348, 4147, 1296, 1399, 1838, 1261, 1662, 1052, 1405, 1402, - /* 16:00-19:45 */ - 1662, 1052, 1405, 1630, 1285, 1520, 1250, 910, 0, 0, 0, 0, 0, 0, 0, 0, // - /* 20:00-23:45 */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // - /* 00:00-03:45 */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // - /* 04:00-07:45 */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 130, 402, 667, // - /* 08:00-11:45 */ - 1023, 1631, 2020, 2420, 2834, 3237, 3638, 4006, 4338, 4597, 4825, 4965, 5111, 5213, 5268, 5317, // - /* 12:00-15:45 */ - 5321, 5271, 5232, 5193, 5044, 4915, 4738, 4499, 3702, 3226, 3046, 2857, 2649, 2421, 2184, 1933, // - /* 16:00-19:45 */ - 1674, 1364, 1070, 754, 447, 193, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, // - /* 20:00-23:45 */ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // - }; - - private static final Integer[] DEFAULT_CONSUMPTION_PREDICTION = { - - /* 00:00-03:450 */ - 1021, 1208, 713, 931, 2847, 2551, 1558, 1234, 433, 633, 1355, 606, 430, 1432, 1121, 502, // - /* 04:00-07:45 */ - 294, 1048, 1194, 914, 1534, 1226, 1235, 977, 578, 1253, 1983, 1417, 513, 929, 1102, 445, // - /* 08:00-11:45 */ - 1208, 2791, 2729, 2609, 2086, 1454, 848, 816, 2610, 3150, 2036, 1180, 359, 1316, 3447, 2104, // - /* 12:00-15:45 */ - 905, 802, 828, 812, 863, 633, 293, 379, 1250, 2296, 2436, 2140, 2135, 1196, 2230, 1725, - /* 16:00-19:45 */ - 2365, 1758, 2325, 2264, 2181, 2167, 2228, 1082, 777, 417, 798, 1268, 409, 830, 1191, 417, // - /* 20:00-23:45 */ - 1087, 2958, 2946, 2235, 1343, 483, 796, 1201, 567, 395, 989, 1066, 370, 989, 1255, 660, // - /* 00:00-03:45 */ - 349, 880, 1186, 580, 327, 911, 1135, 553, 265, 938, 1165, 567, 278, 863, 1239, 658, // - /* 04:00-07:45 */ - 236, 816, 1173, 1131, 498, 550, 1344, 1226, 874, 504, 1733, 1809, 1576, 369, 771, 2583, // - /* 08:00-11:45 */ - 3202, 2174, 1878, 2132, 2109, 1895, 1565, 1477, 1613, 1716, 1867, 1726, 1700, 1787, 1755, 1734, // - /* 12:00-15:45 */ - 1380, 691, 338, 168, 199, 448, 662, 205, 183, 70, 169, 276, 149, 76, 195, 168, // - /* 16:00-19:45 */ - 159, 266, 135, 120, 224, 979, 2965, 1337, 1116, 795, 334, 390, 433, 369, 762, 2908, // - /* 20:00-23:45 */ - 3226, 2358, 1778, 1002, 455, 654, 534, 1587, 1638, 459, 330, 258, 368, 728, 1096, 878 // - }; - - private static final Float[] DEFAULT_HOURLY_PRICES = { 158.95f, 160.98f, 171.95f, 174.96f, // - 161.93f, 152f, 120.01f, 111.03f, // - 105.04f, 105f, 74.23f, 73.28f, // - 67.97f, 72.53f, 89.66f, 150.01f, // - 173.54f, 178.4f, 158.91f, 140.01f, // - 149.99f, 157.43f, 130.9f, 120.14f // - }; - - @Test - public void nullTimeOfUseTariffTest() throws Exception { - - final var clock = new TimeLeapClock(Instant.parse("2021-01-01T13:45:00.00Z"), ZoneOffset.UTC); - final var cm = new DummyComponentManager(clock); - - // Predictions - final var productionPrediction = new DummyPrediction24Hours(DEFAULT_PRODUCTION_PREDICTION); - final var consumptionPrediction = new DummyPrediction24Hours(DEFAULT_CONSUMPTION_PREDICTION); - - // Predictors - final var productionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, productionPrediction, - "_sum/ProductionActivePower"); - final var consumptionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, consumptionPrediction, - "_sum/ConsumptionActivePower"); - - // PredictorManager - final var predictorManager = new DummyPredictorManager(productionPredictor, consumptionPredictor); - - // Price provider - final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.fromHourlyPrices(null, DEFAULT_HOURLY_PRICES); - - // Printing - // System.out.println("Time: " + clock); - // System.out.println(Arrays.toString(predictorManager - // .get24HoursPrediction(ChannelAddress.fromString("_sum/ProductionActivePower")).getValues())); - // System.out.println(Arrays.toString(predictorManager - // .get24HoursPrediction(ChannelAddress.fromString("_sum/ConsumptionActivePower")).getValues())); - - new ControllerTest(new ControllerEssTimeOfUseTariffDischargeImpl()) // - .addReference("predictorManager", predictorManager) // - .addReference("componentManager", cm) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // - .addReference("timeOfUseTariff", timeOfUseTariffProvider) // - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMaxStartHour(8) // - .setMaxEndHour(16) // - .setMode(Mode.AUTOMATIC) // - .setDelayDischargeRiskLevel(DelayDischargeRiskLevel.HIGH) // - .build()) - .next(new TestCase("Cycle - 1") // - .output(AVAILABLE_CAPACITY, null) // - .output(QUATERLY_PRICES_TAKEN, false) // - .output(TARGET_HOURS_CALCULATED, false) // - .output(TARGET_HOURS_IS_EMPTY, true) // - .output(STATE_MACHINE, StateMachine.STANDBY)); - } - - @Test - public void executesDuringMarketTimeTest() throws Exception { - - final var clock = new TimeLeapClock(Instant.parse("2021-01-01T16:00:00.00Z"), ZoneOffset.UTC); - final var cm = new DummyComponentManager(clock); - - // Predictions - final var productionPrediction = new DummyPrediction24Hours(DEFAULT_PRODUCTION_PREDICTION); - final var consumptionPrediction = new DummyPrediction24Hours(DEFAULT_CONSUMPTION_PREDICTION); - - // Predictors - final var productionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, productionPrediction, - "_sum/ProductionActivePower"); - final var consumptionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, consumptionPrediction, - "_sum/ConsumptionActivePower"); - - // PredictorManager - final var predictorManager = new DummyPredictorManager(productionPredictor, consumptionPredictor); - - // Price provider - final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.fromHourlyPrices(ZonedDateTime.now(clock), - DEFAULT_HOURLY_PRICES); - - // Printing - // System.out.println("Time: " + clock); - // System.out.println(Arrays.toString(predictorManager - // .get24HoursPrediction(ChannelAddress.fromString("_sum/ProductionActivePower")).getValues())); - // System.out.println(Arrays.toString(predictorManager - // .get24HoursPrediction(ChannelAddress.fromString("_sum/ConsumptionActivePower")).getValues())); - - new ControllerTest(new ControllerEssTimeOfUseTariffDischargeImpl()) // - .addReference("predictorManager", predictorManager) // - .addReference("componentManager", cm) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // - .addReference("timeOfUseTariff", timeOfUseTariffProvider) // - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMaxStartHour(8) // - .setMaxEndHour(16) // - .setMode(Mode.AUTOMATIC) // - .setDelayDischargeRiskLevel(DelayDischargeRiskLevel.HIGH) // - .build()) - .next(new TestCase("Cycle - 1") // - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .input(ESS_CAPACITY, 12000) // - .input(ESS_SOC, 100) // - .output(AVAILABLE_CAPACITY, 12000) // - .output(QUATERLY_PRICES_TAKEN, true) // - .output(TARGET_HOURS_CALCULATED, true)// - .output(TARGET_HOURS_IS_EMPTY, false)// - .output(TOTAL_CONSUMPTION, 15248) // - .output(REMAINING_CONSUMPTION, 3248.0) // - .output(STATE_MACHINE, StateMachine.ALLOWS_DISCHARGE)) - .next(new TestCase("Cycle - 2") // - .output(QUATERLY_PRICES_TAKEN, false) // - .output(TARGET_HOURS_CALCULATED, false)// - .output(TARGET_HOURS_IS_EMPTY, false) // - .output(STATE_MACHINE, StateMachine.ALLOWS_DISCHARGE)) - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMaxStartHour(8) // - .setMaxEndHour(16) // - .setMode(Mode.OFF) // - .setDelayDischargeRiskLevel(DelayDischargeRiskLevel.HIGH) // - .build()) - .next(new TestCase("Cycle - 3") // - .output(QUATERLY_PRICES_TAKEN, false) // - .output(TARGET_HOURS_CALCULATED, false)// - .output(TARGET_HOURS_IS_EMPTY, true) // - .output(STATE_MACHINE, StateMachine.STANDBY)) - .next(new TestCase("Cycle - 4") // - .timeleap(clock, 15, ChronoUnit.MINUTES)// - .output(QUATERLY_PRICES_TAKEN, false) // - .output(TARGET_HOURS_CALCULATED, false)// - .output(TARGET_HOURS_IS_EMPTY, true) // - .output(STATE_MACHINE, StateMachine.STANDBY)); - } - - @Test - public void executesBeforeMidnight() throws Exception { - - final var clock = new TimeLeapClock(Instant.parse("2021-01-01T21:00:00.00Z"), ZoneOffset.UTC); - final var cm = new DummyComponentManager(clock); - - // Predictions - final var productionPrediction = new DummyPrediction24Hours(DEFAULT_PRODUCTION_PREDICTION); - final var consumptionPrediction = new DummyPrediction24Hours(DEFAULT_CONSUMPTION_PREDICTION); - - // Predictors - final var productionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, productionPrediction, - "_sum/ProductionActivePower"); - final var consumptionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, consumptionPrediction, - "_sum/ConsumptionActivePower"); - - // PredictorManager - final var predictorManager = new DummyPredictorManager(productionPredictor, consumptionPredictor); - - // Price provider - final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.fromHourlyPrices(ZonedDateTime.now(clock), - DEFAULT_HOURLY_PRICES); - - // Printing - // System.out.println("Time: " + clock); - // System.out.println(Arrays.toString(predictorManager - // .get24HoursPrediction(ChannelAddress.fromString("_sum/ProductionActivePower")).getValues())); - // System.out.println(Arrays.toString(predictorManager - // .get24HoursPrediction(ChannelAddress.fromString("_sum/ConsumptionActivePower")).getValues())); - - new ControllerTest(new ControllerEssTimeOfUseTariffDischargeImpl()) // - .addReference("predictorManager", predictorManager) // - .addReference("componentManager", cm) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // - .addReference("timeOfUseTariff", timeOfUseTariffProvider) // - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMaxStartHour(8) // - .setMaxEndHour(16) // - .setMode(Mode.AUTOMATIC) // - .setDelayDischargeRiskLevel(DelayDischargeRiskLevel.HIGH) // - .build()) - .next(new TestCase("Cycle - 1") // - .input(ESS_CAPACITY, 12000) // - .input(ESS_SOC, 100) // - .output(AVAILABLE_CAPACITY, 12000) // - .output(USABLE_CAPACITY, 12000) // - .output(REMAINING_CONSUMPTION, 0.0) // - .output(QUATERLY_PRICES_TAKEN, true) // - .output(TARGET_HOURS_CALCULATED, false) // - .output(TARGET_HOURS_IS_EMPTY, true) // - .output(STATE_MACHINE, StateMachine.ALLOWS_DISCHARGE)); - } - - @Test - public void executesAfterMidnight() throws Exception { - - final var clock = new TimeLeapClock(Instant.parse("2021-01-01T11:00:00.00Z"), ZoneOffset.UTC); - final var cm = new DummyComponentManager(clock); - - // Predictions - final var productionPrediction = new DummyPrediction24Hours(DEFAULT_PRODUCTION_PREDICTION); - final var consumptionPrediction = new DummyPrediction24Hours(DEFAULT_CONSUMPTION_PREDICTION); - - // Predictors - final var productionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, productionPrediction, - "_sum/ProductionActivePower"); - final var consumptionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, consumptionPrediction, - "_sum/ConsumptionActivePower"); - - // PredictorManager - final var predictorManager = new DummyPredictorManager(productionPredictor, consumptionPredictor); - - // Price provider - final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.fromHourlyPrices(ZonedDateTime.now(clock), - DEFAULT_HOURLY_PRICES); - - // Printing - // System.out.println("Time: " + clock); - // System.out.println(Arrays.toString(predictorManager - // .get24HoursPrediction(ChannelAddress.fromString("_sum/ProductionActivePower")).getValues())); - // System.out.println(Arrays.toString(predictorManager - // .get24HoursPrediction(ChannelAddress.fromString("_sum/ConsumptionActivePower")).getValues())); - - new ControllerTest(new ControllerEssTimeOfUseTariffDischargeImpl()) // - .addReference("predictorManager", predictorManager) // - .addReference("componentManager", cm) // - .addReference("cm", new DummyConfigurationAdmin()) // - .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // - .addReference("timeOfUseTariff", timeOfUseTariffProvider) // - .activate(MyConfig.create() // - .setId(CTRL_ID) // - .setEssId(ESS_ID) // - .setMaxStartHour(8) // - .setMaxEndHour(16) // - .setMode(Mode.AUTOMATIC) // - .setDelayDischargeRiskLevel(DelayDischargeRiskLevel.HIGH) // - .build()) - .next(new TestCase("Cycle - 1") // - .output(AVAILABLE_CAPACITY, null) // - .output(USABLE_CAPACITY, null) // - .output(TARGET_HOURS, null) // - .output(QUATERLY_PRICES_TAKEN, true) // - .output(TARGET_HOURS_CALCULATED, false) // - .output(TARGET_HOURS_IS_EMPTY, true) // - .output(STATE_MACHINE, StateMachine.STANDBY)); - } -} diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/.classpath b/io.openems.edge.controller.ess.timeofusetariff/.classpath similarity index 100% rename from io.openems.edge.controller.ess.timeofusetariff.discharge/.classpath rename to io.openems.edge.controller.ess.timeofusetariff/.classpath diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/.gitignore b/io.openems.edge.controller.ess.timeofusetariff/.gitignore similarity index 100% rename from io.openems.edge.controller.ess.timeofusetariff.discharge/.gitignore rename to io.openems.edge.controller.ess.timeofusetariff/.gitignore diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/.project b/io.openems.edge.controller.ess.timeofusetariff/.project similarity index 87% rename from io.openems.edge.controller.ess.timeofusetariff.discharge/.project rename to io.openems.edge.controller.ess.timeofusetariff/.project index d43e1835747..630b81280b6 100644 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/.project +++ b/io.openems.edge.controller.ess.timeofusetariff/.project @@ -1,6 +1,6 @@ - io.openems.edge.controller.ess.timeofusetariff.discharge + io.openems.edge.controller.ess.timeofusetariff diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.controller.ess.timeofusetariff/.settings/org.eclipse.core.resources.prefs similarity index 100% rename from io.openems.edge.controller.ess.timeofusetariff.discharge/.settings/org.eclipse.core.resources.prefs rename to io.openems.edge.controller.ess.timeofusetariff/.settings/org.eclipse.core.resources.prefs diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/bnd.bnd b/io.openems.edge.controller.ess.timeofusetariff/bnd.bnd similarity index 79% rename from io.openems.edge.controller.ess.timeofusetariff.discharge/bnd.bnd rename to io.openems.edge.controller.ess.timeofusetariff/bnd.bnd index 00ade3638af..858e414afb5 100644 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/bnd.bnd +++ b/io.openems.edge.controller.ess.timeofusetariff/bnd.bnd @@ -1,7 +1,7 @@ -Bundle-Name: OpenEMS Edge Controller Ess Time-Of-Use Tariff Discharge +Bundle-Name: OpenEMS Edge Controller Ess Time-Of-Use Tariff Bundle-Vendor: FENECON GmbH Bundle-License: https://opensource.org/licenses/EPL-2.0 -Bundle-Version: 1.0.0.${tstamp} +Bundle-Version: 1.0.0.${tstamp} -buildpath: \ ${buildpath},\ @@ -16,4 +16,4 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.timeofusetariff.api,\ -testpath: \ - ${testpath} \ No newline at end of file + ${testpath} diff --git a/io.openems.edge.controller.ess.timeofusetariff/readme.adoc b/io.openems.edge.controller.ess.timeofusetariff/readme.adoc new file mode 100644 index 00000000000..b55647006db --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/readme.adoc @@ -0,0 +1,24 @@ += ESS Time-of-Use Tariff + +This Controller optimizes the performance of an energy storage system (ESS) in conjunction with a Time-Of-Use (ToU) Tariff provider. + +The primary aim of the controller is to optimize the economic utilization of energy stored within the battery, primarily for self-consumption. To achieve this goal, the controller employs a rolling approach, where it continuously fetches predictions from the prediction service. It takes into account the current state of the battery's capacity, recalculates its operations, and generates a detailed schedule for the Energy Storage System (ESS) for the upcoming 24-hour period, in 15-minute intervals. + +This iterative and dynamic approach ensures that the ESS operates efficiently by staying synchronized with real-time forecasts and the evolving state of the battery, thereby maximizing cost-effectiveness and self-consumption of stored energy. + +== Schedule Calculation Modes + +The schedule calculation process varies depending on the local market conditions. To accommodate this variability, two distinct modes have been introduced to facilitate flexible operation: + +CHARGE_CONSUMPTION:: + This mode is well-suited for markets that permit charging the battery from the grid. In this mode, the controller utilizes production and consumption forecasts along with day-ahead electricity prices to calculate the optimal time periods for charging the battery from the grid. This approach ensures efficient utilization of resources while taking advantage of cost-effective grid charging opportunities. + + +DELAY_DISCHARGE:: + This mode is tailored for markets where grid charging is restricted or discouraged. In such scenarios, optimal time periods are determined based on forecasts and day-ahead pricing information. The controller then schedules the Energy Storage System (ESS) to limit or delay discharging during these specific periods. This strategic approach aims to optimize economic performance by avoiding costly grid interactions when grid charging is not feasible or economical. + + +These two operation modes provide the necessary flexibility to adapt to varying market conditions, allowing for efficient energy management and cost savings based on the specific requirements of your local energy market. + + +https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.ess.timeofusetariff[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/Config.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Config.java similarity index 57% rename from io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/Config.java rename to io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Config.java index adb4b09aaa0..b3876b46341 100644 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/Config.java +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Config.java @@ -1,15 +1,15 @@ -package io.openems.edge.controller.ess.timeofusetariff.discharge; +package io.openems.edge.controller.ess.timeofusetariff; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @ObjectClassDefinition(// - name = "Controller Ess Time-Of-Use Tariff Discharge", // - description = "Optimize behaviour of an ESS in combination with a Time-Of-Use (ToU) Tariff. Applies to German legislation.") + name = "Controller Ess Time-Of-Use Tariff", // + description = "Optimize behaviour of an ESS in combination with a Time-Of-Use (ToU) Tariff.") @interface Config { @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") - String id() default "ctrlEssTimeOfUseTariffDischarge0"; + String id() default "ctrlEssTimeOfUseTariff0"; @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") String alias() default ""; @@ -18,25 +18,25 @@ boolean enabled() default true; @AttributeDefinition(name = "Ess-ID", description = "ID of Ess device.") - String ess_id(); + String ess_id() default "ess0"; @AttributeDefinition(name = "Mode", description = "Set the type of mode.") Mode mode() default Mode.AUTOMATIC; - @AttributeDefinition(name = "Risk level of the customer", description = """ - Low Risk: Less dependence on predictions; Energy in battery should always be used during the night. \ - High Risk: High dependence on predictions; Battery is scheduled to discharge completely based on predictions.""") - DelayDischargeRiskLevel delayDischargeRiskLevel() default DelayDischargeRiskLevel.MEDIUM; + @AttributeDefinition(name = "Control-Mode", description = "Set the control-mode.") + ControlMode controlMode() default ControlMode.DELAY_DISCHARGE; - @AttributeDefinition(name = "Fallback Morning-Hour", description = "Fallback for calculation to stop at this hour") - int maxStartHour() default 8; + @AttributeDefinition(name = "Risk level of the customer", description = """ + Low Risk: Less dependence on predictions; charge/discharge of the battery should always be according to the expected behavior. \ + High Risk: High dependence on predictions; Battery is scheduled to charge/discharge completely based on predictions.""") + RiskLevel riskLevel() default RiskLevel.MEDIUM; - @AttributeDefinition(name = "Fallback Evening-Hour", description = "Fallback for calculation to start at this hour") - int maxEndHour() default 17; + @AttributeDefinition(name = "Max charge power from the grid [W]", description = "Maximum charge power from the grid") + int maxChargePowerFromGrid() default 5000; @AttributeDefinition(name = "Ess target filter", description = "This is auto-generated by 'Ess-ID'.") String ess_target() default "(enabled=true)"; - String webconsole_configurationFactory_nameHint() default "Controller Ess Time-Of-Use Tariff Discharge [{id}]"; + String webconsole_configurationFactory_nameHint() default "Controller Ess Time-Of-Use Tariff [{id}]"; -} +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/ControlMode.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/ControlMode.java new file mode 100644 index 00000000000..11361988ba0 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/ControlMode.java @@ -0,0 +1,12 @@ +package io.openems.edge.controller.ess.timeofusetariff; + +public enum ControlMode { + /** + * Charge consumption from the grid. + */ + CHARGE_CONSUMPTION, // + /** + * Delays discharge during low-price hours. + */ + DELAY_DISCHARGE; // +} \ No newline at end of file diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Mode.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Mode.java new file mode 100644 index 00000000000..d1567cc6892 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Mode.java @@ -0,0 +1,5 @@ +package io.openems.edge.controller.ess.timeofusetariff; + +public enum Mode { + OFF, AUTOMATIC; +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/RiskLevel.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/RiskLevel.java new file mode 100644 index 00000000000..d3623400efa --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/RiskLevel.java @@ -0,0 +1,26 @@ +package io.openems.edge.controller.ess.timeofusetariff; + +public enum RiskLevel { + + /** + * Less dependent on predictions. The storage system behavior is less likely to + * deviate from the predicted behavior. + */ + LOW, + + /** + * Moderately dependent on predictions. The storage system behavior may + * occasionally deviate from the predicted behavior but generally stays within + * expected parameters. + */ + MEDIUM, + + /** + * Heavily reliant on predictions. The storage system behavior is expected to + * closely align with the predicted behavior, but occasional over-consumption + * during peak pricing hours or under-consumption for self-sufficiency may still + * occur. + */ + HIGH + +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Schedule.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Schedule.java new file mode 100644 index 00000000000..03cbdc9167f --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/Schedule.java @@ -0,0 +1,471 @@ +package io.openems.edge.controller.ess.timeofusetariff; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import io.openems.edge.common.type.TypeUtils; + +public class Schedule { + + /* + * List of periods with charge discharge data to optimize ESS. Total number of + * periods are limited to the number of price periods that are existing. + */ + protected List periods = new ArrayList<>(); + + /* ESS Usable capacity. */ + private int essUsableEnergy; + + /* Available Energy in the battery based on current SoC. */ + private int currentAvailableEnergy; + + /* Maximum Discharge energy of the battery in a period. */ + private int maxDischargeEnergyPerPeriod; + + /* Maximum Charge energy of the battery in a period. */ + private int maxChargeEnergyPerPeriod; + + /* Maximum Charge energy of the battery from grid in a period. */ + private int maxAllowedChargeEnergyFromGrid; + + /* Control mode defined by user while configuring. */ + private ControlMode controlMode; + + public static class Period { + protected Integer essInitialEnergy; + protected Integer chargeDischargeEnergy; + protected Integer gridEnergy; + protected float price; + protected int productionPrediction; + protected int consumptionPrediction; + protected int maxDischargeEnergyPerPeriod; + protected int requiredEnergy; + + public Period(Integer essInitialEnergy, Integer chargeDischargeEnergy, Integer gridEnergy, float price, + int productionPrediction, int consumptionPrediction, int maxDischargeEnergyPerPeriod) { + this.essInitialEnergy = essInitialEnergy; + this.chargeDischargeEnergy = chargeDischargeEnergy; + this.price = price; + this.productionPrediction = productionPrediction; + this.consumptionPrediction = consumptionPrediction; + this.maxDischargeEnergyPerPeriod = maxDischargeEnergyPerPeriod; + this.requiredEnergy = this.consumptionPrediction - this.productionPrediction; + } + + /** + * Method to check if force charge is scheduled for the period. + * + * @return True if force charge is scheduled and False otherwise. + */ + public boolean isChargeFromGridScheduled() { + return this.chargeDischargeEnergy < 0 && this.gridEnergy > 0; + } + + /** + * Method to check if delay discharge is scheduled. + * + * @return True if delaying discharge is scheduled and False otherwise. + */ + public boolean isDelayDischargeScheduled() { + return this.chargeDischargeEnergy < this.essInitialEnergy // + && this.gridEnergy > 0 // + && this.requiredEnergy < this.essInitialEnergy // + && this.chargeDischargeEnergy != this.maxDischargeEnergyPerPeriod; + } + + public boolean isExcessPvAvailable() { + return this.requiredEnergy < 0; + } + + /** + * Returns the state of the period. + * + * @param controlMode mode enabled by the customer. + * @return the state. + */ + public StateMachine getStateMachine(ControlMode controlMode) { + + var stateMachine = StateMachine.ALLOWS_DISCHARGE; + + if (this.isExcessPvAvailable() || this.essInitialEnergy == 0) { + stateMachine = StateMachine.CHARGE_FROM_PV; + } + switch (controlMode) { + case CHARGE_CONSUMPTION: + if (this.isChargeFromGridScheduled()) { + stateMachine = StateMachine.CHARGE_FROM_GRID; + } + break; + case DELAY_DISCHARGE: + if (this.isDelayDischargeScheduled()) { + stateMachine = StateMachine.DELAY_DISCHARGE; + } + break; + } + + return stateMachine; + } + } + + public Schedule(ControlMode controlMode, RiskLevel riskLevel, int essUsableEnergy, int currentAvailableEnergy, // + int dischargeEnergy, int chargeEnergy, Float[] prices, Integer[] consumptionPrediciton, // + Integer[] productionPrediction, int maxAllowedChargeEnergyFromGrid) { + + this.controlMode = controlMode; + this.essUsableEnergy = essUsableEnergy; + this.currentAvailableEnergy = currentAvailableEnergy; + this.maxDischargeEnergyPerPeriod = dischargeEnergy; + this.maxChargeEnergyPerPeriod = chargeEnergy; + this.maxAllowedChargeEnergyFromGrid = maxAllowedChargeEnergyFromGrid; + + // Filtering non null values. + var priceValues = Arrays.stream(prices) // + .filter(Objects::nonNull) // + .toArray(Float[]::new); + + for (var index = 0; index < priceValues.length; index++) { + var consumption = consumptionPrediciton[index]; + var production = productionPrediction[index]; + var price = priceValues[index]; + + if (production == null) { + production = 0; + } + + if (consumption == null) { + consumption = 0; + } + + // TODO + // Adjust 'production', 'consumption' based on RIskLevel selected by customer. + + // Creating the period. + // Initially the 'chargeDischarge','grid' and 'initialEnergy' will be 'null'. + var period = new Period(null, null, null, price, production, consumption, this.maxDischargeEnergyPerPeriod); + + // Adding to the list. + this.periods.add(period); + } + + // Creates the initial schedule based on balancing. + this.simulateSchedule(); + } + + /** + * Creates the schedule with periods. + */ + public void createSchedule() { + for (int index = 0; index < this.periods.size(); index++) { + final var period = this.periods.get(index); + final var requiredEnergy = period.requiredEnergy; + final var essInitialEnergy = period.essInitialEnergy; + + if (requiredEnergy >= essInitialEnergy) { + // recalculate the required energy needed. + var requiredEnergyFromGrid = requiredEnergy - essInitialEnergy; + if (requiredEnergyFromGrid > 0) { + this.calculateSchedule(requiredEnergyFromGrid, index); + } + } + // Simulates and updates the schedule. + this.simulateSchedule(); + } + } + + /** + * Simulates and updates the schedule. + * + *

+ * Update in charge or discharge energy in a certain period changes the + * subsequent periods. + * + */ + private void simulateSchedule() { + var essInitialEnergy = this.currentAvailableEnergy; + + for (int index = 0; index < this.periods.size(); index++) { + + // Current period. + final var period = this.periods.get(index); + + // calculate initial energy for current period. + final int essInitialEnergyForCurrentPeriod = TypeUtils.max(0, essInitialEnergy); + + // Update the value. + period.essInitialEnergy = essInitialEnergyForCurrentPeriod; + + // updates 'chargeDischarge energy' and 'Grid energy' values. + this.updateEssAndGridEnergyInPeriod(period); + + // estimating initial Energy for next period. + final int essInitialEnergyForNextPeriod = TypeUtils.max(0, essInitialEnergy - period.chargeDischargeEnergy); + + // Store the value for next period. + essInitialEnergy = essInitialEnergyForNextPeriod; + } + } + + /** + * Updates the 'chargeDischarge energy' and 'Grid energy' values for the + * appropriate period mentioned by index. + * + * @param period The {@link Period}. + */ + private void updateEssAndGridEnergyInPeriod(Period period) { + final var chargeDischargeEnergy = period.chargeDischargeEnergy; + final var requiredEnergy = period.requiredEnergy; + final var essInitialEnergy = period.essInitialEnergy; + final var gridEnergy = period.gridEnergy; + + // Calculate maximum allowed charge and discharge energies + final var maximumAllowedChargeInBattery = TypeUtils.max(this.maxChargeEnergyPerPeriod, + (essInitialEnergy - this.essUsableEnergy)); + final var maximumAllowedDischargeInBattery = TypeUtils.min(this.maxDischargeEnergyPerPeriod, essInitialEnergy); + + // simulate 'Balancing'. + var chargeDischargeEnergyUpdated = period.isExcessPvAvailable() + // Excess PV energy is present + ? TypeUtils.max(maximumAllowedChargeInBattery, requiredEnergy) + // Normal Discharging. + : TypeUtils.min(maximumAllowedDischargeInBattery, requiredEnergy); + + if (gridEnergy != null && chargeDischargeEnergy != null) { + // Not initial run. + switch (this.controlMode) { + case CHARGE_CONSUMPTION: + if (period.isChargeFromGridScheduled()) { + // if force charge is scheduled. + chargeDischargeEnergyUpdated = TypeUtils.max(maximumAllowedChargeInBattery, chargeDischargeEnergy); + } + break; + case DELAY_DISCHARGE: + if (period.isDelayDischargeScheduled()) { + // Delay discharge is already set. + chargeDischargeEnergyUpdated = TypeUtils.min(maximumAllowedDischargeInBattery, + requiredEnergy - gridEnergy); + } + break; + } + } + + // update the chargeDischargeEnergy and grid energy values. + period.chargeDischargeEnergy = chargeDischargeEnergyUpdated; + period.gridEnergy = requiredEnergy - chargeDischargeEnergyUpdated; + } + + /** + * Calculates and schedules the required energy before the index period. + * + * @param requiredEnergy The energy needed to be scheduled in battery. + * @param expensivePeriodIndex The index of the expensive period. + */ + private void calculateSchedule(int requiredEnergy, int expensivePeriodIndex) { + + // Cheapest period index with available charge energy. + var cheapHour = this.getCheapestHourIndexBeforePeriod(expensivePeriodIndex, this.periods, this.essUsableEnergy); + + if (cheapHour == null) { + // no cheap hour Calculated + return; + } + + var cheapPeriod = this.periods.get(cheapHour); + var expensivePeriod = this.periods.get(expensivePeriodIndex); + + // schedule + if (cheapPeriod.price < expensivePeriod.price) { + + // available 'charge' energy when the mode is CHARGE_CONSUMPTION. + // available 'discharge' energy when the mode is DELAY_DISCAHRGE. + var availableEnergy = this.getAvailableEnergy(cheapPeriod); + + // check if the required energy to charge/discharge is more than available + // energy. + if (requiredEnergy > availableEnergy) { + + // update schedule + this.updateSchedule(availableEnergy, cheapPeriod); + + // Calculate to schedule remaining energy. + var remainingEnergy = requiredEnergy - availableEnergy; + this.calculateSchedule(remainingEnergy, expensivePeriodIndex); + } else { + // update schedule + this.updateSchedule(requiredEnergy, cheapPeriod); + } + } + } + + /** + * Returns the cheapest period with available charge energy before the specified + * period index. Returns null if cheapest hour is not found. + * + * @param expensivePeriodIndex The period index before which the cheap hour + * should be found. + * @param periods The list of periods. + * @param essUsableEnergy The usable energy in the battery. + * @return The index of the cheapest hour, or null if none is found. + */ + private Integer getCheapestHourIndexBeforePeriod(int expensivePeriodIndex, List periods, + int essUsableEnergy) { + + Integer cheapHourIndex = null; + for (int index = 0; index < expensivePeriodIndex; index++) { + var period = this.periods.get(index); + var availableEnergy = this.getAvailableEnergy(period); + if (availableEnergy <= 0) { + continue; + } + + // Checks if the battery gets full after the cheap period and before the current + // period. + var batteryCapacityisFull = this.batteryCapacityGetsFull(index, expensivePeriodIndex, periods, + essUsableEnergy); + if (batteryCapacityisFull) { + continue; + } + + if (cheapHourIndex == null || periods.get(index).price < periods.get(cheapHourIndex).price) { + cheapHourIndex = index; + } + } + + return cheapHourIndex; + } + + /** + * Redirects to the appropriate method based on the control mode selected. + * + *

+ * Redirected to {@link Schedule#getAvailableChargeEnergyForPeriod(Period)} when + * {@link ControlMode#CHARGE_CONSUMPTION}. + * + *

+ * Redirected to {@link Schedule#getAvailableDischargeEnergyForPeriod(Period)} + * when {@link ControlMode#DELAY_DISCHARGE}. + * + * @param period The {@link Period}. + * @return the available charge/Discharge energy. + */ + private int getAvailableEnergy(Period period) { + return switch (this.controlMode) { + case CHARGE_CONSUMPTION -> this.getAvailableChargeEnergyForPeriod(period); + case DELAY_DISCHARGE -> this.getAvailableDischargeEnergyForPeriod(period); + }; + } + + /** + * Returns the available charge energy for the period. + * + * @param period The {@link Period}. + * @return the available charge energy. + */ + private int getAvailableChargeEnergyForPeriod(Period period) { + final var availableCapacity = TypeUtils.subtract(this.essUsableEnergy, period.essInitialEnergy); + + // No space for charging. (SoC is already 100.) + if (availableCapacity == 0) { + return 0; + } + + final var maxChargeEnergy = (period.requiredEnergy > 0) + ? -TypeUtils.max(0, this.maxAllowedChargeEnergyFromGrid - period.consumptionPrediction) + : this.maxChargeEnergyPerPeriod; + + if (period.chargeDischargeEnergy < 0) { + // charge is already set for this period. + return TypeUtils.min(availableCapacity, TypeUtils.abs(maxChargeEnergy - period.chargeDischargeEnergy)); + } else { + // No charge set. + return TypeUtils.min(availableCapacity, TypeUtils.abs(maxChargeEnergy)); + } + + } + + /** + * Returns the available Discharge energy for the period. + * + * @param period The {@link Period}. + * @return the available discharge energy. + */ + private int getAvailableDischargeEnergyForPeriod(Period period) { + final var initialEnergyForCurrentPeriod = period.essInitialEnergy; + final var currentChargeDischargeEnergy = period.chargeDischargeEnergy; + + if (initialEnergyForCurrentPeriod == 0 || currentChargeDischargeEnergy < 0) { + // Empty Battery || Excess PV available.. + return 0; + } else { + return TypeUtils.min(this.maxDischargeEnergyPerPeriod, currentChargeDischargeEnergy); + } + } + + /** + * Updates the 'chargeDischarge energy' and 'Grid energy' values for cheap hour + * period. + * + * @param energy the energy to schedule. + * @param period The {@link Period}. + */ + private void updateSchedule(int energy, Period period) { + switch (this.controlMode) { + case CHARGE_CONSUMPTION -> { + energy = -energy; // Charge energy is always negative. + + if (period.chargeDischargeEnergy <= 0) { + // If already charge scheduled for period, add to it. + period.chargeDischargeEnergy += energy; + } else { + // if charge scheduling for first time. + period.chargeDischargeEnergy = energy; + } + + // Update grid energy. + period.gridEnergy = -period.chargeDischargeEnergy; + } + + case DELAY_DISCHARGE -> { + period.chargeDischargeEnergy = TypeUtils.max(period.chargeDischargeEnergy - energy, 0); + + // Update grid energy. + period.gridEnergy += energy; + } + } + } + + /** + * Checks if the battery gets full within cheap period and the current period + * index. + * + * @param from Index of the cheap hour. + * @param to The current index. + * @param periods The list of periods. + * @param essUsableEnergy The Usable energy in the battery. + * @return True if the battery is full within the index range, False otherwise. + */ + private boolean batteryCapacityGetsFull(int from, int to, List periods, int essUsableEnergy) { + // Exclude the cheap hour index (first index) from the search. + return periods.subList(from + 1, to).stream() // + .anyMatch(p -> p.essInitialEnergy == essUsableEnergy); + } + + @Override + public String toString() { + var b = new StringBuilder(); + b.append("\n %10s %10s %10s %10s %10s %10s %10s %10s\n".formatted("Index.", "Product.", "Consumpt.", + "price.", "Battery.", "Grid.", "EssEnergy.", "State.")); + final var iterator = this.periods.listIterator(); + while (iterator.hasNext()) { + final var index = iterator.nextIndex(); + final var period = iterator.next(); + b.append("%10d %10d %10d %10f %10d %10d %10d %10s\n".formatted(index, // + period.productionPrediction, period.consumptionPrediction, // + period.price, period.chargeDischargeEnergy, // + period.gridEnergy, period.essInitialEnergy, // + period.getStateMachine(this.controlMode).toString())); + } + return b.toString(); + } +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/ScheduleUtils.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/ScheduleUtils.java new file mode 100644 index 00000000000..e4da68e180a --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/ScheduleUtils.java @@ -0,0 +1,130 @@ +package io.openems.edge.controller.ess.timeofusetariff; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.SortedMap; +import java.util.UUID; +import java.util.stream.Stream; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonPrimitive; + +import io.openems.common.types.ChannelAddress; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.controller.ess.timeofusetariff.jsonrpc.GetScheduleResponse; + +public class ScheduleUtils { + + /** + * Generates a 24-hour schedule as a {@link JsonArray} based on input data for + * prices and states. The resulting schedule includes timestamped entries for + * each 15-minute interval. and 'states' data. + * + * @param prices The quarterly prices for 24 hours (3-hour past, 21-hour + * future). + * @param states The states for 24 hours (3-hour past, 21-hour future). + * @param timeStamp The time stamp of the first entry in the schedule. + * @return The schedule data as a {@link JsonArray}. + */ + public static JsonArray createSchedule(JsonArray prices, JsonArray states, ZonedDateTime timeStamp) { + + var schedule = JsonUtils.buildJsonArray(); + + // Creates the Json object with 'time stamp', 'price', 'state' and add them to + // the Json array. + for (int index = 0; index < prices.size(); index++) { + var result = JsonUtils.buildJsonObject(); + var price = prices.get(index); + var state = states.get(index); + + if (price.isJsonNull() || state.isJsonNull()) { + continue; + } + + // Calculate the timestamp for the current entry, adding 15 minutes for each + // index. + var entryTimeStamp = timeStamp.plusMinutes(15 * index).format(DateTimeFormatter.ISO_INSTANT); + + result.add("timestamp", new JsonPrimitive(entryTimeStamp)); + result.add("price", price); + result.add("state", state); + + // Add the JSON object to the schedule array. + schedule.add(result.build()); + } + + return schedule.build(); + } + + /** + * Utilizes the previous three hours' data and computes the next 21 hours data + * from the {@link Schedule} provided, then concatenates them to generate a + * 24-hour {@link Schedule}. + * + * @param schedule The {@link Schedule}. + * @param controlMode The {@link ControlMode}. + * @param requestId The JSON-RPC id. + * @param queryResult The historic data. + * @param channeladdressPrices The {@link ChannelAddress} for Quarterly + * prices. + * @param channeladdressStateMachine The {@link ChannelAddress} for the state + * machine. + * @return The {@link GetScheduleResponse}. + */ + public static GetScheduleResponse handleGetScheduleRequest(Schedule schedule, ControlMode controlMode, + UUID requestId, SortedMap> queryResult, + ChannelAddress channeladdressPrices, ChannelAddress channeladdressStateMachine) { + + // Extract the price data + var priceValuesPast = queryResult.values().stream() // + // Only specific channel address values. + .map(t -> t.get(channeladdressPrices)) // + // get as Array + .collect(JsonUtils.toJsonArray()); + + // Extract the State Machine data + var stateMachineValuesPast = queryResult.values().stream() // + // Only specific channel address values. + .map(t -> t.get(channeladdressStateMachine)) // + // Mapping to absolute state machine values since query result gives average + // values. + .map(t -> { + if (t.isJsonPrimitive() && t.getAsJsonPrimitive().isNumber()) { + // 'double' to 'int' for appropriate state machine values. + return new JsonPrimitive(t.getAsInt()); + } + + return JsonNull.INSTANCE; + }) + // get as Array + .collect(JsonUtils.toJsonArray()); + + final var stateMachineValuesFuture = new JsonArray(); + final var priceValuesFuture = new JsonArray(); + + // Create StateMachine for future values based on schedule created. + schedule.periods.forEach(period -> { + priceValuesFuture.add(period.price); + stateMachineValuesFuture.add(period.getStateMachine(controlMode).getValue()); + }); + + var prices = Stream.concat(// + JsonUtils.stream(priceValuesPast), // Last 3 hours data. + JsonUtils.stream(priceValuesFuture)) // Next 21 hours data. + .limit(96) // + .collect(JsonUtils.toJsonArray()); + + var states = Stream.concat(// + JsonUtils.stream(stateMachineValuesPast), // Last 3 hours data + JsonUtils.stream(stateMachineValuesFuture)) // Next 21 hours data. + .limit(96) // + .collect(JsonUtils.toJsonArray()); + + var timestamp = queryResult.firstKey(); + var result = ScheduleUtils.createSchedule(prices, states, timestamp); + + return new GetScheduleResponse(requestId, result); + } +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/StateMachine.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/StateMachine.java new file mode 100644 index 00000000000..c1b2307af3f --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/StateMachine.java @@ -0,0 +1,37 @@ +package io.openems.edge.controller.ess.timeofusetariff; + +import io.openems.common.types.OptionsEnum; + +public enum StateMachine implements OptionsEnum { + + DELAY_DISCHARGE(0, "Delaying the discharge from the battery is scheduled"), // + ALLOWS_DISCHARGE(1, + "No active limitation, Discharge is permitted due to high-price hour or when ample self-generated energy is available"), // + CHARGE_FROM_PV(2, + "No active limitation set, Excess PV energy predicted and battery can charge from surplus PV or feed energy to the grid based on its state of charge (SoC)"), // + CHARGE_FROM_GRID(3, "Charging the battery from the grid scheduled"); // + + private final int value; + private final String name; + + private StateMachine(int value, String name) { + this.value = value; + this.name = name; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public OptionsEnum getUndefined() { + return CHARGE_FROM_PV; + } + +} diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischarge.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffController.java similarity index 54% rename from io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischarge.java rename to io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffController.java index 92ab6a67512..db45f22da53 100644 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/src/io/openems/edge/controller/ess/timeofusetariff/discharge/ControllerEssTimeOfUseTariffDischarge.java +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffController.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.ess.timeofusetariff.discharge; +package io.openems.edge.controller.ess.timeofusetariff; import io.openems.common.channel.PersistencePriority; import io.openems.common.channel.Unit; @@ -9,17 +9,28 @@ import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.controller.api.Controller; -public interface ControllerEssTimeOfUseTariffDischarge extends Controller, OpenemsComponent { +public interface TimeOfUseTariffController extends Controller, OpenemsComponent { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - /** - * Current state of the Time of use tariff discharge controller. + * Current state of the Time of use tariff controller. */ STATE_MACHINE(Doc.of(StateMachine.values()) // .persistencePriority(PersistencePriority.HIGH) // .text("Current state of the Controller")), + QUARTERLY_PRICES(Doc.of(OpenemsType.FLOAT) // + .unit(Unit.EUROS_PER_MEGAWATT_HOUR) // + .text("Price of the electricity for the current Hour")// + .persistencePriority(PersistencePriority.HIGH)), // + + /** + * Aggregated seconds when storage is being force charged from the grid. + */ + CHARGED_TIME(Doc.of(OpenemsType.LONG) // + .unit(Unit.CUMULATED_SECONDS) // + .persistencePriority(PersistencePriority.HIGH)), // + /** * Aggregated seconds when storage is blocked for discharge. */ @@ -27,42 +38,26 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { .unit(Unit.CUMULATED_SECONDS) // .persistencePriority(PersistencePriority.HIGH)), // - DELAYED(Doc.of(OpenemsType.BOOLEAN)// - .text("The controller currently blocks discharge")), - TARGET_HOURS_IS_EMPTY(Doc.of(OpenemsType.BOOLEAN)// - .text("The list of target hours is empty")), - QUATERLY_PRICES_TAKEN(Doc.of(OpenemsType.BOOLEAN)// - .text("The controller retrieves hourly Prices from API successfully")), - TARGET_HOURS_CALCULATED(Doc.of(OpenemsType.BOOLEAN)// - .text("The controller calculates target time to buy from grid successfully")), - TOTAL_CONSUMPTION(Doc.of(OpenemsType.INTEGER) // - .text("Total consmption for the night")), - QUARTERLY_PRICES(Doc.of(OpenemsType.FLOAT) // - .unit(Unit.EUROS_PER_MEGAWATT_HOUR) // - .persistencePriority(PersistencePriority.HIGH) // - .text("Price of the electricity for the current Hour")), - REMAINING_CONSUMPTION(Doc.of(OpenemsType.DOUBLE) // - .text("remaining consmption to charge from grid")), - TARGET_HOURS(Doc.of(OpenemsType.INTEGER) // - .text("Number of Target Hours")), + /** + * Channels for debugging. + */ + CHARGE_DISCHARGE_ENERGY(Doc.of(OpenemsType.INTEGER) // + .text("Charge/Discharge energy calculated for the period.")), // + GRID_ENERGY(Doc.of(OpenemsType.INTEGER) // + .text("Grid energy calculated for the period.")), // + QUARTERLY_PRICES_ARE_EMPTY(Doc.of(OpenemsType.BOOLEAN)// + .text("The list of quarterly prices retrieved are empty")), AVAILABLE_CAPACITY(Doc.of(OpenemsType.INTEGER) // .text("Available capcity in the battery during evening")), // USABLE_CAPACITY(Doc.of(OpenemsType.INTEGER) // .text("Usable capcity in the battery during after taking limit soc into consideration")), // - PRO_MORE_THAN_CON_ACTUAL(Doc.of(OpenemsType.INTEGER) // - .text("Actual Hour of Production more than Consumption")), - PRO_MORE_THAN_CON_SET(Doc.of(OpenemsType.INTEGER) // - .text("Hour of Production more than Consumption set based on risk level")), - PRO_LESS_THAN_CON(Doc.of(OpenemsType.INTEGER) // - .text("Hour of Production less than Consumption")), PREDICTED_PRODUCTION(Doc.of(OpenemsType.INTEGER) // .text("Predicted Production for the current quarterly hour")), PREDICTED_CONSUMPTION(Doc.of(OpenemsType.INTEGER) // .text("Predicted Consumption for the current quarterly hour")), MIN_SOC(Doc.of(OpenemsType.INTEGER) // - .text("Minimum SoC to avoid complete discharge")), - PREDICTED_SOC_WITHOUT_LOGIC(Doc.of(OpenemsType.INTEGER) // - .text("SoC prediction curve without controller logic")),; + .text("Minimum SoC to avoid complete discharge")), // + ; private final Doc doc; @@ -77,68 +72,50 @@ public Doc doc() { } /** - * Gets the Channel for {@link ChannelId#STATE_MACHINE}. + * Gets the Channel for {@link ChannelId#QUARTERLY_PRICES}. * * @return the Channel */ - public default Channel getStateMachineChannel() { - return this.channel(ChannelId.STATE_MACHINE); - } - - /** - * Gets the Status of the Controller. See {@link ChannelId#STATE_MACHINE}. - * - * @return the Channel {@link Value} - */ - public default StateMachine getStateMachine() { - return this.getStateMachineChannel().value().asEnum(); + public default Channel getQuarterlyPricesChannel() { + return this.channel(ChannelId.QUARTERLY_PRICES); } /** - * Internal method to set the 'nextValue' on {@link ChannelId#STATE_MACHINE} + * Internal method to set the 'nextValue' on {@link ChannelId#QUARTERLY_PRICES} * Channel. * * @param value the next value */ - public default void _setStateMachine(StateMachine value) { - this.getStateMachineChannel().setNextValue(value); + public default void _setQuarterlyPrices(Float value) { + this.getQuarterlyPricesChannel().setNextValue(value); } /** - * Gets the Channel for {@link ChannelId#DELAYED}. + * Gets the Channel for {@link ChannelId#STATE_MACHINE}. * * @return the Channel */ - public default Channel getDelayedChannel() { - return this.channel(ChannelId.DELAYED); - } - - /** - * Internal method to set the 'nextValue' on {@link ChannelId#DELAYED} Channel. - * - * @param value the next value - */ - public default void _setDelayed(boolean value) { - this.getDelayedChannel().setNextValue(value); + public default Channel getStateMachineChannel() { + return this.channel(ChannelId.STATE_MACHINE); } /** - * Gets the Channel for {@link ChannelId#QUARTERLY_PRICES}. + * Gets the Status of the Controller. See {@link ChannelId#STATE_MACHINE}. * - * @return the Channel + * @return the Channel {@link Value} */ - public default Channel getQuarterlyPricesChannel() { - return this.channel(ChannelId.QUARTERLY_PRICES); + public default StateMachine getStateMachine() { + return this.getStateMachineChannel().value().asEnum(); } /** - * Internal method to set the 'nextValue' on {@link ChannelId#QUARTERLY_PRICES} + * Internal method to set the 'nextValue' on {@link ChannelId#STATE_MACHINE} * Channel. * * @param value the next value */ - public default void _setQuarterlyPrices(Float value) { - this.getQuarterlyPricesChannel().setNextValue(value); + public default void _setStateMachine(StateMachine value) { + this.getStateMachineChannel().setNextValue(value); } /** @@ -180,59 +157,41 @@ public default void _setPredictedConsumption(Integer value) { } /** - * Gets the Channel for {@link ChannelId#PREDICTED_SOC_WITHOUT_LOGIC}. + * Gets the Channel for {@link ChannelId#CHARGE_DISCHARGE_ENERGY}. * * @return the Channel */ - public default Channel getPredictedSocWithoutLogicChannel() { - return this.channel(ChannelId.PREDICTED_SOC_WITHOUT_LOGIC); + public default Channel getChargeDischargeEnergyChannel() { + return this.channel(ChannelId.CHARGE_DISCHARGE_ENERGY); } /** * Internal method to set the 'nextValue' on - * {@link ChannelId#PREDICTED_SOC_WITHOUT_LOGIC} Channel. + * {@link ChannelId#CHARGE_DISCHARGE_ENERGY} Channel. * * @param value the next value */ - public default void _setPredictedSocWithoutLogic(Integer value) { - this.getPredictedSocWithoutLogicChannel().setNextValue(value); + public default void _setChargeDischargeEnergyChannel(Integer value) { + this.getChargeDischargeEnergyChannel().setNextValue(value); } /** - * Gets the Channel for {@link ChannelId#TARGET_HOURS_CALCULATED}. + * Gets the Channel for {@link ChannelId#GRID_ENERGY}. * * @return the Channel */ - public default Channel getTargetHoursCalculatedChannel() { - return this.channel(ChannelId.TARGET_HOURS_CALCULATED); + public default Channel getGridEnergyChannel() { + return this.channel(ChannelId.GRID_ENERGY); } /** - * Internal method to set the 'nextValue' on - * {@link ChannelId#TARGET_HOURS_CALCULATED} Channel. + * Internal method to set the 'nextValue' on {@link ChannelId#GRID_ENERGY} + * Channel. * * @param value the next value */ - public default void _setTargetHoursCalculated(Boolean value) { - this.getTargetHoursCalculatedChannel().setNextValue(value); - } - - /** - * Gets the Channel for {@link ChannelId#TARGET_HOURS_IS_EMPTY}. - * - * @return the Channel - */ - public default Channel getTargetHoursIsEmptyChannel() { - return this.channel(ChannelId.TARGET_HOURS_IS_EMPTY); + public default void _setGridEnergyChannel(Integer value) { + this.getGridEnergyChannel().setNextValue(value); } - /** - * Internal method to set the 'nextValue' on - * {@link ChannelId#TARGET_HOURS_IS_EMPTY} Channel. - * - * @param value the next value - */ - public default void _setTargetHoursIsEmpty(Boolean value) { - this.getTargetHoursIsEmptyChannel().setNextValue(value); - } } diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImpl.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImpl.java new file mode 100644 index 00000000000..4d486a2f991 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerImpl.java @@ -0,0 +1,409 @@ +package io.openems.edge.controller.ess.timeofusetariff; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.IntStream; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; +import io.openems.common.session.Role; +import io.openems.common.timedata.Resolution; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.jsonapi.JsonApi; +import io.openems.edge.common.type.TypeUtils; +import io.openems.edge.common.user.User; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.controller.ess.emergencycapacityreserve.ControllerEssEmergencyCapacityReserve; +import io.openems.edge.controller.ess.limittotaldischarge.ControllerEssLimitTotalDischarge; +import io.openems.edge.controller.ess.timeofusetariff.jsonrpc.GetScheduleRequest; +import io.openems.edge.ess.api.HybridEss; +import io.openems.edge.ess.api.ManagedSymmetricEss; +import io.openems.edge.ess.power.api.Phase; +import io.openems.edge.ess.power.api.Pwr; +import io.openems.edge.predictor.api.manager.PredictorManager; +import io.openems.edge.timedata.api.Timedata; +import io.openems.edge.timedata.api.TimedataProvider; +import io.openems.edge.timedata.api.utils.CalculateActiveTime; +import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; +import io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.Ess.Time-Of-Use-Tariff", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class TimeOfUseTariffControllerImpl extends AbstractOpenemsComponent + implements TimeOfUseTariffController, Controller, OpenemsComponent, TimedataProvider, JsonApi { + + private static final ChannelAddress SUM_PRODUCTION = new ChannelAddress("_sum", "ProductionActivePower"); + private static final ChannelAddress SUM_CONSUMPTION = new ChannelAddress("_sum", "ConsumptionActivePower"); + private static final int PERIODS_PER_HOUR = 4; + private static final int MINUTES_PER_PERIOD = 15; + private static final Duration TIME_PER_PERIOD = Duration.ofMinutes(MINUTES_PER_PERIOD); + + private final Logger log = LoggerFactory.getLogger(TimeOfUseTariffControllerImpl.class); + + /** + * Delayed Time is aggregated also after restart of OpenEMS. + */ + private final CalculateActiveTime calculateDelayedTime = new CalculateActiveTime(this, + TimeOfUseTariffController.ChannelId.DELAYED_TIME); + + /** + * Charged Time is aggregated also after restart of OpenEMS. + */ + private final CalculateActiveTime calculateChargedTime = new CalculateActiveTime(this, + TimeOfUseTariffController.ChannelId.CHARGED_TIME); + + @Reference + private ConfigurationAdmin cm; + + @Reference + private ComponentManager componentManager; + + @Reference + private PredictorManager predictorManager; + + @Reference + private TimeOfUseTariff timeOfUseTariff; + + private Config config = null; + private Schedule schedule; + private ZonedDateTime nextQuarter = null; + + @Reference(policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private volatile Timedata timedata; + + @Reference(policyOption = ReferencePolicyOption.GREEDY, // + cardinality = ReferenceCardinality.MULTIPLE, // + target = "(&(enabled=true)(isReserveSocEnabled=true))") + private volatile List ctrlEmergencyCapacityReserves = new CopyOnWriteArrayList<>(); + + @Reference(policyOption = ReferencePolicyOption.GREEDY, // + cardinality = ReferenceCardinality.MULTIPLE, // + target = "(enabled=true)") + private volatile List ctrlLimitTotalDischarges = new CopyOnWriteArrayList<>(); + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + private ManagedSymmetricEss ess; + + public TimeOfUseTariffControllerImpl() { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + TimeOfUseTariffController.ChannelId.values() // + ); + } + + @Activate + private void activate(ComponentContext context, Config config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + this.config = config; + + // update filter for 'ess' + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "ess", config.ess_id())) { + return; + } + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public void run() throws OpenemsNamedException { + + // Current Date Time rounded off to NUMBER_OF_MINUTES. + var now = TimeOfUseTariffUtils.getNowRoundedDownToMinutes(this.componentManager.getClock(), MINUTES_PER_PERIOD); + + // Mode given from the configuration. + switch (this.config.mode()) { + case AUTOMATIC -> this.modeAutomatic(now); + case OFF -> this.modeOff(); + } + + this.updateVisualizationChannels(); + } + + /** + * Apply the actual logic of calculating the battery energy, schedules it in + * 15-minute intervals, and determines charge or delay discharge actions. + * + * @param now Current Date Time rounded off to NUMBER_OF_MINUTES. + * @throws OpenemsNamedException on error + */ + private void modeAutomatic(ZonedDateTime now) throws OpenemsNamedException { + + // Runs the logic every interval or when the schedule is not created due to + // unavailability of values. + if (!(this.nextQuarter == null || now.isEqual(this.nextQuarter) || this.schedule.periods.isEmpty())) { + // Sets the charge or discharge of the system based on the mode. + this.setChargeOrDischarge(); + return; + } + + // Prediction values + final var predictionProduction = this.predictorManager.get24HoursPrediction(SUM_PRODUCTION) // + .getValues(); + final var predictionConsumption = this.predictorManager.get24HoursPrediction(SUM_CONSUMPTION) // + .getValues(); + + // Prices contains the price values and the time it is retrieved. + final var prices = this.timeOfUseTariff.getPrices(); + + this.channel(TimeOfUseTariffController.ChannelId.QUARTERLY_PRICES_ARE_EMPTY).setNextValue(prices.isEmpty()); + if (prices.isEmpty()) { + return; + } + + // Ess information. + final var netEssCapacity = this.ess.getCapacity().getOrError(); + final var soc = this.ess.getSoc().getOrError(); + + // Calculate available energy using "netCapacity" and "soc". + var currentAvailableEnergy = netEssCapacity /* [Wh] */ / 100 * soc; + this.channel(TimeOfUseTariffController.ChannelId.AVAILABLE_CAPACITY).setNextValue(currentAvailableEnergy); + + // Calculate the net usable energy of the battery. + final var limitEnergy = this.getLimitEnergy(netEssCapacity); + final var netUsableEnergy = TypeUtils.max(0, netEssCapacity - limitEnergy); + + // Calculate current usable energy [Wh] in the battery. + currentAvailableEnergy = TypeUtils.max(0, currentAvailableEnergy - limitEnergy); + this.channel(TimeOfUseTariffController.ChannelId.USABLE_CAPACITY).setNextValue(currentAvailableEnergy); + + // Power Values for scheduling battery for individual periods. + var power = this.ess.getPower(); + var dischargePower = power.getMaxPower(this.ess, Phase.ALL, Pwr.ACTIVE); + var chargePower = power.getMinPower(this.ess, Phase.ALL, Pwr.ACTIVE); + + // If both values are 0, its likely that components are not yet activated + // completely. + // TODO: Handle cases where max and min power are very low in beginning. + if (dischargePower == 0 && chargePower == 0) { + return; + } + + // Power to Energy. + var dischargeEnergy = dischargePower / PERIODS_PER_HOUR; + var chargeEnergy = chargePower / PERIODS_PER_HOUR; + var allowedChargeEnergyFromGrid = this.config.maxChargePowerFromGrid() / PERIODS_PER_HOUR; + + // Initialize Schedule + this.schedule = new Schedule(this.config.controlMode(), this.config.riskLevel(), netUsableEnergy, + currentAvailableEnergy, dischargeEnergy, chargeEnergy, prices.getValues(), predictionConsumption, + predictionProduction, allowedChargeEnergyFromGrid); + + // Generate Final schedule. + this.schedule.createSchedule(); + + // log the schedule + this.logInfo(this.log, this.schedule.toString()); + + // Update next quarter. + this.nextQuarter = now.plus(TIME_PER_PERIOD); + + // Sets the charge or discharge of the system based on the mode. + this.setChargeOrDischarge(); + } + + /** + * Force charges or delays the discharge if the schedule is set for current + * period. + * + * @throws OpenemsNamedException on error. + */ + private void setChargeOrDischarge() { + if (this.schedule.periods.isEmpty()) { + this.setDefaultValues(); + return; + } + + final var period = this.schedule.periods.get(0); + final var stateMachine = period.getStateMachine(this.config.controlMode()); + var charged = false; + var delayed = false; + + var activePower = switch (stateMachine) { + case CHARGE_FROM_GRID -> { + charged = true; + yield period.chargeDischargeEnergy * PERIODS_PER_HOUR; // energy to power + } + case DELAY_DISCHARGE -> { + delayed = true; + var value = period.chargeDischargeEnergy * PERIODS_PER_HOUR; // energy to power + + if (this.ess instanceof HybridEss hybridEss) { + // DC or Hybrid system: limit AC export power to DC production power + value += TypeUtils.subtract(this.ess.getActivePower().get(), hybridEss.getDcDischargePower().get()); + } + + yield value; + } + // Do not set active power + case ALLOWS_DISCHARGE -> null; + case CHARGE_FROM_PV -> null; + }; + + if (activePower != null) { + try { + this.ess.setActivePowerLessOrEquals(activePower); + } catch (OpenemsNamedException e) { + e.printStackTrace(); + delayed = false; + charged = false; + } + } + + this._setStateMachine(stateMachine); + + // Update the timer. + this.calculateChargedTime.update(charged); + this.calculateDelayedTime.update(delayed); + } + + /** + * Returns the amount of energy that is not usable for scheduling. + * + * @param netEssCapacity net capacity of the battery. + * @return the amount of energy that is limited. + */ + private int getLimitEnergy(int netEssCapacity) { + + // Usable capacity based on minimum SoC from Limit total discharge and emergency + // reserve controllers. + var limitSoc = IntStream.concat(// + this.ctrlLimitTotalDischarges.stream().mapToInt(ctrl -> ctrl.getMinSoc().orElse(0)), // + this.ctrlEmergencyCapacityReserves.stream().mapToInt(ctrl -> ctrl.getActualReserveSoc().orElse(0))) // + .max().orElse(0); + this.channel(TimeOfUseTariffController.ChannelId.MIN_SOC).setNextValue(limitSoc); + + return netEssCapacity /* [Wh] */ / 100 * limitSoc; + } + + /** + * Apply the mode OFF logic. + */ + private void modeOff() { + this.setDefaultValues(); + } + + /** + * This is only to visualize data for better debugging. + */ + private void updateVisualizationChannels() { + + if (this.schedule == null || this.schedule.periods.isEmpty()) { + // Values are not yet calculated. + this.setDefaultValues(); + return; + } + + // First period is always the current period. + final var period = this.schedule.periods.get(0); + + // Set the channels + this._setQuarterlyPrices(period.price); + this._setPredictedConsumption(period.consumptionPrediction); + this._setPredictedProduction(period.productionPrediction); + this._setGridEnergyChannel(period.gridEnergy); + this._setChargeDischargeEnergyChannel(period.chargeDischargeEnergy); + } + + /** + * Sets the Default values to the channels, if the Periods are not yet + * calculated or if the Mode is 'OFF'. + */ + private void setDefaultValues() { + // Update the timer. + this.calculateChargedTime.update(false); + this.calculateDelayedTime.update(false); + + // Default State Machine. + this._setStateMachine(StateMachine.ALLOWS_DISCHARGE); + } + + @Override + public Timedata getTimedata() { + return this.timedata; + } + + @Override + public CompletableFuture handleJsonrpcRequest(User user, JsonrpcRequest request) + throws OpenemsNamedException { + + user.assertRoleIsAtLeast("handleJsonrpcRequest", Role.GUEST); + + switch (request.getMethod()) { + + case GetScheduleRequest.METHOD: + return this.handleGetScheduleRequest(user, GetScheduleRequest.from(request)); + + default: + throw OpenemsError.JSONRPC_UNHANDLED_METHOD.exception(request.getMethod()); + } + } + + /** + * Handles a GetScheduleRequest. + * + * @param user the User0 + * @param request the GetScheduleRequest + * @return the Future JSON-RPC Response + */ + private CompletableFuture handleGetScheduleRequest(User user, + GetScheduleRequest request) { + + final var now = TimeOfUseTariffUtils.getNowRoundedDownToMinutes(this.componentManager.getClock(), 15); + final var fromDate = now.minusHours(3); + final var channeladdressPrices = new ChannelAddress(this.id(), "QuarterlyPrices"); + final var channeladdressStateMachine = new ChannelAddress(this.id(), "StateMachine"); + + SortedMap> queryResult = new TreeMap<>(); + + // Query database for the last three hours, with 15-minute resolution. + try { + queryResult = this.timedata.queryHistoricData(null, fromDate, now, + Set.of(channeladdressPrices, channeladdressStateMachine), new Resolution(15, ChronoUnit.MINUTES)); + } catch (OpenemsNamedException e) { + this.logError(this.log, e.getMessage()); + e.printStackTrace(); + } + + var response = ScheduleUtils.handleGetScheduleRequest(this.schedule, this.config.controlMode(), request.getId(), + queryResult, channeladdressPrices, channeladdressStateMachine); + + return CompletableFuture.completedFuture(response); + } +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleRequest.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleRequest.java new file mode 100644 index 00000000000..ff357bfd827 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleRequest.java @@ -0,0 +1,49 @@ +package io.openems.edge.controller.ess.timeofusetariff.jsonrpc; + +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.jsonrpc.base.JsonrpcRequest; + +/** + * Represents a JSON-RPC Request for 'getSchedule'. + * + *

+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": "UUID",
+ *   "method": "getSchedule",
+ *   "params": {}
+ * }
+ * 
+ */ +public class GetScheduleRequest extends JsonrpcRequest { + + public static final String METHOD = "getSchedule"; + + /** + * Create {@link GetScheduleRequest} from a template {@link JsonrpcRequest}. + * + * @param r the template {@link JsonrpcRequest} + * @return the {@link GetScheduleRequest} + * @throws OpenemsNamedException on parse error + */ + public static GetScheduleRequest from(JsonrpcRequest r) throws OpenemsException { + return new GetScheduleRequest(r); + } + + public GetScheduleRequest() { + super(METHOD); + } + + private GetScheduleRequest(JsonrpcRequest request) { + super(request, METHOD); + } + + @Override + public JsonObject getParams() { + return new JsonObject(); + } + +} diff --git a/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponse.java b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponse.java new file mode 100644 index 00000000000..85413254962 --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/src/io/openems/edge/controller/ess/timeofusetariff/jsonrpc/GetScheduleResponse.java @@ -0,0 +1,48 @@ +package io.openems.edge.controller.ess.timeofusetariff.jsonrpc; + +import java.util.UUID; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; +import io.openems.common.utils.JsonUtils; + +/** + * Represents a JSON-RPC Response for 'getMeters'. + * + *
+ * {
+ *   "jsonrpc": "2.0",
+ *   "id": "UUID",
+ *   "result": {
+ *     'schedule': [{
+ *     	'timestamp':...
+ *      'price':...
+ *      'state':...
+ *     }]
+ *   }
+ * }
+ * 
+ */ +public class GetScheduleResponse extends JsonrpcResponseSuccess { + + private final JsonArray schedule; + + public GetScheduleResponse(JsonArray schedule) { + this(UUID.randomUUID(), schedule); + } + + public GetScheduleResponse(UUID id, JsonArray schedule) { + super(id); + this.schedule = schedule; + } + + @Override + public JsonObject getResult() { + return JsonUtils.buildJsonObject() // + .add("schedule", this.schedule) // + .build(); + } + +} diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/test/.gitignore b/io.openems.edge.controller.ess.timeofusetariff/test/.gitignore similarity index 100% rename from io.openems.edge.controller.ess.timeofusetariff.discharge/test/.gitignore rename to io.openems.edge.controller.ess.timeofusetariff/test/.gitignore diff --git a/io.openems.edge.controller.ess.timeofusetariff.discharge/test/io/openems/edge/controller/ess/timeofusetariff/discharge/MyConfig.java b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/MyConfig.java similarity index 63% rename from io.openems.edge.controller.ess.timeofusetariff.discharge/test/io/openems/edge/controller/ess/timeofusetariff/discharge/MyConfig.java rename to io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/MyConfig.java index 61c588d665c..f862c6f5084 100644 --- a/io.openems.edge.controller.ess.timeofusetariff.discharge/test/io/openems/edge/controller/ess/timeofusetariff/discharge/MyConfig.java +++ b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/MyConfig.java @@ -1,4 +1,4 @@ -package io.openems.edge.controller.ess.timeofusetariff.discharge; +package io.openems.edge.controller.ess.timeofusetariff; import io.openems.common.test.AbstractComponentConfig; import io.openems.common.utils.ConfigUtils; @@ -10,9 +10,9 @@ protected static class Builder { private String id; private String essId; private Mode mode; - private int maxStartHour; - private int maxEndHour; - private DelayDischargeRiskLevel delayDischargeRiskLevel; + private ControlMode controlMode; + private int maxPower; + private RiskLevel riskLevel; private Builder() { } @@ -32,29 +32,29 @@ public Builder setMode(Mode mode) { return this; } - public Builder setMaxStartHour(int maxStartHour) { - this.maxStartHour = maxStartHour; + public Builder setControlMode(ControlMode controlMode) { + this.controlMode = controlMode; return this; } - public Builder setMaxEndHour(int maxEndHour) { - this.maxEndHour = maxEndHour; + public Builder setMaxPower(int maxPower) { + this.maxPower = maxPower; return this; } - public MyConfig build() { - return new MyConfig(this); + public Builder setRiskLevel(RiskLevel riskLevel) { + this.riskLevel = riskLevel; + return this; } - public Builder setDelayDischargeRiskLevel(DelayDischargeRiskLevel delayDischargeRiskLevel) { - this.delayDischargeRiskLevel = delayDischargeRiskLevel; - return this; + public MyConfig build() { + return new MyConfig(this); } } /** * Create a Config builder. - * + * * @return a {@link Builder} */ public static Builder create() { @@ -79,22 +79,23 @@ public Mode mode() { } @Override - public int maxStartHour() { - return this.builder.maxStartHour; + public ControlMode controlMode() { + return this.builder.controlMode; } @Override - public int maxEndHour() { - return this.builder.maxEndHour; + public int maxChargePowerFromGrid() { + return this.builder.maxPower; } @Override - public String ess_target() { - return ConfigUtils.generateReferenceTargetFilter(this.id(), this.ess_id()); + public RiskLevel riskLevel() { + return this.builder.riskLevel; } @Override - public DelayDischargeRiskLevel delayDischargeRiskLevel() { - return this.builder.delayDischargeRiskLevel; + public String ess_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.ess_id()); } + } \ No newline at end of file diff --git a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerTest.java b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerTest.java new file mode 100644 index 00000000000..b979d20301e --- /dev/null +++ b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerTest.java @@ -0,0 +1,477 @@ +package io.openems.edge.controller.ess.timeofusetariff; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.junit.Test; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import io.openems.common.types.ChannelAddress; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.common.test.TimeLeapClock; +import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.ess.test.DummyManagedSymmetricEss; +import io.openems.edge.predictor.api.test.DummyPrediction24Hours; +import io.openems.edge.predictor.api.test.DummyPredictor24Hours; +import io.openems.edge.predictor.api.test.DummyPredictorManager; +import io.openems.edge.timeofusetariff.api.utils.TimeOfUseTariffUtils; +import io.openems.edge.timeofusetariff.test.DummyTimeOfUseTariffProvider; + +public class TimeOfUseTariffControllerTest { + + // Ids + private static final String CTRL_ID = "ctrlEssTimeOfUseTariff0"; + private static final String PREDICTOR_ID = "predictor0"; + private static final String ESS_ID = "ess0"; + + // Ess channels + private static final ChannelAddress ESS_CAPACITY = new ChannelAddress(ESS_ID, "Capacity"); + private static final ChannelAddress MAX_APPARENT_POWER = new ChannelAddress(ESS_ID, "MaxApparentPower"); + private static final ChannelAddress ESS_SOC = new ChannelAddress(ESS_ID, "Soc"); + private static final ChannelAddress STATE_MACHINE = new ChannelAddress(CTRL_ID, "StateMachine"); + + /* + * Default Prediction values + */ + private static final Integer[] DEFAULT_PRODUCTION_PREDICTION_QUARTERLY = { + /* 00:00-03:450 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 04:00-07:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 297, 610, // + /* 08:00-11:45 */ + 913, 1399, 1838, 2261, 2662, 3052, 3405, 3708, 4011, 4270, 4458, 4630, 4794, 4908, 4963, 4960, // + /* 12:00-15:45 */ + 4973, 4940, 4859, 4807, 4698, 4530, 4348, 4147, 1296, 1399, 1838, 1261, 1662, 1052, 1405, 1402, + /* 16:00-19:45 */ + 1662, 1052, 1405, 1630, 1285, 1520, 1250, 910, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 20:00-23:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 00:00-03:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 04:00-07:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 130, 402, 667, // + /* 08:00-11:45 */ + 1023, 1631, 2020, 2420, 2834, 3237, 3638, 4006, 4338, 4597, 4825, 4965, 5111, 5213, 5268, 5317, // + /* 12:00-15:45 */ + 5321, 5271, 5232, 5193, 5044, 4915, 4738, 4499, 3702, 3226, 3046, 2857, 2649, 2421, 2184, 1933, // + /* 16:00-19:45 */ + 1674, 1364, 1070, 754, 447, 193, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + /* 20:00-23:45 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // + }; + + private static final Integer[] DEFAULT_PRODUCTION_PREDICTION_HOURLY = { + /* 00:00-12:00 */ + 0, 0, 0, 0, 0, 0, 0, 74, 297, 610, 913, 1399, + /* 13:00-24:00 */ + 1838, 2261, 2662, 3052, 1520, 1250, 910, 0, 0, 0, 0, 0, // + }; + + private static final Integer[] DEFAULT_CONSUMPTION_PREDICTION_HOURLY = { + /* 00:00-12:00 */ + 1021, 1208, 713, 931, 2847, 2551, 1558, 1234, 433, 633, 1355, 606, // + /* 13:00-24:00 */ + 430, 1432, 1121, 502, 294, 1048, 1194, 914, 1534, 1226, 1235, 977, // + }; + + private static final Integer[] DEFAULT_CONSUMPTION_PREDICTION_QUARTERLY = { + + /* 00:00-03:450 */ + 1021, 1208, 713, 931, 2847, 2551, 1558, 1234, 433, 633, 1355, 606, 430, 1432, 1121, 502, // + /* 04:00-07:45 */ + 294, 1048, 1194, 914, 1534, 1226, 1235, 977, 578, 1253, 1983, 1417, 513, 929, 1102, 445, // + /* 08:00-11:45 */ + 1208, 2791, 2729, 2609, 2086, 1454, 848, 816, 2610, 3150, 2036, 1180, 359, 1316, 3447, 2104, // + /* 12:00-15:45 */ + 905, 802, 828, 812, 863, 633, 293, 379, 1250, 2296, 2436, 2140, 2135, 1196, 2230, 1725, + /* 16:00-19:45 */ + 2365, 1758, 2325, 2264, 2181, 2167, 2228, 1082, 777, 417, 798, 1268, 409, 830, 1191, 417, // + /* 20:00-23:45 */ + 1087, 2958, 2946, 2235, 1343, 483, 796, 1201, 567, 395, 989, 1066, 370, 989, 1255, 660, // + /* 00:00-03:45 */ + 349, 880, 1186, 580, 327, 911, 1135, 553, 265, 938, 1165, 567, 278, 863, 1239, 658, // + /* 04:00-07:45 */ + 236, 816, 1173, 1131, 498, 550, 1344, 1226, 874, 504, 1733, 1809, 1576, 369, 771, 2583, // + /* 08:00-11:45 */ + 3202, 2174, 1878, 2132, 2109, 1895, 1565, 1477, 1613, 1716, 1867, 1726, 1700, 1787, 1755, 1734, // + /* 12:00-15:45 */ + 1380, 691, 338, 168, 199, 448, 662, 205, 183, 70, 169, 276, 149, 76, 195, 168, // + /* 16:00-19:45 */ + 159, 266, 135, 120, 224, 979, 2965, 1337, 1116, 795, 334, 390, 433, 369, 762, 2908, // + /* 20:00-23:45 */ + 3226, 2358, 1778, 1002, 455, 654, 534, 1587, 1638, 459, 330, 258, 368, 728, 1096, 878 // + }; + + private static final Float[] DEFAULT_HOURLY_PRICES = { 158.95f, 160.98f, 171.95f, 174.96f, // + 161.93f, 152f, 120.01f, 111.03f, // + 105.04f, 105f, 74.23f, 73.28f, // + 67.97f, 72.53f, 89.66f, 150.01f, // + 173.54f, 178.4f, 158.91f, 140.01f, // + 149.99f, 157.43f, 130.9f, 120.14f // + }; + + private static final Float[] DEFAULT_HOURLY_PRICES_SUMMER = { 70.95f, 71.98f, 71.95f, 74.96f, // + 78.93f, 80f, 84.01f, 111.03f, // + 105.04f, 105f, 74.23f, 73.28f, // + 67.97f, 72.53f, 89.66f, 150.01f, // + 173.54f, 178.4f, 158.91f, 140.01f, // + 149.99f, 157.43f, 130.9f, 120.14f // + }; + + private static final Integer[] STATES = { 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, // + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // + 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, // + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, // + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 // + }; + + private static final Float[] DEFAULT_PAST_HOURLY_PRICES = { 158.95f, 160.98f, 171.95f, 174.96f, // + 161.93f, 152f, 120.01f, 111.03f, // + 105.04f, 105f, 74.23f, 73.28f, // + }; + + private static final Integer[] PAST_STATES = { 1, 1, 1, 1, // + 1, 3, 3, 1, // + 2, 1, 2, 2, // + }; + + // Predictions + final DummyPrediction24Hours productionPredictionQuarterly = new DummyPrediction24Hours( + DEFAULT_PRODUCTION_PREDICTION_QUARTERLY); + final DummyPrediction24Hours consumptionPredictionQuarterly = new DummyPrediction24Hours( + DEFAULT_CONSUMPTION_PREDICTION_QUARTERLY); + final DummyPrediction24Hours productionPredictionHourly = new DummyPrediction24Hours( + DEFAULT_PRODUCTION_PREDICTION_HOURLY); + final DummyPrediction24Hours consumptionPredictionHourly = new DummyPrediction24Hours( + DEFAULT_CONSUMPTION_PREDICTION_HOURLY); + + @Test + public void scheduleChargeForEveryQuarter() throws Exception { + + final var clock = new TimeLeapClock(Instant.parse("2022-01-01T00:00:00.00Z"), ZoneOffset.UTC); + final var cm = new DummyComponentManager(clock); + + // Predictors + final var productionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, this.productionPredictionQuarterly, + "_sum/ProductionActivePower"); + final var consumptionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, + this.consumptionPredictionQuarterly, "_sum/ConsumptionActivePower"); + + // Predictor Manager + final var predictorManager = new DummyPredictorManager(productionPredictor, consumptionPredictor); + + // Price provider + final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.quarterlyPrices(ZonedDateTime.now(clock), + DEFAULT_HOURLY_PRICES); + + new ControllerTest(new TimeOfUseTariffControllerImpl()) // + .addReference("predictorManager", predictorManager) // + .addReference("componentManager", cm) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("timeOfUseTariff", timeOfUseTariffProvider) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMode(Mode.AUTOMATIC) // + .setControlMode(ControlMode.CHARGE_CONSUMPTION) // + .setRiskLevel(RiskLevel.HIGH) // + .setMaxPower(4000) // + .build()) + .next(new TestCase("Cycle - 1") // + .input(MAX_APPARENT_POWER, 9000) // + .input(ESS_CAPACITY, 12000) // + .input(ESS_SOC, 100)) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMode(Mode.OFF) // + .setControlMode(ControlMode.CHARGE_CONSUMPTION) // + .setRiskLevel(RiskLevel.HIGH) // + .setMaxPower(4000) // + .build()) + .next(new TestCase("Cycle - 2") // + .output(STATE_MACHINE, StateMachine.ALLOWS_DISCHARGE)) // + ; + } + + @Test + public void scheduleChargeForEveryHour() throws Exception { + + final var clock = new TimeLeapClock(Instant.parse("2022-01-01T00:00:00.00Z"), ZoneOffset.UTC); + final var cm = new DummyComponentManager(clock); + + // Predictors + final var productionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, this.productionPredictionHourly, + "_sum/ProductionActivePower"); + final var consumptionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, this.consumptionPredictionHourly, + "_sum/ConsumptionActivePower"); + + // PredictorManager + final var predictorManager = new DummyPredictorManager(productionPredictor, consumptionPredictor); + + // Price provider + final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.hourlyPrices(ZonedDateTime.now(clock), + DEFAULT_HOURLY_PRICES_SUMMER); + + new ControllerTest(new TimeOfUseTariffControllerImpl()) // + .addReference("predictorManager", predictorManager) // + .addReference("componentManager", cm) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("timeOfUseTariff", timeOfUseTariffProvider) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMode(Mode.AUTOMATIC) // + .setControlMode(ControlMode.CHARGE_CONSUMPTION) // + .setRiskLevel(RiskLevel.HIGH) // + .setMaxPower(6000) // + .build()) + .next(new TestCase("Cycle - 1") // + .input(MAX_APPARENT_POWER, 9000) // + .input(ESS_CAPACITY, 12000) // + .input(ESS_SOC, 50) // + ); + } + + @Test + public void scheduleDelayDischargeForEveryHour() throws Exception { + + final var clock = new TimeLeapClock(Instant.parse("2022-01-01T00:00:00.00Z"), ZoneOffset.UTC); + final var cm = new DummyComponentManager(clock); + + // Predictors + final var productionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, this.productionPredictionHourly, + "_sum/ProductionActivePower"); + final var consumptionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, this.consumptionPredictionHourly, + "_sum/ConsumptionActivePower"); + + // PredictorManager + final var predictorManager = new DummyPredictorManager(productionPredictor, consumptionPredictor); + + // Price provider + final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.hourlyPrices(ZonedDateTime.now(clock), + DEFAULT_HOURLY_PRICES_SUMMER); + + new ControllerTest(new TimeOfUseTariffControllerImpl()) // + .addReference("predictorManager", predictorManager) // + .addReference("componentManager", cm) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("timeOfUseTariff", timeOfUseTariffProvider) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMode(Mode.AUTOMATIC) // + .setControlMode(ControlMode.DELAY_DISCHARGE) // + .setRiskLevel(RiskLevel.HIGH) // + .build()) + .next(new TestCase("Cycle - 1") // + .input(MAX_APPARENT_POWER, 9000) // + .input(ESS_CAPACITY, 12000) // + .input(ESS_SOC, 50) // + ); + } + + @Test + public void scheduleDelayDischargeForEveryQuarter() throws Exception { + + final var clock = new TimeLeapClock(Instant.parse("2022-01-01T00:00:00.00Z"), ZoneOffset.UTC); + final var cm = new DummyComponentManager(clock); + + // Predictors + final var productionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, this.productionPredictionQuarterly, + "_sum/ProductionActivePower"); + final var consumptionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, cm, + this.consumptionPredictionQuarterly, "_sum/ConsumptionActivePower"); + + // PredictorManager + final var predictorManager = new DummyPredictorManager(productionPredictor, consumptionPredictor); + + // Price provider + final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.quarterlyPrices(ZonedDateTime.now(clock), + DEFAULT_HOURLY_PRICES); + + new ControllerTest(new TimeOfUseTariffControllerImpl()) // + .addReference("predictorManager", predictorManager) // + .addReference("componentManager", cm) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("ess", new DummyManagedSymmetricEss(ESS_ID)) // + .addReference("timeOfUseTariff", timeOfUseTariffProvider) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setEssId(ESS_ID) // + .setMode(Mode.AUTOMATIC) // + .setRiskLevel(RiskLevel.HIGH) // + .setControlMode(ControlMode.DELAY_DISCHARGE).build()) + .next(new TestCase("Cycle - 1") // + .input(MAX_APPARENT_POWER, 9000) // + .input(ESS_CAPACITY, 12000) // + .input(ESS_SOC, 50) // + ); + } + + @Test + public void scheduleTest() { + + final var essUsableEnergy = 12000; + final var currentAvailableEnergy = 6000; + final var dischargeEnergy = 2250; + final var chargeEnergy = -2250; + var allowedChargeEnergyFromGrid = 0; + + var schedule = new Schedule(ControlMode.DELAY_DISCHARGE, // + RiskLevel.HIGH, // + essUsableEnergy, // + currentAvailableEnergy, // + dischargeEnergy, // + chargeEnergy, // + DEFAULT_HOURLY_PRICES_SUMMER, // + DEFAULT_CONSUMPTION_PREDICTION_HOURLY, // + DEFAULT_PRODUCTION_PREDICTION_HOURLY, // + allowedChargeEnergyFromGrid); + + schedule.createSchedule(); + + var expectedBatteryValues = Arrays.asList(0, 0, 0, 0, // + 873, 2250, 1558, 1160, // + 136, 23, 0, -793, // + -1408, -829, -1541, -2250, // + -1226, -202, 284, 914, // + 1534, 1226, 1235, 977); + + var calculatedBatteryValues = schedule.periods.stream().map(t -> { + return t.chargeDischargeEnergy; + }).collect(Collectors.toList()); + + assertTrue(expectedBatteryValues.equals(calculatedBatteryValues)); + + allowedChargeEnergyFromGrid = 1500; + + schedule = new Schedule(ControlMode.CHARGE_CONSUMPTION, // + RiskLevel.HIGH, // + essUsableEnergy, // + currentAvailableEnergy, // + dischargeEnergy, // + chargeEnergy, // + DEFAULT_HOURLY_PRICES_SUMMER, // + DEFAULT_CONSUMPTION_PREDICTION_HOURLY, // + DEFAULT_PRODUCTION_PREDICTION_HOURLY, // + allowedChargeEnergyFromGrid); + + schedule.createSchedule(); + + expectedBatteryValues = Arrays.asList(-479, -292, -787, -569, // + 2250, 2250, 1558, 1160, // + 136, 23, 442, -793, // + -1408, -829, -1541, -2250, // + -1226, -202, 284, 914, // + 1534, 1226, 1235, 977); + + calculatedBatteryValues = schedule.periods.stream().map(t -> { + return t.chargeDischargeEnergy; + }).collect(Collectors.toList()); + + assertTrue(expectedBatteryValues.equals(calculatedBatteryValues)); + } + + @Test + public void createScheduleTest() { + final var clock = new TimeLeapClock(Instant.parse("2022-01-01T00:00:00.00Z"), ZoneOffset.UTC); + final var timestamp = TimeOfUseTariffUtils.getNowRoundedDownToMinutes(ZonedDateTime.now(), 15).minusHours(3); + + var states = new JsonArray(); + var prices = new JsonArray(); + + // Price provider + final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.quarterlyPrices(ZonedDateTime.now(clock), + DEFAULT_HOURLY_PRICES_SUMMER); + + for (Float price : timeOfUseTariffProvider.getPrices().getValues()) { + prices.add(price); + } + + for (Integer state : STATES) { + states.add(state); + } + + final var result = ScheduleUtils.createSchedule(prices, states, timestamp); + + // Check if the result is same size as prices. + assertEquals(prices.size(), result.size()); + + var expectedLastTimestamp = timestamp.plusDays(1).minusMinutes(15).format(DateTimeFormatter.ISO_INSTANT) + .toString(); + var generatedLastTimestamp = result.get(95).getAsJsonObject().get("timestamp").getAsString(); + + // Check if the last timestamp is as expected. + assertEquals(expectedLastTimestamp, generatedLastTimestamp); + + } + + @Test + public void handleGetScheduleRequestTest() { + final var clock = new TimeLeapClock(Instant.parse("2022-01-01T00:00:00.00Z"), ZoneOffset.UTC); + + final var timestamp = TimeOfUseTariffUtils.getNowRoundedDownToMinutes(ZonedDateTime.now(), 15).minusHours(3); + final var channeladdressPrices = new ChannelAddress("", "QuarterlyPrices"); + final var channeladdressStateMachine = new ChannelAddress("", "StateMachine"); + + SortedMap> dummyQueryResult = new TreeMap<>(); + + for (int i = 0; i < 12; i++) { + SortedMap dummyChannelValues = new TreeMap<>(); + dummyChannelValues.put(channeladdressPrices, new JsonPrimitive(DEFAULT_PAST_HOURLY_PRICES[i])); + dummyChannelValues.put(channeladdressStateMachine, new JsonPrimitive(PAST_STATES[i])); + + dummyQueryResult.put(timestamp.plusMinutes(i * 15), dummyChannelValues); + } + + // Price provider + final var timeOfUseTariffProvider = DummyTimeOfUseTariffProvider.quarterlyPrices(ZonedDateTime.now(clock), + DEFAULT_HOURLY_PRICES); + final var controlMode = ControlMode.CHARGE_CONSUMPTION; + final var schedule = new Schedule(controlMode, RiskLevel.HIGH, 12000, 12000, 2250, -2250, + timeOfUseTariffProvider.getPrices().getValues(), DEFAULT_CONSUMPTION_PREDICTION_QUARTERLY, + DEFAULT_PRODUCTION_PREDICTION_QUARTERLY, 1000); + + schedule.createSchedule(); + + final var result = ScheduleUtils.handleGetScheduleRequest(schedule, controlMode, null, dummyQueryResult, + channeladdressPrices, channeladdressStateMachine); + + JsonUtils.prettyPrint(result.getResult()); + + final var scheduleArray = result.getResult().get("schedule").getAsJsonArray(); + + // Check if the logic generates 96 values. + assertEquals(96, scheduleArray.size()); + + // Check if first value of last three hour data present in schedule. + assertTrue(scheduleArray.get(0).getAsJsonObject().get("price").getAsDouble() == 158.95f); + + // Check if last value of 96 hourly prices array is avoided, since the logic + // limits to 96 values. + assertTrue(scheduleArray.get(95).getAsJsonObject().get("price").getAsDouble() != 120.14f); + + } +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java index a459419a67a..75b97eee9ff 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/AwattarHourly.java @@ -1,6 +1,7 @@ package io.openems.edge.app.timeofusetariff; -import java.util.EnumMap; +import java.util.Map; +import java.util.function.Function; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -13,13 +14,13 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.function.ThrowingTriFunction; import io.openems.common.session.Language; -import io.openems.common.types.EdgeConfig; -import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.CommonProps; import io.openems.edge.app.timeofusetariff.AwattarHourly.Property; import io.openems.edge.common.component.ComponentManager; -import io.openems.edge.core.appmanager.AbstractEnumOpenemsApp; -import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; import io.openems.edge.core.appmanager.AppDescriptor; import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; @@ -27,6 +28,8 @@ import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; /** * Describes a App for AwattarHourly. @@ -38,8 +41,9 @@ "instanceId": UUID, "image": base64, "properties":{ - "CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID": "ctrlEssTimeOfUseTariffDischarge0", - "TIME_OF_USE_TARIF_ID": "timeOfUseTariff0" + "CTRL_ESS_TIME_OF_USE_TARIFF_ID": "ctrlEssTimeOfUseTariff0", + "TIME_OF_USE_TARIFF_PROVIDER_ID": "timeOfUseTariff0", + "CONTROL_MODE": {@link ControlMode} }, "appDescriptor": { "websiteUrl": {@link AppDescriptor#getWebsiteUrl()} @@ -48,15 +52,37 @@ * */ @org.osgi.service.component.annotations.Component(name = "App.TimeOfUseTariff.Awattar") -public class AwattarHourly extends AbstractEnumOpenemsApp implements OpenemsApp { +public class AwattarHourly extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public static enum Property implements Type, Nameable { + // Components + CTRL_ESS_TIME_OF_USE_TARIFF_ID(AppDef.componentId("ctrlEssTimeOfUseTariff0")), // + TIME_OF_USE_TARIFF_PROVIDER_ID(AppDef.componentId("timeOfUseTariff0")), // - public static enum Property implements Nameable { - // Component-IDs - CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID, // - TIME_OF_USE_TARIF_ID, // // Properties - ALIAS, // - ; + ALIAS(CommonProps.alias()); + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Property self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Type.Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } } @Activate @@ -66,34 +92,18 @@ public AwattarHourly(@Reference ComponentManager componentManager, ComponentCont } @Override - protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { return (t, p, l) -> { + final var timeOfUseTariffProviderId = this.getId(t, p, Property.TIME_OF_USE_TARIFF_PROVIDER_ID); + final var ctrlEssTimeOfUseTariffId = this.getId(t, p, Property.CTRL_ESS_TIME_OF_USE_TARIFF_ID); - final var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); - - final var ctrlEssTimeOfUseTariffDischargeId = this.getId(t, p, - Property.CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID, "ctrlEssTimeOfUseTariffDischarge0"); - final var timeOfUseTariffId = this.getId(t, p, Property.TIME_OF_USE_TARIF_ID, "timeOfUseTariff0"); - - // TODO ess id may be changed - var components = Lists.newArrayList(// - new EdgeConfig.Component(ctrlEssTimeOfUseTariffDischargeId, alias, - "Controller.Ess.Time-Of-Use-Tariff.Discharge", JsonUtils.buildJsonObject() // - .addProperty("ess.id", "ess0") // - .build()), // - new EdgeConfig.Component(timeOfUseTariffId, this.getName(l), "TimeOfUseTariff.Awattar", - JsonUtils.buildJsonObject() // - .build())// - ); - return new AppConfiguration(components, - Lists.newArrayList(ctrlEssTimeOfUseTariffDischargeId, "ctrlBalancing0")); - }; - } + final var alias = this.getString(p, l, Property.ALIAS); - @Override - public AppAssistant getAppAssistant(Language language) { - return AppAssistant.create(this.getName(language)) // - .build(); + var components = TimeOfUseProps.getComponents(t, ctrlEssTimeOfUseTariffId, alias, "TimeOfUseTariff.Awattar", + this.getName(l), timeOfUseTariffProviderId, null); + + return new AppConfiguration(components, Lists.newArrayList(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")); + }; } @Override @@ -108,8 +118,8 @@ public OpenemsAppCategory[] getCategories() { } @Override - protected Class getPropertyClass() { - return Property.class; + protected Property[] propertyValues() { + return Property.values(); } @Override @@ -117,4 +127,9 @@ public OpenemsAppCardinality getCardinality() { return OpenemsAppCardinality.SINGLE_IN_CATEGORY; } + @Override + protected AwattarHourly getApp() { + return this; + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java index b7a913fb0ee..3244ea30e6e 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java @@ -1,6 +1,7 @@ package io.openems.edge.app.timeofusetariff; -import java.util.EnumMap; +import java.util.Map; +import java.util.function.Function; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -13,15 +14,13 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.function.ThrowingTriFunction; import io.openems.common.session.Language; -import io.openems.common.types.EdgeConfig; -import io.openems.common.utils.EnumUtils; -import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.CommonProps; import io.openems.edge.app.timeofusetariff.StromdaoCorrently.Property; import io.openems.edge.common.component.ComponentManager; -import io.openems.edge.core.appmanager.AbstractEnumOpenemsApp; import io.openems.edge.core.appmanager.AbstractOpenemsApp; -import io.openems.edge.core.appmanager.AppAssistant; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; import io.openems.edge.core.appmanager.AppDescriptor; import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; @@ -29,8 +28,9 @@ import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; import io.openems.edge.core.appmanager.OpenemsAppCategory; -import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; +import io.openems.edge.core.appmanager.formly.enums.InputType; /** * Describes a App for StromdaoCorrently. @@ -42,9 +42,10 @@ "instanceId": UUID, "image": base64, "properties":{ - "CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID": "ctrlEssTimeOfUseTariffDischarge0", - "TIME_OF_USE_TARIF_ID": "timeOfUseTariff0", - "ZIP_CODE": "12345678" + "CTRL_ESS_TIME_OF_USE_TARIFF_ID": "ctrlEssTimeOfUseTariff0", + "TIME_OF_USE_TARIFF_PROVIDER_ID": "timeOfUseTariff0", + "ZIP_CODE": "12345678", + "CONTROL_MODE": {@link ControlMode} }, "appDescriptor": { "websiteUrl": {@link AppDescriptor#getWebsiteUrl()} @@ -53,16 +54,44 @@ * */ @org.osgi.service.component.annotations.Component(name = "App.TimeOfUseTariff.Stromdao") -public class StromdaoCorrently extends AbstractEnumOpenemsApp implements OpenemsApp { +public class StromdaoCorrently extends + AbstractOpenemsAppWithProps implements OpenemsApp { - public static enum Property implements Nameable { + public static enum Property implements Type, Nameable { // Component-IDs - CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID, // - TIME_OF_USE_TARIF_ID, // + CTRL_ESS_TIME_OF_USE_TARIFF_ID(AppDef.componentId("ctrlEssTimeOfUseTariff0")), // + TIME_OF_USE_TARIFF_PROVIDER_ID(AppDef.componentId("timeOfUseTariff0")), // + // Properties - ALIAS, // - ZIP_CODE // - ; + ALIAS(CommonProps.alias()), // + ZIP_CODE(AppDef.of(StromdaoCorrently.class)// + .setTranslatedLabelWithAppPrefix(".zipCode.label") // + .setTranslatedDescriptionWithAppPrefix(".zipCode.description") // + .setField(JsonFormlyUtil::buildInput, (app, prop, l, params, f) -> // + f.setInputType(InputType.NUMBER) // + .isRequired(true))); + + private final AppDef def; + + private Property( + AppDef def) { + this.def = def; + } + + @Override + public Property self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, Type.Parameter.BundleParameter> getParamter() { + return Type.Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } } @Activate @@ -72,43 +101,19 @@ public StromdaoCorrently(@Reference ComponentManager componentManager, Component } @Override - protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { return (t, p, l) -> { - final var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); - final var zipCode = EnumUtils.getAsString(p, Property.ZIP_CODE); - - final var ctrlEssTimeOfUseTariffDischargeId = this.getId(t, p, - Property.CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID, "ctrlEssTimeOfUseTariffDischarge0"); - final var timeOfUseTariffId = this.getId(t, p, Property.TIME_OF_USE_TARIF_ID, "timeOfUseTariff0"); - - // TODO ess id may be changed - var comp = Lists.newArrayList(// - new EdgeConfig.Component(ctrlEssTimeOfUseTariffDischargeId, alias, - "Controller.Ess.Time-Of-Use-Tariff.Discharge", JsonUtils.buildJsonObject() // - .addProperty("ess.id", "ess0") // - .build()), // - new EdgeConfig.Component(timeOfUseTariffId, this.getName(l), "TimeOfUseTariff.Corrently", - JsonUtils.buildJsonObject() // - .addProperty("zipcode", zipCode) // - .build())// - ); - return new AppConfiguration(comp, Lists.newArrayList(ctrlEssTimeOfUseTariffDischargeId, "ctrlBalancing0")); - }; - } + final var ctrlEssTimeOfUseTariffId = this.getId(t, p, Property.CTRL_ESS_TIME_OF_USE_TARIFF_ID); + final var timeOfUseTariffProviderId = this.getId(t, p, Property.TIME_OF_USE_TARIFF_PROVIDER_ID); - @Override - public AppAssistant getAppAssistant(Language language) { - var bundle = AbstractOpenemsApp.getTranslationBundle(language); - return AppAssistant.create(this.getName(language)) // - .fields(JsonUtils.buildJsonArray() // - .add(JsonFormlyUtil.buildInput(Property.ZIP_CODE) // - .setLabel(TranslationUtil.getTranslation(bundle, this.getAppId() + ".zipCode.label")) // - .setDescription(TranslationUtil.getTranslation(bundle, - this.getAppId() + ".zipCode.description")) // - .isRequired(true) // - .build()) // - .build()) // - .build(); + final var alias = this.getString(p, l, Property.ALIAS); + final var zipCode = this.getString(p, l, Property.ZIP_CODE); + + var comp = TimeOfUseProps.getComponents(t, ctrlEssTimeOfUseTariffId, alias, "TimeOfUseTariff.Corrently", + this.getName(l), timeOfUseTariffProviderId, b -> b.addPropertyIfNotNull("zipcode", zipCode)); + + return new AppConfiguration(comp, Lists.newArrayList(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")); + }; } @Override @@ -123,8 +128,8 @@ public OpenemsAppCategory[] getCategories() { } @Override - protected Class getPropertyClass() { - return Property.class; + protected Property[] propertyValues() { + return Property.values(); } @Override @@ -132,4 +137,9 @@ public OpenemsAppCardinality getCardinality() { return OpenemsAppCardinality.SINGLE_IN_CATEGORY; } + @Override + protected StromdaoCorrently getApp() { + return this; + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java index 7c42d58de16..f0944c4561b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java @@ -17,8 +17,7 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.function.ThrowingTriFunction; import io.openems.common.session.Language; -import io.openems.common.types.EdgeConfig; -import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.CommonProps; import io.openems.edge.app.timeofusetariff.Tibber.Property; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.core.appmanager.AbstractOpenemsApp; @@ -45,9 +44,10 @@ "instanceId": UUID, "image": base64, "properties":{ - "CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID": "ctrlEssTimeOfUseTariffDischarge0", - "TIME_OF_USE_TARIF_ID": "timeOfUseTariff0", - "ACCESS_TOKEN": {token} + "CTRL_ESS_TIME_OF_USE_TARIFF_ID": "ctrlEssTimeOfUseTariff0", + "TIME_OF_USE_TARIFF_PROVIDER_ID": "timeOfUseTariff0", + "ACCESS_TOKEN": {token}, + "CONTROL_MODE": {@link ControlMode} }, "appDescriptor": { "websiteUrl": {@link AppDescriptor#getWebsiteUrl()} @@ -61,26 +61,22 @@ public class Tibber extends AbstractOpenemsAppWithProps, Nameable { // Components - CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID(AppDef.of(Tibber.class) // - .setDefaultValue("ctrlEssTimeOfUseTariffDischarge0")), // - TIME_OF_USE_TARIF_ID(AppDef.of(Tibber.class) // - .setDefaultValue("timeOfUseTariff0")), // + CTRL_ESS_TIME_OF_USE_TARIFF_ID(AppDef.componentId("ctrlEssTimeOfUseTariff0")), // + TIME_OF_USE_TARIFF_PROVIDER_ID(AppDef.componentId("timeOfUseTariff0")), // // Properties - ALIAS(AppDef.of(Tibber.class) // - .setDefaultValueToAppName()), + ALIAS(CommonProps.alias()), // ACCESS_TOKEN(AppDef.of(Tibber.class) // .setTranslatedLabelWithAppPrefix(".accessToken.label") // .setTranslatedDescriptionWithAppPrefix(".accessToken.description") // .setField(JsonFormlyUtil::buildInput, (app, prop, l, params, f) -> // f.setInputType(PASSWORD) // .isRequired(true)) // - .setAllowedToSave(false)), // - ; + .setAllowedToSave(false)); - private final AppDef def; + private final AppDef def; - private Property(AppDef def) { + private Property(AppDef def) { this.def = def; } @@ -90,7 +86,7 @@ public Property self() { } @Override - public AppDef def() { + public AppDef def() { return this.def; } @@ -110,30 +106,21 @@ public Tibber(@Reference ComponentManager componentManager, ComponentContext con @Override protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { return (t, p, l) -> { - final var alias = this.getValueOrDefault(p, Property.ALIAS, this.getName(l)); - final var accessToken = this.getValueOrDefault(p, Property.ACCESS_TOKEN, null); + final var timeOfUseTariffProviderId = this.getId(t, p, Property.TIME_OF_USE_TARIFF_PROVIDER_ID); + final var ctrlEssTimeOfUseTariffId = this.getId(t, p, Property.CTRL_ESS_TIME_OF_USE_TARIFF_ID); - final var ctrlEssTimeOfUseTariffDischargeId = this.getId(t, p, - Property.CTRL_ESS_TIME_OF_USE_TARIF_DISCHARGE_ID, "ctrlEssTimeOfUseTariffDischarge0"); - final var timeOfUseTariffId = this.getId(t, p, Property.TIME_OF_USE_TARIF_ID, "timeOfUseTariff0"); + final var alias = this.getString(p, l, Property.ALIAS); + final var accessToken = this.getString(p, l, Property.ACCESS_TOKEN); if (t == ConfigurationTarget.ADD && (accessToken == null || accessToken.isBlank())) { throw new OpenemsException("Access Token is required!"); } - // TODO ess id may be changed - var comp = Lists.newArrayList(// - new EdgeConfig.Component(ctrlEssTimeOfUseTariffDischargeId, alias, - "Controller.Ess.Time-Of-Use-Tariff.Discharge", JsonUtils.buildJsonObject() // - .addProperty("ess.id", "ess0") // - .build()), // - new EdgeConfig.Component(timeOfUseTariffId, this.getName(l), "TimeOfUseTariff.Tibber", - JsonUtils.buildJsonObject() // - .addPropertyIfNotNull("accessToken", accessToken) // - .build())// - ); - - return new AppConfiguration(comp, Lists.newArrayList(ctrlEssTimeOfUseTariffDischargeId, "ctrlBalancing0")); + var comp = TimeOfUseProps.getComponents(t, ctrlEssTimeOfUseTariffId, alias, "TimeOfUseTariff.Tibber", + this.getName(l), timeOfUseTariffProviderId, + b -> b.addPropertyIfNotNull("accessToken", accessToken)); + + return new AppConfiguration(comp, Lists.newArrayList(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/TimeOfUseProps.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/TimeOfUseProps.java new file mode 100644 index 00000000000..55ad9a46287 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/TimeOfUseProps.java @@ -0,0 +1,55 @@ +package io.openems.edge.app.timeofusetariff; + +import java.util.ArrayList; +import java.util.function.Consumer; + +import com.google.common.collect.Lists; + +import io.openems.common.types.EdgeConfig; +import io.openems.common.types.EdgeConfig.Component; +import io.openems.common.utils.JsonUtils; +import io.openems.common.utils.JsonUtils.JsonObjectBuilder; +import io.openems.edge.core.appmanager.ConfigurationTarget; + +public final class TimeOfUseProps { + + private TimeOfUseProps() { + } + + /** + * Creates the commonly used components for a Time-of-Use. + * + * @param target the {@link ConfigurationTarget} + * @param ctrlEssTimeOfUseTariffId The id of the ToU controller. + * @param controllerAlias the alias of the ToU controller. + * @param providerFactoryId the factoryId of the ToU provider. + * @param providerAlias the alias of the ToU provider. + * @param timeOfUseTariffProviderId the id of the ToU provider. + * @param additionalProperties Consumer for additional configuration of the + * provider. + * @return the components. + */ + public static final ArrayList getComponents(// + final ConfigurationTarget target, // + final String ctrlEssTimeOfUseTariffId, // + final String controllerAlias, // + final String providerFactoryId, // + final String providerAlias, // + final String timeOfUseTariffProviderId, // + final Consumer additionalProperties // + ) { + final var controllerProperties = JsonUtils.buildJsonObject() // + .addProperty("ess.id", "ess0")// + .onlyIf(target == ConfigurationTarget.ADD, b -> b.addProperty("controlMode", "DELAY_DISCHARGE")); + + var providerProperties = JsonUtils.buildJsonObject().onlyIf(additionalProperties != null, additionalProperties); + + return Lists.newArrayList(// + new EdgeConfig.Component(ctrlEssTimeOfUseTariffId, controllerAlias, "Controller.Ess.Time-Of-Use-Tariff", + controllerProperties.build()), // + new EdgeConfig.Component(timeOfUseTariffProviderId, providerAlias, providerFactoryId, + providerProperties.build())// + ); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties index 65f72b19d2c..031ea432cd1 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties @@ -291,7 +291,7 @@ App.PvInverter.Sma.phase.description = Angeschlossene Phase(n) des Wechselrichte App.PvInverter.SolarEdge.Name = SolarEdge PV-Wechselrichter App.PvInverter.SolarEdge.Name.short = SolarEdge -# Time of use Tarif +# Time of use Tariff App.TimeOfUseTariff.Awattar.Name = Awattar HOURLY App.TimeOfUseTariff.Awattar.Name.short = Awattar HOURLY App.TimeOfUseTariff.Stromdao.Name = Stromdao Corrently diff --git a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/DummyTimeOfUseTariffProvider.java b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/DummyTimeOfUseTariffProvider.java index 1e8e04ab2fd..cf75eddebae 100644 --- a/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/DummyTimeOfUseTariffProvider.java +++ b/io.openems.edge.timeofusetariff.api/src/io/openems/edge/timeofusetariff/test/DummyTimeOfUseTariffProvider.java @@ -9,6 +9,17 @@ public class DummyTimeOfUseTariffProvider implements TimeOfUseTariff { private TimeOfUsePrices prices; + /** + * Builds a {@link DummyTimeOfUseTariffProvider} from hourly prices. + * + * @param hourlyPrices an array of 24 hourly prices. + * @param now {@ZonedDateTime} given during test. + * @return a {@link DummyTimeOfUseTariffProvider}. + */ + public static DummyTimeOfUseTariffProvider hourlyPrices(ZonedDateTime now, Float... hourlyPrices) { + return new DummyTimeOfUseTariffProvider(now, hourlyPrices); + } + /** * Builds a {@link DummyTimeOfUseTariffProvider} from hourly prices. * @@ -16,15 +27,15 @@ public class DummyTimeOfUseTariffProvider implements TimeOfUseTariff { * @param now {@ZonedDateTime} given during test. * @return a {@link DummyTimeOfUseTariffProvider}. */ - public static DummyTimeOfUseTariffProvider fromHourlyPrices(ZonedDateTime now, Float... hourlyPrices) { + public static DummyTimeOfUseTariffProvider quarterlyPrices(ZonedDateTime now, Float... hourlyPrices) { var quarterlyPrices = new Float[96]; for (var i = 0; i < 24; i++) { - quarterlyPrices[i] = hourlyPrices[i]; - quarterlyPrices[i + 1] = hourlyPrices[i]; - quarterlyPrices[i + 2] = hourlyPrices[i]; - quarterlyPrices[i + 3] = hourlyPrices[i]; + quarterlyPrices[(i * 4)] = hourlyPrices[i]; + quarterlyPrices[(i * 4) + 1] = hourlyPrices[i]; + quarterlyPrices[(i * 4) + 2] = hourlyPrices[i]; + quarterlyPrices[(i * 4) + 3] = hourlyPrices[i]; } return new DummyTimeOfUseTariffProvider(now, quarterlyPrices); diff --git a/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/TimeOfUseTariffAwattarImpl.java b/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/TimeOfUseTariffAwattarImpl.java index 26d57743758..859f0590c72 100644 --- a/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/TimeOfUseTariffAwattarImpl.java +++ b/io.openems.edge.timeofusetariff.awattar/src/io/openems/edge/timeofusetariff/awattar/TimeOfUseTariffAwattarImpl.java @@ -115,16 +115,13 @@ protected void deactivate() { this.channel(TimeOfUseTariffAwattar.ChannelId.HTTP_STATUS_CODE).setNextValue(httpStatusCode); /* - * Schedule next price update for 2 pm + * Schedule next price update every hour */ var now = ZonedDateTime.now(); - var nextRun = now.withHour(14).truncatedTo(ChronoUnit.HOURS); - if (now.isAfter(nextRun)) { - nextRun = nextRun.plusDays(1); - } - - var duration = Duration.between(now, nextRun); - var delay = duration.getSeconds(); + // We query every hour since Awattar gives the prices for only next 24 hours + // instead of 96. + var nextRun = now.plusHours(1).truncatedTo(ChronoUnit.HOURS); + var delay = Duration.between(now, nextRun).getSeconds(); this.executor.schedule(this.task, delay, TimeUnit.SECONDS); }; From 066d06c3e21cd843121b30984d34eea4a95b49fb Mon Sep 17 00:00:00 2001 From: Thomas Sicking <91258335+tsicking@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:43:24 +0200 Subject: [PATCH 14/27] ElectricityMeter: fix calculateSinglePhaseFromActivePower & calculatePhasesFromReactivePower (#2386) * Bugfix in SinglePhaseMeter.calculateSinglePhaseFromActivePower * Validate bugfix with JUnit test * Add more JUnit test + fix one more copy&paste error --------- Co-authored-by: Stefan Feilmeier --- .../openems/edge/common/test/TestUtils.java | 33 ++ .../edge/meter/api/ElectricityMeter.java | 2 +- .../edge/meter/api/SinglePhaseMeter.java | 2 +- .../test/AbstractDummyElectricityMeter.java | 282 ++++++++++++++++++ .../meter/test/DummyElectricityMeter.java | 16 +- .../DummySinglePhaseElectricityMeter.java | 47 +++ .../edge/meter/api/ElectricityMeterTest.java | 162 ++++++++++ .../edge/meter/api/SinglePhaseMeterTest.java | 52 ++++ 8 files changed, 583 insertions(+), 13 deletions(-) create mode 100644 io.openems.edge.meter.api/src/io/openems/edge/meter/test/AbstractDummyElectricityMeter.java create mode 100644 io.openems.edge.meter.api/src/io/openems/edge/meter/test/DummySinglePhaseElectricityMeter.java create mode 100644 io.openems.edge.meter.api/test/io/openems/edge/meter/api/ElectricityMeterTest.java create mode 100644 io.openems.edge.meter.api/test/io/openems/edge/meter/api/SinglePhaseMeterTest.java diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/TestUtils.java b/io.openems.edge.common/src/io/openems/edge/common/test/TestUtils.java index 3b2d13f09eb..bc7d7741109 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/test/TestUtils.java +++ b/io.openems.edge.common/src/io/openems/edge/common/test/TestUtils.java @@ -3,6 +3,10 @@ import java.io.IOException; import java.net.ServerSocket; +import io.openems.edge.common.channel.Channel; +import io.openems.edge.common.channel.ChannelId; +import io.openems.edge.common.component.OpenemsComponent; + public class TestUtils { private TestUtils() { @@ -23,4 +27,33 @@ public static int findRandomOpenPortOnAllLocalInterfaces() throws IOException { return socket.getLocalPort(); } } + + /** + * Calls {@link Channel#nextProcessImage()} for every Channel of the + * {@link OpenemsComponent}. + * + * @param component the {@link OpenemsComponent} + */ + public static void activateNextProcessImage(OpenemsComponent component) { + component.channels().forEach(channel -> { + channel.nextProcessImage(); + }); + } + + /** + * Sets the value on a Component Channel and activates the Process Image. + * + *

+ * This is useful to simulate a Channel value in a Unit test, as the value + * becomes directly available on the Channel. + * + * @param component the {@link OpenemsComponent} + * @param channelId the {@link ChannelId} + * @param value the new value + */ + public static void withValue(OpenemsComponent component, ChannelId channelId, Object value) { + var channel = component.channel(channelId); + channel.setNextValue(value); + channel.nextProcessImage(); + } } diff --git a/io.openems.edge.meter.api/src/io/openems/edge/meter/api/ElectricityMeter.java b/io.openems.edge.meter.api/src/io/openems/edge/meter/api/ElectricityMeter.java index 3df8317d8ae..bf3724b3165 100644 --- a/io.openems.edge.meter.api/src/io/openems/edge/meter/api/ElectricityMeter.java +++ b/io.openems.edge.meter.api/src/io/openems/edge/meter/api/ElectricityMeter.java @@ -1723,7 +1723,7 @@ public static void calculatePhasesFromReactivePower(ElectricityMeter meter) { var phase = TypeUtils.divide(value.get(), 3); meter.getReactivePowerL1Channel().setNextValue(phase); meter.getReactivePowerL2Channel().setNextValue(phase); - meter.getReactivePowerL2Channel().setNextValue(phase); + meter.getReactivePowerL3Channel().setNextValue(phase); }); } diff --git a/io.openems.edge.meter.api/src/io/openems/edge/meter/api/SinglePhaseMeter.java b/io.openems.edge.meter.api/src/io/openems/edge/meter/api/SinglePhaseMeter.java index abfed297f80..07ccff0bc23 100644 --- a/io.openems.edge.meter.api/src/io/openems/edge/meter/api/SinglePhaseMeter.java +++ b/io.openems.edge.meter.api/src/io/openems/edge/meter/api/SinglePhaseMeter.java @@ -72,7 +72,7 @@ public static void calculateSinglePhaseFromActi var phase = phaseProvider.apply(meter); meter.getActivePowerL1Channel().setNextValue(phase == SinglePhase.L1 ? value : null); meter.getActivePowerL2Channel().setNextValue(phase == SinglePhase.L2 ? value : null); - meter.getActivePowerL2Channel().setNextValue(phase == SinglePhase.L3 ? value : null); + meter.getActivePowerL3Channel().setNextValue(phase == SinglePhase.L3 ? value : null); }); } diff --git a/io.openems.edge.meter.api/src/io/openems/edge/meter/test/AbstractDummyElectricityMeter.java b/io.openems.edge.meter.api/src/io/openems/edge/meter/test/AbstractDummyElectricityMeter.java new file mode 100644 index 00000000000..2da646c4c54 --- /dev/null +++ b/io.openems.edge.meter.api/src/io/openems/edge/meter/test/AbstractDummyElectricityMeter.java @@ -0,0 +1,282 @@ +package io.openems.edge.meter.test; + +import io.openems.edge.common.channel.Channel; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.test.TestUtils; +import io.openems.edge.meter.api.ElectricityMeter; +import io.openems.edge.meter.api.MeterType; + +public abstract class AbstractDummyElectricityMeter> + extends AbstractOpenemsComponent implements ElectricityMeter { + + private MeterType meterType; + + protected AbstractDummyElectricityMeter(String id, + io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, + io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) { + super(firstInitialChannelIds, furtherInitialChannelIds); + for (Channel channel : this.channels()) { + channel.nextProcessImage(); + } + super.activate(null, id, "", true); + } + + protected abstract SELF self(); + + /** + * Set the {@link MeterType}. + * + * @param meterType the meterType + * @return myself + */ + public SELF withMeterType(MeterType meterType) { + this.meterType = meterType; + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#ACTIVE_POWER} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withActivePower(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.ACTIVE_POWER, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#ACTIVE_POWER_L1} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withActivePowerL1(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.ACTIVE_POWER_L1, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#ACTIVE_POWER_L2} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withActivePowerL2(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.ACTIVE_POWER_L2, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#ACTIVE_POWER_L3} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withActivePowerL3(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.ACTIVE_POWER_L3, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#REACTIVE_POWER} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withReactivePower(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.REACTIVE_POWER, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#REACTIVE_POWER_L1} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withReactivePowerL1(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.REACTIVE_POWER_L1, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#REACTIVE_POWER_L2} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withReactivePowerL2(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.REACTIVE_POWER_L2, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#REACTIVE_POWER_L3} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withReactivePowerL3(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.REACTIVE_POWER_L3, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#CURRENT} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withCurrent(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.CURRENT, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#CURRENT_L1} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withCurrentL1(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.CURRENT_L1, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#CURRENT_L2} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withCurrentL2(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.CURRENT_L2, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#CURRENT_L3} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withCurrentL3(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.CURRENT_L3, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#VOLTAGE} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withVoltage(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.VOLTAGE, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#VOLTAGE_L1} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withVoltageL1(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.VOLTAGE_L1, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#VOLTAGE_L2} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withVoltageL2(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.VOLTAGE_L2, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#VOLTAGE_L3} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withVoltageL3(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.VOLTAGE_L3, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#ACTIVE_PRODUCTION_ENERGY} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withActiveProductionEnergy(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#ACTIVE_PRODUCTION_ENERGY_L1} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withActiveProductionEnergyL1(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L1, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#ACTIVE_PRODUCTION_ENERGY_L2} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withActiveProductionEnergyL2(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L2, value); + return this.self(); + } + + /** + * Set {@link ElectricityMeter.ChannelId#ACTIVE_PRODUCTION_ENERGY_L3} of this + * {@link ElectricityMeter}. + * + * @param value the value + * @return myself + */ + public SELF withActiveProductionEnergyL3(Integer value) { + TestUtils.withValue(this, ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY_L3, value); + return this.self(); + } + + @Override + public MeterType getMeterType() { + return this.meterType; + } + +} diff --git a/io.openems.edge.meter.api/src/io/openems/edge/meter/test/DummyElectricityMeter.java b/io.openems.edge.meter.api/src/io/openems/edge/meter/test/DummyElectricityMeter.java index 42d4a9baccf..b5171e58752 100644 --- a/io.openems.edge.meter.api/src/io/openems/edge/meter/test/DummyElectricityMeter.java +++ b/io.openems.edge.meter.api/src/io/openems/edge/meter/test/DummyElectricityMeter.java @@ -1,33 +1,27 @@ package io.openems.edge.meter.test; -import io.openems.edge.common.channel.Channel; -import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.meter.api.ElectricityMeter; -import io.openems.edge.meter.api.MeterType; /** * Provides a simple, simulated ElectricityMeter component that can be used * together with the OpenEMS Component test framework. */ -public class DummyElectricityMeter extends AbstractOpenemsComponent implements ElectricityMeter { +public class DummyElectricityMeter extends AbstractDummyElectricityMeter + implements ElectricityMeter { public static final int MAX_APPARENT_POWER = 10000; public DummyElectricityMeter(String id) { - super(// + super(id, // OpenemsComponent.ChannelId.values(), // ElectricityMeter.ChannelId.values() // ); - for (Channel channel : this.channels()) { - channel.nextProcessImage(); - } - super.activate(null, id, "", true); } @Override - public MeterType getMeterType() { - return MeterType.GRID; + protected DummyElectricityMeter self() { + return this; } } diff --git a/io.openems.edge.meter.api/src/io/openems/edge/meter/test/DummySinglePhaseElectricityMeter.java b/io.openems.edge.meter.api/src/io/openems/edge/meter/test/DummySinglePhaseElectricityMeter.java new file mode 100644 index 00000000000..315763ef7f5 --- /dev/null +++ b/io.openems.edge.meter.api/src/io/openems/edge/meter/test/DummySinglePhaseElectricityMeter.java @@ -0,0 +1,47 @@ +package io.openems.edge.meter.test; + +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.meter.api.ElectricityMeter; +import io.openems.edge.meter.api.SinglePhase; +import io.openems.edge.meter.api.SinglePhaseMeter; + +/** + * Provides a simple, simulated {@link SinglePhaseMeter} ElectricityMeter + * component that can be used together with the OpenEMS Component test + * framework. + */ +public class DummySinglePhaseElectricityMeter extends AbstractDummyElectricityMeter + implements ElectricityMeter, SinglePhaseMeter { + + private SinglePhase phase = SinglePhase.L1; + + public DummySinglePhaseElectricityMeter(String id) { + super(id, // + OpenemsComponent.ChannelId.values(), // + ElectricityMeter.ChannelId.values(), // + SinglePhaseMeter.ChannelId.values() // + ); + } + + @Override + protected DummySinglePhaseElectricityMeter self() { + return this; + } + + /** + * Set the {@link SinglePhase}. + * + * @param phase the phase + * @return myself + */ + public DummySinglePhaseElectricityMeter withPhase(SinglePhase phase) { + this.phase = phase; + return this.self(); + } + + @Override + public SinglePhase getPhase() { + return this.phase; + } + +} \ No newline at end of file diff --git a/io.openems.edge.meter.api/test/io/openems/edge/meter/api/ElectricityMeterTest.java b/io.openems.edge.meter.api/test/io/openems/edge/meter/api/ElectricityMeterTest.java new file mode 100644 index 00000000000..c5d56e17c90 --- /dev/null +++ b/io.openems.edge.meter.api/test/io/openems/edge/meter/api/ElectricityMeterTest.java @@ -0,0 +1,162 @@ +package io.openems.edge.meter.api; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.openems.edge.common.test.TestUtils; +import io.openems.edge.meter.test.DummyElectricityMeter; + +public class ElectricityMeterTest { + + @Test + public void testCalculateSumActivePowerFromPhases() { + var sut = new DummyElectricityMeter("meter0"); // + + // Without calculateSumActivePowerFromPhases + sut.withActivePowerL1(1000); + sut.withActivePowerL2(200); + sut.withActivePowerL3(30); + assertEquals(null, sut.getActivePower().get()); + assertEquals(1000, sut.getActivePowerL1().get().intValue()); + assertEquals(200, sut.getActivePowerL2().get().intValue()); + assertEquals(30, sut.getActivePowerL3().get().intValue()); + + // Activate + ElectricityMeter.calculateSumActivePowerFromPhases(sut); + sut.withActivePowerL1(2000); + sut.withActivePowerL2(300); + sut.withActivePowerL3(40); + TestUtils.activateNextProcessImage(sut); + assertEquals(2340, sut.getActivePower().get().intValue()); + } + + @Test + public void testCalculateSumReactivePowerFromPhases() { + var sut = new DummyElectricityMeter("meter0"); // + + // Without calculateSumReactivePowerFromPhases + sut.withReactivePowerL1(1000); + sut.withReactivePowerL2(200); + sut.withReactivePowerL3(30); + assertEquals(null, sut.getReactivePower().get()); + assertEquals(1000, sut.getReactivePowerL1().get().intValue()); + assertEquals(200, sut.getReactivePowerL2().get().intValue()); + assertEquals(30, sut.getReactivePowerL3().get().intValue()); + + // Activate + ElectricityMeter.calculateSumReactivePowerFromPhases(sut); + sut.withReactivePowerL1(2000); + sut.withReactivePowerL2(300); + sut.withReactivePowerL3(40); + TestUtils.activateNextProcessImage(sut); + assertEquals(2340, sut.getReactivePower().get().intValue()); + } + + @Test + public void testCalculateSumCurrentFromPhases() { + var sut = new DummyElectricityMeter("meter0"); // + + // Without calculateSumCurrentFromPhasess + sut.withCurrentL1(1000); + sut.withCurrentL2(200); + sut.withCurrentL3(30); + assertEquals(null, sut.getCurrent().get()); + assertEquals(1000, sut.getCurrentL1().get().intValue()); + assertEquals(200, sut.getCurrentL2().get().intValue()); + assertEquals(30, sut.getCurrentL3().get().intValue()); + + // Activate + ElectricityMeter.calculateSumCurrentFromPhases(sut); + sut.withCurrentL1(2000); + sut.withCurrentL2(300); + sut.withCurrentL3(40); + TestUtils.activateNextProcessImage(sut); + assertEquals(2340, sut.getCurrent().get().intValue()); + } + + @Test + public void testCalculateAverageVoltageFromPhases() { + var sut = new DummyElectricityMeter("meter0"); // + + // Without calculateAverageVoltageFromPhases + sut.withVoltageL1(1000); + sut.withVoltageL2(200); + sut.withVoltageL3(30); + assertEquals(null, sut.getVoltage().get()); + assertEquals(1000, sut.getVoltageL1().get().intValue()); + assertEquals(200, sut.getVoltageL2().get().intValue()); + assertEquals(30, sut.getVoltageL3().get().intValue()); + + // Activate + ElectricityMeter.calculateAverageVoltageFromPhases(sut); + sut.withVoltageL1(2000); + sut.withVoltageL2(300); + sut.withVoltageL3(40); + TestUtils.activateNextProcessImage(sut); + assertEquals(780, sut.getVoltage().get().intValue()); + } + + @Test + public void testCalculateSumActiveProductionEnergyFromPhases() { + var sut = new DummyElectricityMeter("meter0"); // + + // Without calculateSumActiveProductionEnergyFromPhases + sut.withActiveProductionEnergyL1(1000); + sut.withActiveProductionEnergyL2(200); + sut.withActiveProductionEnergyL3(30); + assertEquals(null, sut.getActiveProductionEnergy().get()); + assertEquals(1000, sut.getActiveProductionEnergyL1().get().intValue()); + assertEquals(200, sut.getActiveProductionEnergyL2().get().intValue()); + assertEquals(30, sut.getActiveProductionEnergyL3().get().intValue()); + + // Activate + ElectricityMeter.calculateSumActiveProductionEnergyFromPhases(sut); + sut.withActiveProductionEnergyL1(2000); + sut.withActiveProductionEnergyL2(300); + sut.withActiveProductionEnergyL3(40); + TestUtils.activateNextProcessImage(sut); + assertEquals(2340, sut.getActiveProductionEnergy().get().intValue()); + } + + @Test + public void testCalculatePhasesFromActivePower() { + var sut = new DummyElectricityMeter("meter0"); // + + // Without calculatePhasesFromActivePower + sut.withActivePower(3000); + assertEquals(3000, sut.getActivePower().get().intValue()); + assertEquals(null, sut.getActivePowerL1().get()); + assertEquals(null, sut.getActivePowerL2().get()); + assertEquals(null, sut.getActivePowerL3().get()); + + // Activate + ElectricityMeter.calculatePhasesFromActivePower(sut); + sut.withActivePower(3000); + TestUtils.activateNextProcessImage(sut); + assertEquals(1000, sut.getActivePowerL1().get().intValue()); + assertEquals(1000, sut.getActivePowerL2().get().intValue()); + assertEquals(1000, sut.getActivePowerL3().get().intValue()); + } + + @Test + public void testCalculatePhasesFromReactivePower() { + var sut = new DummyElectricityMeter("meter0"); // + + // Without calculatePhasesFromReactivePower + sut.withReactivePower(3000); + assertEquals(3000, sut.getReactivePower().get().intValue()); + assertEquals(null, sut.getReactivePowerL1().get()); + assertEquals(null, sut.getReactivePowerL2().get()); + assertEquals(null, sut.getReactivePowerL3().get()); + + // Activate + ElectricityMeter.calculatePhasesFromReactivePower(sut); + sut.withReactivePower(3000); + TestUtils.activateNextProcessImage(sut); + assertEquals(1000, sut.getReactivePowerL1().get().intValue()); + assertEquals(1000, sut.getReactivePowerL2().get().intValue()); + assertEquals(1000, sut.getReactivePowerL3().get().intValue()); + } + +} diff --git a/io.openems.edge.meter.api/test/io/openems/edge/meter/api/SinglePhaseMeterTest.java b/io.openems.edge.meter.api/test/io/openems/edge/meter/api/SinglePhaseMeterTest.java new file mode 100644 index 00000000000..d06f40266d0 --- /dev/null +++ b/io.openems.edge.meter.api/test/io/openems/edge/meter/api/SinglePhaseMeterTest.java @@ -0,0 +1,52 @@ +package io.openems.edge.meter.api; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.openems.edge.common.test.TestUtils; +import io.openems.edge.meter.test.DummySinglePhaseElectricityMeter; + +public class SinglePhaseMeterTest { + + @Test + public void testCalculateSinglePhaseFromActivePower() { + var sut = new DummySinglePhaseElectricityMeter("meter0") // + .withPhase(SinglePhase.L1); + + // Without calculateSinglePhaseFromActivePower + sut.withActivePower(4000); + assertEquals(4000, sut.getActivePower().get().intValue()); + assertEquals(null, sut.getActivePowerL1().get()); + assertEquals(null, sut.getActivePowerL2().get()); + assertEquals(null, sut.getActivePowerL3().get()); + + // Phase 1 + sut.withPhase(SinglePhase.L1); + SinglePhaseMeter.calculateSinglePhaseFromActivePower(sut); + sut.withActivePower(3000); + TestUtils.activateNextProcessImage(sut); + assertEquals(3000, sut.getActivePower().get().intValue()); + assertEquals(3000, sut.getActivePowerL1().get().intValue()); + assertEquals(null, sut.getActivePowerL2().get()); + assertEquals(null, sut.getActivePowerL3().get()); + + // Phase 2 + sut.withPhase(SinglePhase.L2); + sut.withActivePower(2000); + TestUtils.activateNextProcessImage(sut); + assertEquals(2000, sut.getActivePower().get().intValue()); + assertEquals(null, sut.getActivePowerL1().get()); + assertEquals(2000, sut.getActivePowerL2().get().intValue()); + assertEquals(null, sut.getActivePowerL3().get()); + + // Phase 3 + sut.withPhase(SinglePhase.L3); + sut.withActivePower(1000); + TestUtils.activateNextProcessImage(sut); + assertEquals(1000, sut.getActivePower().get().intValue()); + assertEquals(null, sut.getActivePowerL1().get()); + assertEquals(null, sut.getActivePowerL2().get()); + assertEquals(1000, sut.getActivePowerL3().get().intValue()); + } +} From fd67d7756f43bfa772709d3a7b4d511174fae297 Mon Sep 17 00:00:00 2001 From: Thomas Sicking <91258335+tsicking@users.noreply.github.com> Date: Thu, 12 Oct 2023 23:43:51 +0200 Subject: [PATCH 15/27] SunSpec: improvements (#2337) - There are SunSpec Channels which have a constant scale factor, e.g. S305_LAT has a scale factor of -7. The current implementation of the AbstractOpenemsSunSpecComponent cannot handle these, but only those whose scale factor is read via modbus. This PR handles this issue. - Moreover, a DummySunSpecComponent with all default models is added for JUnit testing, as well as a small JUnit test checking that the modbus tasks are not too long. - DummySunSpecComponent: initialize SunSpec modals in ASC order - Refactor addReadTasks to preprocessModbusElements to be able to reuse it for WriteTasks - Create one FC16WriteRegistersTask for all writeable registers of one block This solves some perfomance problems we had with multiple WriteTasks being executed on every Cycle (e.g. write ActivePower and ReactivePower channels) - Extract generateElementToChannelConverter logic was split to two places before; also now I am always applying the 'isDefined' check - even for Points with Scale-Factor - ScaleFactor: get next value to win one cycle (NOTE: this is still too slow with one cycle, but that will be tackled in another PR) --------- Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- .../ElementToChannelScaleFactorConverter.java | 16 +- .../AbstractOpenemsSunSpecComponent.java | 195 +++++++++++------- .../AbstractOpenemsSunSpecComponentTest.java | 32 +++ .../modbus/sunspec/DummySunSpecComponent.java | 57 +++++ .../modbus/sunspec/SunSpecComponentTest.java | 17 ++ io.openems.wrapper/bnd.bnd | 2 +- 7 files changed, 235 insertions(+), 86 deletions(-) create mode 100644 io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java create mode 100644 io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/DummySunSpecComponent.java create mode 100644 io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/SunSpecComponentTest.java diff --git a/cnf/pom.xml b/cnf/pom.xml index ad4135fc31f..68ea97d66f1 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -11,7 +11,7 @@ biz.aQute.bnd.workspace biz.aQute.bnd.workspace.gradle.plugin - 6.4.0 + 7.0.0 diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/ElementToChannelScaleFactorConverter.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/ElementToChannelScaleFactorConverter.java index 0040ad059af..75698cb16c2 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/ElementToChannelScaleFactorConverter.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/ElementToChannelScaleFactorConverter.java @@ -18,6 +18,16 @@ */ public class ElementToChannelScaleFactorConverter extends ElementToChannelConverter { + private static int getValueOrError(OpenemsComponent component, ChannelId channelId) + throws InvalidValueException, IllegalArgumentException { + var channel = (IntegerReadChannel) component.channel(channelId); + var value = channel.getNextValue().orElse(null); + if (value != null) { + return value; + } + return channel.value().getOrError(); + } + public ElementToChannelScaleFactorConverter(OpenemsComponent component, SunSpecPoint point, ChannelId scaleFactorChannel) { super(// @@ -27,8 +37,7 @@ public ElementToChannelScaleFactorConverter(OpenemsComponent component, SunSpecP return null; } try { - return apply(value, - ((IntegerReadChannel) component.channel(scaleFactorChannel)).value().getOrError() * -1); + return apply(value, getValueOrError(component, scaleFactorChannel) * -1); } catch (InvalidValueException | IllegalArgumentException e) { return null; } @@ -37,8 +46,7 @@ public ElementToChannelScaleFactorConverter(OpenemsComponent component, SunSpecP // channel -> element value -> { try { - return apply(value, - ((IntegerReadChannel) component.channel(scaleFactorChannel)).value().getOrError()); + return apply(value, getValueOrError(component, scaleFactorChannel)); } catch (InvalidValueException | IllegalArgumentException e) { return null; } diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponent.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponent.java index d78b6dee4e7..5dafbd32b4d 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponent.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponent.java @@ -1,8 +1,6 @@ package io.openems.edge.bridge.modbus.sunspec; -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Deque; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -17,18 +15,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.Lists; + import io.openems.common.exceptions.OpenemsException; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; import io.openems.edge.bridge.modbus.api.ElementToChannelScaleFactorConverter; import io.openems.edge.bridge.modbus.api.ModbusProtocol; import io.openems.edge.bridge.modbus.api.ModbusUtils; -import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement; import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement; import io.openems.edge.bridge.modbus.api.element.ModbusElement; import io.openems.edge.bridge.modbus.api.element.ModbusRegisterElement; import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement; import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement; +import io.openems.edge.bridge.modbus.api.task.AbstractTask; import io.openems.edge.bridge.modbus.api.task.FC16WriteRegistersTask; import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask; import io.openems.edge.bridge.modbus.api.task.Task; @@ -291,102 +291,137 @@ public boolean isSunSpecInitializationCompleted() { protected void addBlock(int startAddress, SunSpecModel model, Priority priority) throws OpenemsException { this.logInfo(this.log, "Adding SunSpec-Model [" + model.getBlockId() + ":" + model.label() + "] starting at [" + startAddress + "]"); - Deque elements = new ArrayDeque<>(); + var readElements = new ArrayList(); + var writeElements = new ArrayList(); startAddress += 2; for (var i = 0; i < model.points().length; i++) { var point = model.points()[i]; - var element = point.get().generateModbusElement(startAddress); - startAddress += element.length; - elements.add(element); + final var element = point.get().generateModbusElement(startAddress); + // Handle AccessMode + switch (point.get().accessMode) { + case READ_ONLY -> { + readElements.add(element); + } + case READ_WRITE -> { + readElements.add(element); + writeElements.add(element); + } + case WRITE_ONLY -> { + readElements.add(new DummyRegisterElement(element.startAddress, element.length)); + writeElements.add(element); + } + } + + startAddress += element.length; var channelId = point.getChannelId(); this.addChannel(channelId); + this.m(channelId, element, this.generateElementToChannelConverter(model, point)); + } - if (point.get().scaleFactor.isPresent()) { - // This Point needs a ScaleFactor - // - find the ScaleFactor-Point - var scaleFactorName = SunSpecCodeGenerator.toUpperUnderscore(point.get().scaleFactor.get()); - SunSpecPoint scaleFactorPoint = null; - for (var sfPoint : model.points()) { - if (sfPoint.name().equals(scaleFactorName)) { - scaleFactorPoint = sfPoint; - break; - } - } - if (scaleFactorPoint == null) { - // Unable to find ScaleFactor-Point - this.logError(this.log, - "Unable to find ScaleFactor [" + scaleFactorName + "] for Point [" + point.name() + "]"); - } + // Create Tasks and add them to the ModbusProtocol + for (var elements : preprocessModbusElements(readElements)) { + this.modbusProtocol.addTask(// + new FC3ReadRegistersTask(// + elements.get(0).startAddress, priority, elements.toArray(ModbusElement[]::new))); + } + for (var elements : preprocessModbusElements(writeElements)) { + this.modbusProtocol.addTask(// + new FC16WriteRegistersTask(// + elements.get(0).startAddress, elements.toArray(ModbusElement[]::new))); + } + } - // Add a scale-factor mapping between Element and Channel - element = this.m(channelId, element, - new ElementToChannelScaleFactorConverter(this, point, scaleFactorPoint.getChannelId())); + /** + * Converts a list of {@link ModbusElement}s to sublists, prepared for Modbus + * {@link AbstractTask}s. + * + *

    + *
  • Sublists are without holes (i.e. nextStartAddress = currentStartAddress + + * Length + 1) + *
  • Length of sublist <= MAXIMUM_TASK_LENGTH + *
+ * + * @param elements the source elements + * @return list of {@link ModbusElement} lists + */ + protected static List> preprocessModbusElements(List elements) { + var result = Lists.>newArrayList(Lists.newArrayList()); + for (var element : elements) { + // Get last sublist in result + var l = result.get(result.size() - 1); + // Get last element of sublist + var e = l.isEmpty() ? null : l.get(l.size() - 1); + if (( + // Is first element of the sublist? + e == null + // Is element direct successor? + || e.startAddress + e.length == element.startAddress) // + && // Does element fit in task? + l.stream().mapToInt(m -> m.length).sum() + element.length <= MAXIMUM_TASK_LENGTH // + ) { + l.add(element); // Add to existing sublist } else { - // Add a direct mapping between Element and Channel - element = this.m(channelId, element, new ElementToChannelConverter( - // Element -> Channel - value -> { - if (!point.isDefined(value)) { - // This value is set to be 'UNDEFINED' for the given type by SunSpec - return null; - } - return value; - }, - // Channel -> Element - value -> value)); - - } - - // Evaluate Access-Mode of the Channel - switch (point.get().accessMode) { - case READ_ONLY: - // Read-Only -> replace element with dummy - element = new DummyRegisterElement(element.startAddress, - element.startAddress + point.get().type.length - 1); - break; - case READ_WRITE: - case WRITE_ONLY: - // Add a Write-Task - // TODO create one FC16WriteRegistersTask for entire block - final Task writeTask = new FC16WriteRegistersTask(element.startAddress, element); - this.modbusProtocol.addTask(writeTask); - break; + result.add(Lists.newArrayList(element)); // Create new sublist } } - this.addReadTasks(elements, priority); + // Avoid length check for sublist + if (result.get(0).isEmpty()) { + return List.of(); + } + return result; } /** - * Splits the task if it is too long and adds the read tasks. + * Generates a {@link ElementToChannelConverter} for a Point. * - * @param elements the Deque of {@link ModbusElement}s for one block. - * @param priority the reading priority - * @throws OpenemsException on error + *
    + *
  • Check for UNDEFINED value as defined in SunSpec per Type specification + *
  • If a Scale-Factor is defined, try to add it - either as other point of + * model (e.g. "W_SF") or as static value converter + *
+ * + * @param model the {@link SunSpecModel} + * @param point the {@link SunSpecPoint} + * @return an {@link ElementToChannelConverter}, never null */ - private void addReadTasks(Deque elements, Priority priority) throws OpenemsException { - var length = 0; - var taskElements = new ArrayDeque(); - var element = elements.pollFirst(); - while (element != null) { - if (length + element.length > MAXIMUM_TASK_LENGTH) { - this.modbusProtocol.addTask(// - new FC3ReadRegistersTask(// - taskElements.peekFirst().startAddress, priority, // - taskElements.toArray(new AbstractModbusElement[taskElements.size()]))); - length = 0; - taskElements.clear(); - } - taskElements.add(element); - length += element.length; - element = elements.pollFirst(); + protected ElementToChannelConverter generateElementToChannelConverter(SunSpecModel model, SunSpecPoint point) { + // Create converter for 'defined' state + final var valueIsDefinedConverter = new ElementToChannelConverter(// + /* Element -> Channel */ value -> point.isDefined(value) ? value : null, + /* Channel -> Element */ value -> value); + + // Generate Scale-Factor converter (possibly null) + ElementToChannelConverter scaleFactorConverter = null; + if (point.get().scaleFactor.isPresent()) { + final var scaleFactor = point.get().scaleFactor.get(); + final var scaleFactorName = SunSpecCodeGenerator.toUpperUnderscore(scaleFactor); + scaleFactorConverter = Stream.of(model.points()) // + .filter(p -> p.name().equals(scaleFactorName)) // + .map(sfp -> new ElementToChannelScaleFactorConverter(this, point, sfp.getChannelId())) // + // Found matching Scale-Factor Point in SunSpec Modal + .findFirst() + + // Else: try to parse constant Scale-Factor + .orElseGet(() -> { + try { + return new ElementToChannelScaleFactorConverter(Integer.parseInt(scaleFactor)); + } catch (NumberFormatException e) { + // Unable to parse Scale-Factor to static value + this.logError(this.log, "Unable to parse Scale-Factor [" + scaleFactor + "] for Point [" + + point.name() + "]"); + return null; + } + }); // + } + + if (scaleFactorConverter != null) { + return ElementToChannelConverter.chain(valueIsDefinedConverter, scaleFactorConverter); + } else { + return valueIsDefinedConverter; } - this.modbusProtocol.addTask(// - new FC3ReadRegistersTask(// - taskElements.peekFirst().startAddress, priority, // - taskElements.toArray(new AbstractModbusElement[taskElements.size()]))); } /** diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java new file mode 100644 index 00000000000..ed2041643b3 --- /dev/null +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/AbstractOpenemsSunSpecComponentTest.java @@ -0,0 +1,32 @@ +package io.openems.edge.bridge.modbus.sunspec; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; + +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.bridge.modbus.api.element.ModbusElement; +import io.openems.edge.bridge.modbus.api.element.StringWordElement; + +public class AbstractOpenemsSunSpecComponentTest { + + @Test + public void testPreprocessModbusElements() throws OpenemsException { + var elements = new ArrayList(); + var startAddress = 0; + for (var point : DefaultSunSpecModel.S_701.points()) { + var element = point.get().generateModbusElement(startAddress); + startAddress += element.length; + elements.add(element); + } + + var sut = AbstractOpenemsSunSpecComponent.preprocessModbusElements(elements); + assertEquals(2, sut.size()); // two sublists + assertEquals(69, sut.get(0).size()); // first task + assertEquals(1, sut.get(1).size()); // second task + assertEquals(StringWordElement.class, sut.get(1).get(0).getClass()); // second task + } + +} diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/DummySunSpecComponent.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/DummySunSpecComponent.java new file mode 100644 index 00000000000..adbb4e21c59 --- /dev/null +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/DummySunSpecComponent.java @@ -0,0 +1,57 @@ +package io.openems.edge.bridge.modbus.sunspec; + +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.bridge.modbus.api.ModbusComponent; +import io.openems.edge.bridge.modbus.api.task.Task; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.taskmanager.Priority; + +public class DummySunSpecComponent extends AbstractOpenemsSunSpecComponent { + + /** + * All models are active with low priority. + */ + private static final Map ACTIVE_MODELS = Stream.of(DefaultSunSpecModel.values()) + .collect(Collectors.toMap(model -> model, model -> Priority.LOW, (a, b) -> a, TreeMap::new)); + + public DummySunSpecComponent() throws OpenemsException { + super(ACTIVE_MODELS, // + OpenemsComponent.ChannelId.values(), // + ModbusComponent.ChannelId.values()); // + this.addBlocks(); + } + + private void addBlocks() throws OpenemsException { + var startAddress = 40000; + for (var entry : ACTIVE_MODELS.keySet()) { + this.addBlock(startAddress, entry, ACTIVE_MODELS.get(entry)); + } + + } + + @Override + protected void onSunSpecInitializationCompleted() { + } + + /** + * Gets the length of the longest modbus task. + * + * @return the maximum task length + * @throws OpenemsException on error + */ + public int maximumTaskLenghth() throws OpenemsException { + return this.getModbusProtocol() // + .getTaskManager() // + .getTasks() // + .stream() // + .mapToInt(Task::getLength) // + .max().orElse(0); + + } + +} diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/SunSpecComponentTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/SunSpecComponentTest.java new file mode 100644 index 00000000000..c6d3a05997a --- /dev/null +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/sunspec/SunSpecComponentTest.java @@ -0,0 +1,17 @@ +package io.openems.edge.bridge.modbus.sunspec; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsException; + +public class SunSpecComponentTest { + + @Test + public void test() throws OpenemsException { + var component = new DummySunSpecComponent(); + assertTrue(component.maximumTaskLenghth() <= 126); + } + +} diff --git a/io.openems.wrapper/bnd.bnd b/io.openems.wrapper/bnd.bnd index 2c988f8674e..28f4dd69b85 100644 --- a/io.openems.wrapper/bnd.bnd +++ b/io.openems.wrapper/bnd.bnd @@ -21,7 +21,7 @@ Bundle-Description: This wraps external java libraries that do not have OSGi hea info.faljse:SDNotify;version='1.5.0',\ io.reactivex.rxjava3.rxjava;version='3.1.6',\ com.google.gson;version='2.10.1',\ - de.bytefish:pgbulkinsert;version='8.1.1',\ + de.bytefish:pgbulkinsert;version='8.1.2',\ fr.turri:aXMLRPC;version='1.13.0',\ org.dhatim:fastexcel;version='0.15.7',\ org.eclipse.paho.mqttv5.client;version='1.2.5',\ From 0a2a0d55840d6e3b5c1d5659a77020f9ecf58fc7 Mon Sep 17 00:00:00 2001 From: Lukas Rieger <73471197+lukasrgr@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:29:04 +0200 Subject: [PATCH 16/27] Docs: add implementing a UI modal (#2389) --- .../images/modal-line-example-consumption.png | Bin 0 -> 6041 bytes .../ROOT/pages/ui/implementing-a-widget.adoc | 44 +++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 doc/modules/ROOT/assets/images/modal-line-example-consumption.png diff --git a/doc/modules/ROOT/assets/images/modal-line-example-consumption.png b/doc/modules/ROOT/assets/images/modal-line-example-consumption.png new file mode 100644 index 0000000000000000000000000000000000000000..cf99c313bb46e6c6fe679f754c64e7f3c8b35234 GIT binary patch literal 6041 zcmb7ocOaWx^nX-K&91k!6x~Ie(pt4g2O6tJ)uy(PlprL6YD>4aR#Bv_7)4MLH9}Qe zGpK}!5FLWVdWA@h@O`}B-=DvKzJKJ&^Ev0L7?hXi4C7!02aGr;er5xq}#TC zLhbMpKS1ac?075E0Tz76+CS3A&nMCckbywxylJva36nB#it=Z?n$ zPt3XhkoE=L|1Z99QS<+`_}}*1*aKs8!2qx-(`tFe)G@|$ZcO5}yFaFVajTaZpwpNu zWvT7_$|OnX@;`g;?`*nex>u`0o21O~-KTsLQeKxLQ{f#e*>zP>{`yF5Qtj=>yTGTM zCT^ZeeAj<+zuEpYlk*ch=w#T(tXY`e7P41AcB)4g5fI(y7x-$SeG1cyW+JjR0^1Pr z%?rALALbhun%nFP+ftq|Pj=s_Q+odVxwPf!GjYCV-?FN@F1#yIh zg$F2<6YtGawdXDmG8kva8Osf!bDSmD#6OHy^ZB%qG~29^G#j4lKrP0*UB;m@-{7*G zY}{9BMF|VfB=lK~BU;pX6T7kkjG#Y1$Ft*O9%zD_Mtg+!)-P62NF)769B5+?eV=RB z&QSu(O;8_aJ0*N+oz5@j2Iu1|MUIS*er9oEC#M2N;^?wgH;C3(!N}mF6ztI76laUJ zs|l)xEph#B$0LH~Do^Te-G)nlt`6y-d63m_oKo{pB-r4SzPLJe9DgWfyYLz7Ed1Pd zgfh4)faju|5M|k}a(HRdaub!DH$`|zEkM2vrug6J7Dy{iYf?Bj!1h`n=+wD*k;V~R zQP|^P;XAXc>?r4&7vQ&%F2Ax>;I||sykVClT#W`c+MVZ5E$O_sdro)jcAYWqOBvZA5-TP^!z35!V1{BTVTtIOc5k!%H492;Y`K#VvmM$ME~ z5dro$PwfQj)`Ck6-GXjOtPPmY_zm|R4iU0*d4=fZh184eiJn|0s)YULYOQ(2N>5#9 z>w1C?h?>*onYD87@t3yDHOlMLKg4&hPZ7odS0D1#6 zL55)Pc+RPYF}<0zBKGsvyX<%UugqVTi4Mdacpd(Hw(6%(f1J3O}sf3CK6~R$cDpy_5SeILxtkH-k(pO66gdB^0;~TX#S0N8~+!QO! zR#ul)$mCpoA|uEvl{x%I+@<9opU_Y1R9Vg-mFnmDU6yBW)E$qvAxuNo*4Ec?&CSf( zJsI=94$cL&e5n}l=f%1HCw-Q@uLn;3(CdX-J}6Z=1KFCCEnM$RDMa#AEj8<|bCdXJ z$os4m-8)+sgJb%XESlic>;!zgz*Q+Rv_h!+Sl4+cSshXZbJ~-xUN>R?>N+ zK`xUbTX-P_1-_My#=Gn1uPHd!KAd|pM#|Tyl(V!=Hw=%Db{UQ^Mq(1GlQMT@Trb{I z4$z`?H!VK%lmLNF*rtf96^u=io$%OE^_o(f$!F012j-94TbkX(-_OwZAMJZ*Qrh=S z61pl~_4;1jOv6lL*k(jiKi-GnZC?`?bFHk$LLj^YM3Th?#Ww7{J78`6?$s68> z8XKJ5chlh8)IT0-eq#ntHn`IFk`=7pin6^b|5bDTber!2Yr|e|{~4740;LO+P7e(Y zUGtQexB*B*KG8h3j3f+geP*tPDIFTx5DIc;oQwU}#89VI7zC1ZfO-(z-xw(SBAccO zg%2JO%YE#mf;X{6OFOkdpv0JfQB zp>M@Q0KY%~JJIc-N8t4ro8B7%8tg47ku0KoE;bz_Vp#w_lwZ>1&)^z)@2x!sz~B2a zNRdWULc%PseE`IzsipYOyVPu)-8;Ii%~C_u^Lh-Sdc4XBI54`2LDI)eeR#31A+!rW z4F>`+iTiRfD~}f3OTo^Iz!ZQ^i;ue8qm33QSm0e5tb<-%%Ib977oQ+pP~$tgqUOm+jK=%Nq!TuE@f@3M<0B-mXK^E7TYeeoi$+LQBvBKoOp z{e?mG`^zv-Qd`gaOAGqdPE37*)8<+!*z-xpluf}&CF{JYc84imcV8&aRS5Li;*%dw zP~X}k7D`lHV_;!u6~P}yKvz^?UQp6V4Z3k6C3mIE)+;#cd(QoHIl4CUD4Gg0NBKLg zFaPwPR|Mt}V6O0!BO#SCsTX{|TR@>@*(0~7qC#d|D(9<4u{XOuN6$2+P4tAnpZK-{ zTkU95>-z1K%gP+opD62VKBt74I7NvMf!;f(i)^UdmHcOgpwAjzcczqyfD-%O%l4-8 ztzj#ZAA>F2wBaAz4bkPa{i%KzRivb(vV(nn->VSu4&b|L-wfqU=LD4R9UG6xTvywc zI5ry`FigQMLGs7XV}w&!b7`&<&wS-br?zLlA$Sh9I=~Loy=#4MgdAx5>H~Ev6Q4ka zJ~NMsBAnd+sK+tIFp4uozBBEC9LuouGc~m?Mn2bNQ2#`(ub%0w4~_$j*d~%??Xfb^luTc+Sr3AxAZ>N2&)nLrw7}u(g)CkGL9|8>B~_^VhE; zPM}lSp@le;jX7oS)$|^2vKiKGxyotsP%^4voSbJEF_WXf9c2nL zU61ea*tn)a@?%K^gaq#vaNKNq8q|6e+sI{g-GhkJ{wo!Cm-R~pu#xc-v;srx0OCn9 zlFmR_WL7Xv88f*JbWgTXZPwz5S6D_%j=sxjO=_1n2VV2%=P{SH#yoDvigZmrNsa8E zdTKVGVAh8l+x$L7?A#C|mk!*Xzd+?V&yB!QR|Rk7L5bJjnpz9ziqvh$w#**{q2SnYljW(jpZioWm+erF4=lH3St05|i9{PprAkDV`nTAxb&n zNpXMP1fO=_2(eu=(vfn6A09u6Y{nEFc8wY=-tq0#QW4~`^_N-mwyqLK0Y~=$%b|wR zL>MrgftF&mike*&nL{t>3$uxKL;G|p!Z^xZ4}~g_xin2N@Ii7zZAsVc6@%qT%sYIXXSE|s+Z7&Jb*@WxJeUOE1koVJK~=IOmbuW~T+hehKzoPvN0i8a3^f z2TlQ9$6;xBsu*Tzc(lKYn}P8J&z)fs zM;Md*$!a>uvH%@X)3kI3${N?1Uvy2L{Gq^pdmSQNhzfAd%684=7c;beu8yVYi6BcB zna|ky>I!CqZ#!>G^4#(9b0$MCsucS&@%cSaXAr2wQExf+$3y@kjIs~E)SiVuaa=Wl zzS{4lZU-}jp;`v*dgR8ui?sXyhUxbQqH;D!_GnzWFX=aYTnD`LhU?XvXipXBL z^qoqJ0ZON;d4Jo^dBaegc&KS?cQ3;qC??UIAxP_rNZ!F8^2@-t-SJ-O^$A+O?qJWJ zm=p&OKXsLKBd1b+Xx`Qztw|Lp%puc-_8hd?``ANAuOmE~)OrvPb{#{Xj~xDZ2xpW9 z0y#c*>&<_2kXmjpLNtrW>YXz1-g`5ZA?!9_3v9!I=_bRQYoO01IU#gP+WaHN@XLAL zz!}YZRtipm$rsyD)m)cFCY}3N)RsT_wdv*+rE7mDml!=8Vx6;9g|;zk^ZK(0@dlxuU%$uv)?~zt>c2w5NLN+c*-<#m zUMW}HL?=&n!}Te=c%-;+N85@phHE<$olQ(|X>nWOh2}U2Gg65G#>$7Xu(P-*eWgnP?U({8Q=pf&+Wo;S5|ht# zU=0CXVlt@cdWw$$4bnaiur$Nk^uY zUg&$q44zXP8k3KP@IQO2?tPYllYR3>H?t+?&lO7BvhY-JzZ0{uyE(pBbIHsNJgzU^ z;=7jo!!q{gI|p(#`$Bv`?-fK(T}9_+D_UWH-ry_Ewss|pyexH`9ILgF(}XAxbFZZZitAATK5hF zH=_o*tZr*L%3+*Dm`M#UAy0k0$9H15Aa_9NHb9m#7Sbw>vSSx3B4*Yav0Y97IG@#i zv-*GlFe~pE%|N=RWYMIFago{1ggu816 zxn;PNc(?t)?46tr zx~7-P?jMpl?;fNLh)oP`mfw1LpnvqX{{up6t0^UDDvaTuA&H&^@OS84(j z0RxMlRwr|7zT^xEmI6=$9!*J6KRWbu;jWBtPh@j5{xfn4lIgd@_129?4Gnti*;!4E zcsVN2(@qYE`O3xYbSBf2&1Ns=49tGt3HY4&_h1g82O9)|Jeg(GrFsL?qyA+h_S_N! zD>u9{KL*)e$3sG!k^xhcRQWjf{0 zGir9cWgH5GVKORyxKmOR+Ter1kd}G8Y-AN6#YS=;5Z|DO+^wyxCpUov4?1E9)9&WD zETw1*tUrAJehn@S2yy>V@5)?~1JKi_9aT5k+OW}S;Hw3x+Pb6=7_dgNajV)) z2JAXPekLh6zdFsex>a-5Jj@ZcyhlF$Q|tQZ-`$j{>0wJ#ll5^af4AU5 zG0{fOyD#BtgT_R(tw+~HTX;imtX};E&9LG(5swX)`!8qnwEVpyI!^^023Gcsr+FKy zZetl$ez$u-5j-b^U@;1kE97CHj!}O6j_&sv%$cSlfE)4y2Vo z8Wy>LiwcvN0aF%!OXtbt3;?I6eW+af3!%Gfe1PLPzI()+y16`pCxxRFXc7D|Zr$2o11Gt3HBvm({7Hf5Lfe;2bV80XmEB*A*-v;g@7$e5+MltvCu!LD zKI7R3U>?x86#02gGQ+3;*GV*xAZ#i-QGzJ1!Nm$VrrW(zLJZPClJd-y@>wO))`yng z?%XQVjl4uW;z_@;JNJhw>X0M+78Pct-&7jKx7;tdJnW2|R&t}%dN@-)8H%kaz<^}` zce!CES%bvn@Gi}w6t2<+rDYmP$yFtDy!cw|5AF4kRP2pU5l&Iv;JU(IpS5ITH=x3h z&GI-K1ezyw^1qZK4{!Q$zwgTNF=Ua0yv10AyP-F*mF$9dZ(m>gKdstMso(!E-~^UL zGAsk~jat?HWGzAVb_Ghm=H`~G@~1sjhaz_1^iCPn5FqS2{Hf#tz=`sm*>q@ slGpmSYt!Kq Date: Tue, 17 Oct 2023 14:23:33 +0200 Subject: [PATCH 17/27] Bump com.google.guava:guava from 32.1.2-jre to 32.1.3-jre in /cnf (#2390) * Bump com.google.guava:guava from 32.1.2-jre to 32.1.3-jre in /cnf Bumps [com.google.guava:guava](https://github.com/google/guava) from 32.1.2-jre to 32.1.3-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update bnd * Update bndrun * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/build.bnd | 2 +- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cnf/build.bnd b/cnf/build.bnd index e5abf55d31f..01db9b253d6 100644 --- a/cnf/build.bnd +++ b/cnf/build.bnd @@ -40,7 +40,7 @@ buildpath: \ org.osgi.service.metatype;version='1.4.1',\ org.osgi.service.metatype.annotations;version='1.4.1',\ org.osgi.util.promise;version='1.2.0',\ - com.google.guava;version='32.1.2.jre',\ + com.google.guava;version='32.1.3.jre',\ com.google.gson;version='2.10.1',\ testpath: \ diff --git a/cnf/pom.xml b/cnf/pom.xml index 68ea97d66f1..82fbce43d0a 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -37,7 +37,7 @@ com.google.guava guava - 32.1.2-jre + 32.1.3-jre com.google.guava diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index 4bfc10b98fd..b6c4faaf5c4 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -61,7 +61,7 @@ -runbundles: \ Java-WebSocket;version='[1.5.4,1.5.5)',\ com.google.gson;version='[2.10.1,2.10.2)',\ - com.google.guava;version='[32.1.2,32.1.3)',\ + com.google.guava;version='[32.1.3,32.1.4)',\ com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ com.squareup.okio;version='[3.6.0,3.6.1)',\ com.zaxxer.HikariCP;version='[5.0.1,5.0.2)',\ diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index c13f1d2e43d..ffdde6a8492 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -185,7 +185,7 @@ com.fazecast.jSerialComm;version='[2.5.1,2.5.2)',\ com.ghgande.j2mod;version='[2.5.5,2.5.6)',\ com.google.gson;version='[2.10.1,2.10.2)',\ - com.google.guava;version='[32.1.2,32.1.3)',\ + com.google.guava;version='[32.1.3,32.1.4)',\ com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ com.squareup.okio;version='[3.6.0,3.6.1)',\ com.sun.jna;version='[5.13.0,5.13.1)',\ From 11e00010cc42a437f3a8a52ea3e9a4dbe9dd6b21 Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Fri, 20 Oct 2023 15:40:03 +0200 Subject: [PATCH 18/27] FEMS Backports (#2394) * Introduce _sum/UnmanagedConsumptionActivePower This channel value represents the part of the Consumption that is not actively managed by OpenEMS, i.e. it is calculated as _sum/ConsumptionActivePower minus charge power for an electric vehicle charging station, etc. The value is used for forecasting of consumption. * PhoenixContact Meter: add invert option * SunSpec: dynamic scale-factors Handle dynamically changing scale-factors in SunSpec * AppCenter: add required property to AppDef - moved required from field to AppDef - add test for required to field - minor property changes * ESS Cluster: Calculate effective start stop * Alpitronic Hypercharger: fix energy `null` vs 0 * Implement F&F Filipowski MR-AO-1 + Analog Output Controller * ControllerEssGridOptimizedCharge, IoShelly25, IoShellyPlug: set persistence priority to high for some channels Set PersistencePriority of channels to high in ControllerEssGridOptimizedCharge, IoShelly25, IoShellyPlug because they are used in UI * Edge: change firstSetupProtocol from being today to null * AppCenter: FixActivePower App Added a App for a FixActivePower Controller. * UI: Stringify values of formly field type text * Stringify json in formly-field type text to enable editing in UI "Update Component" * UI: Restrict Queries before IBN-Date * adjust calendar to only be able to select all dates after ibn * Bugfix for ENTSO-E * Add Webasto Apps --------- Co-authored-by: Sagar Venu <32655208+venu-sagar@users.noreply.github.com> Co-authored-by: Pooran Chandrashekaraiah <46567310+pooran-c@users.noreply.github.com> Co-authored-by: Michael Grill <59126309+michaelgrill@users.noreply.github.com> Co-authored-by: Hueseyin Sahutoglu <34771592+huseyinsaht@users.noreply.github.com> Co-authored-by: Lorant Meszlenyi <16509320+hydroid7@users.noreply.github.com> Co-authored-by: Sebastian Asen <47855186+sebastianasen@users.noreply.github.com> Co-authored-by: Kai Jeschek <99220919+da-Kai@users.noreply.github.com> Co-authored-by: Lukas Rieger <73471197+lukasrgr@users.noreply.github.com> --- io.openems.edge.application/EdgeApp.bndrun | 4 + .../api/element/AbstractModbusElement.java | 40 ++++ .../modbus/api/task/AbstractReadTask.java | 47 ++-- .../bridge/modbus/sunspec/SunSpecPoint.java | 12 +- .../src/io/openems/edge/common/sum/Sum.java | 47 ++++ .../edge/controller/api/websocket/Utils.java | 2 +- .../ControllerEssGridOptimizedCharge.java | 7 + .../.classpath | 12 + .../.gitignore | 2 + io.openems.edge.controller.io.analog/.project | 23 ++ .../org.eclipse.core.resources.prefs | 2 + io.openems.edge.controller.io.analog/bnd.bnd | 15 ++ .../readme.adoc | 7 + .../edge/controller/io/analog/Config.java | 40 ++++ .../io/analog/ControllerIoAnalog.java | 30 +++ .../io/analog/ControllerIoAnalogImpl.java | 209 ++++++++++++++++++ .../edge/controller/io/analog/Mode.java | 32 +++ .../controller/io/analog/PowerBehavior.java | 69 ++++++ .../test/.gitignore | 0 .../edge/controller/io/analog/MyConfig.java | 101 +++++++++ .../io/analog/MyControllerTest.java | 155 +++++++++++++ .../edge/controller/io/analog/TestStatic.java | 61 +++++ io.openems.edge.core/bnd.bnd | 1 + .../edge/app/api/ModbusTcpApiReadOnly.java | 5 +- .../edge/app/api/ModbusTcpApiReadWrite.java | 4 +- .../edge/app/api/RestJsonApiReadOnly.java | 11 +- .../edge/app/common/props/CommonProps.java | 3 +- .../edge/app/enums/OptionsFactory.java | 7 +- .../openems/edge/app/ess/FixActivePower.java | 172 ++++++++++++++ .../edge/app/ess/PrepareBatteryExtension.java | 21 +- .../io/openems/edge/app/evcs/DezonyEvcs.java | 8 +- .../io/openems/edge/app/evcs/EvcsCluster.java | 8 +- .../openems/edge/app/evcs/HardyBarthEvcs.java | 9 +- .../openems/edge/app/evcs/IesKeywattEvcs.java | 5 +- .../io/openems/edge/app/evcs/KebaEvcs.java | 8 +- .../edge/app/evcs/WebastoNextEvcs.java | 200 +++++++++++++++++ .../edge/app/evcs/WebastoUniteEvcs.java | 200 +++++++++++++++++ .../edge/app/hardware/KMtronic8Channel.java | 2 +- .../io/openems/edge/app/heat/HeatPump.java | 2 +- .../openems/edge/app/heat/HeatingElement.java | 4 +- .../app/integratedsystem/FeneconHome20.java | 8 +- .../app/integratedsystem/FeneconHome30.java | 8 +- .../openems/edge/app/meter/JanitzaMeter.java | 28 +-- .../io/openems/edge/app/meter/KdkMeter.java | 13 +- .../edge/app/meter/MicrocareSdm630Meter.java | 15 +- .../openems/edge/app/meter/SocomecMeter.java | 14 +- .../edge/app/peakshaving/PeakShaving.java | 24 +- .../peakshaving/PhaseAccuratePeakShaving.java | 24 +- .../edge/app/pvinverter/SmaPvInverter.java | 33 ++- .../openems/edge/core/appmanager/AppDef.java | 8 + .../edge/core/appmanager/TranslationUtil.java | 71 +++++- .../formly/builder/SelectBuilder.java | 4 +- .../core/appmanager/translation_de.properties | 11 + .../core/appmanager/translation_en.properties | 11 + .../PredictorManagerImpl.java | 105 ++++----- .../src/io/openems/edge/core/sum/SumImpl.java | 20 +- .../edge/core/appmanager/AppDefTest.java | 66 ++++++ .../io/openems/edge/core/appmanager/Apps.java | 33 +++ .../core/appmanager/TestTranslations.java | 109 +++++++++ .../edge/core/predictormanager/MyConfig.java | 33 +++ .../PredictorManagerImplTest.java | 78 +++++++ .../ess/test/DummyManagedAsymmetricEss.java | 9 +- .../ess/test/DummyManagedSymmetricEss.java | 14 +- .../edge/ess/cluster/ChannelManager.java | 37 ++++ .../openems/edge/ess/cluster/EssCluster.java | 3 +- .../edge/ess/cluster/EssClusterImpl.java | 61 +++-- .../edge/ess/cluster/EssClusterImplTest.java | 36 +++ .../EvcsAlpitronicHyperchargerImpl.java | 2 +- .../io/openems/edge/io/api/AnalogOutput.java | 179 +++++++++++++++ .../edge/io/api/AnalogVoltageOutput.java | 123 +++++++++++ .../io/test/DummyAnalogVoltageOutput.java | 51 +++++ io.openems.edge.io.filipowski/.classpath | 12 + io.openems.edge.io.filipowski/.gitignore | 2 + io.openems.edge.io.filipowski/.project | 23 ++ .../org.eclipse.core.resources.prefs | 2 + io.openems.edge.io.filipowski/bnd.bnd | 15 ++ io.openems.edge.io.filipowski/readme.adoc | 18 ++ .../io/filipowski/analog/mr/AnalogOutput.java | 15 ++ .../edge/io/filipowski/analog/mr/Config.java | 34 +++ .../analog/mr/IoFilipowskiMrAo1.java | 21 ++ .../analog/mr/IoFilipowskiMrAo1Impl.java | 115 ++++++++++ io.openems.edge.io.filipowski/test/.gitignore | 0 .../analog/mr/IoFilipowskiMrAo1ImplTest.java | 27 +++ .../io/filipowski/analog/mr/MyConfig.java | 78 +++++++ .../edge/io/shelly/shelly25/IoShelly25.java | 3 + .../io/shelly/shellyplug/IoShellyPlug.java | 2 + .../edge/meter/phoenixcontact/Config.java | 3 + .../phoenixcontact/PhoenixContactMeter.java | 5 +- .../PhoenixContactMeterImpl.java | 38 +++- .../edge/meter/phoenixcontact/MyConfig.java | 11 + .../predictor/persistencemodel/Config.java | 2 +- .../predictor/similardaymodel/Config.java | 2 +- .../edge/timeofusetariff/entsoe/Config.java | 2 +- .../edge/timeofusetariff/entsoe/Token.java | 19 ++ .../timeofusetariff/entsoe/TouEntsoeImpl.java | 26 ++- tools/build-debian-package.sh | 18 +- tools/common.sh | 3 + tools/prepare-release.sh | 2 +- ui/karma.conf.local.js | 50 +++++ ui/package-lock.json | 51 ++--- ui/src/app/app-routing.module.ts | 14 +- .../app/edge/history/abstracthistorychart.ts | 5 +- .../app/edge/history/abstracthistorywidget.ts | 3 +- ui/src/app/edge/history/historydataservice.ts | 3 +- .../Io/HeatingElement/modal/modal.html | 4 +- .../component/update/update.component.ts | 10 +- .../settings/profile/profile.component.html | 30 ++- ui/src/app/index/index.module.ts | 2 +- ui/src/app/index/login.component.html | 6 +- ui/src/app/index/login.component.ts | 37 +++- ui/src/app/index/login.spec.ts | 29 +++ .../formly-field-checkbox-with-image.html | 21 ++ .../formly-field-checkbox-with-image.ts | 26 +++ .../chart/abstracthistorychart.ts | 7 +- .../modal/help-button/help-button.html | 5 +- .../genericComponents/shared/formatter.ts | 2 +- ui/src/app/shared/header/header.component.ts | 4 +- .../shared/pickdate/pickdate.component.html | 33 +-- .../pickdate/pickdate.component.spec.ts | 132 +++++++++++ .../app/shared/pickdate/pickdate.component.ts | 70 +++++- .../pickdate/popover/popover.component.ts | 6 +- ui/src/app/shared/service/service.ts | 9 +- ui/src/app/shared/shared.module.ts | 7 +- .../status/single/status.component.html | 2 +- .../shared/utils/dateutils/dateutils.spec.ts | 37 ++++ .../app/shared/utils/dateutils/dateutils.ts | 47 ++++ ui/src/assets/i18n/cz.json | 2 +- ui/src/assets/i18n/de.json | 15 +- ui/src/assets/i18n/en.json | 15 +- ui/src/assets/i18n/es.json | 2 +- ui/src/assets/i18n/fr.json | 2 +- ui/src/assets/i18n/nl.json | 2 +- ui/src/environments/index.ts | 11 +- ui/src/themes/openems/environments/theme.ts | 10 +- 134 files changed, 3621 insertions(+), 406 deletions(-) create mode 100644 io.openems.edge.controller.io.analog/.classpath create mode 100644 io.openems.edge.controller.io.analog/.gitignore create mode 100644 io.openems.edge.controller.io.analog/.project create mode 100644 io.openems.edge.controller.io.analog/.settings/org.eclipse.core.resources.prefs create mode 100644 io.openems.edge.controller.io.analog/bnd.bnd create mode 100644 io.openems.edge.controller.io.analog/readme.adoc create mode 100644 io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/Config.java create mode 100644 io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/ControllerIoAnalog.java create mode 100644 io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/ControllerIoAnalogImpl.java create mode 100644 io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/Mode.java create mode 100644 io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/PowerBehavior.java create mode 100644 io.openems.edge.controller.io.analog/test/.gitignore create mode 100644 io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyConfig.java create mode 100644 io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java create mode 100644 io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/TestStatic.java create mode 100644 io.openems.edge.core/src/io/openems/edge/app/ess/FixActivePower.java create mode 100644 io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoNextEvcs.java create mode 100644 io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoUniteEvcs.java create mode 100644 io.openems.edge.core/test/io/openems/edge/core/appmanager/AppDefTest.java create mode 100644 io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java create mode 100644 io.openems.edge.core/test/io/openems/edge/core/predictormanager/MyConfig.java create mode 100644 io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java create mode 100644 io.openems.edge.io.api/src/io/openems/edge/io/api/AnalogOutput.java create mode 100644 io.openems.edge.io.api/src/io/openems/edge/io/api/AnalogVoltageOutput.java create mode 100644 io.openems.edge.io.api/src/io/openems/edge/io/test/DummyAnalogVoltageOutput.java create mode 100644 io.openems.edge.io.filipowski/.classpath create mode 100644 io.openems.edge.io.filipowski/.gitignore create mode 100644 io.openems.edge.io.filipowski/.project create mode 100644 io.openems.edge.io.filipowski/.settings/org.eclipse.core.resources.prefs create mode 100644 io.openems.edge.io.filipowski/bnd.bnd create mode 100644 io.openems.edge.io.filipowski/readme.adoc create mode 100644 io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/AnalogOutput.java create mode 100644 io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/Config.java create mode 100644 io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1.java create mode 100644 io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1Impl.java create mode 100644 io.openems.edge.io.filipowski/test/.gitignore create mode 100644 io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1ImplTest.java create mode 100644 io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/MyConfig.java create mode 100644 io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Token.java create mode 100644 ui/karma.conf.local.js create mode 100644 ui/src/app/index/login.spec.ts create mode 100644 ui/src/app/shared/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.html create mode 100644 ui/src/app/shared/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.ts create mode 100644 ui/src/app/shared/pickdate/pickdate.component.spec.ts create mode 100644 ui/src/app/shared/utils/dateutils/dateutils.spec.ts create mode 100644 ui/src/app/shared/utils/dateutils/dateutils.ts diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index ffdde6a8492..46ee755cd74 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -89,6 +89,7 @@ bnd.identity;id='io.openems.edge.controller.generic.jsonlogic',\ bnd.identity;id='io.openems.edge.controller.highloadtimeslot',\ bnd.identity;id='io.openems.edge.controller.io.alarm',\ + bnd.identity;id='io.openems.edge.controller.io.analog',\ bnd.identity;id='io.openems.edge.controller.io.channelsinglethreshold',\ bnd.identity;id='io.openems.edge.controller.io.fixdigitaloutput',\ bnd.identity;id='io.openems.edge.controller.io.heatingelement',\ @@ -128,6 +129,7 @@ bnd.identity;id='io.openems.edge.fenecon.mini',\ bnd.identity;id='io.openems.edge.fenecon.pro',\ bnd.identity;id='io.openems.edge.goodwe',\ + bnd.identity;id='io.openems.edge.io.filipowski',\ bnd.identity;id='io.openems.edge.io.kmtronic',\ bnd.identity;id='io.openems.edge.io.offgridswitch',\ bnd.identity;id='io.openems.edge.io.revpi',\ @@ -245,6 +247,7 @@ io.openems.edge.controller.generic.jsonlogic;version=snapshot,\ io.openems.edge.controller.highloadtimeslot;version=snapshot,\ io.openems.edge.controller.io.alarm;version=snapshot,\ + io.openems.edge.controller.io.analog;version=snapshot,\ io.openems.edge.controller.io.channelsinglethreshold;version=snapshot,\ io.openems.edge.controller.io.fixdigitaloutput;version=snapshot,\ io.openems.edge.controller.io.heatingelement;version=snapshot,\ @@ -287,6 +290,7 @@ io.openems.edge.fenecon.pro;version=snapshot,\ io.openems.edge.goodwe;version=snapshot,\ io.openems.edge.io.api;version=snapshot,\ + io.openems.edge.io.filipowski;version=snapshot,\ io.openems.edge.io.kmtronic;version=snapshot,\ io.openems.edge.io.offgridswitch;version=snapshot,\ io.openems.edge.io.revpi;version=snapshot,\ diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/element/AbstractModbusElement.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/element/AbstractModbusElement.java index 1e6e41d73e7..e0911063482 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/element/AbstractModbusElement.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/element/AbstractModbusElement.java @@ -11,6 +11,8 @@ import io.openems.common.types.OpenemsType; import io.openems.edge.bridge.modbus.api.AbstractModbusBridge; +import io.openems.edge.bridge.modbus.api.task.AbstractReadTask; +import io.openems.edge.bridge.modbus.api.task.ReadTask; import io.openems.edge.common.type.TypeUtils; /** @@ -36,6 +38,13 @@ public abstract non-sealed class AbstractModbusElement + * By default ({@link FillElementsPriority#DEFAULT} all Element-to-Channel + * mappings are handled in array order of the {@link ReadTask}. Elements marked + * {@link FillElementsPriority#HIGH} are handled first. + * + *

+ * This feature is useful for SunSpec devices, where the dynamic ScaleFactor for + * a Element is only at the end of the SunSpec block. Without HIGH priority, the + * ScaleFactor would always get applied too late. + * + * @param fillElementsPriority the {@link FillElementsPriority} + * @return myself + */ + public SELF fillElementsPriority(FillElementsPriority fillElementsPriority) { + this.fillElementsPriority = fillElementsPriority; + return this.self(); + } + + /** + * FillElementsPriority. Used internally. + * + * @return the {@link FillElementsPriority} + */ + @Deprecated + public FillElementsPriority _getFillElementsPriority() { + return this.fillElementsPriority; + } + /** * Set the input/read value. * diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/task/AbstractReadTask.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/task/AbstractReadTask.java index e9384bf099a..66c104f6fe8 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/task/AbstractReadTask.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/api/task/AbstractReadTask.java @@ -1,6 +1,7 @@ package io.openems.edge.bridge.modbus.api.task; import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; import org.slf4j.Logger; @@ -12,6 +13,7 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.edge.bridge.modbus.api.AbstractModbusBridge; import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement; +import io.openems.edge.bridge.modbus.api.element.AbstractModbusElement.FillElementsPriority; import io.openems.edge.bridge.modbus.api.element.ModbusElement; import io.openems.edge.common.taskmanager.Priority; @@ -84,31 +86,42 @@ private static void validateResponse(Object[] response, int length) throws Opene * @param response the response values * @throws OpenemsException on error */ - @SuppressWarnings("unchecked") private void fillElements(T[] response) throws OpenemsException { - var position = 0; var errors = new ArrayList(); + this.fillElements(FillElementsPriority.HIGH, errors, response); + this.fillElements(FillElementsPriority.DEFAULT, errors, response); + + if (!errors.isEmpty()) { + throw new OpenemsException(String.join(", ", errors)); + } + } + + @SuppressWarnings("unchecked") + private void fillElements(FillElementsPriority priority, List errors, T[] response) { + var position = 0; + for (var element : this.elements) { - if (this.elementClazz.isInstance(element)) { - try { - this.handleResponse((ELEMENT) element, position, response); - } catch (OpenemsException e) { - errors.add("Unable to fill Modbus Element. " // - + element.toString() + " Error: " + e.getMessage()); + // Filter for FillElementsPriority + @SuppressWarnings("deprecation") + var thisPriority = ((AbstractModbusElement) element)._getFillElementsPriority(); + if (thisPriority == priority) { + if (this.elementClazz.isInstance(element)) { + try { + this.handleResponse((ELEMENT) element, position, response); + } catch (OpenemsException e) { + errors.add("Unable to fill Modbus Element. " // + + element.toString() + " Error: " + e.getMessage()); + } + } else { + errors.add("Wrong type while filling Modbus Element. " // + + element.toString() + " " // + + "Expected [" + this.elementClazz.getSimpleName() + "] " // + + "Got [" + element.getClass().getSimpleName() + "]"); } - } else { - errors.add("Wrong type while filling Modbus Element. " // - + element.toString() + " " // - + "Expected [" + this.elementClazz.getSimpleName() + "] " // - + "Got [" + element.getClass().getSimpleName() + "]"); } position = this.calculateNextPosition(element, position); } - - if (!errors.isEmpty()) { - throw new OpenemsException(String.join(", ", errors)); - } } @Override diff --git a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/SunSpecPoint.java b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/SunSpecPoint.java index 5d6affc4cde..0cc555f62d8 100644 --- a/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/SunSpecPoint.java +++ b/io.openems.edge.bridge.modbus/src/io/openems/edge/bridge/modbus/sunspec/SunSpecPoint.java @@ -1,5 +1,7 @@ package io.openems.edge.bridge.modbus.sunspec; +import static io.openems.edge.bridge.modbus.api.element.AbstractModbusElement.FillElementsPriority.HIGH; + import java.util.Optional; import io.openems.common.channel.AccessMode; @@ -109,7 +111,9 @@ public PointImpl(String channelId, String label, String description, String note public final ModbusElement generateModbusElement(Integer startAddress) { return switch (this.type) { case UINT16, ACC16, ENUM16, BITFIELD16 -> new UnsignedWordElement(startAddress); - case INT16, SUNSSF, COUNT -> new SignedWordElement(startAddress); + case SUNSSF -> new SignedWordElement(startAddress)// + .fillElementsPriority(HIGH); + case INT16, COUNT -> new SignedWordElement(startAddress); case UINT32, ACC32, ENUM32, BITFIELD32, IPADDR -> new UnsignedDoublewordElement(startAddress); case INT32 -> new SignedDoublewordElement(startAddress); case UINT64, ACC64 -> new UnsignedQuadruplewordElement(startAddress); @@ -118,8 +122,7 @@ public final ModbusElement generateModbusElement(Integer startAddress) { case PAD -> new DummyRegisterElement(startAddress); case FLOAT64 -> new FloatQuadruplewordElement(startAddress); case EUI48 -> null; - case IPV6ADDR - // TODO this would be UINT128 + case IPV6ADDR // TODO this would be UINT128 -> null; case STRING2 -> new StringWordElement(startAddress, 2); case STRING4 -> new StringWordElement(startAddress, 4); @@ -202,8 +205,7 @@ public static boolean isDefined(PointType type, Object value) { case UINT64 -> !value.equals(0xFFFFFFFFFFFFFFFFL); // TODO correct? case FLOAT32 -> !value.equals(Float.NaN); case FLOAT64 -> false; // TODO not implemented - case PAD - // This point is never needed/reserved + case PAD // This point is never needed/reserved -> false; case STRING12, STRING16, STRING2, STRING20, STRING25, STRING32, STRING4, STRING5, STRING6, STRING7, STRING8 -> diff --git a/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java b/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java index c434dc848b8..6987c541999 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java +++ b/io.openems.edge.common/src/io/openems/edge/common/sum/Sum.java @@ -442,6 +442,24 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { CONSUMPTION_MAX_ACTIVE_POWER(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT) // .persistencePriority(PersistencePriority.VERY_HIGH)), // + /** + * Unmanaged Consumption: Active Power. + * + *

    + *
  • Interface: Sum + *
  • Type: Integer + *
  • Unit: W + *
  • Range: should be only positive + *
  • Note: this value represents the part of the Consumption that is not + * actively managed by OpenEMS, i.e. it is calculated as + * ({@link #CONSUMPTION_ACTIVE_POWER}) minus charge power for an electric + * vehicle charging station, etc. This value is used for forecasting of + * consumption. + *
+ */ + UNMANAGED_CONSUMPTION_ACTIVE_POWER(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.VERY_HIGH)), // /** * Grid-Mode. * @@ -1602,6 +1620,35 @@ public default void _setConsumptionActivePower(int value) { this.getConsumptionActivePowerChannel().setNextValue(value); } + /** + * Gets the Channel for {@link ChannelId#UNMANAGED_CONSUMPTION_ACTIVE_POWER}. + * + * @return the Channel + */ + public default IntegerReadChannel getUnmanagedConsumptionActivePowerChannel() { + return this.channel(ChannelId.UNMANAGED_CONSUMPTION_ACTIVE_POWER); + } + + /** + * Gets the Unmanaged Consumption Active Power in [W]. See + * {@link ChannelId#UNMANAGED_CONSUMPTION_ACTIVE_POWER}. + * + * @return the Channel {@link Value} + */ + public default Value getUnmanagedConsumptionActivePower() { + return this.getUnmanagedConsumptionActivePowerChannel().value(); + } + + /** + * Internal method to set the 'nextValue' on + * {@link ChannelId#UNMANAGED_CONSUMPTION_ACTIVE_POWER} Channel. + * + * @param value the next value + */ + public default void _setUnmanagedConsumptionActivePower(Integer value) { + this.getUnmanagedConsumptionActivePowerChannel().setNextValue(value); + } + /** * Gets the Channel for {@link ChannelId#CONSUMPTION_ACTIVE_POWER_L1}. * diff --git a/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/Utils.java b/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/Utils.java index fcb5515a6eb..d78b02109bc 100644 --- a/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/Utils.java +++ b/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/Utils.java @@ -26,7 +26,7 @@ public static List getEdgeMetadata(Role role) { role, // Role true, // Is Online ZonedDateTime.now(), // now - ZonedDateTime.now(), // + null, // ControllerApiWebsocket.SUM_STATE // )); } diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedCharge.java b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedCharge.java index eb51845fc82..2ee38fba17a 100644 --- a/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedCharge.java +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/src/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedCharge.java @@ -28,12 +28,14 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { * Current state of the delayed charge function. */ DELAY_CHARGE_STATE(Doc.of(DelayChargeState.values()) // + .persistencePriority(PersistencePriority.HIGH) // .text("Current state of the delayed charge function.")), /** * Current state of the sell to grid limit function. */ SELL_TO_GRID_LIMIT_STATE(Doc.of(SellToGridLimitState.values()) // + .persistencePriority(PersistencePriority.HIGH) // .text("Current state of the sell to grid limit function.")), /** @@ -41,6 +43,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ DELAY_CHARGE_MAXIMUM_CHARGE_LIMIT(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.HIGH) // .text("Delay-Charge power limitation.")), // /** @@ -80,6 +83,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { */ SELL_TO_GRID_LIMIT_MINIMUM_CHARGE_LIMIT(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT) // + .persistencePriority(PersistencePriority.HIGH) // .text("Sell to grid limit charge power limitation in a readable AC format.")), /** @@ -120,6 +124,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { * Automatically set, when the original TARGET_MINUTE is set. */ TARGET_EPOCH_SECONDS(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH) // .text("Target minute as epoch seconds independent of the current mode Manual and Automatic.")), /** @@ -129,6 +134,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { * Target minute independent of the current mode Manual and Automatic. */ TARGET_MINUTE(new IntegerDoc() // + .persistencePriority(PersistencePriority.HIGH) // .text("Target minute independent of the current mode Manual and Automatic.") // .onChannelSetNextValue((self, value) -> { value.asOptional().ifPresent(targetTime -> { @@ -159,6 +165,7 @@ public enum ChannelId implements io.openems.edge.common.channel.ChannelId { * Keeps the time, when the delayed charge will start on the current day. */ PREDICTED_CHARGE_START_EPOCH_SECONDS(Doc.of(OpenemsType.LONG) // + .persistencePriority(PersistencePriority.HIGH) // .text("Time when the delayed charge will start on the current day.")), // /** diff --git a/io.openems.edge.controller.io.analog/.classpath b/io.openems.edge.controller.io.analog/.classpath new file mode 100644 index 00000000000..bbfbdbe40e7 --- /dev/null +++ b/io.openems.edge.controller.io.analog/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.controller.io.analog/.gitignore b/io.openems.edge.controller.io.analog/.gitignore new file mode 100644 index 00000000000..c2b941a96de --- /dev/null +++ b/io.openems.edge.controller.io.analog/.gitignore @@ -0,0 +1,2 @@ +/bin_test/ +/generated/ diff --git a/io.openems.edge.controller.io.analog/.project b/io.openems.edge.controller.io.analog/.project new file mode 100644 index 00000000000..530cb677aee --- /dev/null +++ b/io.openems.edge.controller.io.analog/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.controller.io.analog + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.controller.io.analog/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.controller.io.analog/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/io.openems.edge.controller.io.analog/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/io.openems.edge.controller.io.analog/bnd.bnd b/io.openems.edge.controller.io.analog/bnd.bnd new file mode 100644 index 00000000000..a0e0908cf01 --- /dev/null +++ b/io.openems.edge.controller.io.analog/bnd.bnd @@ -0,0 +1,15 @@ +Bundle-Name: OpenEMS Edge Controller IO Analog +Bundle-Vendor: FENECON GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 1.0.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + io.openems.common,\ + io.openems.edge.common,\ + io.openems.edge.controller.api,\ + io.openems.edge.io.api,\ + io.openems.edge.timedata.api + +-testpath: \ + ${testpath} diff --git a/io.openems.edge.controller.io.analog/readme.adoc b/io.openems.edge.controller.io.analog/readme.adoc new file mode 100644 index 00000000000..09d5ad64521 --- /dev/null +++ b/io.openems.edge.controller.io.analog/readme.adoc @@ -0,0 +1,7 @@ += IO Analog Controller + +Controller that sets a analog output according to the configured inputs to OFF|AUTOMATIC|ON. + +In AUTOMATIC mode the output is set dynamically depending on the current surplus power. + +https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.controller.io.analog[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/Config.java b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/Config.java new file mode 100644 index 00000000000..d974a60bdd3 --- /dev/null +++ b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/Config.java @@ -0,0 +1,40 @@ +package io.openems.edge.controller.io.analog; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "Controller IO Analog", // + description = "This Controller sets a analog output according to the configured inputs.") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "ctrlIoAnalog0"; + + @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") + String alias() default ""; + + @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") + boolean enabled() default true; + + @AttributeDefinition(name = "Mode", description = "Set the type of mode.") + Mode mode() default Mode.AUTOMATIC; + + @AttributeDefinition(name = "AnalogOutput-ID", description = "ID of the AnalogOutput") + String analogOutput_id() default "analogIo0"; + + @AttributeDefinition(name = "Target", description = "Target") + int manualTarget() default 0; + + @AttributeDefinition(name = "Maximum power", description = "Maximum power of the hardware device.") + int maximumPower() default 12_000; + + @AttributeDefinition(name = "Power behaviour", description = "Power output of the Device is behaving linear or non-linear (leading/trailing edge)") + PowerBehavior powerBehaviour() default PowerBehavior.LINEAR; + + @AttributeDefinition(name = "AnalogOutput target filter", description = "This is auto-generated by 'AnalogOutput-ID'.") + String analogOutput_target() default "(enabled=true)"; + + String webconsole_configurationFactory_nameHint() default "Controller IO Analog [{id}]"; + +} \ No newline at end of file diff --git a/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/ControllerIoAnalog.java b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/ControllerIoAnalog.java new file mode 100644 index 00000000000..15d9220d3c5 --- /dev/null +++ b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/ControllerIoAnalog.java @@ -0,0 +1,30 @@ +package io.openems.edge.controller.io.analog; + +import static io.openems.common.channel.PersistencePriority.HIGH; +import static io.openems.common.types.OpenemsType.LONG; + +import io.openems.common.channel.Unit; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.controller.api.Controller; + +public interface ControllerIoAnalog extends Controller, OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + CUMULATED_ACTIVE_ENERGY(Doc.of(LONG)// + .unit(Unit.CUMULATED_WATT_HOURS) // + .persistencePriority(HIGH)) // + ; // + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } +} diff --git a/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/ControllerIoAnalogImpl.java b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/ControllerIoAnalogImpl.java new file mode 100644 index 00000000000..54cdb85baf4 --- /dev/null +++ b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/ControllerIoAnalogImpl.java @@ -0,0 +1,209 @@ +package io.openems.edge.controller.io.analog; + +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.sum.Sum; +import io.openems.edge.common.type.TypeUtils; +import io.openems.edge.controller.api.Controller; +import io.openems.edge.io.api.AnalogOutput; +import io.openems.edge.timedata.api.Timedata; +import io.openems.edge.timedata.api.TimedataProvider; +import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "Controller.IO.Analog", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class ControllerIoAnalogImpl extends AbstractOpenemsComponent + implements ControllerIoAnalog, Controller, OpenemsComponent, TimedataProvider { + + /* + * Debounce, to avoid calculations based on deprecated consumption values in + * automatic mode. + */ + private static final int DEFAULT_DEBOUNCE_SEC = 3; + + private final Logger log = LoggerFactory.getLogger(ControllerIoAnalogImpl.class); + + private final CalculateEnergyFromPower calculateCumulatedEnergy = new CalculateEnergyFromPower(this, + ControllerIoAnalog.ChannelId.CUMULATED_ACTIVE_ENERGY); + + @Reference + private ConfigurationAdmin cm; + + @Reference + private Sum sum; + + @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private volatile Timedata timedata = null; + + @Reference(policyOption = ReferencePolicyOption.GREEDY) + private AnalogOutput analogOutput; + + private Instant nextTarget = Instant.MIN; + private Clock clock; + private int lastOutputPower; + private Config config = null; + + public ControllerIoAnalogImpl() { + this(Clock.systemDefaultZone()); + } + + public ControllerIoAnalogImpl(Clock clock) { + super(// + OpenemsComponent.ChannelId.values(), // + Controller.ChannelId.values(), // + ControllerIoAnalog.ChannelId.values() // + ); + this.clock = clock; + } + + @Activate + private void activate(ComponentContext context, Config config) { + this.config = config; + super.activate(context, config.id(), config.alias(), config.enabled()); + + if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "analogOutput", + config.analogOutput_id())) { + return; + } + } + + @Modified + private void modified(ComponentContext context, Config config) throws OpenemsNamedException { + this.config = config; + super.modified(context, config.id(), config.alias(), config.enabled()); + } + + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + public void run() throws OpenemsNamedException { + switch (this.config.mode()) { + case ON -> this.setOutput(this.config.manualTarget()); + case OFF -> this.setOutput(0); + case AUTOMATIC -> this.automaticMode(); + } + } + + private void automaticMode() throws OpenemsNamedException { + + // TODO: hysteresis + + if (this.nextTarget.isBefore(Instant.now(this.clock))) { + + var gridActivePower = this.sum.getGridActivePower().getOrError(); + + // Surplus used by the ess + var essDischargePower = this.sum.getEssDischargePower().orElse(0); + essDischargePower = Math.max(essDischargePower, 0); + + int excessPower; + if (gridActivePower > 0) { + excessPower = 0; + } else { + + var usedPower = calculateUsedPower(this.config.maximumPower(), + this.analogOutput.getDebugSetOutputPercent().orElse(0f), this.config.powerBehaviour()); + excessPower = gridActivePower * -1 - essDischargePower + usedPower; + } + + excessPower = TypeUtils.fitWithin(0, this.config.maximumPower(), excessPower); + this.setOutput(excessPower); + this.nextTarget = Instant.now(this.clock).plus(DEFAULT_DEBOUNCE_SEC, ChronoUnit.SECONDS); + } else { + // Set previous output + this.setOutput(this.lastOutputPower); + } + } + + /** + * Calculate the current power depending on the current settings. + * + *

+ * Attention: Even if the "Power Behaviour" is defining the hardware behavior, + * the real consumption depends on the device itself and we have to assume that + * the unit behaves in a similar way. For the exact values, a separate meter + * would be needed. + * + * @param maximumPower maximum power of the device + * @param currentOutputPercent current output in % + * @param powerBehavior the power behavior as {@link PowerBehavior} + * @return power used by the device + */ + protected static int calculateUsedPower(int maximumPower, float currentOutputPercent, PowerBehavior powerBehavior) { + + var factor = currentOutputPercent / (float) AnalogOutput.SET_OUTPUT_ACCURACY; + return powerBehavior.calculatePowerFromFactor.apply(maximumPower, factor); + } + + /** + * Calculate the set point depending on the current settings. + * + *

+ * Attention: Even if the "Power Behaviour" is defining the hardware behavior, + * the real consumption depends on the device itself and we have to assume that + * the unit behaves in a similar way and is using the calculated power. + * + * @param maximumPower maximum power of the device + * @param targetPower target power + * @param powerBehavior power behavior + * @return current set point in % + */ + protected static float calculateSetPointFromPower(int maximumPower, int targetPower, PowerBehavior powerBehavior) { + + var factor = powerBehavior.calculateFactorFromPower.apply(maximumPower, targetPower); + return factor * (float) AnalogOutput.SET_OUTPUT_ACCURACY; + } + + /** + * Helper function to set the output & updates the cumulated active energy + * channel. + * + * @param power target power value + * @throws OpenemsNamedException on error + */ + private void setOutput(int power) throws OpenemsNamedException { + + // Update the cumulated Energy. + this.calculateCumulatedEnergy.update(power); + + var outputPercent = calculateSetPointFromPower(this.config.maximumPower(), power, this.config.powerBehaviour()); + var currentValue = this.analogOutput.getDebugSetOutputPercent(); + if (!currentValue.isDefined() || currentValue.get() != outputPercent) { + this.logInfo(this.log, "Set output [" + this.config.analogOutput_id() + "] to " + power + "W."); + this.analogOutput.setOutputPercent(outputPercent); + this.lastOutputPower = power; + } + } + + @Override + public Timedata getTimedata() { + return this.timedata; + } +} diff --git a/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/Mode.java b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/Mode.java new file mode 100644 index 00000000000..eb3104ecced --- /dev/null +++ b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/Mode.java @@ -0,0 +1,32 @@ +package io.openems.edge.controller.io.analog; + +import io.openems.common.types.OptionsEnum; + +public enum Mode implements OptionsEnum { + ON(0, "ON signal"), // + OFF(1, "OFF signal"), // + AUTOMATIC(2, "Automatic control"); // + + private final int value; + private final String name; + + private Mode(int value, String name) { + this.value = value; + this.name = name; + } + + @Override + public int getValue() { + return this.value; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public OptionsEnum getUndefined() { + return AUTOMATIC; + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/PowerBehavior.java b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/PowerBehavior.java new file mode 100644 index 00000000000..4c7924c9488 --- /dev/null +++ b/io.openems.edge.controller.io.analog/src/io/openems/edge/controller/io/analog/PowerBehavior.java @@ -0,0 +1,69 @@ +package io.openems.edge.controller.io.analog; + +import java.util.function.BiFunction; + +public enum PowerBehavior { + LINEAR("Linear", new CalculateLinearFactor(), new CalculateLinearPower()), // + NON_LINEAR("Nonlinear", new CalculateNonLinearFactor(), new CalculateNonLinearPower()); + + public final String name; + public final BiFunction calculateFactorFromPower; + public final BiFunction calculatePowerFromFactor; + + private PowerBehavior(String name, BiFunction calculateFactorFromPower, + BiFunction calculatePowerFromFactor) { + this.name = name; + this.calculateFactorFromPower = calculateFactorFromPower; + this.calculatePowerFromFactor = calculatePowerFromFactor; + } + + private static class CalculateLinearFactor implements BiFunction { + + @Override + public Float apply(Integer maximumPower, Integer power) { + if (maximumPower == null || power == null) { + return null; + } + + return power / maximumPower.floatValue(); + } + } + + private static class CalculateNonLinearFactor implements BiFunction { + + @Override + public Float apply(Integer maximumPower, Integer power) { + if (maximumPower == null || power == null) { + return null; + } + + var linearFactor = power / maximumPower.floatValue(); + return (float) (Math.acos(1 - (2 * linearFactor)) / Math.PI); + } + } + + private static class CalculateLinearPower implements BiFunction { + + @Override + public Integer apply(Integer maximumPower, Float factor) { + if (maximumPower == null || factor == null) { + return null; + } + + return Math.round(maximumPower * factor); + } + } + + private static class CalculateNonLinearPower implements BiFunction { + + @Override + public Integer apply(Integer maximumPower, Float factor) { + if (maximumPower == null || factor == null) { + return null; + } + + var linearFactor = ((1 - Math.cos(factor * Math.PI)) / 2); + return (int) Math.round(maximumPower * linearFactor); + } + } +} diff --git a/io.openems.edge.controller.io.analog/test/.gitignore b/io.openems.edge.controller.io.analog/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyConfig.java b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyConfig.java new file mode 100644 index 00000000000..5d739548b9f --- /dev/null +++ b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyConfig.java @@ -0,0 +1,101 @@ +package io.openems.edge.controller.io.analog; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.utils.ConfigUtils; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String analogOutputId; + private Mode mode; + private int manualTarget; + private int maximumPower; + private PowerBehavior powerBehaviour; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + + public Builder setAnalogOutputId(String analogOutputId) { + this.analogOutputId = analogOutputId; + return this; + } + + public Builder setMode(Mode mode) { + this.mode = mode; + return this; + } + + public Builder setManualTarget(int manualTarget) { + this.manualTarget = manualTarget; + return this; + } + + public Builder setMaximumPower(int maximumPower) { + this.maximumPower = maximumPower; + return this; + } + + public Builder setPowerBehaviour(PowerBehavior powerBehaviour) { + this.powerBehaviour = powerBehaviour; + return this; + } + + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String analogOutput_id() { + return this.builder.analogOutputId; + } + + @Override + public String analogOutput_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.analogOutput_id()); + } + + @Override + public Mode mode() { + return this.builder.mode; + } + + @Override + public int manualTarget() { + return this.builder.manualTarget; + } + + @Override + public int maximumPower() { + return this.builder.maximumPower; + } + + @Override + public PowerBehavior powerBehaviour() { + return this.builder.powerBehaviour; + } +} \ No newline at end of file diff --git a/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java new file mode 100644 index 00000000000..f5b3ad37048 --- /dev/null +++ b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java @@ -0,0 +1,155 @@ +package io.openems.edge.controller.io.analog; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; + +import org.junit.Test; + +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.sum.DummySum; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.common.test.TimeLeapClock; +import io.openems.edge.controller.test.ControllerTest; +import io.openems.edge.io.api.AnalogOutput.Range; +import io.openems.edge.io.test.DummyAnalogVoltageOutput; +import io.openems.edge.timedata.test.DummyTimedata; + +public class MyControllerTest { + + private static final String CTRL_ID = "ctrl0"; + private static final String IO_ID = "analogIo0"; + + // Sum channels + private static final ChannelAddress SUM_ESS_DISCHARGE_POWER = new ChannelAddress("_sum", "EssDischargePower"); + private static final ChannelAddress SUM_GRID_ACTIVE_POWER = new ChannelAddress("_sum", "GridActivePower"); + + // AnalogIO channels + private static final ChannelAddress DEBUG_SET_OUTPUT_VOLTAGE = new ChannelAddress(IO_ID, "DebugSetOutputVoltage"); + private static final ChannelAddress DEBUG_SET_OUTPUT_PERCENT = new ChannelAddress(IO_ID, "DebugSetOutputPercent"); + + @Test + public void testOff() throws Exception { + + final var analogOutput = new DummyAnalogVoltageOutput(IO_ID); + final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + new ControllerTest(new ControllerIoAnalogImpl(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("sum", new DummySum()) // + .addReference("timedata", new DummyTimedata("timedata0")) // + .addReference("analogOutput", analogOutput) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setAnalogOutputId(IO_ID) // + .setManualTarget(6_000) // + .setMaximumPower(10_000) // + .setMode(Mode.OFF) // + .setPowerBehaviour(PowerBehavior.LINEAR) // + .build()) + .next(new TestCase() // + .input(SUM_ESS_DISCHARGE_POWER, 2000) // + .input(SUM_GRID_ACTIVE_POWER, -5000)// + .output(DEBUG_SET_OUTPUT_PERCENT, 0f) // + .output(DEBUG_SET_OUTPUT_VOLTAGE, 0)) // + ; + } + + @Test + public void testOn() throws Exception { + + final var analogOutput = new DummyAnalogVoltageOutput(IO_ID, new Range(0, 100, 10000)); + final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + new ControllerTest(new ControllerIoAnalogImpl(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("sum", new DummySum()) // + .addReference("timedata", new DummyTimedata("timedata0")) // + .addReference("analogOutput", analogOutput) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setAnalogOutputId(IO_ID) // + .setManualTarget(6_000) // + .setMaximumPower(10_000) // + .setMode(Mode.ON) // + .setPowerBehaviour(PowerBehavior.LINEAR) // + .build()) + .next(new TestCase() // + .input(SUM_ESS_DISCHARGE_POWER, 0) // + .input(SUM_GRID_ACTIVE_POWER, -5000)// + .output(DEBUG_SET_OUTPUT_PERCENT, 60.000004f) // + .output(DEBUG_SET_OUTPUT_VOLTAGE, 6000)) // + ; + + new ControllerTest(new ControllerIoAnalogImpl(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("sum", new DummySum()) // + .addReference("timedata", new DummyTimedata("timedata0")) // + .addReference("analogOutput", analogOutput) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setAnalogOutputId(IO_ID) // + .setManualTarget(0) // + .setMaximumPower(10_000) // + .setMode(Mode.ON) // + .setPowerBehaviour(PowerBehavior.LINEAR) // + .build()) + .next(new TestCase() // + .input(SUM_ESS_DISCHARGE_POWER, 0) // + .input(SUM_GRID_ACTIVE_POWER, -5000)// + .output(DEBUG_SET_OUTPUT_PERCENT, 0f) // + .output(DEBUG_SET_OUTPUT_VOLTAGE, 0)) // + ; + } + + @Test + public void testAutomatic() throws Exception { + + final var analogOutput = new DummyAnalogVoltageOutput(IO_ID); + final var clock = new TimeLeapClock(Instant.parse("2020-01-01T01:00:00.00Z"), ZoneOffset.UTC); + new ControllerTest(new ControllerIoAnalogImpl(clock)) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("sum", new DummySum()) // + .addReference("timedata", new DummyTimedata("timedata0")) // + .addReference("analogOutput", analogOutput) // + .activate(MyConfig.create() // + .setId(CTRL_ID) // + .setAnalogOutputId(IO_ID) // + .setManualTarget(6_000) // + .setMaximumPower(10_000) // + .setMode(Mode.AUTOMATIC) // + .setPowerBehaviour(PowerBehavior.LINEAR) // + .build()) + .next(new TestCase() // + .input(SUM_ESS_DISCHARGE_POWER, 0) // + .input(SUM_GRID_ACTIVE_POWER, -5000)// + .output(DEBUG_SET_OUTPUT_PERCENT, 50f) // + .output(DEBUG_SET_OUTPUT_VOLTAGE, 5000)) // + .next(new TestCase() // + .input(SUM_ESS_DISCHARGE_POWER, 0) // + .input(SUM_GRID_ACTIVE_POWER, -2444)// + .output(DEBUG_SET_OUTPUT_PERCENT, 50f) // + .output(DEBUG_SET_OUTPUT_VOLTAGE, 5000)) // + + .next(new TestCase() // + .timeleap(clock, 4, ChronoUnit.SECONDS) // + .input(SUM_ESS_DISCHARGE_POWER, 0) // + .input(SUM_GRID_ACTIVE_POWER, -2444)// + .output(DEBUG_SET_OUTPUT_PERCENT, 24.439999f) // + .output(DEBUG_SET_OUTPUT_VOLTAGE, 2400)) // 100mV Steps + + .next(new TestCase() // + .timeleap(clock, 4, ChronoUnit.SECONDS) // + .input(SUM_ESS_DISCHARGE_POWER, 0) // + .input(SUM_GRID_ACTIVE_POWER, -12444)// + .output(DEBUG_SET_OUTPUT_PERCENT, 100f) // + .output(DEBUG_SET_OUTPUT_VOLTAGE, 10000)) // + + .next(new TestCase() // + .timeleap(clock, 4, ChronoUnit.SECONDS) // + .input(SUM_ESS_DISCHARGE_POWER, 0) // + .input(SUM_GRID_ACTIVE_POWER, 1000)// + .output(DEBUG_SET_OUTPUT_PERCENT, 0f) // + .output(DEBUG_SET_OUTPUT_VOLTAGE, 0)) // + ; + } +} diff --git a/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/TestStatic.java b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/TestStatic.java new file mode 100644 index 00000000000..ccd1eac01ad --- /dev/null +++ b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/TestStatic.java @@ -0,0 +1,61 @@ +package io.openems.edge.controller.io.analog; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class TestStatic { + + @Test + public void calculateUsedPower() { + + assertEquals(6_000, (int) ControllerIoAnalogImpl.calculateUsedPower(12_000, 50f, PowerBehavior.LINEAR)); + + assertEquals(2_400, (int) ControllerIoAnalogImpl.calculateUsedPower(12_000, 20f, PowerBehavior.LINEAR)); + + assertEquals(9_000, (int) ControllerIoAnalogImpl.calculateUsedPower(12_000, 75f, PowerBehavior.LINEAR)); + + assertEquals(6_000, (int) ControllerIoAnalogImpl.calculateUsedPower(12_000, 50f, PowerBehavior.NON_LINEAR)); + + assertEquals(1_146, (int) ControllerIoAnalogImpl.calculateUsedPower(12_000, 20f, PowerBehavior.NON_LINEAR)); + + assertEquals(10_243, (int) ControllerIoAnalogImpl.calculateUsedPower(12_000, 75f, PowerBehavior.NON_LINEAR)); + + } + + @Test + public void powerBehaviour() { + + // Linear Factor from Power + assertEquals((Float) 0.2f, PowerBehavior.LINEAR.calculateFactorFromPower.apply(10000, 2000)); + + // Non-linear Factor from Power (e.g. for (leading/trailing edge) devices) + assertEquals((Float) 0.29516724f, PowerBehavior.NON_LINEAR.calculateFactorFromPower.apply(10000, 2000)); + + // Linear Power from Factor + assertEquals((Integer) 2000, PowerBehavior.LINEAR.calculatePowerFromFactor.apply(10000, 0.2f)); + + // Non-linear Power from Factor + assertEquals((Integer) 2000, PowerBehavior.NON_LINEAR.calculatePowerFromFactor.apply(10000, 0.29516724f)); + + /* + * Null values + */ + assertNull(PowerBehavior.LINEAR.calculateFactorFromPower.apply(null, null)); + assertNull(PowerBehavior.LINEAR.calculateFactorFromPower.apply(null, 2000)); + assertNull(PowerBehavior.LINEAR.calculateFactorFromPower.apply(2000, null)); + assertNull(PowerBehavior.NON_LINEAR.calculateFactorFromPower.apply(null, null)); + assertNull(PowerBehavior.NON_LINEAR.calculateFactorFromPower.apply(null, 2000)); + assertNull(PowerBehavior.NON_LINEAR.calculateFactorFromPower.apply(2000, null)); + + /* + * Range 0 - 10000 W in 100 W steps + */ + for (var i = 0; i <= 10000; i += 100) { + assertEquals((Integer) i, // + PowerBehavior.NON_LINEAR.calculatePowerFromFactor.apply(10000, // + PowerBehavior.NON_LINEAR.calculateFactorFromPower.apply(10000, i))); + } + } +} diff --git a/io.openems.edge.core/bnd.bnd b/io.openems.edge.core/bnd.bnd index 46caa7deb27..670793f4591 100644 --- a/io.openems.edge.core/bnd.bnd +++ b/io.openems.edge.core/bnd.bnd @@ -10,6 +10,7 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.controller.api,\ io.openems.edge.controller.api.backend,\ io.openems.edge.ess.api,\ + io.openems.edge.evcs.api,\ io.openems.edge.io.api,\ io.openems.edge.meter.api,\ io.openems.edge.predictor.api,\ diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java index cbb4e8f98cf..1f7b67ba2fc 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java @@ -1,5 +1,7 @@ package io.openems.edge.app.api; +import static io.openems.edge.app.common.props.CommonProps.alias; + import java.util.Map; import java.util.function.Function; @@ -63,8 +65,7 @@ public static enum Property implements Type { var active = app.componentManager.getEdgeConfig() diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java index 04112baf2af..50b5ab76a68 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java @@ -76,9 +76,9 @@ public static enum Property implements Type { - field.isRequired(true) // - .setInputType(NUMBER) // + field.setInputType(NUMBER) // .setMin(0); }) // )), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadOnly.java b/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadOnly.java index 9b1658bac6c..d0c5923fb5d 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadOnly.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadOnly.java @@ -1,5 +1,7 @@ package io.openems.edge.app.api; +import static io.openems.edge.app.common.props.CommonProps.alias; + import java.util.Map; import java.util.function.Function; @@ -64,8 +66,7 @@ public static enum Property implements Type { var active = app.componentManager.getEdgeConfig() @@ -75,14 +76,14 @@ public static enum Property implements Type def; + private final AppDef def; - private Property(AppDef def) { + private Property(AppDef def) { this.def = def; } @Override - public AppDef def() { + public AppDef def() { return this.def; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/common/props/CommonProps.java b/io.openems.edge.core/src/io/openems/edge/app/common/props/CommonProps.java index ba4c29b5d52..c487cf2332c 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/common/props/CommonProps.java +++ b/io.openems.edge.core/src/io/openems/edge/app/common/props/CommonProps.java @@ -40,7 +40,8 @@ public static final AppDef alias() { return AppDef.copyOfGeneric(defaultDef(), def -> def // .setTranslatedLabel("alias") // .setDefaultValueToAppName() // - .setField(JsonFormlyUtil::buildInputFromNameable)); + .setField(JsonFormlyUtil::buildInputFromNameable)) // + .setRequired(true); } /** diff --git a/io.openems.edge.core/src/io/openems/edge/app/enums/OptionsFactory.java b/io.openems.edge.core/src/io/openems/edge/app/enums/OptionsFactory.java index 0830a219621..c05f89ed3fd 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/enums/OptionsFactory.java +++ b/io.openems.edge.core/src/io/openems/edge/app/enums/OptionsFactory.java @@ -1,10 +1,9 @@ package io.openems.edge.app.enums; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; -import java.util.stream.Collectors; import io.openems.common.session.Language; @@ -33,7 +32,7 @@ public interface OptionsFactory { public static OptionsFactory of(TranslatableEnum[] values) { return l -> Arrays.stream(values) // .map(e -> Map.entry(e.getTranslation(l), e.getValue())) // - .collect(Collectors.toSet()); + .toList(); } /** @@ -57,5 +56,5 @@ public static & TranslatableEnum> OptionsFactory of(Class * @param l the language of the options * @return the options where the key is the label and the value the value */ - public Set> options(Language l); + public List> options(Language l); } \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/app/ess/FixActivePower.java b/io.openems.edge.core/src/io/openems/edge/app/ess/FixActivePower.java new file mode 100644 index 00000000000..4b3884f8c73 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/ess/FixActivePower.java @@ -0,0 +1,172 @@ +package io.openems.edge.app.ess; + +import static io.openems.edge.app.common.props.CommonProps.alias; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.common.collect.Lists; +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.ComponentProps; +import io.openems.edge.app.enums.Phase; +import io.openems.edge.app.ess.FixActivePower.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.Nameable; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppPermissions; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; + +/** + * Describes a fix active power app. + * + *

+  {
+    "appId":"App.Ess.FixActivePower",
+    "alias":"Leistungsvorgabe",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"ESS_ID": "ess0"
+    },
+    "appDescriptor": {
+    	"websiteUrl": {@link AppDescriptor#getWebsiteUrl()}
+    }
+  }
+ * 
+ */ +@Component(name = "App.Ess.FixActivePower") +public class FixActivePower extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public enum Property implements Type, Nameable { + // Components + CTRL_FIX_ACTIVE_POWER_ID(AppDef.componentId("ctrlFixActivePower0")), // + + // Properties + ALIAS(alias()), // + ESS_ID(ComponentProps.pickManagedSymmetricEssId()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Property self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public FixActivePower(// + @Reference final ComponentManager componentManager, // + final ComponentContext componentContext, // + @Reference final ConfigurationAdmin cm, // + @Reference final ComponentUtil componentUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.ESS }; + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE; + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var ctrlFixActivePowerId = this.getId(t, p, Property.CTRL_FIX_ACTIVE_POWER_ID); + + final var alias = this.getString(p, l, Property.ALIAS); + final var essId = this.getString(p, Property.ESS_ID); + + final var components = Lists.newArrayList(// + new EdgeConfig.Component(ctrlFixActivePowerId, alias, "Controller.Ess.FixActivePower", // + JsonUtils.buildJsonObject() // + .addProperty("enabled", true) // + .addProperty("ess_id", essId) // + .onlyIf(t == ConfigurationTarget.ADD, // + b -> b.addProperty("mode", "MANUAL_OFF") // + .addProperty("hybridEssMode", "TARGET_DC") // + .addProperty("power", 0) // + .addProperty("relationship", "EQUALS") // + .addProperty("phase", Phase.ALL)) // + .build()) // + ); + + // TODO improve scheduler configuration + final var schedulerIds = Lists.newArrayList(// + ctrlFixActivePowerId, // + "ctrlPrepareBatteryExtension0", // + "ctrlEmergencyCapacityReserve0", // + "ctrlGridOptimizedCharge0" // + ); + + return new AppConfiguration(components, schedulerIds); + }; + } + + @Override + public OpenemsAppPermissions getAppPermissions() { + return OpenemsAppPermissions.create() // + .setCanSee(Role.ADMIN) // + .build(); + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + protected FixActivePower getApp() { + return this; + } +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/ess/PrepareBatteryExtension.java b/io.openems.edge.core/src/io/openems/edge/app/ess/PrepareBatteryExtension.java index ea29fcdebb4..8f339c4e3f5 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/ess/PrepareBatteryExtension.java +++ b/io.openems.edge.core/src/io/openems/edge/app/ess/PrepareBatteryExtension.java @@ -1,5 +1,7 @@ package io.openems.edge.app.ess; +import static io.openems.edge.app.common.props.CommonProps.alias; + import java.util.Map; import java.util.function.Function; @@ -67,20 +69,19 @@ public enum Property implements Type f.isRequired(true) // - .setMin(0) // - .setMax(100))), // - ; + .setRequired(true) // + .setField(JsonFormlyUtil::buildRange, (app, prop, l, param, field) -> { + field.setMin(0) // + .setMax(100); + })); - private final AppDef def; + private final AppDef def; - private Property(AppDef def) { + private Property(AppDef def) { this.def = def; } @@ -90,7 +91,7 @@ public Property self() { } @Override - public AppDef def() { + public AppDef def() { return this.def; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/DezonyEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/DezonyEvcs.java index b30a495d6a2..d9fb85b42cc 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/DezonyEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/DezonyEvcs.java @@ -1,5 +1,7 @@ package io.openems.edge.app.evcs; +import static io.openems.edge.app.common.props.CommonProps.alias; + import java.util.Map; import java.util.OptionalInt; import java.util.function.Function; @@ -69,13 +71,13 @@ public enum Property implements Type def.setDefaultValue("192.168.50.88") // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true)))), // + .setRequired(true))), // PORT(AppDef.copyOfGeneric(CommunicationProps.port(), // def -> def.setDefaultValue(5000) // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true)))), // + .setRequired(true))), // MAX_HARDWARE_POWER_ACCEPT_PROPERTY(AppDef.of() // .setAllowedToSave(false)), // MAX_HARDWARE_POWER(EvcsProps.clusterMaxHardwarePowerSingleCp(MAX_HARDWARE_POWER_ACCEPT_PROPERTY, EVCS_ID)), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java index 0fa0b6db699..f72a5bf9287 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java @@ -1,5 +1,6 @@ package io.openems.edge.app.evcs; +import static io.openems.edge.app.common.props.CommonProps.alias; import static io.openems.edge.core.appmanager.formly.builder.SelectBuilder.DEFAULT_COMPONENT_2_LABEL; import static io.openems.edge.core.appmanager.formly.builder.SelectBuilder.DEFAULT_COMPONENT_2_VALUE; import static io.openems.edge.core.appmanager.formly.enums.InputType.NUMBER; @@ -84,17 +85,16 @@ public static enum Property implements Type { f.setOptions( app.getComponentUtil().getEnabledComponentsOfStartingId("evcs").stream() .filter(t -> !t.id().startsWith("evcsCluster")).toList(), DEFAULT_COMPONENT_2_LABEL, DEFAULT_COMPONENT_2_VALUE) // - .isRequired(true) // .isMulti(true); }) // .setDefaultValue((app, property, l, parameter) -> new JsonArray()) // @@ -103,10 +103,10 @@ public static enum Property implements Type field.setInputType(NUMBER) // .setMin(0) // - .isRequired(true) // .setUnit(Unit.WATT, l)) // .bidirectional(EVCS_CLUSTER_ID, "hardwarePowerLimitPerPhase", ComponentManagerSupplier::getComponentManager, AppDef.multiplyWith(3)))), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java index 9fdba3b83d1..80e957ac45b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java @@ -169,10 +169,11 @@ public Function, BundleParameter> getParamter public enum SubPropertyFirstChargepoint implements PropertyParent { ALIAS(AppDef.copyOfGeneric(CommonProps.alias()) // .setAutoGenerateField(false) // + .setRequired(true) // .setDefaultValue((app, property, l, parameter) -> // new JsonPrimitive(TranslationUtil.getTranslation(parameter.bundle(), "App.Evcs.HardyBarth.alias.value", // TranslationUtil.getTranslation(parameter.bundle(), "right")))) // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true) // + .wrapField((app, property, l, parameter, field) -> field .setDefaultValueCases(new DefaultValueOptions(Property.NUMBER_OF_CHARGING_STATIONS, // new Case(1, app.getName(l)), // new Case(2, TranslationUtil.getTranslation(parameter.bundle(), // @@ -181,7 +182,7 @@ public enum SubPropertyFirstChargepoint implements PropertyParent { IP(AppDef.copyOfGeneric(CommunicationProps.ip()) // .setDefaultValue("192.168.25.30") // .setAutoGenerateField(false) // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true))), // + .setRequired(true)), // ; private final AppDef def; @@ -232,11 +233,11 @@ public enum SubPropertySecondChargepoint implements PropertyParent { .setDefaultValue((app, property, l, parameter) -> // new JsonPrimitive(TranslationUtil.getTranslation(parameter.bundle(), "App.Evcs.HardyBarth.alias.value", // TranslationUtil.getTranslation(parameter.bundle(), "left")))) // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true))), // + .setRequired(true)), // IP_CP_2(AppDef.copyOfGeneric(CommunicationProps.ip()) // .setDefaultValue("192.168.25.31") // .setAutoGenerateField(false) // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true))), // + .setRequired(true)), // ; private final AppDef def; diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java index 97007ccfcf1..db71aec8305 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java @@ -75,15 +75,14 @@ public static enum Property implements Type // - field.isRequired(true))), // + .setRequired(true)), // OCCP_CONNECTOR_IDENTIFIER(AppDef.of(IesKeywattEvcs.class) // .setTranslatedLabelWithAppPrefix(".connector.label") // .setTranslatedDescriptionWithAppPrefix(".connector.description") // .setDefaultValue(1) // + .setRequired(true) // .setField(JsonFormlyUtil::buildInputFromNameable, (app, property, l, parameter, field) -> // field.setInputType(NUMBER) // - .isRequired(true) // .setMin(0))), // MAX_HARDWARE_POWER_ACCEPT_PROPERTY(AppDef.of() // .setAllowedToSave(false)), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java index 67801bf87ee..d082b46e052 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java @@ -1,5 +1,7 @@ package io.openems.edge.app.evcs; +import static io.openems.edge.app.common.props.CommonProps.alias; + import java.util.Map; import java.util.OptionalInt; import java.util.function.Function; @@ -18,7 +20,6 @@ import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.common.utils.JsonUtils; -import io.openems.edge.app.common.props.CommonProps; import io.openems.edge.app.common.props.CommunicationProps; import io.openems.edge.app.evcs.KebaEvcs.Property; import io.openems.edge.common.component.ComponentManager; @@ -68,9 +69,10 @@ public enum Property implements Type + { + "appId":"App.Evcs.Webasto.Next", + "alias":"Webasto Next Ladestation", + "instanceId": UUID, + "image": base64, + "properties":{ + "EVCS_ID": "evcs0", + "CTRL_EVCS_ID": "ctrlEvcs0", + "MODBUS_ID": "modbus0", + "IP":"192.168.25.11", + "MODBUS_UNIT_ID": 1 + }, + "appDescriptor": { + "websiteUrl": {@link AppDescriptor#getWebsiteUrl()} + } + } + * + */ +@Component(name = "App.Evcs.Webasto.Next") +public class WebastoNextEvcs extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public enum Property implements Type, Nameable { + // Component-IDs + EVCS_ID(AppDef.componentId("evcs0")), // + CTRL_EVCS_ID(AppDef.componentId("ctrlEvcs0")), // + MODBUS_ID(AppDef.componentId("modbus0")), // + // Properties + ALIAS(alias()), // + IP(AppDef.copyOfGeneric(CommunicationProps.ip(), def -> def // + .setRequired(true))), // + MODBUS_UNIT_ID(AppDef.copyOfGeneric(modbusUnitId(), def -> def // + .setDefaultValue(1))), // + MAX_HARDWARE_POWER_ACCEPT_PROPERTY(AppDef.of() // + .setAllowedToSave(false)), // + MAX_HARDWARE_POWER(EvcsProps.clusterMaxHardwarePowerSingleCp(MAX_HARDWARE_POWER_ACCEPT_PROPERTY, EVCS_ID)), // + UNOFFICIAL_APP_WARNING(CommonProps.installationHintOfUnofficialApp()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + + } + + @Activate + public WebastoNextEvcs(// + @Reference ComponentManager componentManager, // + ComponentContext componentContext, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var bundle = AbstractOpenemsApp.getTranslationBundle(l); + final var controllerAlias = TranslationUtil.getTranslation(bundle, "App.Evcs.controller.alias"); + + // values the user enters + final var alias = this.getString(p, l, Property.ALIAS); + final var ip = this.getString(p, Property.IP); + final var modbusUnitId = this.getInt(p, Property.MODBUS_UNIT_ID); + + // values which are being auto generated by the appmanager + final var evcsId = this.getId(t, p, Property.EVCS_ID); + final var ctrlEvcsId = this.getId(t, p, Property.CTRL_EVCS_ID); + final var modbusId = this.getId(t, p, Property.MODBUS_ID); + + var maxHardwarePowerPerPhase = OptionalInt.empty(); + if (p.containsKey(Property.MAX_HARDWARE_POWER)) { + maxHardwarePowerPerPhase = OptionalInt.of(this.getInt(p, Property.MAX_HARDWARE_POWER)); + } + + final var components = Lists.newArrayList(// + new EdgeConfig.Component(evcsId, alias, "Evcs.Webasto.Next", JsonUtils.buildJsonObject() // + .addProperty("modbus.id", modbusId) // + .addProperty("modbusUnitId", modbusUnitId) // + .build()), // + new EdgeConfig.Component(ctrlEvcsId, controllerAlias, "Controller.Evcs", JsonUtils.buildJsonObject() // + .addProperty("evcs.id", evcsId) // + .build()), // + new EdgeConfig.Component(modbusId, + TranslationUtil.getTranslation(bundle, "App.Evcs.Webasto.Next.modbus.alias"), + "Bridge.Modbus.Tcp", JsonUtils.buildJsonObject() // + .addProperty("ip", ip) // + .onlyIf(t == ConfigurationTarget.ADD, b -> b // + .addProperty("port", 502)) // + .build()) // + ); + + return new AppConfiguration(// + components, // + Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0"), // + null, // + EvcsCluster.dependency(t, this.componentManager, this.componentUtil, maxHardwarePowerPerPhase, + evcsId) // + ); + }; + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.EVCS }; + } + + @Override + protected OpenemsAppStatus getStatus() { + return OpenemsAppStatus.BETA; + } + + @Override + protected WebastoNextEvcs getApp() { + return this; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoUniteEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoUniteEvcs.java new file mode 100644 index 00000000000..e781332e983 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoUniteEvcs.java @@ -0,0 +1,200 @@ +package io.openems.edge.app.evcs; + +import static io.openems.edge.app.common.props.CommonProps.alias; +import static io.openems.edge.app.common.props.CommunicationProps.modbusUnitId; + +import java.util.Map; +import java.util.OptionalInt; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.common.collect.Lists; +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.CommonProps; +import io.openems.edge.app.common.props.CommunicationProps; +import io.openems.edge.app.evcs.WebastoUniteEvcs.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.Nameable; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.OpenemsAppStatus; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.Type.Parameter; +import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; + +/** + * Describes a Webasto Unite evcs App. + * + *
+  {
+    "appId":"App.Evcs.Webasto.Unite",
+    "alias":"Webasto Unite Ladestation",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+      "EVCS_ID": "evcs0",
+      "CTRL_EVCS_ID": "ctrlEvcs0",
+      "MODBUS_ID": "modbus0",
+      "IP":"192.168.25.11",
+      "MODBUS_UNIT_ID": 255
+    },
+    "appDescriptor": {
+    	"websiteUrl": {@link AppDescriptor#getWebsiteUrl()}
+    }
+  }
+ * 
+ */ +@Component(name = "App.Evcs.Webasto.Unite") +public class WebastoUniteEvcs extends AbstractOpenemsAppWithProps + implements OpenemsApp { + + public enum Property implements Type, Nameable { + // Component-IDs + EVCS_ID(AppDef.componentId("evcs0")), // + CTRL_EVCS_ID(AppDef.componentId("ctrlEvcs0")), // + MODBUS_ID(AppDef.componentId("modbus0")), // + // Properties + ALIAS(alias()), // + IP(AppDef.copyOfGeneric(CommunicationProps.ip(), def -> def // + .setRequired(true))), // + MODBUS_UNIT_ID(AppDef.copyOfGeneric(modbusUnitId(), def -> def // + .setDefaultValue(255))), // + MAX_HARDWARE_POWER_ACCEPT_PROPERTY(AppDef.of() // + .setAllowedToSave(false)), // + MAX_HARDWARE_POWER(EvcsProps.clusterMaxHardwarePowerSingleCp(MAX_HARDWARE_POWER_ACCEPT_PROPERTY, EVCS_ID)), // + UNOFFICIAL_APP_WARNING(CommonProps.installationHintOfUnofficialApp()), // + ; + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Type self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, BundleParameter> getParamter() { + return Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + + } + + @Activate + public WebastoUniteEvcs(// + @Reference ComponentManager componentManager, // + ComponentContext componentContext, // + @Reference ConfigurationAdmin cm, // + @Reference ComponentUtil componentUtil // + ) { + super(componentManager, componentContext, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var bundle = AbstractOpenemsApp.getTranslationBundle(l); + final var controllerAlias = TranslationUtil.getTranslation(bundle, "App.Evcs.controller.alias"); + + // values the user enters + final var alias = this.getString(p, l, Property.ALIAS); + final var ip = this.getString(p, Property.IP); + final var modbusUnitId = this.getInt(p, Property.MODBUS_UNIT_ID); + + // values which are being auto generated by the appmanager + final var evcsId = this.getId(t, p, Property.EVCS_ID); + final var ctrlEvcsId = this.getId(t, p, Property.CTRL_EVCS_ID); + final var modbusId = this.getId(t, p, Property.MODBUS_ID); + + var maxHardwarePowerPerPhase = OptionalInt.empty(); + if (p.containsKey(Property.MAX_HARDWARE_POWER)) { + maxHardwarePowerPerPhase = OptionalInt.of(this.getInt(p, Property.MAX_HARDWARE_POWER)); + } + + final var components = Lists.newArrayList(// + new EdgeConfig.Component(evcsId, alias, "Evcs.Webasto.Unite", JsonUtils.buildJsonObject() // + .addProperty("modbus.id", modbusId) // + .addProperty("modbusUnitId", modbusUnitId) // + .build()), // + new EdgeConfig.Component(ctrlEvcsId, controllerAlias, "Controller.Evcs", JsonUtils.buildJsonObject() // + .addProperty("evcs.id", evcsId) // + .build()), // + new EdgeConfig.Component(modbusId, + TranslationUtil.getTranslation(bundle, "App.Evcs.Webasto.Unite.modbus.alias"), + "Bridge.Modbus.Tcp", JsonUtils.buildJsonObject() // + .addProperty("ip", ip) // + .onlyIf(t == ConfigurationTarget.ADD, b -> b // + .addProperty("port", 502)) // + .build()) // + ); + + return new AppConfiguration(// + components, // + Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0"), // + null, // + EvcsCluster.dependency(t, this.componentManager, this.componentUtil, maxHardwarePowerPerPhase, + evcsId) // + ); + }; + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .build(); + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.MULTIPLE; + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.EVCS }; + } + + @Override + protected OpenemsAppStatus getStatus() { + return OpenemsAppStatus.BETA; + } + + @Override + protected WebastoUniteEvcs getApp() { + return this; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java b/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java index f7587ae501f..1932d92f04b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java +++ b/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java @@ -70,7 +70,7 @@ public static enum Property implements Type def.setTranslatedDescriptionWithAppPrefix(".ip.description") // .setDefaultValue("192.168.1.199") // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true)))), // + .setRequired(true))), // CHECK(AppDef.copyOfGeneric(CommonProps.installationHint(// (app, property, l, parameter) -> TranslationUtil.getTranslation(parameter.bundle, // "App.Hardware.KMtronic8Channel.installationHint")))), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java index 6de2dd0bcce..3ec9607b394 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java @@ -202,7 +202,7 @@ AppDef heatPumpRelayContactDef(int contactPosition) { Nameable.of("OUTPUT_CHANNEL_1"), Nameable.of("OUTPUT_CHANNEL_2")), b -> b.setTranslatedLabelWithAppPrefix(".outputChannel" + contactPosition + ".label") // .setTranslatedDescription("App.Heat.outputChannel.description") // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true)) // + .setRequired(true) // .setAutoGenerateField(false)); } diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java index aae809d44c6..33b23dc89ec 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java @@ -104,20 +104,20 @@ public static enum Property implements Type { field.setInputType(NUMBER) // .setUnit(WATT, l) // - .isRequired(true) // .setMin(0); })), // HYSTERESIS(AppDef.of(HeatingElement.class) // .setTranslatedLabelWithAppPrefix(".hysteresis.label") // .setTranslatedDescriptionWithAppPrefix(".hysteresis.description") // .setDefaultValue(60) // + .setRequired(true) // .setField(JsonFormlyUtil::buildInput, (app, property, l, parameter, field) -> { field.setInputType(NUMBER) // .setUnit(SECONDS, l) // - .isRequired(true) // .setMin(0); }) // .bidirectional(CTRL_IO_HEATING_ELEMENT_ID, "minimumSwitchingTime", // diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java index 03c7c4a0c35..22c546600be 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java @@ -9,11 +9,11 @@ import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.ctrlEssSurplusFeedToGrid; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.emergencyMeter; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.ess; -import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.modbusExternal; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.gridMeter; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.gridOptimizedCharge; -import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.modbusInternal; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.io; +import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.modbusExternal; +import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.modbusInternal; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.power; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.predictor; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.prepareBatteryExtension; @@ -129,9 +129,7 @@ public enum Property implements PropertyParent { ALIAS(alias()), // SAFETY_COUNTRY(AppDef.copyOfGeneric(safetyCountry(), def -> def // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }))), // + .setRequired(true))), // FEED_IN_TYPE(feedInType()), // MAX_FEED_IN_POWER(maxFeedInPower(FEED_IN_TYPE)), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome30.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome30.java index 73acc0a2682..826f1a2097d 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome30.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome30.java @@ -9,11 +9,11 @@ import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.ctrlEssSurplusFeedToGrid; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.emergencyMeter; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.ess; -import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.modbusExternal; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.gridMeter; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.gridOptimizedCharge; -import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.modbusInternal; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.io; +import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.modbusExternal; +import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.modbusInternal; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.power; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.predictor; import static io.openems.edge.app.integratedsystem.FeneconHomeComponents.prepareBatteryExtension; @@ -129,9 +129,7 @@ public enum Property implements PropertyParent { ALIAS(alias()), // SAFETY_COUNTRY(AppDef.copyOfGeneric(safetyCountry(), def -> def // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }))), // + .setRequired(true))), // FEED_IN_TYPE(feedInType()), // MAX_FEED_IN_POWER(maxFeedInPower(FEED_IN_TYPE)), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java index cb089045579..7bda819248f 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/JanitzaMeter.java @@ -84,43 +84,39 @@ public enum Property implements Type def // .setTranslatedLabelWithAppPrefix(".productModel") // .setDefaultValue(JanitzaModel.UMG_96_RME.getValue()) // + .setRequired(true) // .setField(JsonFormlyUtil::buildSelect, (app, property, l, parameter, field) -> { - field.setOptions(OptionsFactory.of(JanitzaModel.class), l) // - .isRequired(true); + field.setOptions(OptionsFactory.of(JanitzaModel.class), l); }))), // TYPE(AppDef.copyOfGeneric(MeterProps.type(MeterType.GRID), def -> def // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }))), // + .setRequired(true))), // INTEGRATION_TYPE(CommunicationProps.modbusType() // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - })), // + .setRequired(true)), // IP(MeterProps.ip() // .setDefaultValue("10.4.0.12") // + .setRequired(true) // .wrapField((app, property, l, parameter, field) -> { field.onlyShowIf((Exp.currentModelValue(INTEGRATION_TYPE) // .equal(Exp.staticValue(ModbusType.TCP)))); - field.isRequired(true); })), // PORT(MeterProps.port() // + .setRequired(true) // .wrapField((app, property, l, parameter, field) -> { field.onlyShowIf((Exp.currentModelValue(INTEGRATION_TYPE) // .equal(Exp.staticValue(ModbusType.TCP)))); })), // - SELECTED_MODBUS_ID(AppDef.copyOfGeneric(ComponentProps.pickSerialModbusId(), - def -> def.wrapField((app, property, l, parameter, field) -> { + SELECTED_MODBUS_ID(AppDef.copyOfGeneric(ComponentProps.pickSerialModbusId(), def -> def // + .setRequired(true) // + .wrapField((app, property, l, parameter, field) -> { if (PropsUtil.isHomeInstalled(app.getAppManagerUtil())) { field.readonly(true); } - field.isRequired(true); field.onlyShowIf(Exp.currentModelValue(INTEGRATION_TYPE) // .equal(Exp.staticValue(ModbusType.RTU))); - })).setAutoGenerateField(false)), // + })) // + .setAutoGenerateField(false)), // MODBUS_UNIT_ID(MeterProps.modbusUnitId() // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }) // + .setRequired(true) // .setDefaultValue(7) // .setAutoGenerateField(false)), // MODBUS_GROUP(CommunicationProps.modbusGroup(// diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/KdkMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/KdkMeter.java index 2a3415d5087..db8afccdd5d 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/KdkMeter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/KdkMeter.java @@ -72,16 +72,17 @@ public enum Property implements Type def.wrapField((app, property, l, parameter, field) -> { + MODBUS_ID(AppDef.copyOfGeneric(ComponentProps.pickModbusId(), def -> def // + .setRequired(true) // + .wrapField((app, property, l, parameter, field) -> { if (PropsUtil.isHomeInstalled(app.getAppManagerUtil())) { field.readonly(true); } - field.isRequired(true); })).setAutoGenerateField(false)), // - MODBUS_UNIT_ID(AppDef.copyOfGeneric(MeterProps.modbusUnitId(), def -> def.setDefaultValue(7) // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true))) // - .setAutoGenerateField(false)), // + MODBUS_UNIT_ID(AppDef.copyOfGeneric(MeterProps.modbusUnitId(), def -> def // + .setRequired(true) // + .setDefaultValue(7) // + .setAutoGenerateField(false))), // MODBUS_GROUP(AppDef.copyOfGeneric(CommunicationProps.modbusGroup(// MODBUS_ID, MODBUS_ID.def(), MODBUS_UNIT_ID, MODBUS_UNIT_ID.def()))), // ; diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/MicrocareSdm630Meter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/MicrocareSdm630Meter.java index 57516f6c91d..8b8f579e0d5 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/MicrocareSdm630Meter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/MicrocareSdm630Meter.java @@ -73,14 +73,13 @@ public enum Property implements Type def.wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }).setAutoGenerateField(false))), // - MODBUS_UNIT_ID(AppDef.copyOfGeneric(MeterProps.modbusUnitId(), // - def -> def.setAutoGenerateField(false) // - .setDefaultValue(10) // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true)))), // + MODBUS_ID(AppDef.copyOfGeneric(ComponentProps.pickModbusId(), def -> def // + .setRequired(true) // + .setAutoGenerateField(false))), // + MODBUS_UNIT_ID(AppDef.copyOfGeneric(MeterProps.modbusUnitId(), def -> def // + .setRequired(true) // + .setAutoGenerateField(false) // + .setDefaultValue(10))), // MODBUS_GROUP(CommunicationProps.modbusGroup(MODBUS_ID, MODBUS_ID.def(), // MODBUS_UNIT_ID, MODBUS_UNIT_ID.def())), // UNOFFICIAL_APP_WARNING(CommonProps.installationHintOfUnofficialApp()), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java index aa8372c6a01..957e3173d01 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java @@ -71,17 +71,17 @@ public enum Property implements Type def.wrapField((app, property, l, parameter, field) -> { + MODBUS_ID(AppDef.copyOfGeneric(ComponentProps.pickModbusId(), def -> def // + .setRequired(true) // + .wrapField((app, property, l, parameter, field) -> { if (PropsUtil.isHomeInstalled(app.getAppManagerUtil())) { field.readonly(true); } - field.isRequired(true); }).setAutoGenerateField(false))), // - MODBUS_UNIT_ID(AppDef.copyOfGeneric(MeterProps.modbusUnitId(), // - def -> def.setAutoGenerateField(false) // - .setDefaultValue(7) // - .wrapField((app, property, l, parameter, field) -> field.isRequired(true)))), // + MODBUS_UNIT_ID(AppDef.copyOfGeneric(MeterProps.modbusUnitId(), def -> def // + .setRequired(true) // + .setAutoGenerateField(false) // + .setDefaultValue(7))), // MODBUS_GROUP(AppDef.copyOfGeneric(CommunicationProps.modbusGroup(// MODBUS_ID, MODBUS_ID.def(), MODBUS_UNIT_ID, MODBUS_UNIT_ID.def()))); diff --git a/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PeakShaving.java b/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PeakShaving.java index 6ad2d50b3a4..763c9c3e9a3 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PeakShaving.java +++ b/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PeakShaving.java @@ -67,25 +67,21 @@ public enum Property implements Type def.wrapField((app, property, l, parameter, field) -> field.isRequired(true)) // - .bidirectional(CTRL_PEAK_SHAVING_ID, "ess.id", // - ComponentManagerSupplier::getComponentManager))), // - METER_ID(AppDef.copyOfGeneric(ComponentProps.pickElectricityGridMeterId(), - def -> def.wrapField((app, property, l, parameter, field) -> field.isRequired(true)) // - .bidirectional(CTRL_PEAK_SHAVING_ID, "meter.id", // - ComponentManagerSupplier::getComponentManager))), // + ESS_ID(AppDef.copyOfGeneric(ComponentProps.pickManagedSymmetricEssId(), def -> def // + .setRequired(true) // + .bidirectional(CTRL_PEAK_SHAVING_ID, "ess.id", // + ComponentManagerSupplier::getComponentManager))), // + METER_ID(AppDef.copyOfGeneric(ComponentProps.pickElectricityGridMeterId(), def -> def // + .setRequired(true) // + .bidirectional(CTRL_PEAK_SHAVING_ID, "meter.id", // + ComponentManagerSupplier::getComponentManager))), // PEAK_SHAVING_POWER(AppDef.copyOfGeneric(PeakShavingProps.peakShavingPower(), def -> def // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }) // + .setRequired(true) // .setAutoGenerateField(false) // .bidirectional(CTRL_PEAK_SHAVING_ID, "peakShavingPower", ComponentManagerSupplier::getComponentManager))), // RECHARGE_POWER(AppDef.copyOfGeneric(PeakShavingProps.rechargePower(), def -> def // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }) // + .setRequired(true) // .setAutoGenerateField(false) // .bidirectional(CTRL_PEAK_SHAVING_ID, "rechargePower", // ComponentManagerSupplier::getComponentManager))), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PhaseAccuratePeakShaving.java b/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PhaseAccuratePeakShaving.java index 607db919baa..306dc49e3ac 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PhaseAccuratePeakShaving.java +++ b/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PhaseAccuratePeakShaving.java @@ -69,25 +69,21 @@ public enum Property implements Type def.wrapField((app, property, l, parameter, field) -> field.isRequired(true)) // - .bidirectional(CTRL_PEAK_SHAVING_ID, "ess.id", // - ComponentManagerSupplier::getComponentManager))), // - METER_ID(AppDef.copyOfGeneric(ComponentProps.pickElectricityGridMeterId(), - def -> def.wrapField((app, property, l, parameter, field) -> field.isRequired(true)) // - .bidirectional(CTRL_PEAK_SHAVING_ID, "meter.id", // - ComponentManagerSupplier::getComponentManager))), // + ESS_ID(AppDef.copyOfGeneric(ComponentProps.pickManagedSymmetricEssId(), def -> def // + .setRequired(true) // + .bidirectional(CTRL_PEAK_SHAVING_ID, "ess.id", // + ComponentManagerSupplier::getComponentManager))), // + METER_ID(AppDef.copyOfGeneric(ComponentProps.pickElectricityGridMeterId(), def -> def // + .setRequired(true) // + .bidirectional(CTRL_PEAK_SHAVING_ID, "meter.id", // + ComponentManagerSupplier::getComponentManager))), // PEAK_SHAVING_POWER(AppDef.copyOfGeneric(PeakShavingProps.peakShavingPowerPerPhase(), def -> def // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }) // + .setRequired(true) // .setAutoGenerateField(false) // .bidirectional(CTRL_PEAK_SHAVING_ID, "peakShavingPower", // ComponentManagerSupplier::getComponentManager))), // RECHARGE_POWER(AppDef.copyOfGeneric(PeakShavingProps.rechargePowerPerPhase(), def -> def // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }) // + .setRequired(true) // .setAutoGenerateField(false) // .bidirectional(CTRL_PEAK_SHAVING_ID, "rechargePower", // ComponentManagerSupplier::getComponentManager))), // diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SmaPvInverter.java b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SmaPvInverter.java index e41c7ba5736..d4752587df7 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SmaPvInverter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SmaPvInverter.java @@ -1,5 +1,7 @@ package io.openems.edge.app.pvinverter; +import static io.openems.edge.app.common.props.CommonProps.alias; + import java.util.Map; import java.util.function.Function; @@ -22,6 +24,7 @@ import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.AppDef; import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentManagerSupplier; import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ConfigurationTarget; import io.openems.edge.core.appmanager.Nameable; @@ -66,33 +69,27 @@ public static enum Property implements Type def // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }))), // + .setRequired(true))), // PORT(AppDef.copyOfGeneric(CommonPvInverterConfiguration.port(), def -> def // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }))), // + .setRequired(true))), // MODBUS_UNIT_ID(AppDef.copyOfGeneric(CommonPvInverterConfiguration.modbusUnitId(), def -> def // .setTranslatedDescriptionWithAppPrefix(".modbusUnitId.description") // - .wrapField((app, property, l, parameter, field) -> { - field.isRequired(true); - }))), // + .setRequired(true))), // PHASE(AppDef.of(SmaPvInverter.class) // .setTranslatedLabelWithAppPrefix(".phase.label") // ) .setTranslatedDescriptionWithAppPrefix(".phase.description") // .setDefaultValue(Phase.ALL.name()) // - .bidirectional(PV_INVERTER_ID, "phase", a -> a.componentManager) // - .setField(JsonFormlyUtil::buildSelect, (app, property, l, parameter, field) -> // - field.setOptions(OptionsFactory.of(Phase.class), l) // - .isRequired(true))); + .setRequired(true) // + .setField(JsonFormlyUtil::buildSelect, (app, property, l, parameter, field) -> { + field.setOptions(OptionsFactory.of(Phase.class), l); + }) // + .bidirectional(PV_INVERTER_ID, "phase", ComponentManagerSupplier::getComponentManager)); - private final AppDef def; + private final AppDef def; - private Property(AppDef def) { + private Property(AppDef def) { this.def = def; } @@ -102,7 +99,7 @@ public Property self() { } @Override - public AppDef def() { + public AppDef def() { return this.def; } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java index 97aa97ab874..0474c35d82a 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppDef.java @@ -170,6 +170,8 @@ public static interface FieldValuesConsumer { */ private FieldValuesSupplier defaultValue; + private boolean required = false; + /** * Function to get the {@link FormlyBuilder} for the input. */ @@ -713,6 +715,11 @@ public AppDef setIsAllowedToSee(// return this.self(); } + public final AppDef setRequired(boolean required) { + this.required = required; + return this.self(); + } + /** * Appends the given predicates and collections them into one which checks that * every predicate returns true to determine if the current field should be @@ -904,6 +911,7 @@ private final FieldValuesSupplier translateWit doIfPresent(app, property, l, parameter, this.label, field::setLabel); doIfPresent(app, property, l, parameter, this.description, field::setDescription); doIfPresent(app, property, l, parameter, this.defaultValue, field::setDefaultValue); + field.isRequired(this.required); return field; }; } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/TranslationUtil.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/TranslationUtil.java index ac07214cca6..20d240aaf38 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/TranslationUtil.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/TranslationUtil.java @@ -1,12 +1,77 @@ package io.openems.edge.core.appmanager; import java.text.MessageFormat; +import java.util.HashSet; import java.util.MissingResourceException; import java.util.Objects; import java.util.ResourceBundle; +import java.util.Set; public class TranslationUtil { + private static Translator instance = new NormalTranslator(); + + /** + * Enables the debug mode for getting translations. + * + * @return the {@link DebugTranslator} to get debug metrics. + */ + public static DebugTranslator enableDebugMode() { + return (DebugTranslator) (instance = new DebugTranslator()); + } + + /** + * Disables the debug mode. + */ + public static void disableDebugMode() { + instance = new NormalTranslator(); + } + + private interface Translator { + + public String getTranslation(ResourceBundle translationBundle, String key, Object... params); + + } + + public static final class DebugTranslator implements Translator { + + private final Set missingKeys = new HashSet(); + + private DebugTranslator() { + } + + @Override + public String getTranslation(ResourceBundle translationBundle, String key, Object... params) { + final var translation = getNullableTranslation(translationBundle, key, params); + if (translation == null) { + this.missingKeys.add(key); + return key; + } + return translation; + } + + public Set getMissingKeys() { + return this.missingKeys; + } + + } + + public static final class NormalTranslator implements Translator { + + private NormalTranslator() { + } + + @Override + public String getTranslation(ResourceBundle translationBundle, String key, Object... params) { + final var translation = getNullableTranslation(translationBundle, key, params); + if (translation == null) { + return key; + } + return translation; + } + + } + /** * Gets the value for the given key from the translationBundle. * @@ -17,11 +82,7 @@ public class TranslationUtil { * the format is invalid */ public static String getTranslation(ResourceBundle translationBundle, String key, Object... params) { - final var translation = getNullableTranslation(translationBundle, key, params); - if (translation == null) { - return key; - } - return translation; + return instance.getTranslation(translationBundle, key, params); } /** diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/formly/builder/SelectBuilder.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/formly/builder/SelectBuilder.java index 773947ba7d6..a97cce0660d 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/formly/builder/SelectBuilder.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/formly/builder/SelectBuilder.java @@ -100,7 +100,9 @@ public SelectBuilder setOptions(List items, Function new JsonPrimitive(t.getKey()), // + t -> new JsonPrimitive(t.getValue())); } /** diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties index 031ea432cd1..449fc88c3fd 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_de.properties @@ -131,6 +131,14 @@ App.Evcs.Alpitronic.modbus.alias = Kommunikation zur Alpitronic Ladestation App.Evcs.Dezony.Name = dezony IQ Ladestation App.Evcs.Dezony.Name.short = dezony IQ +App.Evcs.Webasto.Next.Name = Webasto Next Ladestation +App.Evcs.Webasto.Next.Name.short = Webasto Next +App.Evcs.Webasto.Next.modbus.alias = Kommunikation zur Webasto Next Ladestation + +App.Evcs.Webasto.Unite.Name = Webasto Unite Ladestation +App.Evcs.Webasto.Unite.Name.short = Webasto Unite +App.Evcs.Webasto.Unite.modbus.alias = Kommunikation zur Webasto Unite Ladestation + # Hardware App.Hardware.KMtronic8Channel.Name = FEMS Relaisboard 8-Kanal TCP App.Hardware.KMtronic8Channel.Name.short = FEMS Relaisboard 8-Kanal TCP @@ -323,3 +331,6 @@ App.PvSelfConsumption.SelfConsumptionOptimization.meter.description = ID des Net App.Ess.PrepareBatteryExtension.Name = Batterienachrüstung App.Ess.PrepareBatteryExtension.Name.short = Batterienachrüstung App.Ess.PrepareBatteryExtension.targetSoc.label = Ziel Soc + +App.Ess.FixActivePower.Name = Manuelle Be-/Entladung +App.Ess.FixActivePower.Name.short = Manuelle Be-/Entladung diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties index 9972be49b18..45233b633d4 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/translation_en.properties @@ -131,6 +131,14 @@ App.Evcs.Alpitronic.modbus.alias = Communication to the Alpitronic charging stat App.Evcs.Dezony.Name = dezony IQ charging station App.Evcs.Dezony.Name.short = dezony IQ +App.Evcs.Webasto.Next.Name = Webasto Next charging station +App.Evcs.Webasto.Next.Name.short = Webasto Next +App.Evcs.Webasto.Next.modbus.alias = Communication to the Webasto Next charging station + +App.Evcs.Webasto.Unite.Name = Webasto Unite charging station +App.Evcs.Webasto.Unite.Name.short = Webasto Unite +App.Evcs.Webasto.Unite.modbus.alias = Communication to the Webasto Unite charging station + # Hardware App.Hardware.KMtronic8Channel.Name = FEMS Relay board 8-channel TCP App.Hardware.KMtronic8Channel.Name.short = FEMS Relay board 8-channel TCP @@ -323,3 +331,6 @@ App.PvSelfConsumption.SelfConsumptionOptimization.meter.description = ID of the App.Ess.PrepareBatteryExtension.Name = Prepare Battery Extension App.Ess.PrepareBatteryExtension.Name.short = Prepare Battery Extension App.Ess.PrepareBatteryExtension.targetSoc.label = Target Soc + +App.Ess.FixActivePower.Name = Manual Charge/Discharge +App.Ess.FixActivePower.Name.short = Manual Charge/Discharge diff --git a/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java index ab0df778ebe..dde7623e0fe 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/predictormanager/PredictorManagerImpl.java @@ -17,6 +17,8 @@ import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcRequest; @@ -44,6 +46,8 @@ public class PredictorManagerImpl extends AbstractOpenemsComponent implements PredictorManager, OpenemsComponent, JsonApi { + private final Logger log = LoggerFactory.getLogger(PredictorManagerImpl.class); + @Reference private ConfigurationAdmin cm; @@ -115,9 +119,15 @@ public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress) { // No explicit predictor found if (channelAddress.getComponentId().equals(Sum.SINGLETON_COMPONENT_ID)) { // This is a Sum-Channel. Try to get predictions for each source channel. - var channelId = Sum.ChannelId.valueOf( - io.openems.edge.common.channel.ChannelId.channelIdCamelToUpper(channelAddress.getChannelId())); - return this.getPredictionSum(channelId); + try { + return this.getPredictionSum(Sum.ChannelId.valueOf( + io.openems.edge.common.channel.ChannelId.channelIdCamelToUpper(channelAddress.getChannelId()))); + + } catch (IllegalArgumentException e) { + this.logWarn(this.log, "Unable to find ChannelId for " + channelAddress); + return Prediction24Hours.EMPTY; + } + } else { return Prediction24Hours.EMPTY; } @@ -130,51 +140,35 @@ public Prediction24Hours get24HoursPrediction(ChannelAddress channelAddress) { * @return the {@link Prediction24Hours} */ private Prediction24Hours getPredictionSum(Sum.ChannelId channelId) { - switch (channelId) { - case CONSUMPTION_ACTIVE_ENERGY: - case CONSUMPTION_ACTIVE_POWER_L1: - case CONSUMPTION_ACTIVE_POWER_L2: - case CONSUMPTION_ACTIVE_POWER_L3: - case CONSUMPTION_MAX_ACTIVE_POWER: - case ESS_ACTIVE_CHARGE_ENERGY: - case ESS_ACTIVE_DISCHARGE_ENERGY: - case ESS_ACTIVE_POWER: - case ESS_ACTIVE_POWER_L1: - case ESS_ACTIVE_POWER_L2: - case ESS_ACTIVE_POWER_L3: - case ESS_CAPACITY: - case ESS_DC_CHARGE_ENERGY: - case ESS_DC_DISCHARGE_ENERGY: - case ESS_DISCHARGE_POWER: - case ESS_MAX_APPARENT_POWER: - case ESS_REACTIVE_POWER: - case ESS_SOC: - case GRID_ACTIVE_POWER: - case GRID_ACTIVE_POWER_L1: - case GRID_ACTIVE_POWER_L2: - case GRID_ACTIVE_POWER_L3: - case GRID_BUY_ACTIVE_ENERGY: - case GRID_MAX_ACTIVE_POWER: - case GRID_MIN_ACTIVE_POWER: - case GRID_MODE: - case GRID_SELL_ACTIVE_ENERGY: - case PRODUCTION_ACTIVE_ENERGY: - case PRODUCTION_AC_ACTIVE_ENERGY: - case PRODUCTION_AC_ACTIVE_POWER_L1: - case PRODUCTION_AC_ACTIVE_POWER_L2: - case PRODUCTION_AC_ACTIVE_POWER_L3: - case PRODUCTION_DC_ACTIVE_ENERGY: - case PRODUCTION_MAX_ACTIVE_POWER: - case PRODUCTION_MAX_AC_ACTIVE_POWER: - case PRODUCTION_MAX_DC_ACTUAL_POWER: - case HAS_IGNORED_COMPONENT_STATES: - return Prediction24Hours.EMPTY; + return switch (channelId) { + case CONSUMPTION_ACTIVE_ENERGY, // + CONSUMPTION_ACTIVE_POWER_L1, CONSUMPTION_ACTIVE_POWER_L2, CONSUMPTION_ACTIVE_POWER_L3, + CONSUMPTION_MAX_ACTIVE_POWER, // - case CONSUMPTION_ACTIVE_POWER: - // TODO - return Prediction24Hours.EMPTY; + ESS_ACTIVE_CHARGE_ENERGY, ESS_ACTIVE_DISCHARGE_ENERGY, ESS_ACTIVE_POWER, ESS_ACTIVE_POWER_L1, + ESS_ACTIVE_POWER_L2, ESS_ACTIVE_POWER_L3, ESS_CAPACITY, ESS_DC_CHARGE_ENERGY, ESS_DC_DISCHARGE_ENERGY, + ESS_DISCHARGE_POWER, ESS_MAX_APPARENT_POWER, ESS_REACTIVE_POWER, ESS_SOC, // + + GRID_ACTIVE_POWER, GRID_ACTIVE_POWER_L1, GRID_ACTIVE_POWER_L2, GRID_ACTIVE_POWER_L3, + GRID_BUY_ACTIVE_ENERGY, GRID_MAX_ACTIVE_POWER, GRID_MIN_ACTIVE_POWER, GRID_MODE, + GRID_SELL_ACTIVE_ENERGY, // - case PRODUCTION_DC_ACTUAL_POWER: { + PRODUCTION_ACTIVE_ENERGY, PRODUCTION_AC_ACTIVE_ENERGY, PRODUCTION_AC_ACTIVE_POWER_L1, + PRODUCTION_AC_ACTIVE_POWER_L2, PRODUCTION_AC_ACTIVE_POWER_L3, PRODUCTION_DC_ACTIVE_ENERGY, + PRODUCTION_MAX_ACTIVE_POWER, PRODUCTION_MAX_AC_ACTIVE_POWER, PRODUCTION_MAX_DC_ACTUAL_POWER, // + + HAS_IGNORED_COMPONENT_STATES -> + Prediction24Hours.EMPTY; + + case UNMANAGED_CONSUMPTION_ACTIVE_POWER -> + // Fallback for elder systems that only provide predictors for + // ConsumptionActivePower by default + this.get24HoursPrediction(new ChannelAddress("_sum", "ConsumptionActivePower")); + + // TODO + case CONSUMPTION_ACTIVE_POWER -> Prediction24Hours.EMPTY; + + case PRODUCTION_DC_ACTUAL_POWER -> { // Sum up "ActualPower" prediction of all EssDcChargers List chargers = this.componentManager.getEnabledComponentsOfType(EssDcCharger.class); var predictions = new Prediction24Hours[chargers.size()]; @@ -183,9 +177,10 @@ private Prediction24Hours getPredictionSum(Sum.ChannelId channelId) { predictions[i] = this.get24HoursPrediction( new ChannelAddress(charger.id(), EssDcCharger.ChannelId.ACTUAL_POWER.id())); } - return Prediction24Hours.sum(predictions); + yield Prediction24Hours.sum(predictions); } - case PRODUCTION_AC_ACTIVE_POWER: { + + case PRODUCTION_AC_ACTIVE_POWER -> { // Sum up "ActivePower" prediction of all ElectricityMeter List meters = this.componentManager.getEnabledComponentsOfType(ElectricityMeter.class) .stream() // @@ -209,18 +204,14 @@ private Prediction24Hours getPredictionSum(Sum.ChannelId channelId) { predictions[i] = this.get24HoursPrediction( new ChannelAddress(meter.id(), ElectricityMeter.ChannelId.ACTIVE_POWER.id())); } - return Prediction24Hours.sum(predictions); + yield Prediction24Hours.sum(predictions); } - case PRODUCTION_ACTIVE_POWER: - return Prediction24Hours.sum(// - this.getPredictionSum(Sum.ChannelId.PRODUCTION_AC_ACTIVE_POWER), // - this.getPredictionSum(Sum.ChannelId.PRODUCTION_DC_ACTUAL_POWER) // + case PRODUCTION_ACTIVE_POWER -> Prediction24Hours.sum(// + this.getPredictionSum(Sum.ChannelId.PRODUCTION_AC_ACTIVE_POWER), // + this.getPredictionSum(Sum.ChannelId.PRODUCTION_DC_ACTUAL_POWER) // ); - } - - // should never come here - return Prediction24Hours.EMPTY; + }; } /** diff --git a/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java b/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java index db59b5fe960..90c35aef806 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/sum/SumImpl.java @@ -34,6 +34,7 @@ import io.openems.edge.ess.api.MetaEss; import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.ess.dccharger.api.EssDcCharger; +import io.openems.edge.evcs.api.Evcs; import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.api.VirtualMeter; import io.openems.edge.timedata.api.Timedata; @@ -167,6 +168,9 @@ private void calculateChannelValues() { // cabling errors, etc. final var productionAcActiveEnergyNegative = new CalculateLongSum(); + // Consumption + final var managedConsumptionActivePower = new CalculateIntegerSum(); + for (OpenemsComponent component : this.componentManager.getEnabledComponents()) { if (component instanceof SymmetricEss) { /* @@ -222,14 +226,17 @@ private void calculateChannelValues() { switch (meter.getMeterType()) { case PRODUCTION_AND_CONSUMPTION: // TODO PRODUCTION_AND_CONSUMPTION + // Production Power is positive, Consumption is negative break; case CONSUMPTION_METERED: // TODO CONSUMPTION_METERED + // Consumption is positive break; case CONSUMPTION_NOT_METERED: // TODO CONSUMPTION_NOT_METERED + // Consumption is positive break; case GRID: @@ -269,6 +276,12 @@ private void calculateChannelValues() { productionDcActualPower.addValue(charger.getActualPowerChannel()); productionMaxDcActualPower.addValue(charger.getMaxActualPowerChannel()); productionDcActiveEnergy.addValue(charger.getActualEnergyChannel()); + + } else if (component instanceof Evcs evcs) { + /* + * Electric Vehicle Charging Station + */ + managedConsumptionActivePower.addValue(evcs.getChargePowerChannel()); } } @@ -356,8 +369,9 @@ private void calculateChannelValues() { productionActiveEnergySum); // Consumption - this._setConsumptionActivePower(TypeUtils.sum(// - essActivePowerSum, gridActivePowerSum, productionAcActivePowerSum)); + var consumptionActivePower = TypeUtils.sum(// + essActivePowerSum, gridActivePowerSum, productionAcActivePowerSum); + this._setConsumptionActivePower(consumptionActivePower); this._setConsumptionActivePowerL1(TypeUtils.sum(// essActivePowerL1Sum, gridActivePowerL1Sum, productionAcActivePowerL1Sum)); this._setConsumptionActivePowerL2(TypeUtils.sum(// @@ -366,6 +380,8 @@ private void calculateChannelValues() { essActivePowerL3Sum, gridActivePowerL3Sum, productionAcActivePowerL3Sum)); this._setConsumptionMaxActivePower(TypeUtils.sum(// essMaxApparentPowerSum, gridMaxActivePowerSum, productionMaxAcActivePowerSum)); + this._setUnmanagedConsumptionActivePower( + TypeUtils.subtract(consumptionActivePower, managedConsumptionActivePower.calculate())); var enterTheSystem = TypeUtils.sum(essActiveDischargeEnergySum, gridBuyActiveEnergySum, productionAcActiveEnergySum); diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppDefTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppDefTest.java new file mode 100644 index 00000000000..54a406a582f --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppDefTest.java @@ -0,0 +1,66 @@ +package io.openems.edge.core.appmanager; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +import io.openems.common.session.Language; +import io.openems.edge.app.TestC; +import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; + +public class AppDefTest { + + private TestC testCApp; + + @Before + public void beforeEach() throws Exception { + new AppManagerTestBundle(null, null, t -> { + return ImmutableList.of(// + this.testCApp = Apps.testC(t) // + ); + }); + } + + @Test + public void testRequiredTrue() { + final var def = AppDef.of() // + .setField(JsonFormlyUtil::buildInputFromNameable) // + .setRequired(true); + + final var field = def.getField().get(this.testCApp, Nameable.of("test"), Language.DEFAULT, new Object()); + final var jsonField = field.build(); + + final var templateOptions = jsonField.get("templateOptions").getAsJsonObject(); + assertTrue(templateOptions.get("required").getAsBoolean()); + } + + @Test + public void testRequiredFalse() { + final var def = AppDef.of() // + .setField(JsonFormlyUtil::buildInputFromNameable) // + .setRequired(false); + + final var field = def.getField().get(this.testCApp, Nameable.of("test"), Language.DEFAULT, new Object()); + final var jsonField = field.build(); + + final var templateOptions = jsonField.get("templateOptions").getAsJsonObject(); + assertFalse(templateOptions.has("required") && templateOptions.get("required").getAsBoolean()); + } + + @Test + public void testRequiredNotSet() { + final var def = AppDef.of() // + .setField(JsonFormlyUtil::buildInputFromNameable); + + final var field = def.getField().get(this.testCApp, Nameable.of("test"), Language.DEFAULT, new Object()); + final var jsonField = field.build(); + + final var templateOptions = jsonField.get("templateOptions").getAsJsonObject(); + assertFalse(templateOptions.has("required") && templateOptions.get("required").getAsBoolean()); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java index 39786106e03..7f00b8bc02b 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java @@ -15,11 +15,14 @@ import io.openems.edge.app.api.ModbusTcpApiReadOnly; import io.openems.edge.app.api.ModbusTcpApiReadWrite; import io.openems.edge.app.api.RestJsonApiReadOnly; +import io.openems.edge.app.ess.FixActivePower; import io.openems.edge.app.ess.PrepareBatteryExtension; import io.openems.edge.app.evcs.EvcsCluster; import io.openems.edge.app.evcs.HardyBarthEvcs; import io.openems.edge.app.evcs.IesKeywattEvcs; import io.openems.edge.app.evcs.KebaEvcs; +import io.openems.edge.app.evcs.WebastoNextEvcs; +import io.openems.edge.app.evcs.WebastoUniteEvcs; import io.openems.edge.app.heat.HeatPump; import io.openems.edge.app.integratedsystem.FeneconHome; import io.openems.edge.app.integratedsystem.FeneconHome20; @@ -225,6 +228,26 @@ public static final IesKeywattEvcs iesKeywattEvcs(AppManagerTestBundle t) { return app(t, IesKeywattEvcs::new, "App.Evcs.IesKeywatt"); } + /** + * Test method for creating a {@link WebastoNextEvcs}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final WebastoNextEvcs webastoNext(AppManagerTestBundle t) { + return app(t, WebastoNextEvcs::new, "App.Evcs.Webasto.Next"); + } + + /** + * Test method for creating a {@link WebastoUniteEvcs}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final WebastoUniteEvcs webastoUnite(AppManagerTestBundle t) { + return app(t, WebastoUniteEvcs::new, "App.Evcs.Webasto.Unite"); + } + /** * Test method for creating a {@link EvcsCluster}. * @@ -297,6 +320,16 @@ public static final MicrocareSdm630Meter microcareSdm630Meter(AppManagerTestBund // ess-controller + /** + * Test method for creating a {@link FixActivePower}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final FixActivePower fixActivePower(AppManagerTestBundle t) { + return app(t, FixActivePower::new, "App.Ess.FixActivePower"); + } + /** * Test method for creating a {@link PrepareBatteryExtension}. * diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java new file mode 100644 index 00000000000..6dd6fdc2d32 --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java @@ -0,0 +1,109 @@ +package io.openems.edge.core.appmanager; + +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.integratedsystem.TestFeneconHome; +import io.openems.edge.app.integratedsystem.TestFeneconHome20; +import io.openems.edge.app.integratedsystem.TestFeneconHome30; +import io.openems.edge.common.test.DummyUser; + +public class TestTranslations { + + private record TestTranslation(OpenemsApp app, boolean validateAppAssistant, JsonObject config) { + + } + + private List apps; + + @Before + public void beforeEach() throws Exception { + this.apps = new ArrayList<>(); + new AppManagerTestBundle(null, null, t -> { + this.apps.add(new TestTranslation(Apps.feneconHome(t), true, TestFeneconHome.fullSettings())); + this.apps.add(new TestTranslation(Apps.feneconHome20(t), true, TestFeneconHome20.fullSettings())); + this.apps.add(new TestTranslation(Apps.feneconHome30(t), true, TestFeneconHome30.fullSettings())); + this.apps.add(new TestTranslation(Apps.awattarHourly(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.stromdaoCorrently(t), true, JsonUtils.buildJsonObject() // + .addProperty("ZIP_CODE", "123456789") // + .build())); + this.apps.add(new TestTranslation(Apps.tibber(t), true, JsonUtils.buildJsonObject() // + .addProperty("ACCESS_TOKEN", "123456789") // + .build())); + this.apps.add(new TestTranslation(Apps.modbusTcpApiReadOnly(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.modbusTcpApiReadWrite(t), true, JsonUtils.buildJsonObject() // + .addProperty("API_TIMEOUT", 60) // + .add("COMPONENT_IDS", new JsonArray()) // + .build())); + this.apps.add(new TestTranslation(Apps.restJsonApiReadOnly(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.hardyBarthEvcs(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.kebaEvcs(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.iesKeywattEvcs(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.webastoNext(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.webastoUnite(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.evcsCluster(t), true, new JsonObject())); + this.apps.add(new TestTranslation(Apps.heatPump(t), false, JsonUtils.buildJsonObject() // + .addProperty("OUTPUT_CHANNEL_1", "io0/Relay1") // + .addProperty("OUTPUT_CHANNEL_2", "io0/Relay2") // + .build())); + this.apps.add(new TestTranslation(Apps.gridOptimizedCharge(t), true, JsonUtils.buildJsonObject() // + .addProperty("MAXIMUM_SELL_TO_GRID_POWER", 60) // + .build())); + this.apps.add(new TestTranslation(Apps.selfConsumptionOptimization(t), true, JsonUtils.buildJsonObject() // + .addProperty("ESS_ID", "ess0") // + .addProperty("METER_ID", "meter0") // + .build())); + this.apps.add(new TestTranslation(Apps.socomecMeter(t), false, JsonUtils.buildJsonObject() // + .addProperty("MODBUS_ID", "modbus0") // + .build())); + this.apps.add(new TestTranslation(Apps.fixActivePower(t), true, JsonUtils.buildJsonObject() // + .addProperty("ESS_ID", "ess0") // + .build())); + this.apps.add(new TestTranslation(Apps.prepareBatteryExtension(t), true, new JsonObject())); + return this.apps.stream().map(TestTranslation::app).toList(); + }); + } + + @Test + public void testGermanTranslation() throws Exception { + this.testTranslations(Language.DE); + } + + @Test + public void testEnglishTranslation() throws Exception { + this.testTranslations(Language.EN); + } + + private void testTranslations(Language l) throws Exception { + final var user = new DummyUser("1", "password", l, Role.ADMIN); + + final var debugTranslator = TranslationUtil.enableDebugMode(); + + for (var entry : this.apps) { + final var app = entry.app(); + if (entry.validateAppAssistant()) { + app.getAppAssistant(user); + } + if (entry.config() != null) { + app.getAppConfiguration(ConfigurationTarget.ADD, entry.config(), l); + } + } + + assertTrue( + "Missing Translation Keys for Language " + l + " [" + + String.join(", ", debugTranslator.getMissingKeys()) + "]", + debugTranslator.getMissingKeys().isEmpty()); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/predictormanager/MyConfig.java b/io.openems.edge.core/test/io/openems/edge/core/predictormanager/MyConfig.java new file mode 100644 index 00000000000..188805cc969 --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/predictormanager/MyConfig.java @@ -0,0 +1,33 @@ +package io.openems.edge.core.predictormanager; + +import io.openems.common.test.AbstractComponentConfig; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + + private Builder() { + } + + public MyConfig build() { + return new MyConfig(this); + } + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, PredictorManagerImpl.SINGLETON_COMPONENT_ID); + this.builder = builder; + } +} \ No newline at end of file diff --git a/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java new file mode 100644 index 00000000000..57abd82542a --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java @@ -0,0 +1,78 @@ +package io.openems.edge.core.predictormanager; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyComponentManager; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.common.test.TimeLeapClock; +import io.openems.edge.predictor.api.oneday.Prediction24Hours; +import io.openems.edge.predictor.api.oneday.Predictor24Hours; +import io.openems.edge.predictor.api.test.DummyPrediction24Hours; +import io.openems.edge.predictor.api.test.DummyPredictor24Hours; + +public class PredictorManagerImplTest { + + private static final String PREDICTOR_ID = "predictor0"; + private static final Integer[] DEFAULT_CONSUMPTION_PREDICTION = { + /* 00:00-03:450 */ + 1021, 1208, 713, 931, 2847, 2551, 1558, 1234, 433, 633, 1355, 606, 430, 1432, 1121, 502, // + /* 04:00-07:45 */ + 294, 1048, 1194, 914, 1534, 1226, 1235, 977, 578, 1253, 1983, 1417, 513, 929, 1102, 445, // + /* 08:00-11:45 */ + 1208, 2791, 2729, 2609, 2086, 1454, 848, 816, 2610, 3150, 2036, 1180, 359, 1316, 3447, 2104, // + /* 12:00-15:45 */ + 905, 802, 828, 812, 863, 633, 293, 379, 296, 296, 436, 140, 135, 196, 230, 175, // + /* 16:00-19:45 */ + 365, 758, 325, 264, 181, 167, 228, 1082, 777, 417, 798, 1268, 409, 830, 1191, 417, // + /* 20:00-23:45 */ + 1087, 2958, 2946, 2235, 1343, 483, 796, 1201, 567, 395, 989, 1066, 370, 989, 1255, 660, // + /* 00:00-03:45 */ + 349, 880, 1186, 580, 327, 911, 1135, 553, 265, 938, 1165, 567, 278, 863, 1239, 658, // + /* 04:00-07:45 */ + 236, 816, 1173, 1131, 498, 550, 1344, 1226, 874, 504, 1733, 1809, 1576, 369, 771, 2583, // + /* 08:00-11:45 */ + 3202, 2174, 1878, 2132, 2109, 1895, 1565, 1477, 1613, 1716, 1867, 1726, 1700, 1787, 1755, 1734, // + /* 12:00-15:45 */ + 1380, 691, 338, 168, 199, 448, 662, 205, 183, 70, 169, 276, 149, 76, 195, 168, // + /* 16:00-19:45 */ + 159, 266, 135, 120, 224, 979, 2965, 1337, 1116, 795, 334, 390, 433, 369, 762, 2908, // + /* 20:00-23:45 */ + 3226, 2358, 1778, 1002, 455, 654, 534, 1587, 1638, 459, 330, 258, 368, 728, 1096, 878 // + }; + + @Test + public void test() throws OpenemsException, Exception { + var clock = new TimeLeapClock(Instant.parse("2020-01-01T00:00:00.00Z"), ZoneOffset.UTC); + var componentManager = new DummyComponentManager(clock); + var consumptionPrediction = new DummyPrediction24Hours(DEFAULT_CONSUMPTION_PREDICTION); + var consumptionPredictor = new DummyPredictor24Hours(PREDICTOR_ID, componentManager, consumptionPrediction, + "_sum/ConsumptionActivePower"); + + var sut = new PredictorManagerImpl(); + new ComponentTest(sut) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("componentManager", componentManager) // + .addReference("predictors", List.of(consumptionPredictor)) // + .activate(MyConfig.create()// + .build()); + + assertEquals(Prediction24Hours.EMPTY, sut.get24HoursPrediction(new ChannelAddress("_sum", "FooBar"))); + + assertArrayEquals(// + // First 96 elements only + Stream.of(DEFAULT_CONSUMPTION_PREDICTION).limit(96).toArray(Integer[]::new), + sut.get24HoursPrediction(new ChannelAddress("_sum", "UnmanagedConsumptionActivePower")).getValues()); + } + +} diff --git a/io.openems.edge.ess.api/src/io/openems/edge/ess/test/DummyManagedAsymmetricEss.java b/io.openems.edge.ess.api/src/io/openems/edge/ess/test/DummyManagedAsymmetricEss.java index 8bbfe4e6f0d..f7fbfa7e312 100644 --- a/io.openems.edge.ess.api/src/io/openems/edge/ess/test/DummyManagedAsymmetricEss.java +++ b/io.openems.edge.ess.api/src/io/openems/edge/ess/test/DummyManagedAsymmetricEss.java @@ -4,6 +4,7 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.startstop.StartStoppable; import io.openems.edge.ess.api.AsymmetricEss; import io.openems.edge.ess.api.ManagedAsymmetricEss; import io.openems.edge.ess.api.ManagedSymmetricEss; @@ -14,8 +15,8 @@ * Provides a simple, simulated ManagedAsymmetricEss component that can be used * together with the OpenEMS Component test framework. */ -public class DummyManagedAsymmetricEss extends DummyManagedSymmetricEss - implements ManagedAsymmetricEss, ManagedSymmetricEss, AsymmetricEss, SymmetricEss, OpenemsComponent { +public class DummyManagedAsymmetricEss extends DummyManagedSymmetricEss implements ManagedAsymmetricEss, + ManagedSymmetricEss, AsymmetricEss, SymmetricEss, StartStoppable, OpenemsComponent { private Consumer asymmetricApplyPowerCallback = null; @@ -25,8 +26,8 @@ public DummyManagedAsymmetricEss(String id, Power power) { SymmetricEss.ChannelId.values(), // AsymmetricEss.ChannelId.values(), // ManagedSymmetricEss.ChannelId.values(), // - ManagedAsymmetricEss.ChannelId.values() // - ); + ManagedAsymmetricEss.ChannelId.values(), // + StartStoppable.ChannelId.values()); } public DummyManagedAsymmetricEss(String id) { diff --git a/io.openems.edge.ess.api/src/io/openems/edge/ess/test/DummyManagedSymmetricEss.java b/io.openems.edge.ess.api/src/io/openems/edge/ess/test/DummyManagedSymmetricEss.java index 4c7caa2cb49..ed047088e76 100644 --- a/io.openems.edge.ess.api/src/io/openems/edge/ess/test/DummyManagedSymmetricEss.java +++ b/io.openems.edge.ess.api/src/io/openems/edge/ess/test/DummyManagedSymmetricEss.java @@ -2,9 +2,12 @@ import java.util.function.Consumer; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.startstop.StartStop; +import io.openems.edge.common.startstop.StartStoppable; import io.openems.edge.common.sum.GridMode; import io.openems.edge.ess.api.ManagedSymmetricEss; import io.openems.edge.ess.api.SymmetricEss; @@ -15,7 +18,7 @@ * together with the OpenEMS Component test framework. */ public class DummyManagedSymmetricEss extends AbstractOpenemsComponent - implements ManagedSymmetricEss, SymmetricEss, OpenemsComponent { + implements ManagedSymmetricEss, SymmetricEss, StartStoppable, OpenemsComponent { public static final int MAX_APPARENT_POWER = Integer.MAX_VALUE; @@ -42,8 +45,8 @@ public DummyManagedSymmetricEss(String id, Power power) { this(id, power, // OpenemsComponent.ChannelId.values(), // ManagedSymmetricEss.ChannelId.values(), // - SymmetricEss.ChannelId.values() // - ); + SymmetricEss.ChannelId.values(), // + StartStoppable.ChannelId.values()); } public DummyManagedSymmetricEss(String id) { @@ -198,4 +201,9 @@ public SymmetricApplyPowerRecord(int activePower, int reactivePower) { this.reactivePower = reactivePower; } } + + @Override + public void setStartStop(StartStop value) throws OpenemsNamedException { + this._setStartStop(value); + } } diff --git a/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/ChannelManager.java b/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/ChannelManager.java index 01207dc3527..ab6c8053495 100644 --- a/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/ChannelManager.java +++ b/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/ChannelManager.java @@ -1,5 +1,8 @@ package io.openems.edge.ess.cluster; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.groupingBy; + import java.util.List; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -8,6 +11,8 @@ import io.openems.edge.common.channel.AbstractChannelListenerManager; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.startstop.StartStop; +import io.openems.edge.common.startstop.StartStoppable; import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.type.TypeUtils; import io.openems.edge.ess.api.AsymmetricEss; @@ -56,6 +61,8 @@ protected void activate(List esss) { // ManagedSymmetricEss this.calculate(INTEGER_SUM, esss, ManagedSymmetricEss.ChannelId.ALLOWED_CHARGE_POWER); this.calculate(INTEGER_SUM, esss, ManagedSymmetricEss.ChannelId.ALLOWED_DISCHARGE_POWER); + // StartStoppable + this.calculateStartStop(esss); } /** @@ -94,6 +101,36 @@ private void calculateGridMode(List esss) { } } + /** + * Calculate effective StartStop status of {@link SymmetricEss}. + * + * @param esss the List of {@link SymmetricEss} + */ + private void calculateStartStop(List esss) { + final var startStoppableEss = esss.stream() // + .filter(StartStoppable.class::isInstance) // + .map(StartStoppable.class::cast) // + .toList(); + final BiConsumer, Value> callback = (oldValue, newValue) -> { + final var essMap = startStoppableEss.stream() // + .collect(groupingBy(StartStoppable::getStartStop)); + + var result = StartStop.UNDEFINED; + if (!startStoppableEss.isEmpty()) { + if (essMap.getOrDefault(StartStop.START, emptyList()).size() == startStoppableEss.size()) { + result = StartStop.START; + } + if (essMap.getOrDefault(StartStop.STOP, emptyList()).size() == startStoppableEss.size()) { + result = StartStop.STOP; + } + } + this.parent._setStartStop(result); + }; + startStoppableEss.forEach(ess -> { + this.addOnChangeListener(ess, StartStoppable.ChannelId.START_STOP, callback); + }); + } + /** * Calculate weighted State-Of-Charge of {@link SymmetricEss}. * diff --git a/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/EssCluster.java b/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/EssCluster.java index 73644bbabd2..60cc255d77b 100644 --- a/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/EssCluster.java +++ b/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/EssCluster.java @@ -3,6 +3,7 @@ import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.modbusslave.ModbusSlave; +import io.openems.edge.common.startstop.StartStoppable; import io.openems.edge.ess.api.AsymmetricEss; import io.openems.edge.ess.api.ManagedAsymmetricEss; import io.openems.edge.ess.api.ManagedSymmetricEss; @@ -10,7 +11,7 @@ import io.openems.edge.ess.api.SymmetricEss; public interface EssCluster extends ManagedAsymmetricEss, AsymmetricEss, ManagedSymmetricEss, SymmetricEss, MetaEss, - OpenemsComponent, ModbusSlave { + OpenemsComponent, ModbusSlave, StartStoppable { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { ; diff --git a/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/EssClusterImpl.java b/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/EssClusterImpl.java index 813cc7fe9b5..1f97b086105 100644 --- a/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/EssClusterImpl.java +++ b/io.openems.edge.ess.cluster/src/io/openems/edge/ess/cluster/EssClusterImpl.java @@ -102,7 +102,7 @@ public EssClusterImpl() { @Activate private void activate(ComponentContext context, Config config) throws OpenemsException { this.config = config; - super.activate(context, config.id(), config.alias(), config.enabled()); + this.activate(context, config.id(), config.alias(), config.enabled()); if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "Ess", config.ess_ids())) { return; } @@ -130,7 +130,7 @@ public void applyPower(int activePowerL1, int reactivePowerL1, int activePowerL2 @Override public int getPowerPrecision() { Integer result = null; - for (SymmetricEss ess : this.esss) { + for (var ess : this.esss) { if (ess instanceof ManagedSymmetricEss) { result = TypeUtils.min(result, ((ManagedSymmetricEss) ess).getPowerPrecision()); } @@ -161,49 +161,30 @@ public void handleEvent(Event event) { return; } switch (event.getTopic()) { - case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE: - this.handleStartStop(); - break; + case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE -> this.handleStartStop(); } } /** - * Handles the Start/Stop target from {@link Config} or set via - * {@link #setStartStop(StartStop)}. + * Starts/Stops all ESS in the Cluster as required by Config or call to + * setStartStop(). */ private void handleStartStop() { - StartStop target = StartStop.UNDEFINED; - - switch (this.config.startStop()) { - case AUTO: { - target = this.startStopTarget.get(); - break; - } - case START: { - target = StartStop.START; - break; - } - case STOP: { - target = StartStop.STOP; - break; - } - } - - if (target == StartStop.UNDEFINED) { - this.logInfo(this.log, "Start-Stop-Target is Undefined"); + var target = this.getStartStopTarget(); + if (target == this.getStartStop()) { return; } - for (SymmetricEss ess : this.esss) { - if (ess instanceof StartStoppable) { - try { - ((StartStoppable) ess).setStartStop(target); - } catch (OpenemsNamedException e) { - this.logError(this.log, e.getMessage()); - e.printStackTrace(); - } - } - } + this.esss.stream() // + .filter(StartStoppable.class::isInstance) // + .map(StartStoppable.class::cast) // + .forEach(ess -> { + try { + ess.setStartStop(target); + } catch (OpenemsNamedException e) { + this.logError(this.log, e.getMessage()); + } + }); } @Override @@ -215,4 +196,12 @@ public synchronized String[] getEssIds() { public void setStartStop(StartStop value) { this.startStopTarget.set(value); } + + private StartStop getStartStopTarget() { + return switch (this.config.startStop()) { + case AUTO -> this.startStopTarget.get(); + case START -> StartStop.START; + case STOP -> StartStop.STOP; + }; + } } diff --git a/io.openems.edge.ess.cluster/test/io/openems/edge/ess/cluster/EssClusterImplTest.java b/io.openems.edge.ess.cluster/test/io/openems/edge/ess/cluster/EssClusterImplTest.java index 8778d12276f..bc0f208f1af 100644 --- a/io.openems.edge.ess.cluster/test/io/openems/edge/ess/cluster/EssClusterImplTest.java +++ b/io.openems.edge.ess.cluster/test/io/openems/edge/ess/cluster/EssClusterImplTest.java @@ -3,6 +3,7 @@ import org.junit.Test; import io.openems.common.types.ChannelAddress; +import io.openems.edge.common.startstop.StartStop; import io.openems.edge.common.startstop.StartStopConfig; import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.test.AbstractComponentTest.TestCase; @@ -47,6 +48,9 @@ public class EssClusterImplTest { "AllowedDischargePower"); private static final ChannelAddress ESS2_ALLOWED_DISCHARGE_POWER = new ChannelAddress(ESS2_ID, "AllowedDischargePower"); + private static final ChannelAddress CLUSTER_START_STOP = new ChannelAddress(CLUSTER_ID, "StartStop"); + private static final ChannelAddress ESS1_START_STOP = new ChannelAddress(ESS1_ID, "StartStop"); + private static final ChannelAddress ESS2_START_STOP = new ChannelAddress(ESS2_ID, "StartStop"); @Test public void testCluster() throws Exception { @@ -145,4 +149,36 @@ public void testSoc() throws Exception { ) // ; } + + @Test + public void testStartStop() throws Exception { + new ComponentTest(new EssClusterImpl()) // + .addReference("power", new DummyPower()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("addEss", new DummyManagedSymmetricEss(ESS1_ID)) // + .addReference("addEss", new DummyManagedSymmetricEss(ESS2_ID)) // + .activate(MyConfig.create() // + .setId(CLUSTER_ID) // + .setEssIds(ESS1_ID, ESS2_ID) // + .setStartStop(StartStopConfig.START) // + .build()) + .next(new TestCase() // + .input(ESS1_START_STOP, StartStop.UNDEFINED) // + .input(ESS2_START_STOP, StartStop.STOP) // + .output(CLUSTER_START_STOP, StartStop.UNDEFINED)) // + .next(new TestCase() // + .input(ESS1_START_STOP, StartStop.STOP) // + .input(ESS2_START_STOP, StartStop.STOP) // + .output(CLUSTER_START_STOP, StartStop.STOP)) // + .next(new TestCase() // + .input(ESS1_START_STOP, StartStop.START) // + .input(ESS2_START_STOP, StartStop.STOP) // + .output(CLUSTER_START_STOP, StartStop.UNDEFINED)) // + .next(new TestCase() // + .input(ESS1_START_STOP, StartStop.START) // + .input(ESS2_START_STOP, StartStop.START) // + .output(CLUSTER_START_STOP, StartStop.START)) // + + ; + } } \ No newline at end of file diff --git a/io.openems.edge.evcs.alpitronic.hypercharger/src/io/openems/edge/evcs/hypercharger/EvcsAlpitronicHyperchargerImpl.java b/io.openems.edge.evcs.alpitronic.hypercharger/src/io/openems/edge/evcs/hypercharger/EvcsAlpitronicHyperchargerImpl.java index b00045ddae0..c3442232ad9 100644 --- a/io.openems.edge.evcs.alpitronic.hypercharger/src/io/openems/edge/evcs/hypercharger/EvcsAlpitronicHyperchargerImpl.java +++ b/io.openems.edge.evcs.alpitronic.hypercharger/src/io/openems/edge/evcs/hypercharger/EvcsAlpitronicHyperchargerImpl.java @@ -161,7 +161,7 @@ public void handleEvent(Event event) { } switch (event.getTopic()) { case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: - this.calculateTotalEnergy.update(this.getChargePower().orElse(0)); + this.calculateTotalEnergy.update(this.getChargePower().get()); break; case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE: this.writeHandler.run(); diff --git a/io.openems.edge.io.api/src/io/openems/edge/io/api/AnalogOutput.java b/io.openems.edge.io.api/src/io/openems/edge/io/api/AnalogOutput.java new file mode 100644 index 00000000000..7de88c55c68 --- /dev/null +++ b/io.openems.edge.io.api/src/io/openems/edge/io/api/AnalogOutput.java @@ -0,0 +1,179 @@ +package io.openems.edge.io.api; + +import java.util.function.Consumer; + +import org.osgi.annotation.versioning.ProviderType; + +import io.openems.common.channel.AccessMode; +import io.openems.common.channel.Unit; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.OpenemsType; +import io.openems.common.utils.IntUtils; +import io.openems.common.utils.IntUtils.Round; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.FloatDoc; +import io.openems.edge.common.channel.FloatReadChannel; +import io.openems.edge.common.channel.FloatWriteChannel; +import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.component.OpenemsComponent; + +@ProviderType +public interface AnalogOutput extends OpenemsComponent { + + /** + * Output will be set in Unit.PERCENT, because the power is strongly depending + * on the controller and its power behavior. + */ + public static final int SET_OUTPUT_ACCURACY = 100; + + /** + * Gets the information about the AnalogOutput range, to handle its output + * values. + * + * @param offset The offset of the range e.g. offset of 6000 if the target + * should be given from 6A to 24A + * @param precision Gets the smallest positive value that can be set. Unit is + * depending on the control Unit. Example: + *
    + *
  • Device allows setting of voltage in 0.1V steps. It + * should return 100. + *
  • Device allows setting of ampere in 1A steps. It should + * return 1000. + *
+ * @param maximum The maximum value that can be set e.g. 24000mA or 10000mV. + */ + public record Range(int offset, int precision, int maximum) { + } + + /** + * Provides a consumer that sets the individual output channel of the + * implementation. + * + *

+ * Accept is called on SetNextWrite of {@link ChannelId#SET_OUTPUT_PERCENT}. The + * consumed value is already formatted to the current range and precision. + * + *

+ * Setting the value in a method like setOutputChannel(int output) directly in + * the implementation would look like it is a common method for other + * controllers + * + * @return consumer, setting the individual output channel + */ + public Consumer setOutputChannel(); + + /** + * Range that can be used, limited by the analog IO hardware. + * + *

+ * E.g. Hardware can be set from 0 to 10V with 0.1V steps. + * + * @return maximum range. + */ + public Range range(); + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + + /** + * Holds writes of the Relay Output for debugging. + * + *

    + *
  • Type: Integer + *
  • Range: 0 - 100 + *
  • Unit: % + *
+ */ + DEBUG_SET_OUTPUT_PERCENT(Doc.of(OpenemsType.FLOAT) // + .unit(Unit.PERCENT)), // + + /** + * Set Relay Output. + * + *

+ * The Output set by a controller is set in Unit.PERCENT, because the power is + * strongly depending on the controller and its power behavior. + * + *

    + *
  • Type: Float + *
  • Unit: % + *
  • Range: 0 - 1000 + *
+ */ + SET_OUTPUT_PERCENT(new FloatDoc() // + .accessMode(AccessMode.READ_WRITE) // + .unit(Unit.PERCENT) // + .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_SET_OUTPUT_PERCENT) + .onChannelSetNextWrite((analogOutput, value) -> { + + final var offset = analogOutput.range().offset(); + final var max = analogOutput.range().maximum(); + + var setOutput = offset + (max - offset) * /* Factor */(value / (float) SET_OUTPUT_ACCURACY); + + var setValidOutput = IntUtils.roundToPrecision(// + setOutput, Round.HALF_UP, analogOutput.range().precision()); + + // Set output channel. + analogOutput.setOutputChannel().accept(setValidOutput); + })); + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + /** + * Gets the current set output as factor. + * + * @return current factor + */ + public default float getSetOutputPercentAsFactor() { + return this.getDebugSetOutputPercent().orElse(0f) / (float) SET_OUTPUT_ACCURACY; + } + + /** + * Gets the Channel for {@link ChannelId#SET_OUTPUT_PERCENT}. + * + * @return the Channel + */ + public default FloatWriteChannel getSetOutputPercentChannel() { + return this.channel(ChannelId.SET_OUTPUT_PERCENT); + } + + /** + * Sets the output value of the AnalogOutput in %. See + * {@link ChannelId#SET_OUTPUT_PERCENT}. + * + * @param value the next write value + * @throws OpenemsNamedException on error + */ + public default void setOutputPercent(Float value) throws OpenemsNamedException { + this.getSetOutputPercentChannel().setNextWriteValue(value); + } + + /** + * Gets the Channel for {@link ChannelId#DEBUG_SET_OUTPUT_PERCENT}. + * + * @return the Channel + */ + public default FloatReadChannel getDebugSetOutputPercentChannel() { + return this.channel(ChannelId.DEBUG_SET_OUTPUT_PERCENT); + } + + /** + * Gets the set output value of the I/O. See + * {@link ChannelId#DEBUG_SET_OUTPUT_PERCENT}. + * + * @return the Channel {@link Value} + */ + public default Value getDebugSetOutputPercent() { + return this.getDebugSetOutputPercentChannel().value(); + } +} diff --git a/io.openems.edge.io.api/src/io/openems/edge/io/api/AnalogVoltageOutput.java b/io.openems.edge.io.api/src/io/openems/edge/io/api/AnalogVoltageOutput.java new file mode 100644 index 00000000000..f433f7a2a53 --- /dev/null +++ b/io.openems.edge.io.api/src/io/openems/edge/io/api/AnalogVoltageOutput.java @@ -0,0 +1,123 @@ +package io.openems.edge.io.api; + +import java.util.function.Consumer; + +import org.osgi.annotation.versioning.ProviderType; + +import io.openems.common.channel.AccessMode; +import io.openems.common.channel.Unit; +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.types.OpenemsType; +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.channel.IntegerReadChannel; +import io.openems.edge.common.channel.IntegerWriteChannel; +import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; +import io.openems.edge.common.modbusslave.ModbusType; + +@ProviderType +public interface AnalogVoltageOutput extends AnalogOutput { + + @Override + default Consumer setOutputChannel() { + return (output) -> { + try { + this.setOutputVoltage(output); + } catch (OpenemsNamedException e) { + e.printStackTrace(); + } + }; + } + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + + /** + * Holds writes of the Relay Output for debugging in mV. + * + *
    + *
  • Type: Integer + *
  • Range: 0 - range().maximum() + *
  • Unit: mV + *
+ */ + DEBUG_SET_OUTPUT_VOLTAGE(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIVOLT)), // + + /** + * Set Relay Output in Voltage. + * + *
    + *
  • Type: Integer + *
  • Range: 0 - range().maximum() + *
  • Unit: mV + *
+ */ + SET_OUTPUT_VOLTAGE(Doc.of(OpenemsType.INTEGER) // + .accessMode(AccessMode.READ_WRITE) // + .unit(Unit.MILLIVOLT) // + .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_SET_OUTPUT_VOLTAGE)); + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + /** + * Gets the Channel for {@link ChannelId#SET_OUTPUT_VOLTAGE}. + * + * @return the Channel + */ + public default IntegerWriteChannel getSetOutputVoltageChannel() { + return this.channel(ChannelId.SET_OUTPUT_VOLTAGE); + } + + /** + * Sets the voltage output value of the AnalogOutput in mV. See + * {@link ChannelId#SET_OUTPUT_VOLTAGE}. + * + * @param value the next write value + * @throws OpenemsNamedException on error + */ + public default void setOutputVoltage(Integer value) throws OpenemsNamedException { + this.getSetOutputVoltageChannel().setNextWriteValue(value); + } + + /** + * Gets the Channel for {@link ChannelId#DEBUG_SET_OUTPUT_VOLTAGE}. + * + * @return the Channel + */ + public default IntegerReadChannel getDebugSetOutputVoltageChannel() { + return this.channel(ChannelId.DEBUG_SET_OUTPUT_VOLTAGE); + } + + /** + * Gets the set voltage output value of the I/O. See + * {@link ChannelId#DEBUG_SET_OUTPUT_VOLTAGE}. + * + * @return the Channel {@link Value} + */ + public default Value getDebugSetOutputVoltage() { + return this.getDebugSetOutputVoltageChannel().value(); + } + + /** + * Used for Modbus/TCP Api Controller. Provides a Modbus table for the Channels + * of this Component. + * + * @param accessMode filters the Modbus-Records that should be shown + * @return the {@link ModbusSlaveNatureTable} + */ + public static ModbusSlaveNatureTable getModbusSlaveNatureTable(AccessMode accessMode) { + return ModbusSlaveNatureTable.of(AnalogVoltageOutput.class, accessMode, 80) // + .channel(0, ChannelId.SET_OUTPUT_VOLTAGE, ModbusType.UINT16) // + .build(); + } +} diff --git a/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyAnalogVoltageOutput.java b/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyAnalogVoltageOutput.java new file mode 100644 index 00000000000..6fea40643cc --- /dev/null +++ b/io.openems.edge.io.api/src/io/openems/edge/io/test/DummyAnalogVoltageOutput.java @@ -0,0 +1,51 @@ +package io.openems.edge.io.test; + +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.test.DummyComponentContext; +import io.openems.edge.io.api.AnalogOutput; +import io.openems.edge.io.api.AnalogVoltageOutput; + +/** + * Provides a simple, simulated Analog Voltage Output component that can be used + * together with the OpenEMS Component test framework. + */ +public class DummyAnalogVoltageOutput extends AbstractOpenemsComponent implements AnalogOutput, AnalogVoltageOutput { + + private Range range; + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + ; + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } + + public DummyAnalogVoltageOutput(String id) { + this(id, new Range(0, 100, 10000)); + } + + public DummyAnalogVoltageOutput(String id, Range range) { + super(// + OpenemsComponent.ChannelId.values(), // + AnalogOutput.ChannelId.values(), // + AnalogVoltageOutput.ChannelId.values() // + ); + super.activate(new DummyComponentContext(), id, "", true); + this.range = range; + } + + @Override + public Range range() { + return this.range; + } +} diff --git a/io.openems.edge.io.filipowski/.classpath b/io.openems.edge.io.filipowski/.classpath new file mode 100644 index 00000000000..bbfbdbe40e7 --- /dev/null +++ b/io.openems.edge.io.filipowski/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/io.openems.edge.io.filipowski/.gitignore b/io.openems.edge.io.filipowski/.gitignore new file mode 100644 index 00000000000..c2b941a96de --- /dev/null +++ b/io.openems.edge.io.filipowski/.gitignore @@ -0,0 +1,2 @@ +/bin_test/ +/generated/ diff --git a/io.openems.edge.io.filipowski/.project b/io.openems.edge.io.filipowski/.project new file mode 100644 index 00000000000..ba55fffab87 --- /dev/null +++ b/io.openems.edge.io.filipowski/.project @@ -0,0 +1,23 @@ + + + io.openems.edge.io.filipowski + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/io.openems.edge.io.filipowski/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.io.filipowski/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/io.openems.edge.io.filipowski/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/io.openems.edge.io.filipowski/bnd.bnd b/io.openems.edge.io.filipowski/bnd.bnd new file mode 100644 index 00000000000..1af09a07b06 --- /dev/null +++ b/io.openems.edge.io.filipowski/bnd.bnd @@ -0,0 +1,15 @@ +Bundle-Name: OpenEMS Edge IO Filipowski +Bundle-Vendor: FENECON GmbH +Bundle-License: https://opensource.org/licenses/EPL-2.0 +Bundle-Version: 1.0.0.${tstamp} + +-buildpath: \ + ${buildpath},\ + com.ghgande.j2mod,\ + io.openems.common,\ + io.openems.edge.bridge.modbus,\ + io.openems.edge.common,\ + io.openems.edge.io.api + +-testpath: \ + ${testpath} diff --git a/io.openems.edge.io.filipowski/readme.adoc b/io.openems.edge.io.filipowski/readme.adoc new file mode 100644 index 00000000000..fcaa1ebad67 --- /dev/null +++ b/io.openems.edge.io.filipowski/readme.adoc @@ -0,0 +1,18 @@ += Filipowski (F&F) Analog Output + +This bundle implements the MR-AO-1 analog output module, which has four possible analog outputs. + +Compatible with +- https://www.fif.com.pl/en/io-extension-modules/408-analog-voltage-output-mr-ao-1.html[MR-AO-1] + +Implemented Natures +- AnalogOutput + +Default Configuration + - Baud rate: 9600 + - Data bits: 8 + - Parity: NONE + - Start bits: 1 + - *Stop bits: 2* + +https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.io.analog.filipowski.mr[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/AnalogOutput.java b/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/AnalogOutput.java new file mode 100644 index 00000000000..3d26f3e56c6 --- /dev/null +++ b/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/AnalogOutput.java @@ -0,0 +1,15 @@ +package io.openems.edge.io.filipowski.analog.mr; + +public enum AnalogOutput { + OUTPUT_1(3000), // + OUTPUT_2(3001), // + OUTPUT_3(3002), // + OUTPUT_4(3003) // + ; + + public final int startAddress; + + private AnalogOutput(int startAddress) { + this.startAddress = startAddress; + } +} diff --git a/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/Config.java b/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/Config.java new file mode 100644 index 00000000000..c263d1ad0a3 --- /dev/null +++ b/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/Config.java @@ -0,0 +1,34 @@ +package io.openems.edge.io.filipowski.analog.mr; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(// + name = "IO F&F Filipowski MR-AO-1", // + description = "One analog output of a F&F MR-AO-1 module") +@interface Config { + + @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") + String id() default "analogIo0"; + + @AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID") + String alias() default ""; + + @AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?") + boolean enabled() default true; + + @AttributeDefinition(name = "Analog output", description = "ID of the analog output.") + AnalogOutput analogOutput() default AnalogOutput.OUTPUT_1; + + @AttributeDefinition(name = "Modbus-ID", description = "ID of Modbus bridge.") + String modbus_id() default "modbus0"; + + @AttributeDefinition(name = "Modbus Unit-ID", description = "The Unit-ID of the Modbus device.") + int modbusUnitId() default 100; + + @AttributeDefinition(name = "Modbus target filter", description = "This is auto-generated by 'Modbus-ID'.") + String Modbus_target() default "(enabled=true)"; + + String webconsole_configurationFactory_nameHint() default "IO F&F Filipowski MR-AO-1 [{id}]"; + +} \ No newline at end of file diff --git a/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1.java b/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1.java new file mode 100644 index 00000000000..7e155c54205 --- /dev/null +++ b/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1.java @@ -0,0 +1,21 @@ +package io.openems.edge.io.filipowski.analog.mr; + +import io.openems.edge.common.channel.Doc; +import io.openems.edge.common.component.OpenemsComponent; + +public interface IoFilipowskiMrAo1 extends OpenemsComponent { + + public enum ChannelId implements io.openems.edge.common.channel.ChannelId { + ; + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.doc; + } + } +} diff --git a/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1Impl.java b/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1Impl.java new file mode 100644 index 00000000000..ea29a1b0e6a --- /dev/null +++ b/io.openems.edge.io.filipowski/src/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1Impl.java @@ -0,0 +1,115 @@ +package io.openems.edge.io.filipowski.analog.mr; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.metatype.annotations.Designate; + +import io.openems.common.channel.AccessMode; +import io.openems.common.exceptions.OpenemsException; +import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; +import io.openems.edge.bridge.modbus.api.BridgeModbus; +import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; +import io.openems.edge.bridge.modbus.api.ModbusComponent; +import io.openems.edge.bridge.modbus.api.ModbusProtocol; +import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement; +import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask; +import io.openems.edge.bridge.modbus.api.task.FC6WriteRegisterTask; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.modbusslave.ModbusSlave; +import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; +import io.openems.edge.common.modbusslave.ModbusSlaveTable; +import io.openems.edge.common.taskmanager.Priority; +import io.openems.edge.io.api.AnalogOutput; +import io.openems.edge.io.api.AnalogVoltageOutput; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "IO.Flipowski.MR-AO-1", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +public class IoFilipowskiMrAo1Impl extends AbstractOpenemsModbusComponent implements IoFilipowskiMrAo1, AnalogOutput, + AnalogVoltageOutput, ModbusComponent, OpenemsComponent, ModbusSlave { + + private static final int MAXIMUM_VOLTAGE = 10_000; // mV + private static final int PRECISION = 100; // mV + private static final int OFFSET = 0; // mV + + @Reference + private ConfigurationAdmin cm; + + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) + protected void setModbus(BridgeModbus modbus) { + super.setModbus(modbus); + } + + private Config config = null; + + public IoFilipowskiMrAo1Impl() { + super(// + OpenemsComponent.ChannelId.values(), // + ModbusComponent.ChannelId.values(), // + AnalogOutput.ChannelId.values(), // + AnalogVoltageOutput.ChannelId.values(), // + IoFilipowskiMrAo1.ChannelId.values() // + ); + } + + @Activate + private void activate(ComponentContext context, Config config) throws OpenemsException { + this.config = config; + if (super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm, + "Modbus", config.modbus_id())) { + return; + } + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Override + protected ModbusProtocol defineModbusProtocol() throws OpenemsException { + final var address = this.config.analogOutput().startAddress; + return new ModbusProtocol(this, // + + // Output voltage + new FC3ReadRegistersTask(address, Priority.HIGH, // + m(AnalogVoltageOutput.ChannelId.SET_OUTPUT_VOLTAGE, new UnsignedWordElement(address), + ElementToChannelConverter.SCALE_FACTOR_2) // + ), + + new FC6WriteRegisterTask(address, m(AnalogVoltageOutput.ChannelId.SET_OUTPUT_VOLTAGE, + new UnsignedWordElement(address), ElementToChannelConverter.SCALE_FACTOR_2) // + )); + } + + @Override + public String debugLog() { + return this.getDebugSetOutputVoltage().asOptional().map(t -> t + "mV").orElse("?"); + } + + @Override + public Range range() { + return new Range(OFFSET, PRECISION, MAXIMUM_VOLTAGE); + } + + @Override + public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { + return new ModbusSlaveTable(// + OpenemsComponent.getModbusSlaveNatureTable(accessMode), // + AnalogVoltageOutput.getModbusSlaveNatureTable(accessMode), // + ModbusSlaveNatureTable.of(IoFilipowskiMrAo1.class, accessMode, 100)// + .build()); + } +} diff --git a/io.openems.edge.io.filipowski/test/.gitignore b/io.openems.edge.io.filipowski/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1ImplTest.java b/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1ImplTest.java new file mode 100644 index 00000000000..11ef8cd64dc --- /dev/null +++ b/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/IoFilipowskiMrAo1ImplTest.java @@ -0,0 +1,27 @@ +package io.openems.edge.io.filipowski.analog.mr; + +import org.junit.Test; + +import io.openems.edge.bridge.modbus.test.DummyModbusBridge; +import io.openems.edge.common.test.AbstractComponentTest.TestCase; +import io.openems.edge.common.test.ComponentTest; +import io.openems.edge.common.test.DummyConfigurationAdmin; + +public class IoFilipowskiMrAo1ImplTest { + + private static final String COMPONENT_ID = "component0"; + private static final String MODBUS_ID = "modbus0"; + + @Test + public void test() throws Exception { + new ComponentTest(new IoFilipowskiMrAo1Impl()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .activate(MyConfig.create() // + .setId(COMPONENT_ID) // + .setModbusId(MODBUS_ID) // + .setRelayContact(AnalogOutput.OUTPUT_1) // + .build()) + .next(new TestCase()); + } +} diff --git a/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/MyConfig.java b/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/MyConfig.java new file mode 100644 index 00000000000..012a2f3deb9 --- /dev/null +++ b/io.openems.edge.io.filipowski/test/io/openems/edge/io/filipowski/analog/mr/MyConfig.java @@ -0,0 +1,78 @@ +package io.openems.edge.io.filipowski.analog.mr; + +import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.utils.ConfigUtils; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String modbusId = null; + private int modbusUnitId; + private AnalogOutput relayContact; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setModbusId(String modbusId) { + this.modbusId = modbusId; + return this; + } + + public Builder setModbusUnitId(int modbusUnitId) { + this.modbusUnitId = modbusUnitId; + return this; + } + + public Builder setRelayContact(AnalogOutput relayContact) { + this.relayContact = relayContact; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String modbus_id() { + return this.builder.modbusId; + } + + @Override + public String Modbus_target() { + return ConfigUtils.generateReferenceTargetFilter(this.id(), this.modbus_id()); + } + + @Override + public int modbusUnitId() { + return this.builder.modbusUnitId; + } + + @Override + public AnalogOutput analogOutput() { + return this.builder.relayContact; + } +} \ No newline at end of file diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25.java index 63561d81293..b276675dc5e 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25.java @@ -4,6 +4,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.channel.Level; +import io.openems.common.channel.PersistencePriority; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.BooleanDoc; @@ -38,6 +39,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId */ RELAY_1(new BooleanDoc() // .accessMode(AccessMode.READ_WRITE) // + .persistencePriority(PersistencePriority.HIGH) // .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY_1)), /** * Holds writes to Relay Output 2 for debugging. @@ -60,6 +62,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId */ RELAY_2(new BooleanDoc() // .accessMode(AccessMode.READ_WRITE) // + .persistencePriority(PersistencePriority.HIGH) // .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY_2)), /** * Slave Communication Failed Fault. diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/IoShellyPlug.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/IoShellyPlug.java index 3edcb55b602..de21b2cc702 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/IoShellyPlug.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellyplug/IoShellyPlug.java @@ -4,6 +4,7 @@ import io.openems.common.channel.AccessMode; import io.openems.common.channel.Level; +import io.openems.common.channel.PersistencePriority; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.BooleanDoc; @@ -41,6 +42,7 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId */ RELAY(new BooleanDoc() // .accessMode(AccessMode.READ_WRITE) // + .persistencePriority(PersistencePriority.HIGH) // .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY)), /** * Slave Communication Failed Fault. diff --git a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/Config.java b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/Config.java index 6365fa87076..7797744593a 100644 --- a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/Config.java +++ b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/Config.java @@ -32,6 +32,9 @@ @AttributeDefinition(name = "Meter-Type", description = "What is measured by this Meter?") MeterType type() default MeterType.PRODUCTION; + @AttributeDefinition(name = "Invert measurement", description = "Inverts power and current, swaps production and consumption energy") + boolean invert() default false; + String webconsole_configurationFactory_nameHint() default "Meter Phoenix Contact [{id}]"; } \ No newline at end of file diff --git a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeter.java b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeter.java index 83393e025e0..a3437147482 100644 --- a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeter.java +++ b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeter.java @@ -1,9 +1,12 @@ package io.openems.edge.meter.phoenixcontact; +import io.openems.edge.bridge.modbus.api.ModbusComponent; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.modbusslave.ModbusSlave; +import io.openems.edge.meter.api.ElectricityMeter; -public interface PhoenixContactMeter extends OpenemsComponent { +public interface PhoenixContactMeter extends ElectricityMeter, ModbusComponent, OpenemsComponent, ModbusSlave { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { ; diff --git a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImpl.java b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImpl.java index 1d1c7af710e..5645a4f13a1 100644 --- a/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImpl.java +++ b/io.openems.edge.meter.phoenixcontact/src/io/openems/edge/meter/phoenixcontact/PhoenixContactMeterImpl.java @@ -1,5 +1,6 @@ package io.openems.edge.meter.phoenixcontact; +import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.INVERT_IF_TRUE; import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_3; import static io.openems.edge.bridge.modbus.api.element.WordOrder.LSWMSW; @@ -15,6 +16,7 @@ import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.Designate; +import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent; import io.openems.edge.bridge.modbus.api.BridgeModbus; @@ -24,6 +26,9 @@ import io.openems.edge.bridge.modbus.api.element.FloatDoublewordElement; import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.modbusslave.ModbusSlave; +import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; +import io.openems.edge.common.modbusslave.ModbusSlaveTable; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.meter.api.ElectricityMeter; import io.openems.edge.meter.api.MeterType; @@ -35,12 +40,13 @@ configurationPolicy = ConfigurationPolicy.REQUIRE // ) public class PhoenixContactMeterImpl extends AbstractOpenemsModbusComponent - implements ElectricityMeter, PhoenixContactMeter, ModbusComponent, OpenemsComponent { + implements ElectricityMeter, PhoenixContactMeter, ModbusComponent, OpenemsComponent, ModbusSlave { @Reference private ConfigurationAdmin cm; private MeterType type = MeterType.PRODUCTION; + private boolean invert = false; public PhoenixContactMeterImpl() { super(// @@ -59,6 +65,7 @@ protected void setModbus(BridgeModbus modbus) { @Activate private void activate(ComponentContext context, Config config) throws OpenemsException { this.type = config.type(); + this.invert = config.invert(); if (super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm, "Modbus", config.modbus_id())) { return; @@ -92,24 +99,28 @@ protected ModbusProtocol defineModbusProtocol() throws OpenemsException { .wordOrder(LSWMSW), SCALE_FACTOR_3), // new DummyRegisterElement(0x8016, 0x8015), // m(ElectricityMeter.ChannelId.ACTIVE_POWER, new FloatDoublewordElement(0x8016) // - .wordOrder(LSWMSW)), // + .wordOrder(LSWMSW), INVERT_IF_TRUE(this.invert)), // new DummyRegisterElement(0x8018, 0x801D), // m(ElectricityMeter.ChannelId.ACTIVE_POWER_L1, new FloatDoublewordElement(0x801E) // - .wordOrder(LSWMSW)), // + .wordOrder(LSWMSW), INVERT_IF_TRUE(this.invert)), // m(ElectricityMeter.ChannelId.ACTIVE_POWER_L2, new FloatDoublewordElement(0x8020) // - .wordOrder(LSWMSW)), // + .wordOrder(LSWMSW), INVERT_IF_TRUE(this.invert)), // m(ElectricityMeter.ChannelId.ACTIVE_POWER_L3, new FloatDoublewordElement(0x8022) // - .wordOrder(LSWMSW)), // + .wordOrder(LSWMSW), INVERT_IF_TRUE(this.invert)), // new DummyRegisterElement(0x8024, 0x803C), // m(ElectricityMeter.ChannelId.VOLTAGE, new FloatDoublewordElement(0x803D) // .wordOrder(LSWMSW), SCALE_FACTOR_3)), // new FC3ReadRegistersTask(0x8100, Priority.HIGH, // - m(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, new FloatDoublewordElement(0x8100) // - .wordOrder(LSWMSW)), // + m(this.invert ? ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY + : ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, + new FloatDoublewordElement(0x8100) // + .wordOrder(LSWMSW)), // new DummyRegisterElement(0x8102, 0x8105), // - m(ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, new FloatDoublewordElement(0x8106) // - .wordOrder(LSWMSW)) // + m(this.invert ? ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY + : ElectricityMeter.ChannelId.ACTIVE_CONSUMPTION_ENERGY, + new FloatDoublewordElement(0x8106) // + .wordOrder(LSWMSW)) // )); return modbusProtocol; @@ -124,4 +135,13 @@ public String debugLog() { public MeterType getMeterType() { return this.type; } + + @Override + public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { + return new ModbusSlaveTable(// + OpenemsComponent.getModbusSlaveNatureTable(accessMode), // + ElectricityMeter.getModbusSlaveNatureTable(accessMode), // + ModbusSlaveNatureTable.of(PhoenixContactMeter.class, accessMode, 100) // + .build()); + } } diff --git a/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/MyConfig.java b/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/MyConfig.java index f06eb052eb4..f168618fdee 100644 --- a/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/MyConfig.java +++ b/io.openems.edge.meter.phoenixcontact/test/io/openems/edge/meter/phoenixcontact/MyConfig.java @@ -12,6 +12,7 @@ protected static class Builder { private String modbusId = null; private int modbusUnitId; private MeterType meterType; + private boolean invert = false; private Builder() { } @@ -36,6 +37,11 @@ public Builder setMeterType(MeterType meterType) { return this; } + public Builder setInvert(boolean invert) { + this.invert = invert; + return this; + } + public MyConfig build() { return new MyConfig(this); } @@ -77,4 +83,9 @@ public MeterType type() { return this.builder.meterType; } + @Override + public boolean invert() { + return this.builder.invert; + } + } \ No newline at end of file diff --git a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java index c8e895869cd..eb792a5dd38 100644 --- a/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java +++ b/io.openems.edge.predictor.persistencemodel/src/io/openems/edge/predictor/persistencemodel/Config.java @@ -21,7 +21,7 @@ // TODO "_sum/ConsumptionActivePower" holds also actively controlled // consumption; replace, once we introduce a // 'Sum-Non-Regulated-Consumption'-Channel - String[] channelAddresses() default { "_sum/ProductionActivePower", "_sum/ConsumptionActivePower" }; + String[] channelAddresses() default { "_sum/ProductionActivePower", "_sum/UnmanagedConsumptionActivePower" }; String webconsole_configurationFactory_nameHint() default "Predictor Persistence-Model [{id}]"; diff --git a/io.openems.edge.predictor.similardaymodel/src/io/openems/edge/predictor/similardaymodel/Config.java b/io.openems.edge.predictor.similardaymodel/src/io/openems/edge/predictor/similardaymodel/Config.java index 2f323371893..da9d03e9a43 100644 --- a/io.openems.edge.predictor.similardaymodel/src/io/openems/edge/predictor/similardaymodel/Config.java +++ b/io.openems.edge.predictor.similardaymodel/src/io/openems/edge/predictor/similardaymodel/Config.java @@ -21,7 +21,7 @@ int numOfWeeks() default 4; @AttributeDefinition(name = "Channel-Addresses", description = "List of Channel-Addresses this Predictor is used for, e.g. '*/ActivePower', '*/ActualPower'") - String[] channelAddresses() default { "_sum/ProductionActivePower", "_sum/ConsumptionActivePower" }; + String[] channelAddresses() default { "_sum/ProductionActivePower", "_sum/UnmanagedConsumptionActivePower" }; String webconsole_configurationFactory_nameHint() default "Predictor Similarday-Model [{id}]"; diff --git a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java index dfc74112e38..e0f5ac2ac9b 100644 --- a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java +++ b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Config.java @@ -19,7 +19,7 @@ boolean enabled() default true; @AttributeDefinition(name = "Security Token", description = "Security token for the ENTSO-E Transparency Platform", type = AttributeType.PASSWORD) - String securityToken(); + String securityToken() default ""; @AttributeDefinition(name = "Bidding Zone", description = "Zone corresponding to the customer's location") BiddingZone biddingZone(); diff --git a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Token.java b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Token.java new file mode 100644 index 00000000000..f00cbfc9c4e --- /dev/null +++ b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/Token.java @@ -0,0 +1,19 @@ +package io.openems.edge.timeofusetariff.entsoe; + +public final class Token { + + // DO NOT PUBLISH THIS TOKEN + public static final String TOKEN = null; + // DO NOT PUBLISH THIS TOKEN + + private Token() { + } + + protected static final String parseOrNull(String token) { + if (token != null && !token.isBlank()) { + return token; + } + return TOKEN; + } + +} diff --git a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java index cea8876928d..ee9de90b674 100644 --- a/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java +++ b/io.openems.edge.timeofusetariff.entsoe/src/io/openems/edge/timeofusetariff/entsoe/TouEntsoeImpl.java @@ -7,6 +7,7 @@ import java.time.temporal.ChronoUnit; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; @@ -27,10 +28,12 @@ import com.google.common.collect.ImmutableSortedMap; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; import io.openems.common.utils.ThreadPoolUtils; import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.currency.Currency; import io.openems.edge.common.meta.Meta; import io.openems.edge.timeofusetariff.api.TimeOfUsePrices; import io.openems.edge.timeofusetariff.api.TimeOfUseTariff; @@ -55,7 +58,9 @@ public class TouEntsoeImpl extends AbstractOpenemsComponent implements TouEntsoe private Meta meta; private Config config = null; + private String securityToken = ""; private ZonedDateTime updateTimeStamp = null; + private ScheduledFuture future = null; public TouEntsoeImpl() { super(// @@ -76,7 +81,8 @@ private void activate(ComponentContext context, Config config) { return; } - if (config.securityToken() == null || config.securityToken().isBlank()) { + this.securityToken = Token.parseOrNull(config.securityToken()); + if (this.securityToken == null) { this.logError(this.log, "Please configure Security Token to access ENTSO-E"); return; } @@ -84,6 +90,9 @@ private void activate(ComponentContext context, Config config) { // React on updates to Currency. this.meta.getCurrencyChannel().onChange(this.onCurrencyChange); + + // Schedule once + this.scheduleTask(0); } @Deactivate @@ -98,12 +107,15 @@ protected void deactivate() { * * @param seconds execute task in seconds */ - private void scheduleTask(long seconds) { - this.executor.schedule(this.task, seconds, TimeUnit.SECONDS); + private synchronized void scheduleTask(long seconds) { + if (this.future != null) { + this.future.cancel(false); + } + this.future = this.executor.schedule(this.task, seconds, TimeUnit.SECONDS); } private final Runnable task = () -> { - var token = this.config.securityToken(); + var token = this.securityToken; var areaCode = this.config.biddingZone().code; var fromDate = ZonedDateTime.now().truncatedTo(ChronoUnit.HOURS); var toDate = fromDate.plusDays(1); @@ -113,7 +125,11 @@ private void scheduleTask(long seconds) { final var result = EntsoeApi.query(token, areaCode, fromDate, toDate); final var entsoeCurrency = Utils.parseCurrency(result); final var globalCurrency = this.meta.getCurrency(); - final var exchangeRate = globalCurrency.name() == entsoeCurrency // + if (globalCurrency == Currency.UNDEFINED) { + throw new OpenemsException("Global Currency is UNDEFINED. Please configure it in Core.Meta component"); + } + + final var exchangeRate = globalCurrency.name().equals(entsoeCurrency) // ? 1 // No need to fetch exchange rate from API. : Utils.exchangeRateParser(ExchangeRateApi.getExchangeRates(), globalCurrency); diff --git a/tools/build-debian-package.sh b/tools/build-debian-package.sh index 5f31b6f636e..15da1c5b623 100755 --- a/tools/build-debian-package.sh +++ b/tools/build-debian-package.sh @@ -12,6 +12,7 @@ main() { common_build_edge_and_ui_in_parallel prepare_deb_template build_deb + create_version_file clean_deb_template echo "# FINISHED" } @@ -29,19 +30,20 @@ initialize_environment() { if [[ "$VERSION" == *"-SNAPSHOT" ]]; then # Replace unwanted characters with '.', compliant with Debian version # Ref: https://unix.stackexchange.com/a/23673 - GIT_BRANCH="$(git branch --show-current | tr -cs 'a-zA-Z0-9\n' '.')" - GIT_HASH="" + VERSION_DEV_BRANCH="$(git branch --show-current)" + VERSION_DEV_COMMIT="" if [[ $(git diff --stat) != '' ]]; then - GIT_HASH="dirty" + VERSION_DEV_COMMIT="dirty" else - GIT_HASH="$(git rev-parse --short HEAD)" + VERSION_DEV_COMMIT="$(git rev-parse --short HEAD)" fi - DATE=$(date "+%Y%m%d.%H%M") + 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="${GIT_BRANCH}.${DATE}.${GIT_HASH}" + 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 DEB_FILE="${PACKAGE_NAME}.deb" + VERSION_FILE="${PACKAGE_NAME}.version" } print_header() { @@ -80,6 +82,10 @@ build_deb() { cd .. } +create_version_file() { + echo $VERSION > $VERSION_FILE +} + clean_deb_template() { cd tools/debian git clean -fd diff --git a/tools/common.sh b/tools/common.sh index 76672e4a218..48c8883b8e8 100644 --- a/tools/common.sh +++ b/tools/common.sh @@ -27,6 +27,9 @@ common_update_version_in_code() { sed --in-place "s/\(VERSION_MINOR = \)\([0-9]\+\);$/\1$VERSION_MINOR;/" $SRC_OPENEMS_CONSTANTS sed --in-place "s/\(VERSION_PATCH = \)\([0-9]\+\);$/\1$VERSION_PATCH;/" $SRC_OPENEMS_CONSTANTS sed --in-place "s/\(VERSION_STRING = \)\"\(.*\)\";$/\1\"$VERSION_STRING\";/" $SRC_OPENEMS_CONSTANTS + sed --in-place "s/\(VERSION_DEV_BRANCH = \)\"\(.*\)\";$/\1\"${VERSION_DEV_BRANCH/\//\\/}\";/" $SRC_OPENEMS_CONSTANTS + sed --in-place "s/\(VERSION_DEV_COMMIT = \)\"\(.*\)\";$/\1\"$VERSION_DEV_COMMIT\";/" $SRC_OPENEMS_CONSTANTS + sed --in-place "s/\(VERSION_DEV_BUILD_TIME = \)\"\(.*\)\";$/\1\"$VERSION_DEV_BUILD_TIME\";/" $SRC_OPENEMS_CONSTANTS echo "## Update $SRC_PACKAGE_JSON" sed --in-place "s/^\( \"version\": \"\).*\(\".*$\)/\1$VERSION\2/" $SRC_PACKAGE_JSON diff --git a/tools/prepare-release.sh b/tools/prepare-release.sh index ce00d9ee744..02a5e62262a 100644 --- a/tools/prepare-release.sh +++ b/tools/prepare-release.sh @@ -31,4 +31,4 @@ initialize_environment() { git checkout $SRC_CHANGELOG_CONSTANTS 2>/dev/null } -main; exit \ No newline at end of file +main; exit diff --git a/ui/karma.conf.local.js b/ui/karma.conf.local.js new file mode 100644 index 00000000000..b45ee0b947f --- /dev/null +++ b/ui/karma.conf.local.js @@ -0,0 +1,50 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/ngv'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ], + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'], + customLaunchers: { + ChromeHeadlessCI: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + }, + singleRun: false, + restartOnFileChange: true, + }); +}; diff --git a/ui/package-lock.json b/ui/package-lock.json index 2890aa9c62a..2b5deaf97b5 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -5361,9 +5361,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -6674,12 +6674,6 @@ "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==", "dev": true }, - "node_modules/@types/parse-json": { - "version": "2023.10.0-SNAPSHOT", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, "node_modules/@types/q": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", @@ -11785,6 +11779,12 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/@types/parse-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", + "integrity": "sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==", + "dev": true + }, "node_modules/cosmiconfig/node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -13244,15 +13244,15 @@ } }, "node_modules/eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", + "@eslint/js": "8.51.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -27625,9 +27625,9 @@ } }, "@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "dev": true }, "@humanwhocodes/config-array": { @@ -28627,11 +28627,6 @@ "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==", "dev": true }, - "@types/parse-json": { - "version": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, "@types/q": { "version": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", "integrity": "sha512-qYi3YV9inU/REEfxwVcGZzbS3KG/Xs90lv0Pr+lDtuVjBPGd1A+eciXzVSaRvLify132BfcvhvEjeVahrUl0Ug==", @@ -32300,6 +32295,12 @@ "yaml": "^1.10.0" }, "dependencies": { + "@types/parse-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", + "integrity": "sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -33377,15 +33378,15 @@ "dev": true }, "eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", + "@eslint/js": "8.51.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index 250ec32c7ed..d404cf3903a 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -50,8 +50,12 @@ import { UserComponent } from './user/user.component'; const routes: Routes = [ + + // TODO should be removed in the future + { path: 'index', redirectTo: 'login', pathMatch: 'full' }, { path: '', redirectTo: 'login', pathMatch: 'full' }, { path: 'login', component: LoginComponent, data: { navbarTitle: environment.uiTitle } }, + { path: 'overview', component: OverViewComponent }, { path: 'user', component: UserComponent }, @@ -114,12 +118,12 @@ const routes: Routes = [ ] }, - { path: 'demo', component: LoginComponent } + { path: 'demo', component: LoginComponent }, + // Fallback + { path: '**', pathMatch: 'full', redirectTo: 'login' } ]; -export const appRoutingProviders: any[] = [ - -]; +export const appRoutingProviders: any[] = []; @NgModule({ imports: [ @@ -127,4 +131,4 @@ export const appRoutingProviders: any[] = [ ], exports: [RouterModule] }) -export class AppRoutingModule { } +export class AppRoutingModule { } \ No newline at end of file diff --git a/ui/src/app/edge/history/abstracthistorychart.ts b/ui/src/app/edge/history/abstracthistorychart.ts index b76999a8b37..4dfa95191ab 100644 --- a/ui/src/app/edge/history/abstracthistorychart.ts +++ b/ui/src/app/edge/history/abstracthistorychart.ts @@ -11,6 +11,7 @@ import { ChannelAddress, Edge, EdgeConfig, Service, Utils } from "src/app/shared 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 { @@ -85,7 +86,7 @@ export abstract class AbstractHistoryChart { this.setLabel(config); this.getChannelAddresses(edge, config).then(channelAddresses => { - let request = new QueryHistoricTimeseriesDataRequest(fromDate, toDate, channelAddresses, resolution); + let request = new QueryHistoricTimeseriesDataRequest(DateUtils.maxDate(fromDate, this.edge?.firstSetupProtocol), toDate, channelAddresses, resolution); edge.sendRequest(this.service.websocket, request).then(response => { resolve(response as QueryHistoricTimeseriesDataResponse); }).catch(error => { @@ -126,7 +127,7 @@ export abstract class AbstractHistoryChart { let response: Promise = new Promise((resolve, reject) => { this.service.getCurrentEdge().then(edge => { this.service.getConfig().then(config => { - edge.sendRequest(this.service.websocket, new QueryHistoricTimeseriesEnergyPerPeriodRequest(fromDate, toDate, channelAddresses, resolution)).then(response => { + edge.sendRequest(this.service.websocket, new QueryHistoricTimeseriesEnergyPerPeriodRequest(DateUtils.maxDate(fromDate, this.edge?.firstSetupProtocol), toDate, channelAddresses, resolution)).then(response => { resolve(response as QueryHistoricTimeseriesEnergyPerPeriodResponse ?? new QueryHistoricTimeseriesEnergyPerPeriodResponse(response.id, { timestamps: [null], data: { null: null } })); diff --git a/ui/src/app/edge/history/abstracthistorywidget.ts b/ui/src/app/edge/history/abstracthistorywidget.ts index 5ebc4322f01..9c2b2d7306f 100644 --- a/ui/src/app/edge/history/abstracthistorywidget.ts +++ b/ui/src/app/edge/history/abstracthistorywidget.ts @@ -3,6 +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'; // NOTE: Auto-refresh of widgets is currently disabled to reduce server load export abstract class AbstractHistoryWidget { @@ -54,7 +55,7 @@ export abstract class AbstractHistoryWidget { this.service.getCurrentEdge().then(edge => { this.service.getConfig().then(config => { this.getChannelAddresses(edge, config).then(channelAddresses => { - let request = new QueryHistoricTimeseriesDataRequest(fromDate, toDate, channelAddresses, resolution); + let request = new QueryHistoricTimeseriesDataRequest(DateUtils.maxDate(fromDate, edge?.firstSetupProtocol), toDate, channelAddresses, resolution); edge.sendRequest(this.service.websocket, request).then(response => { let result = (response as QueryHistoricTimeseriesDataResponse).result; if (Object.keys(result.data).length != 0 && Object.keys(result.timestamps).length != 0) { diff --git a/ui/src/app/edge/history/historydataservice.ts b/ui/src/app/edge/history/historydataservice.ts index fc42fd2a7a7..f64210cf359 100644 --- a/ui/src/app/edge/history/historydataservice.ts +++ b/ui/src/app/edge/history/historydataservice.ts @@ -4,6 +4,7 @@ import { DataService } from "../../shared/genericComponents/shared/dataservice"; import { QueryHistoricTimeseriesEnergyRequest } from "../../shared/jsonrpc/request/queryHistoricTimeseriesEnergyRequest"; import { QueryHistoricTimeseriesEnergyResponse } from "../../shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; import { ChannelAddress, Edge, Service, Websocket } from "../../shared/shared"; +import { DateUtils } from "src/app/shared/utils/dateutils/dateutils"; @Injectable() export class HistoryDataService extends DataService { @@ -30,7 +31,7 @@ export class HistoryDataService extends DataService { if (Object.entries(this.channelAddresses).length > 0) { this.service.historyPeriod.subscribe(date => { - edge.sendRequest(this.websocket, new QueryHistoricTimeseriesEnergyRequest(date.from, date.to, Object.values(this.channelAddresses))) + edge.sendRequest(this.websocket, new QueryHistoricTimeseriesEnergyRequest(DateUtils.maxDate(date.from, edge?.firstSetupProtocol), date.to, Object.values(this.channelAddresses))) .then((response) => { let allComponents = {}; let result = (response as QueryHistoricTimeseriesEnergyResponse).result; diff --git a/ui/src/app/edge/live/Controller/Io/HeatingElement/modal/modal.html b/ui/src/app/edge/live/Controller/Io/HeatingElement/modal/modal.html index 5fd22c09ef6..f83d0dc6aef 100644 --- a/ui/src/app/edge/live/Controller/Io/HeatingElement/modal/modal.html +++ b/ui/src/app/edge/live/Controller/Io/HeatingElement/modal/modal.html @@ -1,5 +1,5 @@ - + diff --git a/ui/src/app/edge/settings/component/update/update.component.ts b/ui/src/app/edge/settings/component/update/update.component.ts index 3fc71d73e09..e238c5652be 100644 --- a/ui/src/app/edge/settings/component/update/update.component.ts +++ b/ui/src/app/edge/settings/component/update/update.component.ts @@ -61,7 +61,15 @@ export class ComponentUpdateComponent implements OnInit { Utils.deepCopy(property.schema, field); fields.push(field); if (component.properties[property.id]) { - model[property_id] = component.properties[property.id]; + + // filter arrays with nested objects + if (Array.isArray(component.properties[property.id]) && component.properties[property.id]?.length > 0 && component.properties[property.id]?.every(element => typeof element === 'object')) { + + // Stringify json for objects nested inside an array + model[property_id] = JSON.stringify(component.properties[property.id]); + } else { + model[property_id] = component.properties[property.id]; + } } } this.form = new FormGroup({}); diff --git a/ui/src/app/edge/settings/profile/profile.component.html b/ui/src/app/edge/settings/profile/profile.component.html index e1e8cb31824..4ead9357e59 100644 --- a/ui/src/app/edge/settings/profile/profile.component.html +++ b/ui/src/app/edge/settings/profile/profile.component.html @@ -25,8 +25,10 @@ {{ edge.producttype }} - {{ environment.edgeShortName }} Version - {{ edge.version | version:edge.role }} + {{ environment.edgeShortName }} Version + {{ edge.version | + version:edge.role }} Rolle @@ -97,9 +99,33 @@

{{ item.alias }} Download Protocol + + + General.manual + + Download Protocol + + + General.manual + + + + + + + General.manual + + + + + + + General.manual + + diff --git a/ui/src/app/index/index.module.ts b/ui/src/app/index/index.module.ts index 9402ff92492..7d383c6f7ef 100644 --- a/ui/src/app/index/index.module.ts +++ b/ui/src/app/index/index.module.ts @@ -3,9 +3,9 @@ import { NgModule } from '@angular/core'; import { RegistrationModule } from '../registration/registration.module'; import { SharedModule } from './../shared/shared.module'; import { FilterComponent } from './filter/filter.component'; -import { LoginComponent } from './login.component'; import { OverViewComponent } from './overview/overview.component'; import { SumStateComponent } from './shared/sumState'; +import { LoginComponent } from './login.component'; @NgModule({ imports: [ diff --git a/ui/src/app/index/login.component.html b/ui/src/app/index/login.component.html index 9591f2528bc..8c901dfc4d6 100644 --- a/ui/src/app/index/login.component.html +++ b/ui/src/app/index/login.component.html @@ -36,7 +36,7 @@ - + @@ -47,7 +47,7 @@
- E-Mail / Login.passwordLabel + E-Mail / Login.user @@ -76,7 +76,7 @@ - + Login diff --git a/ui/src/app/index/login.component.ts b/ui/src/app/index/login.component.ts index 8927e8f19a4..7da5d9265c5 100644 --- a/ui/src/app/index/login.component.ts +++ b/ui/src/app/index/login.component.ts @@ -16,6 +16,7 @@ export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { public form: FormGroup; private stopOnDestroy: Subject = new Subject(); private page = 0; + protected formIsDisabled: boolean = false; constructor( public service: Service, @@ -35,7 +36,7 @@ export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { // TODO add websocket status observable const interval = setInterval(() => { if (this.websocket.status === 'online') { - this.router.navigateByUrl('/overview'); + this.router.navigate(['/overview']); clearInterval(interval); } }, 1000); @@ -66,14 +67,42 @@ export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { } } + /** + * Trims credentials + * + * @param password the password + * @param username the username + * @returns trimmed credentials + */ + public static trimCredentials(password: string, username?: string): { password: string, username?: string } { + return { + password: password?.trim(), + ...(username && { username: username?.trim() }) + }; + } + /** * Login to OpenEMS Edge or Backend. * * @param param data provided in login form */ public doLogin(param: { username?: string, password: string }) { - this.websocket.login(new AuthenticateWithPasswordRequest(param)); - this.router.navigateByUrl("/overview"); + + param = LoginComponent.trimCredentials(param.password, param.username); + + // Prevent that user submits via keyevent 'enter' multiple times + if (this.formIsDisabled) { + return; + } + + this.formIsDisabled = true; + this.websocket.login(new AuthenticateWithPasswordRequest(param)) + .finally(() => { + + // Unclean + this.ngOnInit(); + this.formIsDisabled = false; + }); } /** @@ -108,4 +137,4 @@ export class LoginComponent implements OnInit, AfterContentChecked, OnDestroy { this.stopOnDestroy.next(); this.stopOnDestroy.complete(); } -} \ No newline at end of file +} diff --git a/ui/src/app/index/login.spec.ts b/ui/src/app/index/login.spec.ts new file mode 100644 index 00000000000..dbb2f8e75ce --- /dev/null +++ b/ui/src/app/index/login.spec.ts @@ -0,0 +1,29 @@ +import { LoginComponent } from "./login.component"; + +describe('Login', () => { + const password = " password "; + const username = " username "; + + it('#trimCredentials should trim password and username', () => { + { + // Username and password - OpenEMS Backend + expect(LoginComponent.trimCredentials(password, username)).toEqual({ password: "password", username: "username" }); + } + { + // Only Password - OpenEMS Edge + expect(LoginComponent.trimCredentials(password)).toEqual({ password: "password" }); + } + { + // Password is null + expect(LoginComponent.trimCredentials(null)).toEqual({ password: undefined }); + } + { + // Username is null + expect(LoginComponent.trimCredentials(password, null)).toEqual({ password: "password" }); + } + { + // Username and password are null + expect(LoginComponent.trimCredentials(null, null)).toEqual({ password: undefined }); + } + }); +}); \ No newline at end of file diff --git a/ui/src/app/shared/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.html b/ui/src/app/shared/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.html new file mode 100644 index 00000000000..4cd06e749bb --- /dev/null +++ b/ui/src/app/shared/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.html @@ -0,0 +1,21 @@ + + + + + + {{props.label}} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/app/shared/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.ts b/ui/src/app/shared/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.ts new file mode 100644 index 00000000000..6a5f5b0f4c3 --- /dev/null +++ b/ui/src/app/shared/formly/formly-field-checkbox-image/formly-field-checkbox-with-image.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { FieldWrapper } from '@ngx-formly/core'; + +@Component({ + selector: 'formly-field-checkbox-with-image', + templateUrl: './formly-field-checkbox-with-image.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FormlyFieldCheckboxWithImageComponent extends FieldWrapper implements OnInit { + + protected value: any; + + public ngOnInit() { + // If the default value is not set in beginning. + this.value = this.field.defaultValue; + } + + /** + * Needs to be updated manually, because @Angular Formly-Form doesnt do it on its own + */ + protected updateFormControl(event: CustomEvent) { + this.value = event.detail.checked; + this.formControl.setValue(this.value); + } + +} \ No newline at end of file diff --git a/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts b/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts index 8750b6cd559..4348f80e3f9 100644 --- a/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts +++ b/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts @@ -17,6 +17,7 @@ import { QueryHistoricTimeseriesDataResponse } from '../../jsonrpc/response/quer import { QueryHistoricTimeseriesEnergyResponse } from '../../jsonrpc/response/queryHistoricTimeseriesEnergyResponse'; import { ChartAxis, HistoryUtils, YAxisTitle } from '../../service/utils'; import { ChannelAddress, Edge, EdgeConfig, Service, Utils } from "../../shared"; +import { DateUtils } from '../../utils/dateutils/dateutils'; // NOTE: Auto-refresh of widgets is currently disabled to reduce server load @Directive() @@ -346,7 +347,7 @@ export abstract class AbstractHistoryChart implements OnInit { this.service.getCurrentEdge().then(edge => { this.service.getConfig().then(async () => { let channelAddresses = (await this.getChannelAddresses()).powerChannels; - let request = new QueryHistoricTimeseriesDataRequest(fromDate, toDate, channelAddresses, resolution); + let request = new QueryHistoricTimeseriesDataRequest(DateUtils.maxDate(fromDate, this.edge?.firstSetupProtocol), toDate, channelAddresses, resolution); edge.sendRequest(this.service.websocket, request).then(response => { let result = (response as QueryHistoricTimeseriesDataResponse)?.result; if (Object.keys(result).length != 0) { @@ -394,7 +395,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(fromDate, toDate, channelAddresses, resolution); + let request = new QueryHistoricTimeseriesEnergyPerPeriodRequest(DateUtils.maxDate(fromDate, this.edge?.firstSetupProtocol), toDate, channelAddresses, resolution); if (channelAddresses.length > 0) { edge.sendRequest(this.service.websocket, request).then(response => { @@ -446,7 +447,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 QueryHistoricTimeseriesEnergyRequest(fromDate, toDate, channelAddresses); + let request = new QueryHistoricTimeseriesEnergyRequest(DateUtils.maxDate(fromDate, edge?.firstSetupProtocol), toDate, channelAddresses); if (channelAddresses.length > 0) { edge.sendRequest(this.service.websocket, request).then(response => { let result = (response as QueryHistoricTimeseriesEnergyResponse)?.result; diff --git a/ui/src/app/shared/genericComponents/modal/help-button/help-button.html b/ui/src/app/shared/genericComponents/modal/help-button/help-button.html index 72728c683f8..b0e195c58ba 100644 --- a/ui/src/app/shared/genericComponents/modal/help-button/help-button.html +++ b/ui/src/app/shared/genericComponents/modal/help-button/help-button.html @@ -1,3 +1,6 @@ - +
+ +
+
\ No newline at end of file diff --git a/ui/src/app/shared/genericComponents/shared/formatter.ts b/ui/src/app/shared/genericComponents/shared/formatter.ts index 11a3c28bd91..bb2e8002a2f 100644 --- a/ui/src/app/shared/genericComponents/shared/formatter.ts +++ b/ui/src/app/shared/genericComponents/shared/formatter.ts @@ -18,7 +18,7 @@ export namespace Formatter { export const FORMAT_CELSIUS = (value: number) => { // TODO apply correct locale - return formatNumber(value, 'de', '1.0-0') + " °C"; + return formatNumber(value, 'de', '1.0-0') + " °C"; }; export const FORMAT_PERCENT = (value: number) => { diff --git a/ui/src/app/shared/header/header.component.ts b/ui/src/app/shared/header/header.component.ts index bf86839aa6b..96b97bb4747 100644 --- a/ui/src/app/shared/header/header.component.ts +++ b/ui/src/app/shared/header/header.component.ts @@ -163,7 +163,9 @@ export class HeaderComponent implements OnInit, OnDestroy, AfterViewChecked { this.cdRef.detectChanges(); } if (event.detail.value == "IndexHistory") { - this.router.navigate(["/device/" + this.service.currentEdge.value.id + "/history"], { replaceUrl: true }); + + /** Creates bug of being infinite forwarded betweeen live and history, if not relatively routed */ + this.router.navigate(['../history'], { relativeTo: this.route }); this.cdRef.detectChanges(); } } diff --git a/ui/src/app/shared/pickdate/pickdate.component.html b/ui/src/app/shared/pickdate/pickdate.component.html index 02df1632914..8164d823a3b 100644 --- a/ui/src/app/shared/pickdate/pickdate.component.html +++ b/ui/src/app/shared/pickdate/pickdate.component.html @@ -1,22 +1,11 @@ - - - - - - - {{service.historyPeriod.value.getText(translate)}} - - - - - - - - - - - - - - \ No newline at end of file + + + + + + {{service.historyPeriod.value.getText(translate)}} + + + + + \ No newline at end of file diff --git a/ui/src/app/shared/pickdate/pickdate.component.spec.ts b/ui/src/app/shared/pickdate/pickdate.component.spec.ts new file mode 100644 index 00000000000..9d847ca2e24 --- /dev/null +++ b/ui/src/app/shared/pickdate/pickdate.component.spec.ts @@ -0,0 +1,132 @@ +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 { PickDateComponent } from "./pickdate.component"; + +export function expectPreviousPeriod(testContext: TestContext, firstSetupProtocol: Date, expectToBe: boolean): void { + expect(PickDateComponent.isPreviousPeriodAllowed(testContext.service, firstSetupProtocol)).toBe(expectToBe); +}; + +export function expectNextPeriod(testContext: TestContext, expectToBe: boolean): void { + expect(PickDateComponent.isNextPeriodAllowed(testContext.service)).toBe(expectToBe); +}; + +describe('Pickdate', () => { + + let TEST_CONTEXT: TestContext; + beforeEach(() => + TEST_CONTEXT = sharedSetup() + ); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Day-View: firstSetupProtocol = today', () => { + const firstSetupProtocol = new Date(); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, false); + expectNextPeriod(TEST_CONTEXT, false); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Day-View: firstSetupProtocol = yesterday', () => { + const firstSetupProtocol = startOfDay(subDays(new Date(), 1)); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, true); + expectNextPeriod(TEST_CONTEXT, false); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Week-View: firstSetupProtocol = current week', () => { + const firstSetupProtocol = new Date(); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, false); + expectNextPeriod(TEST_CONTEXT, false); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Week-View: firstSetupProtocol = Start of previous week, current period = current week', () => { + const firstSetupProtocol = startOfWeek(subWeeks(new Date(), 1), { weekStartsOn: 1 }); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, true); + expectNextPeriod(TEST_CONTEXT, false); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Week-View: firstSetupProtocol = Today, current period = previous week', () => { + const firstSetupProtocol = new Date(); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, false); + expectNextPeriod(TEST_CONTEXT, false); + }); + + const previousWeekPeriod = new DefaultTypes.HistoryPeriod(startOfWeek(subWeeks(new Date(), 1), { weekStartsOn: 1 }), endOfWeek(subWeeks(new Date(), 1), { weekStartsOn: 1 })); + const currentWeekPeriod = new DefaultTypes.HistoryPeriod(startOfWeek(new Date(), { weekStartsOn: 1 }), endOfWeek(new Date(), { weekStartsOn: 1 })); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Week-View: firstSetupProtocol = previous week, current period = previous week', () => { + TEST_CONTEXT.service.historyPeriod.next(previousWeekPeriod); + const firstSetupProtocol = startOfWeek(subWeeks(new Date(), 1), { weekStartsOn: 1 }); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, false); + expectNextPeriod(TEST_CONTEXT, true); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Week-View: firstSetupProtocol = 2 weeks ago, current period = previous week', () => { + TEST_CONTEXT.service.historyPeriod.next(previousWeekPeriod); + const firstSetupProtocol = startOfWeek(subWeeks(new Date(), 2), { weekStartsOn: 1 }); + + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, true); + expectNextPeriod(TEST_CONTEXT, true); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Week-View: firstSetupProtocol = 2 weeks ago, current period = current week', () => { + TEST_CONTEXT.service.historyPeriod.next(currentWeekPeriod); + const firstSetupProtocol = startOfWeek(subWeeks(new Date(), 2), { weekStartsOn: 1 }); + + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, true); + expectNextPeriod(TEST_CONTEXT, false); + }); + + const previousMonthPeriod = new DefaultTypes.HistoryPeriod(startOfMonth(subMonths(new Date(), 1)), endOfMonth(subMonths(new Date(), 1))); + const currentMonthPeriod = new DefaultTypes.HistoryPeriod(startOfMonth(new Date()), endOfMonth(new Date())); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Month-View: firstSetupProtocol = today, current period = current month', () => { + const firstSetupProtocol = new Date(); + TEST_CONTEXT.service.historyPeriod.next(currentMonthPeriod); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, false); + expectNextPeriod(TEST_CONTEXT, false); + }); + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Month-View: firstSetupProtocol = start of current month, current period = previous month', () => { + TEST_CONTEXT.service.historyPeriod.next(previousMonthPeriod); + const firstSetupProtocol = startOfMonth(subMonths(new Date(), 1)); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, false); + expectNextPeriod(TEST_CONTEXT, true); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Month-View: firstSetupProtocol = 2 months ago, current period = previous month', () => { + TEST_CONTEXT.service.historyPeriod.next(previousMonthPeriod); + const firstSetupProtocol = startOfMonth(subMonths(new Date(), 2)); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, true); + expectNextPeriod(TEST_CONTEXT, true); + }); + + const previousYearPeriod = new DefaultTypes.HistoryPeriod(startOfYear(subYears(new Date(), 1)), endOfYear(subYears(new Date(), 1))); + const currentYearPeriod = new DefaultTypes.HistoryPeriod(startOfYear(new Date()), endOfYear(new Date())); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Year-View: firstSetupProtocol = today, current period = current year', () => { + const firstSetupProtocol = new Date(); + TEST_CONTEXT.service.historyPeriod.next(currentYearPeriod); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, false); + expectNextPeriod(TEST_CONTEXT, false); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Year-View: firstSetupProtocol = previous year, current period = previous year', () => { + TEST_CONTEXT.service.historyPeriod.next(previousYearPeriod); + const firstSetupProtocol = startOfYear(subYears(new Date(), 1)); + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, false); + expectNextPeriod(TEST_CONTEXT, true); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Year-View: firstSetupProtocol = 2 years ago, current period = previous year', () => { + TEST_CONTEXT.service.historyPeriod.next(previousYearPeriod); + const firstSetupProtocol = startOfYear(subYears(new Date(), 2)); + + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, true); + expectNextPeriod(TEST_CONTEXT, true); + }); + + it('#isPreviousPeriodAllowed && #isNextPeriodAllowed - Year-View: firstSetupProtocol = 2 years ago, current period = this year', () => { + TEST_CONTEXT.service.historyPeriod.next(currentYearPeriod); + const firstSetupProtocol = startOfYear(subYears(new Date(), 2)); + + expectPreviousPeriod(TEST_CONTEXT, firstSetupProtocol, true); + expectNextPeriod(TEST_CONTEXT, false); + }); +}); \ No newline at end of file diff --git a/ui/src/app/shared/pickdate/pickdate.component.ts b/ui/src/app/shared/pickdate/pickdate.component.ts index 324128ed149..c846acf04e0 100644 --- a/ui/src/app/shared/pickdate/pickdate.component.ts +++ b/ui/src/app/shared/pickdate/pickdate.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { PopoverController } from '@ionic/angular'; import { TranslateService } from '@ngx-translate/core'; -import { addMonths, addYears, differenceInMilliseconds, endOfDay, endOfMonth, endOfYear, subMonths, subYears } from 'date-fns'; +import { addMonths, addYears, differenceInDays, differenceInMilliseconds, endOfDay, endOfMonth, endOfYear, isAfter, isBefore, startOfDay, startOfMonth, startOfWeek, startOfYear, subMonths, subYears } from 'date-fns'; import { addDays, addWeeks, endOfWeek, isFuture, subDays, subWeeks } from 'date-fns/esm'; import { DefaultTypes } from '../service/defaulttypes'; import { Edge, Service } from '../shared'; @@ -14,6 +14,10 @@ import { PickDatePopoverComponent } from './popover/popover.component'; export class PickDateComponent implements OnInit, OnDestroy { public disableArrow: boolean | null = null; + protected isAllowedToSeeDay: boolean = true; + + protected isForwardArrowAllowed: boolean = false; + protected isBackArrowAllowed: boolean = true; private changePeriodTimeout = null; private edge: Edge | null = null; @@ -28,6 +32,9 @@ export class PickDateComponent implements OnInit, OnDestroy { this.checkArrowAutomaticForwarding(); this.service.getCurrentEdge().then(edge => { this.edge = edge; + + this.isBackArrowAllowed = PickDateComponent.isPreviousPeriodAllowed(this.service, this.edge?.firstSetupProtocol); + this.isForwardArrowAllowed = PickDateComponent.isNextPeriodAllowed(this.service); }); } @@ -41,6 +48,8 @@ export class PickDateComponent implements OnInit, OnDestroy { * checks if arrow has to be disabled/enabled and if automatic forwarding is needed dependend on the date */ 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))) { @@ -118,6 +127,8 @@ export class PickDateComponent implements OnInit, OnDestroy { */ public setDateRange(period: DefaultTypes.HistoryPeriod) { this.service.historyPeriod.next(period); + this.isBackArrowAllowed = PickDateComponent.isPreviousPeriodAllowed(this.service, this.edge?.firstSetupProtocol); + this.isForwardArrowAllowed = PickDateComponent.isNextPeriodAllowed(this.service); } public goForward() { @@ -203,7 +214,7 @@ export class PickDateComponent implements OnInit, OnDestroy { if (this.changePeriodTimeout != null) { clearTimeout(this.changePeriodTimeout); } - this.disableArrow = false; + this.setDateRange(new DefaultTypes.HistoryPeriod(subDays(this.service.historyPeriod.value.from, 1), subDays((endOfDay(this.service.historyPeriod.value.to)), 1))); break; } @@ -212,7 +223,6 @@ export class PickDateComponent implements OnInit, OnDestroy { if (this.changePeriodTimeout != null) { clearTimeout(this.changePeriodTimeout); } - this.disableArrow = false; this.setDateRange(new DefaultTypes.HistoryPeriod(subWeeks(this.service.historyPeriod.value.from, 1), subWeeks(endOfWeek(this.service.historyPeriod.value.to, { weekStartsOn: 1 }), 1))); break; } @@ -221,7 +231,6 @@ export class PickDateComponent implements OnInit, OnDestroy { if (this.changePeriodTimeout != null) { clearTimeout(this.changePeriodTimeout); } - this.disableArrow = false; this.setDateRange(new DefaultTypes.HistoryPeriod(subMonths(this.service.historyPeriod.value.from, 1), endOfMonth(subMonths(this.service.historyPeriod.value.to, 1)))); break; } @@ -230,12 +239,10 @@ export class PickDateComponent implements OnInit, OnDestroy { if (this.changePeriodTimeout != null) { clearTimeout(this.changePeriodTimeout); } - this.disableArrow = false; this.setDateRange(new DefaultTypes.HistoryPeriod(subYears(this.service.historyPeriod.value.from, 1), endOfYear(subYears(this.service.historyPeriod.value.to, 1)))); break; } case DefaultTypes.PeriodString.CUSTOM: { - this.disableArrow = false; 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; this.setDateRange(new DefaultTypes.HistoryPeriod(subDays(this.service.historyPeriod.value.from, dateDistance), subDays(this.service.historyPeriod.value.to, dateDistance))); @@ -335,4 +342,55 @@ export class PickDateComponent implements OnInit, OnDestroy { this.checkArrowAutomaticForwarding(); }); } + + /** + * Checks if previous time period is allowed to be queried + * + * @param service the service + * @param firstSetupProtocol the date of setting up the edge + * @returns true, if requested fromDate is not before firstSetupProtocolDate + */ + public static isPreviousPeriodAllowed(service: Service, firstSetupProtocol: Date | null): boolean { + + if (!firstSetupProtocol) { + return true; + } + + switch (service.periodString) { + case DefaultTypes.PeriodString.DAY: + return isBefore(startOfDay(firstSetupProtocol), endOfDay(subDays(service.historyPeriod.value.from, 1))); + case DefaultTypes.PeriodString.WEEK: + return isBefore(firstSetupProtocol, endOfWeek(subWeeks(service.historyPeriod.value.from, 1))); + case DefaultTypes.PeriodString.MONTH: + 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.CUSTOM: + var timeRange: number = differenceInDays(service.historyPeriod.value.to, service.historyPeriod.value.from); + return isBefore(startOfDay(firstSetupProtocol), startOfDay(subDays(service.historyPeriod.value.from, timeRange))); + } + } + + /** + * Checks if next time period is allowed to be queried + * + * @param service the service + * @returns true, if requested toDate is not in the future + */ + public static isNextPeriodAllowed(service: Service): boolean { + + switch (service.periodString) { + case DefaultTypes.PeriodString.DAY: + return isAfter(new Date(), startOfDay(addDays(service.historyPeriod.value.to, 1))); + case DefaultTypes.PeriodString.WEEK: + return isAfter(new Date(), startOfDay(startOfWeek(addWeeks(service.historyPeriod.value.to, 1), { weekStartsOn: 1 }))); + case DefaultTypes.PeriodString.MONTH: + 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.CUSTOM: + var timeRange: number = differenceInDays(service.historyPeriod.value.to, service.historyPeriod.value.from); + return isAfter(startOfDay(new Date()), addDays(service.historyPeriod.value.to, timeRange)); + } + } } \ No newline at end of file diff --git a/ui/src/app/shared/pickdate/popover/popover.component.ts b/ui/src/app/shared/pickdate/popover/popover.component.ts index 1c28f96c2c0..5dedd296166 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 } from '../../shared'; +import { Service, Utils } from '../../shared'; @Component({ selector: 'pickdatepopover', @@ -54,7 +54,9 @@ export class PickDatePopoverComponent implements OnInit { ) { } ngOnInit() { - this.locale = this.translate.getBrowserLang(); + // 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(); } /** diff --git a/ui/src/app/shared/service/service.ts b/ui/src/app/shared/service/service.ts index 51d7d10bd90..76ed4c1b582 100644 --- a/ui/src/app/shared/service/service.ts +++ b/ui/src/app/shared/service/service.ts @@ -25,6 +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'; @Injectable() export class Service extends AbstractService { @@ -251,7 +252,7 @@ export class Service extends AbstractService { continue; } - let request = new QueryHistoricTimeseriesEnergyRequest(source.fromDate, source.toDate, source.channels); + let request = new QueryHistoricTimeseriesEnergyRequest(DateUtils.maxDate(source.fromDate, edge?.firstSetupProtocol), source.toDate, source.channels); edge.sendRequest(this.websocket, request).then(response => { let result = (response as QueryHistoricTimeseriesEnergyResponse).result; if (Object.keys(result.data).length != 0) { @@ -308,7 +309,7 @@ export class Service extends AbstractService { edge.isOnline, edge.lastmessage, edge.sumState, - edge.firstSetupProtocol + DateUtils.stringToDate(edge.firstSetupProtocol?.toString()) ); value.edges[edge.id] = mappedEdge; mappedResult.push(mappedEdge); @@ -348,9 +349,7 @@ export class Service extends AbstractService { edgeData.isOnline, edgeData.lastmessage, edgeData.sumState, - edgeData.firstSetupProtocol - ); - + DateUtils.stringToDate(edgeData.firstSetupProtocol?.toString())); this.currentEdge.next(currentEdge); value.edges[edgeData.id] = currentEdge; this.metadata.next(value); diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index b9a17435a4f..9bee72c334b 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -35,6 +35,7 @@ import { Service } from './service/service'; import { Utils } from './service/utils'; import { Websocket } from './service/websocket'; import { FormlyFieldWithLoadingAnimationComponent } from './formly/formly-skeleton-wrapper'; +import { FormlyFieldCheckboxWithImageComponent } from './formly/formly-field-checkbox-image/formly-field-checkbox-with-image'; export function IpValidator(control: FormControl): ValidationErrors { return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(control.value) ? null : { 'ip': true }; @@ -75,7 +76,8 @@ export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { { name: 'form-field-checkbox-hyperlink', component: FormlyCheckBoxHyperlinkWrapperComponent }, { name: 'formly-wrapper-default-of-cases', component: FormlyWrapperDefaultValueWithCasesComponent }, { name: 'panel', component: PanelWrapperComponent }, - { name: 'formly-field-modal', component: FormlyFieldModalComponent } + { name: 'formly-field-modal', component: FormlyFieldModalComponent }, + { name: 'formly-field-checkbox-with-image', component: FormlyFieldCheckboxWithImageComponent } ], types: [ { name: 'input', component: InputTypeComponent }, @@ -112,7 +114,8 @@ export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { FormlyWrapperDefaultValueWithCasesComponent, FormlyFieldModalComponent, PanelWrapperComponent, - FormlyFieldWithLoadingAnimationComponent + FormlyFieldWithLoadingAnimationComponent, + FormlyFieldCheckboxWithImageComponent ], exports: [ // modules diff --git a/ui/src/app/shared/status/single/status.component.html b/ui/src/app/shared/status/single/status.component.html index 7e9d802f51a..41acd8689de 100644 --- a/ui/src/app/shared/status/single/status.component.html +++ b/ui/src/app/shared/status/single/status.component.html @@ -1,6 +1,6 @@ - General.systemState + General.SYSTEM_STATE diff --git a/ui/src/app/shared/utils/dateutils/dateutils.spec.ts b/ui/src/app/shared/utils/dateutils/dateutils.spec.ts new file mode 100644 index 00000000000..57cdc88a0f5 --- /dev/null +++ b/ui/src/app/shared/utils/dateutils/dateutils.spec.ts @@ -0,0 +1,37 @@ +import { DateUtils } from "./dateutils"; + +describe('DateUtils', () => { + + const dates: Date[] = [ + new Date(Date.parse("2023-01-01")), + new Date(Date.parse("2023-01-02")) + ]; + + it('#minDate - smallest date', () => { + + // valid params + expect(DateUtils.minDate(...dates)).toEqual(dates[0]); + + // no params + expect(DateUtils.minDate()).toEqual(null); + + // null as param + expect(isNaN(DateUtils.minDate(null, null)?.getTime())).toBe(true); + }); + + it('#maxDate - biggest date', () => { + // valid params + expect(DateUtils.maxDate(...dates)).toEqual(dates[1]); + + // no params + expect(DateUtils.maxDate()).toEqual(null); + + // null as param + expect(isNaN(DateUtils.maxDate(null, null)?.getTime())).toBe(true); + }); + + it('#stringToDate - converts string to date', () => { + expect(DateUtils.stringToDate('2023-01-02')).toEqual(new Date(Date.parse('2023-01-02'))); + expect(DateUtils.stringToDate('wrong format')).toEqual(null); + }); +}); \ No newline at end of file diff --git a/ui/src/app/shared/utils/dateutils/dateutils.ts b/ui/src/app/shared/utils/dateutils/dateutils.ts new file mode 100644 index 00000000000..2ffc1aaa1a4 --- /dev/null +++ b/ui/src/app/shared/utils/dateutils/dateutils.ts @@ -0,0 +1,47 @@ + +export namespace DateUtils { + + /** + * Filters for the biggest date + * + * @param dates the dates to be compared + * @returns the max date + */ + export function maxDate(...dates: Date[]) { + + if (dates.length === 0 || dates.every(element => typeof element === null)) { + return null; + } + + return new Date( + Math.max(...dates.filter(date => !!date).map(Number)) + ); + } + + /** + * Filters for the smallest date + * + * @param dates the dates to be compared + * @returns the min date + */ + export function minDate(...dates: Date[]) { + + if (dates.length === 0 || dates.every(element => typeof element === null)) { + return null; + } + + return new Date( + Math.min(...dates.filter(date => !!date).map(Number)) + ); + } + + /** + * Converts string to date + * + * @param date the date + * @returns the date if valid, else null + */ + export function stringToDate(date: string) { + return isNaN(new Date(date)?.getTime()) ? null : new Date(date); + } +} \ No newline at end of file diff --git a/ui/src/assets/i18n/cz.json b/ui/src/assets/i18n/cz.json index b177dc8a9f7..8e5d96d5446 100644 --- a/ui/src/assets/i18n/cz.json +++ b/ui/src/assets/i18n/cz.json @@ -58,7 +58,7 @@ "soc": "Stav nabití", "state": "Stát", "storageSystem": "Systém bateriového úložiÅ¡tÄ›", - "systemState": "Stav systému", + "SYSTEM_STATE": "Stav systému", "TOTAL": "celková spotÅ™eba", "totalState": "Celkový stav", "warning": "Varování", diff --git a/ui/src/assets/i18n/de.json b/ui/src/assets/i18n/de.json index 421a45347d8..40b9a3c5147 100644 --- a/ui/src/assets/i18n/de.json +++ b/ui/src/assets/i18n/de.json @@ -503,6 +503,7 @@ "soc": "Ladezustand", "state": "Zustand", "storageSystem": "Speichersystem", + "SYSTEM_STATE": "Systemstatus", "TOTAL": "Gesamt", "totalState": "Gesamtstatus", "warning": "Warnung", @@ -559,7 +560,7 @@ "NO_EDGE_AVAILABLE": "Sie haben noch kein {{edgeShortName}} hinzugefügt.", "VISIBLE_HERE_AFTER_INSTALLATION": "Nachdem Ihr {{edgeShortName}} durch einen Installateur in Betrieb genommen wurde, sehen Sie es an dieser Stelle.", "NO_EDGE_FOR_USER": "Leider wurde noch kein {{edgeShortName}} mit Ihrem Account verknüpft.", - "FIRST_SETUP_PROTOCOL": "Commissioning" + "FIRST_SETUP_PROTOCOL": "Inbetriebnahme" }, "INSTALLATION": { "ATTENTION_MESSAGE": "Beachten Sie, dass die Auswahl nachträglich nicht mehr rückgängig gemacht werden kann.", @@ -690,7 +691,7 @@ "CHOOSE": "Typ", "CONSTANT_VALUE": "Cos φ Festwert", "DYNAMIC_LIMITATION_ACTIVATED": "Dynamische Begrenzung der Einspeisung aktiviert?", - "DYNAMIC_LIMITATION": "Dynamische Begrenzung der Einspeisung (z.B. 70% Abregelung)", + "DYNAMIC_LIMITATION": "Dynamische Begrenzung der Einspeisung", "EXTERNAL_CONTROLLER_CHECK": "Der Rundsteuerempfänger wurde lt. Anleitung ordnungsgemäß und vollständig installiert.", "EXTERNAL_LIMITATION_ACTIVATED": "Rundsteuerempfänger aktiviert?", "EXTERNAL_LIMITATION": "Rundsteuerempfänger (Externe Abregelung durch Netzbetreiber)", @@ -702,7 +703,8 @@ "QU_ENABLED_CURVE": "Blindleistungs-Spannungskennlinie Q(U)", "SHADE_MANAGEMENT_DEACTIVATED": "Schattenmanagement deaktivert", "TITLE": "Einspeisemanagement", - "EXTERNAL_CONTROLLER_RECIEVER": "Rundsteuerempfänger" + "EXTERNAL_CONTROLLER_RECIEVER": "Rundsteuerempfänger", + "PLACE_HOLDER": "Wähle eine Option" }, "PROTOCOL_INSTALLER_AND_CUSTOMER": { "CITY": "Ort", @@ -737,7 +739,7 @@ "WEST": "West" }, "INSTALLED_POWER": "Installierte leistung [Wâ‚š]", - "MARKED_AS": " MPPT {{number}} (beschriftet mit \"PV{{ number}}\")", + "MARKED_AS": " MPPT {{ mppt }} (beschriftet mit \"PV{{ pv }}\")", "METER_TYPE_WITH_LABEL": "Zählertyp {{ label }}{{ number }}", "METER_TYPE": "Zählertyp", "MODBUS_SOCOMEC_DESCRIPTION": "Der Zähler muss mit den folgenden Parametern konfiguriert werden: Kommunikationsgeschwindigkeit (baud) \"9600\", Kommunikationsparität (PrtY) \"n\", Kommunikations-Stopbit (StoP) \"1\"", @@ -761,11 +763,12 @@ "VALID_DATA": "Geben Sie gültige Daten ein, um zu speichern.", "TITLE_DC": "DC-PV-Installation", "AC_NOT_CREATED": "AC zähler nicht erstellt!", - "METER_SELECTION_WARNING": "Bitte wählen Sie den Zähler aus, indem Sie den Erzeuger hinzufügen." + "METER_SELECTION_WARNING": "Bitte wählen Sie den Zähler aus, indem Sie den Erzeuger hinzufügen.", + "DUPLICATE": " MPPT {{ mppt }} doppelt belegt (beschriftet mit \"PV{{ pv }}\")" }, "PROTOCOL_SERIAL_NUMBERS": { "BATTERY_MODULE": "Batteriemodul ", - "BATTERY_TOWER": "Batterie Turm {{ towerNumber }}", + "BATTERY_TOWER": "Batterie Turm {{ number }}", "BESS_COMPONENTS": "Speichersystemkomponenten", "BMS_BOX": "BMS Box & Sockel", "CONFIRM": "speichern", diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 16ef8bf7c22..fd2475b53fd 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -503,6 +503,7 @@ "soc": "State of charge", "state": "State", "storageSystem": "Storage System", + "SYSTEM_STATE": "System state", "TOTAL": "Total", "totalState": "Total state", "warning": "Warning", @@ -559,7 +560,7 @@ "NO_EDGE_AVAILABLE": "You have not added any {{edgeShortName}} yet.", "NO_EDGE_FOR_USER": "Sorry, no {{value}} has been associated with your account yet.", "VISIBLE_HERE_AFTER_INSTALLATION": "After your {{value}} has been commissioned by an installer, you will see it at this point.", - "FIRST_SETUP_PROTOCOL": "Inbetriebnahme" + "FIRST_SETUP_PROTOCOL": "Commissioning" }, "INSTALLATION": { "ATTENTION_MESSAGE": "Note that the selection cannot be undone afterwards.", @@ -689,7 +690,7 @@ "CHOOSE": "Choose", "CONSTANT_VALUE": "Cos φ value", "DYNAMIC_LIMITATION_ACTIVATED": "Dynamic feed-in limitation activated?", - "DYNAMIC_LIMITATION": "Dynamic limitation of the feed-in (e.g. 70% limit)", + "DYNAMIC_LIMITATION": "Dynamic limitation of the feed-in", "EXTERNAL_CONTROLLER_CHECK": "The external control reciever was installed properly and completely according to the instructions.", "EXTERNAL_LIMITATION_ACTIVATED": "External control reciever activated?", "EXTERNAL_LIMITATION": "External controller (External limitation by the DSO)", @@ -701,7 +702,8 @@ "QU_ENABLED_CURVE": "Reactive power - Q(U) characteristics", "SHADE_MANAGEMENT_DEACTIVATED": "Shade management disabled?", "TITLE": "Feed-In management", - "EXTERNAL_CONTROLLER_RECIEVER": "Ripple control reciever" + "EXTERNAL_CONTROLLER_RECIEVER": "Ripple control reciever", + "PLACE_HOLDER": "Select option" }, "PROTOCOL_INSTALLER_AND_CUSTOMER": { "CITY": "City", @@ -736,7 +738,7 @@ "WEST": "West" }, "INSTALLED_POWER": "Installed power[Wâ‚š]", - "MARKED_AS": " MPPT {{number}} (marked as \"PV{{ number}}\")", + "MARKED_AS": " MPPT {{ mppt }} (marked as \"PV{{ pv }}\")", "METER_TYPE_WITH_LABEL": "Type of the sensor {{ label }}{{ number }}", "METER_TYPE": "Type of the sensor", "MODBUS_SOCOMEC_DESCRIPTION": "The sensor has to be configured with the following settings: Communication speed (baud) \"9600\", Communication parity (partY) \"n\", Communication Stopbit (StoP) \"1\"", @@ -760,11 +762,12 @@ "VALID_DATA": "Enter valid data to save.", "TITLE_DC": "DC-PV-Installation", "AC_NOT_CREATED": "AC Meter not created!", - "METER_SELECTION_WARNING": "Please select the meter by adding generator." + "METER_SELECTION_WARNING": "Please select the meter by adding generator.", + "DUPLICATE": " MPPT {{ mppt }} double occupied (marked as \"PV{{ pv }}\")" }, "PROTOCOL_SERIAL_NUMBERS": { "BATTERY_MODULE": "Battery module ", - "BATTERY_TOWER": "Battery Tower {{ towerNumber }}", + "BATTERY_TOWER": "Battery Tower {{ number }}", "BESS_COMPONENTS": "BESS components", "BMS_BOX": "BMS Box & Base", "CONFIRM": "Confirm", diff --git a/ui/src/assets/i18n/es.json b/ui/src/assets/i18n/es.json index 08f62315273..20142996ddc 100644 --- a/ui/src/assets/i18n/es.json +++ b/ui/src/assets/i18n/es.json @@ -56,7 +56,7 @@ "soc": "Cargo", "state": "Estado", "storageSystem": "Almacenamiento", - "systemState": "Estado del sistema", + "SYSTEM_STATE": "Estado del sistema", "TOTAL": "consumo total", "totalState": "Condición general", "warning": "Advertencia", diff --git a/ui/src/assets/i18n/fr.json b/ui/src/assets/i18n/fr.json index 672e13de83a..f3938ebbec6 100644 --- a/ui/src/assets/i18n/fr.json +++ b/ui/src/assets/i18n/fr.json @@ -58,7 +58,7 @@ "soc": "état de charge", "state": "Etat", "storageSystem": "Système de stockage", - "systemState": "System state", + "SYSTEM_STATE": "État du système", "TOTAL": "Total", "totalState": "Total state", "warning": "Warning", diff --git a/ui/src/assets/i18n/nl.json b/ui/src/assets/i18n/nl.json index a3f1625db7d..88b4510cc6b 100644 --- a/ui/src/assets/i18n/nl.json +++ b/ui/src/assets/i18n/nl.json @@ -53,7 +53,7 @@ "soc": "Laadstatus", "state": "Staat", "storageSystem": "Batterij", - "systemState": "Systeemstatus", + "SYSTEM_STATE": "Systeemstatus", "TOTAL": "totale verbruik", "totalState": "Algemene staat", "warning": "Waarschuwing", diff --git a/ui/src/environments/index.ts b/ui/src/environments/index.ts index 0607848065f..ce6d0321e26 100644 --- a/ui/src/environments/index.ts +++ b/ui/src/environments/index.ts @@ -20,8 +20,8 @@ export interface Environment { readonly docsUrlPrefix: string; readonly links: { - readonly COMMON_STORAGE: string, + readonly COMMON_STORAGE: string, readonly EVCS_KEBA_KECONTACT: string, readonly EVCS_HARDY_BARTH: string, readonly EVCS_OCPP_IESKEYWATTSINGLE: string, @@ -31,9 +31,16 @@ export interface Environment { readonly CONTROLLER_IO_CHANNEL_SINGLE_THRESHOLD: string, readonly CONTROLLER_IO_FIX_DIGITAL_OUTPUT: string, readonly CONTROLLER_IO_HEAT_PUMP_SG_READY: string, + readonly CONTROLLER_IO_HEATING_ELEMENT: string, + + readonly CONTROLLER_API_MODBUSTCP_READ: string, + readonly CONTROLLER_API_MODBUSTCP_READWRITE: string, + + readonly CONTROLLER_API_REST_READ: string, + readonly CONTROLLER_API_REST_READWRITE: string, readonly SETTINGS_ALERTING: string, readonly SETTINGS_NETWORK_CONFIGURATION: string, - } + }, readonly PRODUCT_TYPES: (translate: TranslateService) => Filter } \ No newline at end of file diff --git a/ui/src/themes/openems/environments/theme.ts b/ui/src/themes/openems/environments/theme.ts index 13eee45e1eb..aca145134e8 100644 --- a/ui/src/themes/openems/environments/theme.ts +++ b/ui/src/themes/openems/environments/theme.ts @@ -20,9 +20,17 @@ export const theme = { CONTROLLER_IO_CHANNEL_SINGLE_THRESHOLD: "io.openems.edge.controller.io.channelsinglethreshold/readme.adoc", CONTROLLER_IO_FIX_DIGITAL_OUTPUT: "io.openems.edge.controller.io.fixdigitaloutput/readme.adoc", CONTROLLER_IO_HEAT_PUMP_SG_READY: "io.openems.edge.controller.io.heatpump.sgready/readme.adoc", + CONTROLLER_IO_HEATING_ELEMENT: "io.openems.edge.controller.io.heatingelement/readme.adoc", + + CONTROLLER_API_MODBUSTCP_READ: "io.openems.edge.controller.api.modbus/readme.adoc", + CONTROLLER_API_MODBUSTCP_READWRITE: "io.openems.edge.controller.api.modbus/readme.adoc", + + CONTROLLER_API_REST_READ: "io.openems.edge.controller.api.rest/readme.adoc", + CONTROLLER_API_REST_READWRITE: "io.openems.edge.controller.api.rest/readme.adoc", SETTINGS_ALERTING: null, - SETTINGS_NETWORK_CONFIGURATION: null + SETTINGS_NETWORK_CONFIGURATION: null, + EVCS_CLUSTER: "io.openems.edge.evcs.cluster/readme.adoc" }, PRODUCT_TYPES: () => null }; \ No newline at end of file From af719caf72f55e4025ed813f553be9a5b1a06ffe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:03:50 +0200 Subject: [PATCH 19/27] Bump com.squareup.okhttp3:okhttp from 4.11.0 to 4.12.0 in /cnf (#2399) * Bump com.squareup.okhttp3:logging-interceptor in /cnf Bumps [com.squareup.okhttp3:logging-interceptor](https://github.com/square/okhttp) from 4.11.0 to 4.12.0. - [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/okhttp/compare/parent-4.11.0...parent-4.12.0) --- updated-dependencies: - dependency-name: com.squareup.okhttp3:logging-interceptor dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Bump com.squareup.okhttp3:okhttp from 4.11.0 to 4.12.0 in /cnf Bumps [com.squareup.okhttp3:okhttp](https://github.com/square/okhttp) from 4.11.0 to 4.12.0. - [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/okhttp/compare/parent-4.11.0...parent-4.12.0) --- updated-dependencies: - dependency-name: com.squareup.okhttp3:okhttp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update bnd --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 4 ++-- io.openems.wrapper/bnd.bnd | 4 ++-- io.openems.wrapper/okhttp.bnd | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index 82fbce43d0a..111a2784304 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -48,13 +48,13 @@ com.squareup.okhttp3 logging-interceptor - 4.11.0 + 4.12.0 com.squareup.okhttp3 okhttp - 4.11.0 + 4.12.0 diff --git a/io.openems.wrapper/bnd.bnd b/io.openems.wrapper/bnd.bnd index 28f4dd69b85..3910ab10609 100644 --- a/io.openems.wrapper/bnd.bnd +++ b/io.openems.wrapper/bnd.bnd @@ -8,8 +8,8 @@ Bundle-Description: This wraps external java libraries that do not have OSGi hea com.influxdb:influxdb-client-java;version='6.10.0',\ com.influxdb:influxdb-client-utils;version='6.10.0',\ com.influxdb:flux-dsl;version='6.10.0',\ - com.squareup.okhttp3:logging-interceptor;version='4.11.0',\ - com.squareup.okhttp3:okhttp;version='4.11.0',\ + com.squareup.okhttp3:logging-interceptor;version='4.12.0',\ + com.squareup.okhttp3:okhttp;version='4.12.0',\ com.squareup.retrofit2:retrofit;version='2.9.0',\ com.squareup.retrofit2:converter-gson;version='2.9.0',\ com.squareup.retrofit2:converter-scalars;version='2.9.0',\ diff --git a/io.openems.wrapper/okhttp.bnd b/io.openems.wrapper/okhttp.bnd index 0385973f1f5..b3239568dee 100644 --- a/io.openems.wrapper/okhttp.bnd +++ b/io.openems.wrapper/okhttp.bnd @@ -2,11 +2,11 @@ Bundle-Name: okttp Bundle-Description: Squares meticulous HTTP client for Java and Kotlin. Bundle-DocURL: https://square.github.io/okhttp/ Bundle-License: https://opensource.org/licenses/Apache-2.0 -Bundle-Version: 4.11.0 +Bundle-Version: 4.12.0 Include-Resource: \ - @logging-interceptor-4.11.0.jar,\ - @okhttp-4.11.0.jar,\ + @logging-interceptor-4.12.0.jar,\ + @okhttp-4.12.0.jar,\ Export-Package: \ okhttp3,\ From 7e45591c4e030835cbe6aafa7187aaa3ec854e7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:18:00 +0200 Subject: [PATCH 20/27] Bump com.google.guava:failureaccess from 1.0.1 to 1.0.2 in /cnf (#2400) * Bump com.google.guava:failureaccess from 1.0.1 to 1.0.2 in /cnf Bumps com.google.guava:failureaccess from 1.0.1 to 1.0.2. --- updated-dependencies: - dependency-name: com.google.guava:failureaccess dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index 111a2784304..6d82f91a473 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -42,7 +42,7 @@ com.google.guava failureaccess - 1.0.1 + 1.0.2 diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index b6c4faaf5c4..923187ee9a5 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -62,7 +62,7 @@ Java-WebSocket;version='[1.5.4,1.5.5)',\ com.google.gson;version='[2.10.1,2.10.2)',\ com.google.guava;version='[32.1.3,32.1.4)',\ - com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ + com.google.guava.failureaccess;version='[1.0.2,1.0.3)',\ com.squareup.okio;version='[3.6.0,3.6.1)',\ com.zaxxer.HikariCP;version='[5.0.1,5.0.2)',\ io.openems.backend.alerting;version=snapshot,\ diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index 46ee755cd74..e9a32971466 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -188,7 +188,7 @@ com.ghgande.j2mod;version='[2.5.5,2.5.6)',\ com.google.gson;version='[2.10.1,2.10.2)',\ com.google.guava;version='[32.1.3,32.1.4)',\ - com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ + com.google.guava.failureaccess;version='[1.0.2,1.0.3)',\ com.squareup.okio;version='[3.6.0,3.6.1)',\ com.sun.jna;version='[5.13.0,5.13.1)',\ io.openems.common;version=snapshot,\ From b4a73b8b972b99c20dd7eda8d36e9d5107d74a49 Mon Sep 17 00:00:00 2001 From: AnasShetla <141644226+AnasShetla@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:10:23 +0200 Subject: [PATCH 21/27] UI: Fix Infinity sign in charts & the wrong date formating (#2372) --- .../history/common/energy/chart/chart.spec.ts | 14 +++++----- .../edge/history/common/energy/chart/chart.ts | 6 ++--- .../settings/profile/profile.component.html | 2 +- ui/src/app/shared/service/utils.ts | 27 ++++++++++++------- 4 files changed, 29 insertions(+), 20 deletions(-) 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 628a183f1d9..20a81e26ec2 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 @@ -18,7 +18,7 @@ describe('History EnergyMonitor', () => { it('getChartData()', () => { { - // Line-Chart + // Line - Chart expectView(defaultEMS, TEST_CONTEXT, 'line', History.DAY, { datasets: { @@ -34,7 +34,9 @@ describe('History EnergyMonitor', () => { labels: LABELS(History.DAY.dataChannelWithValues.result.timestamps), options: History.LINE_CHART_OPTIONS('hour') } + }); + } { @@ -59,7 +61,7 @@ describe('History EnergyMonitor', () => { } { - // // Bar-Chart Year + // Bar-Chart Year expectView(defaultEMS, TEST_CONTEXT, 'bar', History.MONTH, { datasets: { @@ -67,8 +69,8 @@ describe('History EnergyMonitor', () => { DATA('Erzeugung: 22.491 kWh', [908, 967, 900, 926, 403, 597, 957, null, 1579, 556, 852, 976, 1026, 724, 839, 749, 709, 978, 607, 790, 652, null, 1011, 697, 908, null, 1466, 808, 906, null]), // Only one of the two following datasets is shown in legend - DATA('Direktverbrauch: 5.808,7 kWh', [191.524, 214.083, 198.811, 196.842, 184.218, 201.167, 175.916, 0, 347.243, 166.862, 176.461, 218.586, 229.496, 228.661, 211.608, 217.075, 177.422, 179.495, 200.029, 229.434, 229.765, 0, 360.727, 171.324, 206.255, 0, 442.327, 225.59, 227.751, 0]), - DATA('Direktverbrauch: 5.808,7 kWh', [191.524, 214.083, 198.811, 196.842, 184.218, 201.167, 175.916, 0, 347.243, 166.862, 176.461, 218.586, 229.496, 228.661, 211.608, 217.075, 177.422, 179.495, 200.029, 229.434, 229.765, 0, 360.727, 171.324, 206.255, 0, 442.327, 225.59, 227.751, 0]), + DATA('Direktverbrauch: 5.808,7 kWh', [191.524, 214.083, 198.811, 196.842, 184.218, 201.167, 175.916, null, 347.243, 166.862, 176.461, 218.586, 229.496, 228.661, 211.608, 217.075, 177.422, 179.495, 200.029, 229.434, 229.765, null, 360.727, 171.324, 206.255, null, 442.327, 225.59, 227.751, null]), + DATA('Direktverbrauch: 5.808,7 kWh', [191.524, 214.083, 198.811, 196.842, 184.218, 201.167, 175.916, null, 347.243, 166.862, 176.461, 218.586, 229.496, 228.661, 211.608, 217.075, 177.422, 179.495, 200.029, 229.434, 229.765, null, 360.727, 171.324, 206.255, null, 442.327, 225.59, 227.751, null]), DATA('Beladung: 3.944,3 kWh', [113.476, 162.917, 150.189, 157.158, 149.782, 159.833, 155.084, null, 228.757, 128.138, 157.539, 59.414, 156.504, 107.339, 156.392, 158.925, 158.578, 121.505, 120.971, 154.566, 173.235, null, 204.273, 156.676, 143.745, null, 247.673, 157.41, 104.249, null]), DATA('Entladung: 3.394,4 kWh', [112.818, 126.532, 139.622, 133.212, 169.24, 98.705, 109.367, null, 204.267, 118.504, 121.261, 74.97, 144.175, 89.897, 141.582, 111.261, 122.274, 106.232, 139.405, 132.225, 143.86, null, 235.044, 63.914, 123.844, null, 242.102, 130.546, 59.571, null]), DATA('Netzeinspeisung: 12.738 kWh', [603, 590, 551, 572, 69, 236, 626, null, 1003, 261, 518, 698, 640, 388, 471, 373, 373, 677, 286, 406, 249, null, 446, 369, 558, null, 776, 425, 574, null]), @@ -90,8 +92,8 @@ describe('History EnergyMonitor', () => { DATA('Erzeugung: 68.466 kWh', [1912, 3816, 7165, 10452, 20841, 22491, 1546, null, null, null, null, null]), // Only one of the two following datasets is shown in legend - DATA('Direktverbrauch: 22.466,2 kWh', [1597.394, 2056.891, 3150.228, 3720.697, 5506.053, 5808.6720000000005, 546.405, 0, 0, 0, 0, 0]), - DATA('Direktverbrauch: 22.466,2 kWh', [1597.394, 2056.891, 3150.228, 3720.697, 5506.053, 5808.6720000000005, 546.405, 0, 0, 0, 0, 0]), + DATA('Direktverbrauch: 22.466,2 kWh', [1597.394, 2056.891, 3150.228, 3720.697, 5506.053, 5808.6720000000005, 546.405, null, null, null, null, null]), + DATA('Direktverbrauch: 22.466,2 kWh', [1597.394, 2056.891, 3150.228, 3720.697, 5506.053, 5808.6720000000005, 546.405, null, null, null, null, null]), DATA('Beladung: 15.296,8 kWh', [294.606, 1673.109, 3337.772, 3074.303, 2495.947, 3944.328, 372.595, null, null, null, null, null]), DATA('Entladung: 12.898,2 kWh', [208.491, 1339.036, 2911.126, 2555.138, 2123.751, 3394.43, 335.402, null, null, null, null, null]), DATA('Netzeinspeisung: 30.703 kWh', [20, 86, 677, 3657, 12839, 12738, 627, null, null, null, null, null]), diff --git a/ui/src/app/edge/history/common/energy/chart/chart.ts b/ui/src/app/edge/history/common/energy/chart/chart.ts index b0eaf2b6f2e..590b397328f 100644 --- a/ui/src/app/edge/history/common/energy/chart/chart.ts +++ b/ui/src/app/edge/history/common/energy/chart/chart.ts @@ -97,11 +97,11 @@ export class ChartComponent extends AbstractHistoryChart { ...[chartType === 'bar' && { name: translate.instant('General.directConsumption'), nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => { - return energyValues.result.data['_sum/ProductionActiveEnergy'] - energyValues.result.data['_sum/GridSellActiveEnergy'] - energyValues.result.data['_sum/EssDcChargeEnergy']; + return Utils.subtractSafely(energyValues.result.data['_sum/ProductionActiveEnergy'], energyValues.result.data['_sum/GridSellActiveEnergy'], energyValues.result.data['_sum/EssDcChargeEnergy']); }, converter: () => - data['ProductionActivePower']?.map((value, index) => - value - data['GridSell'][index] - data['EssCharge'][index])?.map(value => HistoryUtils.ValueConverter.NEGATIVE_AS_ZERO(value)), + data['ProductionActivePower']?.map((value, index) => Utils.subtractSafely(value, data['GridSell'][index], data['EssCharge'][index])) + ?.map(value => HistoryUtils.ValueConverter.NEGATIVE_AS_ZERO(value)), color: 'rgb(244,164,96)', stack: [1, 2], order: 2 diff --git a/ui/src/app/edge/settings/profile/profile.component.html b/ui/src/app/edge/settings/profile/profile.component.html index 4ead9357e59..d0f6a5e361b 100644 --- a/ui/src/app/edge/settings/profile/profile.component.html +++ b/ui/src/app/edge/settings/profile/profile.component.html @@ -36,7 +36,7 @@
Index.FIRST_SETUP_PROTOCOL - {{ edge.firstSetupProtocol | date:'dd.MM.YYYY hh:mm' }} + {{ edge.firstSetupProtocol | date:'dd.MM.YYYY HH:mm' }} diff --git a/ui/src/app/shared/service/utils.ts b/ui/src/app/shared/service/utils.ts index d1e329e26fa..dad82bc7111 100644 --- a/ui/src/app/shared/service/utils.ts +++ b/ui/src/app/shared/service/utils.ts @@ -105,20 +105,27 @@ export class Utils { } /** - * Safely subtracts two - possibly 'null' - values: v1 - v2 + * Subtracts values from each other - possibly null values * - * @param v1 - * @param v2 + * @param values the values + * @returns a number, if at least one value is not null, else null */ - public static subtractSafely(v1: number, v2: number): number { - if (v1 == null) { - return v2; - } else if (v2 == null) { - return v1; - } else { - return v1 - v2; + public static subtractSafely(...values: (number | null)[]): number { + let result = null; + + for (const value of values) { + if (value !== null) { + if (result === null) { + result = value; + } else { + result -= value; + } + } } + + return result; } + /** * Safely divides two - possibly 'null' - values: v1 / v2 * From 90e870fc71f274f85af7adbb3fb8af562a3ae920 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:12:49 +0200 Subject: [PATCH 22/27] Bump org.jsoup:jsoup from 1.16.1 to 1.16.2 in /cnf (#2398) * Bump org.jsoup:jsoup from 1.16.1 to 1.16.2 in /cnf Bumps [org.jsoup:jsoup](https://github.com/jhy/jsoup) from 1.16.1 to 1.16.2. - [Release notes](https://github.com/jhy/jsoup/releases) - [Changelog](https://github.com/jhy/jsoup/blob/master/CHANGES) - [Commits](https://github.com/jhy/jsoup/compare/jsoup-1.16.1...jsoup-1.16.2) --- updated-dependencies: - dependency-name: org.jsoup:jsoup dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update bndrun --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index 6d82f91a473..514e82f1ea6 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -306,7 +306,7 @@ org.jsoup jsoup - 1.16.1 + 1.16.2 org.osgi diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index e9a32971466..d5983a7ac23 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -391,7 +391,7 @@ org.eclipse.jetty.util;version='[9.4.28,9.4.29)',\ org.eclipse.paho.mqttv5.client;version='[1.2.5,1.2.6)',\ org.jetbrains.kotlin.osgi-bundle;version='[1.9.10,1.9.11)',\ - org.jsoup;version='[1.16.1,1.16.2)',\ + org.jsoup;version='[1.16.2,1.16.3)',\ org.jsr-305;version='[3.0.2,3.0.3)',\ org.openmuc.jmbus;version='[3.3.0,3.3.1)',\ org.openmuc.jrxtx;version='[1.0.1,1.0.2)',\ From 7df7406f8705d2cf54007d5949e40b991ffcce6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=B6cker?= <39899210+DerStoecki@users.noreply.github.com> Date: Wed, 25 Oct 2023 13:46:16 +0200 Subject: [PATCH 23/27] Move TimeLeapClock from edge.common to common (#2395) * Moved TimeLeapClock from edge.common to common UnitTests of Backend have no access to TimeLeapClock, this commit fixes the issue. Co-authored-by: Kai Jeschek <99220919+da-Kai@users.noreply.github.com> * Fix sorting of imports --------- Co-authored-by: Kai Jeschek <99220919+da-Kai@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- .../src/io/openems}/common/test/TimeLeapClock.java | 2 +- .../openems/edge/battery/protection/BatteryProtectionTest.java | 2 +- .../protection/currenthandler/MaxCurrentHandlerTest.java | 2 +- .../fenecon/commercial/BatteryFeneconCommercialImplTest.java | 2 +- .../edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java | 2 +- .../battery/fenecon/home/statemachine/GoRunningHandlerTest.java | 2 +- .../BatteryInverterKacoBlueplanetGridsaveImplTest.java | 2 +- .../sinexcel/BatteryInverterSinexcelImplTest.java | 2 +- .../modbus/api/worker/internal/DefectiveComponentsTest.java | 2 +- .../modbus/api/worker/internal/TasksSupplierImplTest.java | 2 +- .../src/io/openems/edge/common/component/ClockProvider.java | 2 +- .../src/io/openems/edge/common/test/AbstractComponentTest.java | 1 + .../controller/api/backend/ControllerApiBackendImplTest.java | 2 +- .../edge/controller/api/mqtt/ControllerApiMqttImplTest.java | 2 +- .../CharacteristicImplTest.java | 2 +- .../edge/controller/ess/cycle/ControllerEssCycleImplTest.java | 2 +- .../ess/delaycharge/ControllerEssDelayChargeImplTest.java | 2 +- .../fixstateofcharge/ControllerEssFixStateOfChargeImplTest.java | 2 +- .../ControllerEssGridOptimizedChargeImplTest.java | 2 +- .../ControllerEssLimitTotalDischargeImplTest.java | 2 +- ...ControllerEssReactivePowerVoltageCharacteristicImplTest.java | 2 +- .../controller/ess/standby/ControllerEssStandbyImplTest.java | 2 +- .../ess/timeofusetariff/TimeOfUseTariffControllerTest.java | 2 +- .../io/openems/edge/controller/evcs/ControllerEvcsImplTest.java | 2 +- .../io/openems/edge/controller/io/analog/MyControllerTest.java | 2 +- .../ControllerIoChannelSingleThresholdImplTest.java | 2 +- .../io/heatingelement/ControllerIoHeatingElementImplTest.java | 2 +- .../io/heatingelement/ControllerIoHeatingElementImplTest2.java | 2 +- .../io/heatingelement/ControllerIoHeatingElementImplTest3.java | 2 +- .../heatpump/sgready/ControllerIoHeatPumpSgReadyImplTest.java | 2 +- .../ControllerEssBalancingScheduleImplTest.java | 2 +- .../ControllerEssTimeslotPeakshavingImplTest.java | 2 +- .../edge/core/predictormanager/PredictorManagerImplTest.java | 2 +- .../ess/generic/common/AllowedChargeDischargeHandlerTest.java | 2 +- .../edge/ess/generic/offgrid/EssGenericOffGridImplTest.java | 2 +- .../generic/symmetric/EssGenericManagedSymmetricImplTest.java | 2 +- .../persistencemodel/PredictorPersistenceModelImplTest.java | 2 +- .../similardaymodel/PredictorSimilardayModelImplTest.java | 2 +- .../io/openems/edge/scheduler/daily/SchedulerDailyImplTest.java | 2 +- .../src/io/openems/edge/simulator/app/SimulatorAppImpl.java | 2 +- .../reacting/SimulatorEssSymmetricReactingImplTest.java | 2 +- 41 files changed, 41 insertions(+), 40 deletions(-) rename {io.openems.edge.common/src/io/openems/edge => io.openems.common/src/io/openems}/common/test/TimeLeapClock.java (97%) diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/TimeLeapClock.java b/io.openems.common/src/io/openems/common/test/TimeLeapClock.java similarity index 97% rename from io.openems.edge.common/src/io/openems/edge/common/test/TimeLeapClock.java rename to io.openems.common/src/io/openems/common/test/TimeLeapClock.java index 5d8ac6159a3..2ae5327b773 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/test/TimeLeapClock.java +++ b/io.openems.common/src/io/openems/common/test/TimeLeapClock.java @@ -1,4 +1,4 @@ -package io.openems.edge.common.test; +package io.openems.common.test; import java.time.Clock; import java.time.Instant; diff --git a/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java index 91843f76acd..cd6d7edaa67 100644 --- a/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java +++ b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/BatteryProtectionTest.java @@ -7,6 +7,7 @@ import org.junit.Test; import io.openems.common.channel.Unit; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; import io.openems.edge.battery.protection.currenthandler.ChargeMaxCurrentHandler; @@ -21,7 +22,6 @@ import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; public class BatteryProtectionTest { diff --git a/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/currenthandler/MaxCurrentHandlerTest.java b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/currenthandler/MaxCurrentHandlerTest.java index 68eb6017a93..e32a33584d1 100644 --- a/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/currenthandler/MaxCurrentHandlerTest.java +++ b/io.openems.edge.battery.api/test/io/openems/edge/battery/protection/currenthandler/MaxCurrentHandlerTest.java @@ -8,10 +8,10 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.edge.battery.protection.BatteryProtectionTest; import io.openems.edge.common.linecharacteristic.PolyLine; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; public class MaxCurrentHandlerTest { diff --git a/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/BatteryFeneconCommercialImplTest.java b/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/BatteryFeneconCommercialImplTest.java index 9a1f2fbc049..1226ccc9ffc 100644 --- a/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/BatteryFeneconCommercialImplTest.java +++ b/io.openems.edge.battery.fenecon.commercial/test/io/openems/edge/battery/fenecon/commercial/BatteryFeneconCommercialImplTest.java @@ -5,6 +5,7 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.api.Battery; import io.openems.edge.battery.fenecon.commercial.statemachine.StateMachine; @@ -15,7 +16,6 @@ import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.io.test.DummyInputOutput; public class BatteryFeneconCommercialImplTest { diff --git a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java index 5561d443158..1faf9d74e77 100644 --- a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java +++ b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeImplTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import io.openems.common.function.ThrowingRunnable; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.api.Battery; import io.openems.edge.battery.fenecon.home.statemachine.StateMachine; @@ -21,7 +22,6 @@ import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.io.test.DummyInputOutput; public class BatteryFeneconHomeImplTest { diff --git a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/statemachine/GoRunningHandlerTest.java b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/statemachine/GoRunningHandlerTest.java index c8e784f10c2..638dd4a31a5 100644 --- a/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/statemachine/GoRunningHandlerTest.java +++ b/io.openems.edge.battery.fenecon.home/test/io/openems/edge/battery/fenecon/home/statemachine/GoRunningHandlerTest.java @@ -14,8 +14,8 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.common.function.BooleanConsumer; +import io.openems.common.test.TimeLeapClock; import io.openems.edge.battery.fenecon.home.statemachine.GoRunningHandler.SubState; -import io.openems.edge.common.test.TimeLeapClock; public class GoRunningHandlerTest { diff --git a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/test/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImplTest.java b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/test/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImplTest.java index da4fd9eb4ba..75802d537e1 100644 --- a/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/test/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImplTest.java +++ b/io.openems.edge.batteryinverter.kaco.blueplanetgridsave/test/io/openems/edge/batteryinverter/kaco/blueplanetgridsave/BatteryInverterKacoBlueplanetGridsaveImplTest.java @@ -8,6 +8,7 @@ import org.junit.Test; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.api.Battery; import io.openems.edge.battery.test.DummyBattery; @@ -23,7 +24,6 @@ import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; public class BatteryInverterKacoBlueplanetGridsaveImplTest { diff --git a/io.openems.edge.batteryinverter.sinexcel/test/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcelImplTest.java b/io.openems.edge.batteryinverter.sinexcel/test/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcelImplTest.java index 6f0d21efe63..6092ebde7d6 100644 --- a/io.openems.edge.batteryinverter.sinexcel/test/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcelImplTest.java +++ b/io.openems.edge.batteryinverter.sinexcel/test/io/openems/edge/batteryinverter/sinexcel/BatteryInverterSinexcelImplTest.java @@ -7,6 +7,7 @@ import org.junit.Test; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.api.Battery; import io.openems.edge.battery.test.DummyBattery; @@ -23,7 +24,6 @@ import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; public class BatteryInverterSinexcelImplTest { diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponentsTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponentsTest.java index 6074b868e7c..3f8d57ea014 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponentsTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/DefectiveComponentsTest.java @@ -8,7 +8,7 @@ import org.junit.Test; -import io.openems.edge.common.test.TimeLeapClock; +import io.openems.common.test.TimeLeapClock; public class DefectiveComponentsTest { diff --git a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImplTest.java b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImplTest.java index 4b8f1091639..0af815bda47 100644 --- a/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImplTest.java +++ b/io.openems.edge.bridge.modbus/test/io/openems/edge/bridge/modbus/api/worker/internal/TasksSupplierImplTest.java @@ -10,11 +10,11 @@ import org.junit.Test; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.test.TimeLeapClock; import io.openems.edge.bridge.modbus.DummyModbusComponent; import io.openems.edge.bridge.modbus.api.worker.DummyReadTask; import io.openems.edge.bridge.modbus.api.worker.DummyWriteTask; import io.openems.edge.common.taskmanager.Priority; -import io.openems.edge.common.test.TimeLeapClock; public class TasksSupplierImplTest { diff --git a/io.openems.edge.common/src/io/openems/edge/common/component/ClockProvider.java b/io.openems.edge.common/src/io/openems/edge/common/component/ClockProvider.java index f24b2035826..9c15e7d02cb 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/component/ClockProvider.java +++ b/io.openems.edge.common/src/io/openems/edge/common/component/ClockProvider.java @@ -2,7 +2,7 @@ import java.time.Clock; -import io.openems.edge.common.test.TimeLeapClock; +import io.openems.common.test.TimeLeapClock; /** * {@link ClockProvider} provides a Clock - real or mocked like diff --git a/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java b/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java index 3b5a78b47da..03bccc75aa1 100644 --- a/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java +++ b/io.openems.edge.common/src/io/openems/edge/common/test/AbstractComponentTest.java @@ -29,6 +29,7 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.function.ThrowingRunnable; import io.openems.common.test.AbstractComponentConfig; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; import io.openems.common.types.OptionsEnum; diff --git a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/ControllerApiBackendImplTest.java b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/ControllerApiBackendImplTest.java index 3e532abad03..33b18aa730c 100644 --- a/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/ControllerApiBackendImplTest.java +++ b/io.openems.edge.controller.api.backend/test/io/openems/edge/controller/api/backend/ControllerApiBackendImplTest.java @@ -7,12 +7,12 @@ import org.junit.Test; import io.openems.common.channel.PersistencePriority; +import io.openems.common.test.TimeLeapClock; import io.openems.common.websocket.DummyWebsocketServer; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyCycle; -import io.openems.edge.common.test.TimeLeapClock; public class ControllerApiBackendImplTest { diff --git a/io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/ControllerApiMqttImplTest.java b/io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/ControllerApiMqttImplTest.java index dfd7b8d1972..a47a6bb4a1d 100644 --- a/io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/ControllerApiMqttImplTest.java +++ b/io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/ControllerApiMqttImplTest.java @@ -6,10 +6,10 @@ import org.junit.Test; import io.openems.common.channel.PersistencePriority; +import io.openems.common.test.TimeLeapClock; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; public class ControllerApiMqttImplTest { diff --git a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java index dfd2465fab9..26f3eb8d98f 100644 --- a/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java +++ b/io.openems.edge.controller.ess.activepowervoltagecharacteristic/test/io/openems/edge/controller/ess/activepowervoltagecharacteristic/CharacteristicImplTest.java @@ -6,12 +6,12 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.meter.test.DummyElectricityMeter; diff --git a/io.openems.edge.controller.ess.cycle/test/io/openems/edge/controller/ess/cycle/ControllerEssCycleImplTest.java b/io.openems.edge.controller.ess.cycle/test/io/openems/edge/controller/ess/cycle/ControllerEssCycleImplTest.java index ba67610bd1c..5e94d2ee022 100644 --- a/io.openems.edge.controller.ess.cycle/test/io/openems/edge/controller/ess/cycle/ControllerEssCycleImplTest.java +++ b/io.openems.edge.controller.ess.cycle/test/io/openems/edge/controller/ess/cycle/ControllerEssCycleImplTest.java @@ -6,11 +6,11 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.ess.cycle.statemachine.StateMachine.State; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; diff --git a/io.openems.edge.controller.ess.delaycharge/test/io/openems/edge/controller/ess/delaycharge/ControllerEssDelayChargeImplTest.java b/io.openems.edge.controller.ess.delaycharge/test/io/openems/edge/controller/ess/delaycharge/ControllerEssDelayChargeImplTest.java index 76d49d60995..4c495a15a52 100644 --- a/io.openems.edge.controller.ess.delaycharge/test/io/openems/edge/controller/ess/delaycharge/ControllerEssDelayChargeImplTest.java +++ b/io.openems.edge.controller.ess.delaycharge/test/io/openems/edge/controller/ess/delaycharge/ControllerEssDelayChargeImplTest.java @@ -6,10 +6,10 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; diff --git a/io.openems.edge.controller.ess.fixstateofcharge/test/io/openems/edge/controller/ess/fixstateofcharge/ControllerEssFixStateOfChargeImplTest.java b/io.openems.edge.controller.ess.fixstateofcharge/test/io/openems/edge/controller/ess/fixstateofcharge/ControllerEssFixStateOfChargeImplTest.java index 8d4f3e573cc..4f233eb845a 100644 --- a/io.openems.edge.controller.ess.fixstateofcharge/test/io/openems/edge/controller/ess/fixstateofcharge/ControllerEssFixStateOfChargeImplTest.java +++ b/io.openems.edge.controller.ess.fixstateofcharge/test/io/openems/edge/controller/ess/fixstateofcharge/ControllerEssFixStateOfChargeImplTest.java @@ -8,12 +8,12 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.ess.fixstateofcharge.api.AbstractFixStateOfCharge; import io.openems.edge.controller.ess.fixstateofcharge.api.EndCondition; import io.openems.edge.controller.ess.fixstateofcharge.statemachine.StateMachine; diff --git a/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImplTest.java b/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImplTest.java index b45aa415753..32a31d6d43c 100644 --- a/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImplTest.java +++ b/io.openems.edge.controller.ess.gridoptimizedcharge/test/io/openems/edge/controller/ess/gridoptimizedcharge/ControllerEssGridOptimizedChargeImplTest.java @@ -22,6 +22,7 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.function.ThrowingRunnable; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; @@ -30,7 +31,6 @@ import io.openems.edge.common.test.Plot; import io.openems.edge.common.test.Plot.AxisFormat; import io.openems.edge.common.test.Plot.Data; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyHybridEss; import io.openems.edge.ess.test.DummyManagedSymmetricEss; diff --git a/io.openems.edge.controller.ess.limittotaldischarge/test/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImplTest.java b/io.openems.edge.controller.ess.limittotaldischarge/test/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImplTest.java index 026da010263..d86836ed672 100644 --- a/io.openems.edge.controller.ess.limittotaldischarge/test/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImplTest.java +++ b/io.openems.edge.controller.ess.limittotaldischarge/test/io/openems/edge/controller/ess/limittotaldischarge/ControllerEssLimitTotalDischargeImplTest.java @@ -4,10 +4,10 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; diff --git a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ControllerEssReactivePowerVoltageCharacteristicImplTest.java b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ControllerEssReactivePowerVoltageCharacteristicImplTest.java index b32e62d04c8..1b4492ede55 100644 --- a/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ControllerEssReactivePowerVoltageCharacteristicImplTest.java +++ b/io.openems.edge.controller.ess.reactivepowervoltagecharacteristic/test/io/openems/edge/controller/ess/reactivepowervoltagecharacteristic/ControllerEssReactivePowerVoltageCharacteristicImplTest.java @@ -6,12 +6,12 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.meter.test.DummyElectricityMeter; diff --git a/io.openems.edge.controller.ess.standby/test/io/openems/edge/controller/ess/standby/ControllerEssStandbyImplTest.java b/io.openems.edge.controller.ess.standby/test/io/openems/edge/controller/ess/standby/ControllerEssStandbyImplTest.java index 2760edbc83f..1b4830c6993 100644 --- a/io.openems.edge.controller.ess.standby/test/io/openems/edge/controller/ess/standby/ControllerEssStandbyImplTest.java +++ b/io.openems.edge.controller.ess.standby/test/io/openems/edge/controller/ess/standby/ControllerEssStandbyImplTest.java @@ -8,13 +8,13 @@ import org.junit.Before; import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.sum.Sum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.ess.standby.statemachine.StateMachine.State; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; diff --git a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerTest.java b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerTest.java index b979d20301e..354279f29a8 100644 --- a/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerTest.java +++ b/io.openems.edge.controller.ess.timeofusetariff/test/io/openems/edge/controller/ess/timeofusetariff/TimeOfUseTariffControllerTest.java @@ -18,12 +18,12 @@ import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.predictor.api.test.DummyPrediction24Hours; diff --git a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java index d2ae71d58ca..1c4f620d146 100644 --- a/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java +++ b/io.openems.edge.controller.evcs/test/io/openems/edge/controller/evcs/ControllerEvcsImplTest.java @@ -7,12 +7,12 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.filter.DisabledRampFilter; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.evcs.api.ChargeMode; import io.openems.edge.evcs.api.Status; diff --git a/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java index f5b3ad37048..c0d7997b5b9 100644 --- a/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java +++ b/io.openems.edge.controller.io.analog/test/io/openems/edge/controller/io/analog/MyControllerTest.java @@ -6,11 +6,11 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.io.api.AnalogOutput.Range; import io.openems.edge.io.test.DummyAnalogVoltageOutput; diff --git a/io.openems.edge.controller.io.channelsinglethreshold/test/io/openems/edge/controller/io/channelsinglethreshold/ControllerIoChannelSingleThresholdImplTest.java b/io.openems.edge.controller.io.channelsinglethreshold/test/io/openems/edge/controller/io/channelsinglethreshold/ControllerIoChannelSingleThresholdImplTest.java index 9c94d238b93..7a780ffba36 100644 --- a/io.openems.edge.controller.io.channelsinglethreshold/test/io/openems/edge/controller/io/channelsinglethreshold/ControllerIoChannelSingleThresholdImplTest.java +++ b/io.openems.edge.controller.io.channelsinglethreshold/test/io/openems/edge/controller/io/channelsinglethreshold/ControllerIoChannelSingleThresholdImplTest.java @@ -2,10 +2,10 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.io.test.DummyInputOutput; diff --git a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest.java b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest.java index 7f494b7cbfb..a2c130c4c3a 100644 --- a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest.java +++ b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest.java @@ -6,11 +6,11 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.io.heatingelement.enums.Level; import io.openems.edge.controller.io.heatingelement.enums.Mode; import io.openems.edge.controller.io.heatingelement.enums.WorkMode; diff --git a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest2.java b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest2.java index 694426a0353..31ec267cf14 100644 --- a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest2.java +++ b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest2.java @@ -6,11 +6,11 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.io.heatingelement.enums.Level; import io.openems.edge.controller.io.heatingelement.enums.Mode; import io.openems.edge.controller.io.heatingelement.enums.Status; diff --git a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest3.java b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest3.java index 58ca4e678ce..f5d21a8d14d 100644 --- a/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest3.java +++ b/io.openems.edge.controller.io.heatingelement/test/io/openems/edge/controller/io/heatingelement/ControllerIoHeatingElementImplTest3.java @@ -6,11 +6,11 @@ import org.junit.Test; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.io.heatingelement.enums.Level; import io.openems.edge.controller.io.heatingelement.enums.Mode; import io.openems.edge.controller.io.heatingelement.enums.WorkMode; diff --git a/io.openems.edge.controller.io.heatpump.sgready/test/io/openems/edge/controller/io/heatpump/sgready/ControllerIoHeatPumpSgReadyImplTest.java b/io.openems.edge.controller.io.heatpump.sgready/test/io/openems/edge/controller/io/heatpump/sgready/ControllerIoHeatPumpSgReadyImplTest.java index 94c1a73345d..25c1389f41a 100644 --- a/io.openems.edge.controller.io.heatpump.sgready/test/io/openems/edge/controller/io/heatpump/sgready/ControllerIoHeatPumpSgReadyImplTest.java +++ b/io.openems.edge.controller.io.heatpump.sgready/test/io/openems/edge/controller/io/heatpump/sgready/ControllerIoHeatPumpSgReadyImplTest.java @@ -6,11 +6,11 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.DummySum; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.io.test.DummyInputOutput; diff --git a/io.openems.edge.controller.symmetric.balancingschedule/test/io/openems/edge/controller/symmetric/balancingschedule/ControllerEssBalancingScheduleImplTest.java b/io.openems.edge.controller.symmetric.balancingschedule/test/io/openems/edge/controller/symmetric/balancingschedule/ControllerEssBalancingScheduleImplTest.java index e16d6e8a9af..7501e1d9d60 100644 --- a/io.openems.edge.controller.symmetric.balancingschedule/test/io/openems/edge/controller/symmetric/balancingschedule/ControllerEssBalancingScheduleImplTest.java +++ b/io.openems.edge.controller.symmetric.balancingschedule/test/io/openems/edge/controller/symmetric/balancingschedule/ControllerEssBalancingScheduleImplTest.java @@ -6,13 +6,13 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.meter.test.DummyElectricityMeter; diff --git a/io.openems.edge.controller.symmetric.timeslotpeakshaving/test/io/openems/edge/controller/timeslotpeakshaving/ControllerEssTimeslotPeakshavingImplTest.java b/io.openems.edge.controller.symmetric.timeslotpeakshaving/test/io/openems/edge/controller/timeslotpeakshaving/ControllerEssTimeslotPeakshavingImplTest.java index e220113135d..0a1fabd78ea 100644 --- a/io.openems.edge.controller.symmetric.timeslotpeakshaving/test/io/openems/edge/controller/timeslotpeakshaving/ControllerEssTimeslotPeakshavingImplTest.java +++ b/io.openems.edge.controller.symmetric.timeslotpeakshaving/test/io/openems/edge/controller/timeslotpeakshaving/ControllerEssTimeslotPeakshavingImplTest.java @@ -6,11 +6,11 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.ControllerTest; import io.openems.edge.ess.test.DummyManagedSymmetricEss; import io.openems.edge.ess.test.DummyPower; diff --git a/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java index 57abd82542a..8d8ffaa367a 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/predictormanager/PredictorManagerImplTest.java @@ -11,11 +11,11 @@ import org.junit.Test; import io.openems.common.exceptions.OpenemsException; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.predictor.api.oneday.Prediction24Hours; import io.openems.edge.predictor.api.oneday.Predictor24Hours; import io.openems.edge.predictor.api.test.DummyPrediction24Hours; diff --git a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/common/AllowedChargeDischargeHandlerTest.java b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/common/AllowedChargeDischargeHandlerTest.java index 0c7770f7108..8d8c1e1a08d 100644 --- a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/common/AllowedChargeDischargeHandlerTest.java +++ b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/common/AllowedChargeDischargeHandlerTest.java @@ -8,10 +8,10 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.edge.common.component.ClockProvider; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.ess.generic.symmetric.AllowedChargeDischargeHandler; import io.openems.edge.ess.generic.symmetric.EssGenericManagedSymmetricImpl; diff --git a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/offgrid/EssGenericOffGridImplTest.java b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/offgrid/EssGenericOffGridImplTest.java index 58c43a3c097..5fa9e0840b5 100644 --- a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/offgrid/EssGenericOffGridImplTest.java +++ b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/offgrid/EssGenericOffGridImplTest.java @@ -5,13 +5,13 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.edge.battery.test.DummyBattery; import io.openems.edge.batteryinverter.test.DummyOffGridBatteryInverter; import io.openems.edge.common.startstop.StartStopConfig; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.ess.test.DummyOffGridSwitch; public class EssGenericOffGridImplTest { diff --git a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/EssGenericManagedSymmetricImplTest.java b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/EssGenericManagedSymmetricImplTest.java index 6f3e3a284e7..ac2258c43ba 100644 --- a/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/EssGenericManagedSymmetricImplTest.java +++ b/io.openems.edge.ess.generic/test/io/openems/edge/ess/generic/symmetric/EssGenericManagedSymmetricImplTest.java @@ -8,6 +8,7 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.battery.test.DummyBattery; import io.openems.edge.batteryinverter.test.DummyManagedSymmetricBatteryInverter; @@ -17,7 +18,6 @@ import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.ess.generic.common.GenericManagedEss; import io.openems.edge.ess.generic.symmetric.statemachine.StateMachine.State; import io.openems.edge.ess.test.DummyPower; diff --git a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PredictorPersistenceModelImplTest.java b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PredictorPersistenceModelImplTest.java index 52a71cde1c5..0c9c3d91a1c 100644 --- a/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PredictorPersistenceModelImplTest.java +++ b/io.openems.edge.predictor.persistencemodel/test/io/openems/edge/predictor/persistencemodel/PredictorPersistenceModelImplTest.java @@ -10,10 +10,10 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.timedata.test.DummyTimedata; public class PredictorPersistenceModelImplTest { diff --git a/io.openems.edge.predictor.similardaymodel/test/io/openems/edge/predictor/similardaymodel/PredictorSimilardayModelImplTest.java b/io.openems.edge.predictor.similardaymodel/test/io/openems/edge/predictor/similardaymodel/PredictorSimilardayModelImplTest.java index 297aee13000..5566c29081e 100644 --- a/io.openems.edge.predictor.similardaymodel/test/io/openems/edge/predictor/similardaymodel/PredictorSimilardayModelImplTest.java +++ b/io.openems.edge.predictor.similardaymodel/test/io/openems/edge/predictor/similardaymodel/PredictorSimilardayModelImplTest.java @@ -10,10 +10,10 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.timedata.test.DummyTimedata; public class PredictorSimilardayModelImplTest { diff --git a/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/SchedulerDailyImplTest.java b/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/SchedulerDailyImplTest.java index 3ebd39b361e..9f0d5e94cb1 100644 --- a/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/SchedulerDailyImplTest.java +++ b/io.openems.edge.scheduler.daily/test/io/openems/edge/scheduler/daily/SchedulerDailyImplTest.java @@ -11,11 +11,11 @@ import org.junit.Test; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.test.TimeLeapClock; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentManager; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.controller.test.DummyController; import io.openems.edge.scheduler.api.Scheduler; diff --git a/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorAppImpl.java b/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorAppImpl.java index 2efacba7ac0..ca6c1215434 100644 --- a/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorAppImpl.java +++ b/io.openems.edge.simulator/src/io/openems/edge/simulator/app/SimulatorAppImpl.java @@ -48,6 +48,7 @@ import io.openems.common.jsonrpc.request.DeleteComponentConfigRequest; import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest.Property; import io.openems.common.session.Role; +import io.openems.common.test.TimeLeapClock; import io.openems.common.timedata.Resolution; import io.openems.common.types.ChannelAddress; import io.openems.common.types.OpenemsType; @@ -61,7 +62,6 @@ import io.openems.edge.common.cycle.Cycle; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.jsonapi.JsonApi; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.common.type.TypeUtils; import io.openems.edge.common.user.User; import io.openems.edge.simulator.app.ExecuteSimulationRequest.Profile; diff --git a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/symmetric/reacting/SimulatorEssSymmetricReactingImplTest.java b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/symmetric/reacting/SimulatorEssSymmetricReactingImplTest.java index aa3e55dc51e..b6e2fe8934f 100644 --- a/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/symmetric/reacting/SimulatorEssSymmetricReactingImplTest.java +++ b/io.openems.edge.simulator/test/io/openems/edge/simulator/ess/symmetric/reacting/SimulatorEssSymmetricReactingImplTest.java @@ -6,12 +6,12 @@ import org.junit.Test; +import io.openems.common.test.TimeLeapClock; import io.openems.common.types.ChannelAddress; import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.test.AbstractComponentTest.TestCase; import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; -import io.openems.edge.common.test.TimeLeapClock; import io.openems.edge.ess.test.DummyPower; import io.openems.edge.ess.test.ManagedSymmetricEssTest; From d6291452f01c2a370d722a50c884706c3121c529 Mon Sep 17 00:00:00 2001 From: Lukas Rieger <73471197+lukasrgr@users.noreply.github.com> Date: Fri, 27 Oct 2023 13:12:24 +0200 Subject: [PATCH 24/27] UI: Refactor GridHistory like ProductionHistory (#2191) - Change production charts legend label 'Total' to have same hiding style as others - Change order of gridBuy and gridSell to be the same as in history- energy monitor --- ui/src/app/app-routing.module.ts | 2 +- ui/src/app/edge/history/common/common.ts | 25 +++ .../common/energy/chart/channels.spec.ts | 26 +-- .../energy/chart/chart.constants.spec.ts | 10 +- .../history/common/energy/chart/chart.spec.ts | 26 ++- .../edge/history/common/energy/chart/chart.ts | 8 +- .../common/grid/chart/chart.constants.spec.ts | 13 ++ .../history/common/grid/chart/chart.spec.ts | 79 ++++++++ .../edge/history/common/grid/chart/chart.ts | 106 ++++++++++ .../edge/history/common/grid/flat/flat.html | 9 + .../app/edge/history/common/grid/flat/flat.ts | 8 + ui/src/app/edge/history/common/grid/grid.ts | 28 +++ .../common/grid/overview/overview.html | 5 + .../history/common/grid/overview/overview.ts | 7 + .../production/chart/productionMeterChart.ts | 2 +- .../common/production/chart/totalAcChart.ts | 2 +- .../common/production/chart/totalChart.ts | 5 +- .../{Production.ts => production.ts} | 0 .../gridchartoverview.component.html | 43 ---- .../gridchartoverview.component.ts | 31 --- .../edge/history/grid/widget.component.html | 32 --- .../app/edge/history/grid/widget.component.ts | 64 ------ .../app/edge/history/history.component.html | 2 +- ui/src/app/edge/history/history.module.ts | 14 +- .../live/common/autarchy/modal/modal.spec.ts | 4 +- .../consumption/modal/modal.constants.spec.ts | 2 +- .../common/consumption/modal/modal.spec.ts | 2 +- .../app/edge/live/common/grid/flat/flat.html | 4 +- .../live/common/grid/modal/constants.spec.ts | 2 +- .../edge/live/common/grid/modal/modal.spec.ts | 12 +- .../app/edge/live/common/grid/modal/modal.ts | 48 +++-- .../selfconsumption/modal/modal.spec.ts | 4 +- ui/src/app/shared/edge/edgeconfig.spec.ts | 2 +- .../chart/abstracthistorychart.ts | 3 +- .../shared/genericComponents/chart/chart.ts | 8 +- .../shared/testing/common.ts | 188 ++++++++++++++++++ .../shared/{ => testing}/tester.ts | 28 +-- ui/src/app/shared/service/defaulttypes.ts | 53 +++++ ui/src/app/shared/service/utils.ts | 9 +- 39 files changed, 630 insertions(+), 286 deletions(-) create mode 100644 ui/src/app/edge/history/common/common.ts create mode 100644 ui/src/app/edge/history/common/grid/chart/chart.constants.spec.ts create mode 100644 ui/src/app/edge/history/common/grid/chart/chart.spec.ts create mode 100644 ui/src/app/edge/history/common/grid/chart/chart.ts create mode 100644 ui/src/app/edge/history/common/grid/flat/flat.html create mode 100644 ui/src/app/edge/history/common/grid/flat/flat.ts create mode 100644 ui/src/app/edge/history/common/grid/grid.ts create mode 100644 ui/src/app/edge/history/common/grid/overview/overview.html create mode 100644 ui/src/app/edge/history/common/grid/overview/overview.ts rename ui/src/app/edge/history/common/production/{Production.ts => production.ts} (100%) delete mode 100644 ui/src/app/edge/history/grid/gridchartoverview/gridchartoverview.component.html delete mode 100644 ui/src/app/edge/history/grid/gridchartoverview/gridchartoverview.component.ts delete mode 100644 ui/src/app/edge/history/grid/widget.component.html delete mode 100644 ui/src/app/edge/history/grid/widget.component.ts create mode 100644 ui/src/app/shared/genericComponents/shared/testing/common.ts rename ui/src/app/shared/genericComponents/shared/{ => testing}/tester.ts (91%) diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index d404cf3903a..df6aa4b3135 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -6,12 +6,12 @@ import { ChangelogViewComponent } from './changelog/view/view'; import { EdgeComponent } from './edge/edge.component'; import { ChannelthresholdChartOverviewComponent } from './edge/history/channelthreshold/channelthresholdchartoverview/channelthresholdchartoverview.component'; import { OverviewComponent as AutarchyChartOverviewComponent } from './edge/history/common/autarchy/overview/overview'; +import { OverviewComponent as GridChartOverviewComponent } from './edge/history/common/grid/overview/overview'; import { OverviewComponent as ProductionChartOverviewComponent } from './edge/history/common/production/overview/overview'; import { OverviewComponent as SelfconsumptionChartOverviewComponent } from './edge/history/common/selfconsumption/overview/overview'; import { ConsumptionChartOverviewComponent } from './edge/history/consumption/consumptionchartoverview/consumptionchartoverview.component'; import { DelayedSellToGridChartOverviewComponent } from './edge/history/delayedselltogrid/symmetricpeakshavingchartoverview/delayedselltogridchartoverview.component'; import { FixDigitalOutputChartOverviewComponent } from './edge/history/fixdigitaloutput/fixdigitaloutputchartoverview/fixdigitaloutputchartoverview.component'; -import { GridChartOverviewComponent } from './edge/history/grid/gridchartoverview/gridchartoverview.component'; import { GridOptimizedChargeChartOverviewComponent } from './edge/history/gridoptimizedcharge/gridoptimizedchargechartoverview/gridoptimizedchargechartoverview.component'; import { HeatingelementChartOverviewComponent } from './edge/history/heatingelement/heatingelementchartoverview/heatingelementchartoverview.component'; import { HeatPumpChartOverviewComponent } from './edge/history/heatpump/heatpumpchartoverview/heatpumpchartoverview.component'; diff --git a/ui/src/app/edge/history/common/common.ts b/ui/src/app/edge/history/common/common.ts new file mode 100644 index 00000000000..184212fecc7 --- /dev/null +++ b/ui/src/app/edge/history/common/common.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; + +import { Common_Autarchy } from './autarchy/Autarchy'; +import { CommonEnergyMonitor } from './energy/energy'; +import { Common_Grid } from './grid/grid'; +import { Common_Production } from './production/production'; +import { Common_Selfconsumption } from './selfconsumption/SelfConsumption'; + +@NgModule({ + imports: [ + Common_Autarchy, + CommonEnergyMonitor, + Common_Grid, + Common_Production, + Common_Selfconsumption + ], + exports: [ + Common_Autarchy, + CommonEnergyMonitor, + Common_Grid, + Common_Production, + Common_Selfconsumption + ] +}) +export class Common { } \ No newline at end of file diff --git a/ui/src/app/edge/history/common/energy/chart/channels.spec.ts b/ui/src/app/edge/history/common/energy/chart/channels.spec.ts index 5feb0c2f085..4ba57e3661b 100644 --- a/ui/src/app/edge/history/common/energy/chart/channels.spec.ts +++ b/ui/src/app/edge/history/common/energy/chart/channels.spec.ts @@ -1,4 +1,6 @@ -import { OeChartTester } from "src/app/shared/genericComponents/shared/tester"; +import { TimeUnit } from "chart.js"; +import { OeTester } from "src/app/shared/genericComponents/shared/testing/common"; +import { OeChartTester } from "src/app/shared/genericComponents/shared/testing/tester"; import { QueryHistoricTimeseriesDataResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesDataResponse"; import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse"; import { QueryHistoricTimeseriesEnergyResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; @@ -86,7 +88,7 @@ export namespace History { "quarter": "[Q]Q - YYYY", "year": "YYYY" }, - "unit": period + "unit": period as TimeUnit }, "bounds": "ticks" } @@ -168,7 +170,7 @@ export namespace History { "quarter": "[Q]Q - YYYY", "year": "YYYY" }, - "unit": period + "unit": period as TimeUnit }, "offset": true, "bounds": "ticks" @@ -184,23 +186,13 @@ export namespace History { "responsive": true } }); - export type OeChannels = { - - /** Always one value for each channel from a {@link QueryHistoricTimeseriesEnergyResponse} */ - energyChannelWithValues: QueryHistoricTimeseriesEnergyResponse, - - /** data from a {@link QueryHistoricTimeseriesEnergyPerPeriodResponse} */ - energyPerPeriodChannelWithValues?: QueryHistoricTimeseriesEnergyPerPeriodResponse, - /** data from a {@link QueryHistoricTimeseriesDataResponse} */ - dataChannelWithValues?: QueryHistoricTimeseriesDataResponse - } /** * up to 288 datapoints (5 min aggregated values) from a * * {@link Day.energyPerPeriodChannelWithValues} and {@link Day.dataChannelWithValues} * */ - export const DAY: History.OeChannels = ({ + export const DAY: OeTester.Types.Channels = ({ energyChannelWithValues: new QueryHistoricTimeseriesEnergyResponse("0", { data: { '_sum/GridBuyActiveEnergy': 938, @@ -1952,7 +1944,7 @@ export namespace History { /** * up to 164 datapoints(1 hour values) from a {@link Day.energyPerPeriodChannelWithValues} and {@link Day.dataChannelWithValues} * */ - export const WEEK: OeChannels = { + export const WEEK: OeTester.Types.Channels = { energyChannelWithValues: new QueryHistoricTimeseriesEnergyResponse("0", { data: { '_sum/GridBuyActiveEnergy': 2368, @@ -3150,7 +3142,7 @@ export namespace History { /** * up to 31 datapoints(1 day values) from a {@link Day.energyPerPeriodChannelWithValues} and {@link Day.dataChannelWithValues}*/ - export const MONTH: OeChannels = { + export const MONTH: OeTester.Types.Channels = { energyChannelWithValues: new QueryHistoricTimeseriesEnergyResponse("0", { data: { '_sum/GridBuyActiveEnergy': 773000, @@ -3392,7 +3384,7 @@ export namespace History { /** * up to 12 datapoints(1 month values) from a {@link Day.energyPerPeriodChannelWithValues} and {@link Day.dataChannelWithValues}*/ - export const YEAR: OeChannels = { + export const YEAR: OeTester.Types.Channels = { energyChannelWithValues: new QueryHistoricTimeseriesEnergyResponse("0", { data: { '_sum/GridBuyActiveEnergy': 23209000, diff --git a/ui/src/app/edge/history/common/energy/chart/chart.constants.spec.ts b/ui/src/app/edge/history/common/energy/chart/chart.constants.spec.ts index 316ea76fb46..a03de828041 100644 --- a/ui/src/app/edge/history/common/energy/chart/chart.constants.spec.ts +++ b/ui/src/app/edge/history/common/energy/chart/chart.constants.spec.ts @@ -1,12 +1,12 @@ +import { DummyConfig } from "src/app/shared/edge/edgeconfig.spec"; +import { OeTester } from "src/app/shared/genericComponents/shared/testing/common"; import { EdgeConfig } from "src/app/shared/shared"; -import { TestContext, removeFunctions } from "src/app/shared/test/utils.spec"; -import { History } from "src/app/edge/history/common/energy/chart/channels.spec"; +import { removeFunctions, TestContext } from "src/app/shared/test/utils.spec"; -import { DummyConfig } from "src/app/shared/edge/edgeconfig.spec"; -import { OeChartTester } from "../../../../../shared/genericComponents/shared/tester"; +import { OeChartTester } from "../../../../../shared/genericComponents/shared/testing/tester"; import { ChartComponent } from "./chart"; -export function expectView(config: EdgeConfig, testContext: TestContext, chartType: 'line' | 'bar', channels: History.OeChannels, view: OeChartTester.View): void { +export function expectView(config: EdgeConfig, testContext: TestContext, chartType: 'line' | 'bar', channels: OeTester.Types.Channels, view: OeChartTester.View): void { expect(removeFunctions(OeChartTester .apply(ChartComponent .getChartData(DummyConfig.convertDummyEdgeConfigToRealEdgeConfig(config), chartType, testContext.translate), chartType, channels, testContext))) 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 20a81e26ec2..fbb9e4f7ffc 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 @@ -26,8 +26,8 @@ describe('History EnergyMonitor', () => { DATA('Erzeugung: 47,6 kWh', [null, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, null, null, null, 0.002, 0.03, 0.027, 0.03, 0.039, 0.074, 0.093, 0.12, 0.12, 0.116, 0.106, 0.099, 0.101, 0.113, 0.131, 0.141, 0.131, 0.132, 0.105, 0.139, 0.165, 0.195, 0.255, 0.385, 0.458, 0.402, 0.428, 0.56, 0.615, 0.715, 0.7, 0.807, 0.796, 0.79, 0.813, 0.854, 0.832, 1.052, 1.427, 1.481, 1.765, 1.291, 1.625, 2.138, 1.686, 1.367, 1.562, 1.271, 1.176, 2.542, 2.91, 2.616, 2.193, 2.039, 2.376, 2.919, 3.862, 3.793, 4.309, 3.932, 4.126, 4.406, 4.757, 4.728, 5.231, 4.4, 4.169, 5.232, 5.77, 5.3, 6.327, 6.636, 4.573, 3.678, 3.422, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), DATA('Beladung: 15,8 kWh', [null, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, null, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.065, 0.063, 0.185, 0.262, 0.09500000000000003, 0.265, 0.48300000000000004, 0.537, 0.639, 0, 0, 0, 0, 0.18799999999999994, 0.701, 0.586, 0.881, 1.204, 1.282, 1.547, 0.988, 1.353, 1.94, 1.564, 1.2469999999999999, 1.4140000000000001, 1.0479999999999998, 0.2499999999999999, 1.9089999999999998, 2.7, 2.3810000000000002, 1.861, 1.729, 1.859, 1.6680000000000001, 3.225, 2.763, 3.847, 3.59, 2.3530000000000006, 4.143, 4.478999999999999, 4.382, 2.2329999999999997, 0.7170000000000005, 0.07699999999999996, 0.03200000000000003, 0.06099999999999994, 0.027000000000000135, 0.07099999999999973, 0.057000000000000384, 0.012000000000000455, 0.0259999999999998, 0.04800000000000004, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), DATA('Entladung: 7,2 kWh', [null, null, null, 0.081, 0.244, 0.398, 0.221, 0.214, 0.214, 0.214, 0.308, 0.204, 0.108, 0.109, 0.108, 0.171, 0.197, 0.081, 0.084, 0.085, 0.16, 0.295, 0.188, 0.167, 0.165, 0.175, 0.337, 0.183, 0.093, 0.095, 0.095, 0.194, 0.251, 0.169, 0.122, 0.113, 0.156, 0.301, 0.303, 0.242, 0.204, 0.2, 0.266, 0.343, 0.135, 0.097, 0.096, null, null, null, null, 0.089, 0.089, 0.10900000000000001, 0.265, 0.20199999999999999, 0.175, 0.218, 0.178, 0.324, 0.331, 0.16100000000000003, 0.119, 0.11299999999999999, 0.136, 0.26, 0.10500000000000001, 0.066, 0.05099999999999999, 0.05600000000000001, 0.14100000000000001, 0.043999999999999984, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.17200000000000004, 0.30799999999999994, 0.27, 0.2589999999999999, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), - DATA('Netzeinspeisung: 15,6 kWh', [null, null, null, 0, 0, 0.006, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.004, 0, 0, 0, 0.004, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.005, 0, 0, 0, 0, 0, 0.001, 0.002, null, null, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.001, 0.004, 0, 0.004, 0, 0, 0, 0, 0.005, 0.013, 0.006, 0.004, 0.017, 0.015, 0.017, 0.011, 0, 0, 0, 0, 0.029, 0.015, 0.013, 0.019, 0.014, 0.007, 0.016, 0, 0.018, 0.022, 0, 0.012, 0.011, 0.007, 0, 0.033, 0.007, 0.003, 0.004, 0.011, 0, 0.038, 0, 0, 0.019, 0, 0.016, 0.014, 0.018, 0, 1.119, 3.453, 3.608, 3.941, 4.392, 3.786, 4.805, 4.688, 3.095, 2.32, 2.851, 3.058, 4.044, 5.011, 2.789, 6.53, 5.029, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), - DATA('Netzbezug: 0,9 kWh', [null, null, null, 0.031, 0.018, 0, 0.02, 0.016, 0.015, 0.014, 0.009, 0.02, 0.025, 0.025, 0.025, 0.021, 0.012, 0.009, 0.01, 0.011, 0.005, 0.003, 0, 0.015, 0.018, 0.023, 0, 0, 0, 0.002, 0.002, 0.003, 0.015, 0.008, 0.022, 0.027, 0.016, 0.003, 0.002, 0, 0.028, 0.027, 0.017, 0.001, 0, 0, 0, null, null, null, null, 0.011, 0.01, 0.004, 0.006, 0.007, 0.018, 0.008, 0.012, 0.009, 0.004, 0.013, 0.015, 0.012, 0, 0, 0, 0.002, 0, 0.005, 0.001, 0.03, 0.062, 0, 0, 0, 0, 0, 0, 0, 0, 0.015, 0.005, 0.004, 0.007, 0, 0, 0, 0, 0, 0, 0, 0.005, 0, 0, 0, 0, 0, 0, 0.021, 0, 0, 0, 0, 0, 0.003, 0, 0.004, 0, 0, 0.032, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), + DATA('Einspeisung: 15,6 kWh', [null, null, null, 0, 0, 0.006, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.004, 0, 0, 0, 0.004, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.005, 0, 0, 0, 0, 0, 0.001, 0.002, null, null, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.001, 0.004, 0, 0.004, 0, 0, 0, 0, 0.005, 0.013, 0.006, 0.004, 0.017, 0.015, 0.017, 0.011, 0, 0, 0, 0, 0.029, 0.015, 0.013, 0.019, 0.014, 0.007, 0.016, 0, 0.018, 0.022, 0, 0.012, 0.011, 0.007, 0, 0.033, 0.007, 0.003, 0.004, 0.011, 0, 0.038, 0, 0, 0.019, 0, 0.016, 0.014, 0.018, 0, 1.119, 3.453, 3.608, 3.941, 4.392, 3.786, 4.805, 4.688, 3.095, 2.32, 2.851, 3.058, 4.044, 5.011, 2.789, 6.53, 5.029, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), + DATA('Bezug: 0,9 kWh', [null, null, null, 0.031, 0.018, 0, 0.02, 0.016, 0.015, 0.014, 0.009, 0.02, 0.025, 0.025, 0.025, 0.021, 0.012, 0.009, 0.01, 0.011, 0.005, 0.003, 0, 0.015, 0.018, 0.023, 0, 0, 0, 0.002, 0.002, 0.003, 0.015, 0.008, 0.022, 0.027, 0.016, 0.003, 0.002, 0, 0.028, 0.027, 0.017, 0.001, 0, 0, 0, null, null, null, null, 0.011, 0.01, 0.004, 0.006, 0.007, 0.018, 0.008, 0.012, 0.009, 0.004, 0.013, 0.015, 0.012, 0, 0, 0, 0.002, 0, 0.005, 0.001, 0.03, 0.062, 0, 0, 0, 0, 0, 0, 0, 0, 0.015, 0.005, 0.004, 0.007, 0, 0, 0, 0, 0, 0, 0, 0.005, 0, 0, 0, 0, 0, 0, 0.021, 0, 0, 0, 0, 0, 0.003, 0, 0.004, 0, 0, 0.032, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), DATA('Verbrauch: 24,4 kWh', [null, null, null, 0.112, 0.262, 0.392, 0.24, 0.23, 0.229, 0.227, 0.317, 0.224, 0.133, 0.135, 0.133, 0.192, 0.209, 0.09, 0.095, 0.096, 0.164, 0.297, 0.184, 0.182, 0.183, 0.198, 0.333, 0.183, 0.093, 0.097, 0.098, 0.197, 0.266, 0.177, 0.144, 0.14, 0.173, 0.304, 0.305, 0.237, 0.232, 0.227, 0.283, 0.344, 0.135, 0.096, 0.095, null, null, null, null, 0.102, 0.129, 0.14, 0.301, 0.248, 0.267, 0.319, 0.31, 0.452, 0.451, 0.28, 0.234, 0.226, 0.249, 0.39, 0.242, 0.199, 0.179, 0.166, 0.28, 0.239, 0.192, 0.187, 0.187, 0.19, 0.303, 0.146, 0.062, 0.062, 0.064, 0.887, 1.119, 1.07, 1.057, 0.596, 0.138, 0.233, 0.152, 0.209, 0.192, 0.202, 0.308, 0.254, 0.175, 0.122, 0.108, 0.137, 0.216, 0.947, 0.599, 0.203, 0.232, 0.328, 0.299, 0.52, 1.213, 0.641, 1.03, 0.442, 0.374, 1.758, 0.249, 0.26, 0.346, 1.879, 0.23, 0.484, 1.26, 1.317, 1.488, 1.451, 1.892, 1.466, 1.332, 0.523, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), DATA('Ladezustand', History.DAY.dataChannelWithValues.result.data['_sum/EssSoc']) ], @@ -36,11 +36,9 @@ describe('History EnergyMonitor', () => { } }); - } { - // Line-Chart expectView(defaultEMS, TEST_CONTEXT, 'line', History.WEEK, { @@ -49,8 +47,8 @@ describe('History EnergyMonitor', () => { DATA('Erzeugung: 200,9 kWh', [0, 0, 0, 0, 0.06877777777777777, 0.10641666666666667, 0.24808333333333335, 0.9343333333333333, 2.7069166666666664, 4.60225, 6.1075, 7.152166666666667, 7.8105, 7.919833333333333, 7.5575, 5.898916666666667, 3.4225, 1.20825, 0.6315833333333334, 0.4348333333333333, 0.11625, 0.0555, 0, 0, 0, 0, 0, 0, 0.05566666666666666, 0.11616666666666667, 0.41533333333333333, 0.80975, 1.3233333333333333, 1.5246666666666668, 4.180416666666667, 2.5433333333333334, 2.1981666666666664, 4.257916666666667, 5.337583333333333, 3.255, 2.7370833333333335, 1.9298333333333333, 1.0460833333333333, 0.5075, 0.12633333333333333, 0.0575, 0, 0, 0, 0, 0, 0, 0.03266666666666666, 0.08233333333333333, 0.3933333333333333, 1.09875, 1.88925, 4.037166666666667, 6.144166666666667, 7.2335, 7.912333333333333, 7.1735, 7.83025, 6.541166666666667, 3.7155, 1.372, 0.4713333333333333, 0.29875, 0.12891666666666665, 0.0605, 0.0014166666666666668, 0, 0, 0, 0, 0, 0.07055555555555555, 0.126, 0.22975, 0.9369166666666666, 2.7914166666666667, 4.741666666666667, 6.264666666666667, 7.398416666666667, 7.854166666666667, 8.1385, 7.7740833333333335, 6.136583333333333, 3.59375, 0.9946666666666666, 0.39208333333333334, 0.3069090909090909, 0.12022222222222223, 0.0585, 0.00008333333333333333, 0, 0, 0, 0, 0, 0.04644444444444444, 0.123, 0.47733333333333333, 1.2674166666666666, 2.0323333333333333, 2.60675, 2.39825, 1.2404166666666667, 0.7430833333333333, 0.72275, 0.706, 2.8409166666666663, 3.1284166666666664, 1.23975, 0.7388333333333333, 0.3690833333333333, 0.11475, 0.05725, 0, 0, 0, 0, 0, 0, 0.03622222222222222, 0.11033333333333332, 0.41425, 1.2955833333333333, 2.0244166666666668, 1.6163333333333332, 1.624, 5.705, 4.2615, 2.9964166666666667, 4.293333333333333, 4.474083333333333, 2.6373333333333333, 0.5760833333333334, 0.7170833333333334, 0.3575, 0.16566666666666666, 0.061, 0, 0, 0, 0, 0, 0, 0.04122222222222222, 0.09633333333333333, 0.18325, 0.4275, 1.8598181818181818, 3.429, 1.2262857142857142, 2.923, 4.695, 4.4568, 5.333916666666667, 4.859545454545455, 2.6625, 2.284, 0.7131666666666666, 0.4491, 0.1561, 0.0615, 0.0006, 0]), DATA('Beladung: 38,7 kWh', [0, 0, 0, 0, 0, 0, 0.053916666666666696, 0.7623333333333333, 2.138083333333333, 2.88375, 0.040750000000000064, 0.05616666666666692, 0.06824999999999992, 0.05333333333333279, 0.052833333333333066, 0.06191666666666684, 0.05941666666666645, 0.06133333333333324, 0.041166666666666796, 0.27449999999999997, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.03533333333333333, 0.07091666666666663, 0.9209166666666666, 0.1278333333333337, 3.353, 0.12391666666666712, 0.05908333333333271, 0.05958333333333332, 0.039833333333333165, 0.0448333333333335, 0.05183333333333362, 0.06066666666666665, 0, 0.08024999999999993, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.05283333333333329, 0.24774999999999991, 1.4625000000000001, 2.159, 0.05708333333333382, 0.05458333333333343, 0.052666666666666195, 0.06583333333333297, 0.053749999999999964, 0.057000000000000384, 0.2017500000000001, 0, 0.23058333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2.041166666666667, 3.0615000000000006, 0.07808333333333373, 0.0442499999999999, 0.06674999999999986, 0.051916666666667055, 0.04716666666666658, 0.054250000000000576, 0.049249999999999794, 0.051416666666666555, 0.0595, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.32333333333333336, 0.589, 1.2433333333333332, 2.029166666666667, 0.4844166666666667, 0, 0, 0, 0.03066666666666662, 2.34975, 0.07308333333333294, 0.06908333333333316, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2748333333333334, 1.13575, 0, 0.8175833333333332, 0.6326666666666667, 5.248833333333334, 1.257083333333333, 0, 0.8414166666666665, 0, 0.27624999999999966, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2515, 0.5249090909090908, 2.5081666666666664, 0.6641428571428571, 2.0005, 2.226888888888889, 1.0726000000000004, 0.020500000000000185, 0.12281818181818238, 0.04666666666666641, 0.04499999999999993, 0, 0.2635, 0, 0, 0, 0]), DATA('Entladung: 31,8 kWh', [0.16255555555555554, 0.15308333333333335, 0.13358333333333333, 0.23645454545454547, 0.16444444444444445, 0.15000000000000002, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.8418333333333334, 0.40008333333333335, 0.3143333333333333, 0.87825, 0.15983333333333336, 0.15433333333333335, 0.12808333333333335, 0.13336363636363638, 0.12544444444444444, 0.10674999999999998, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.011833333333333584, 0, 0.6314166666666667, 0.3428333333333333, 0.1775, 0.24691666666666665, 0.32225, 0.20191666666666666, 0.17116666666666666, 0.15227272727272728, 0.13366666666666666, 0.1650833333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.17574999999999985, 0, 0.307, 0.30425, 0.8374166666666666, 0.5858333333333334, 0.233, 0.12245454545454545, 0.19341666666666665, 0.16675, 0.16018181818181818, 0.1677777777777778, 0.07708333333333334, 0.2829166666666666, 0.3085000000000002, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.3293636363636363, 0.3361111111111111, 0.9013333333333333, 0.21000000000000002, 0.6339166666666667, 0.11658333333333333, 0.17658333333333334, 0.13425, 0.19072727272727272, 0.1686666666666667, 0.10341666666666666, 0, 0, 0, 0, 0, 0.13316666666666643, 1.2479166666666668, 0.5, 0, 0, 0, 0, 0.2433333333333333, 2.1755833333333334, 1.2095833333333335, 2.0555000000000003, 0.67025, 0.17266666666666666, 0.16391666666666665, 0.13858333333333334, 0.07441666666666667, 0.20381818181818182, 0.18944444444444444, 0.3955, 0, 0, 0.049249999999999794, 0, 0, 0, 0, 0.6410833333333334, 0, 0.3483333333333345, 0, 1.5750000000000002, 0.34658333333333335, 0.8439166666666669, 0.42374999999999996, 1.0724166666666668, 0.33325, 0.3395, 0.6474166666666666, 0.4916666666666667, 0.18208333333333335, 0.13436363636363638, 0.15433333333333332, 0.12583333333333332, 0.019250000000000017, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.3953333333333334, 0, 0.5301, 0.377875, 0.5781, 0.150375]), - DATA('Netzeinspeisung: 119,7 kWh', [0.0023333333333333335, 0, 0, 0, 0, 0, 0, 0.014166666666666666, 0.02808333333333333, 0.9546666666666667, 4.150583333333333, 6.431333333333333, 5.737583333333333, 5.6714166666666666, 5.873333333333333, 5.049083333333333, 3.122, 1.0374166666666667, 0.22808333333333333, 0.02, 0, 0, 0, 0.008333333333333333, 0.0030833333333333333, 0.008333333333333333, 0, 0.007727272727272728, 0, 0, 0.00275, 0.013833333333333335, 0.017416666666666667, 0.006083333333333333, 0.5646666666666667, 2.2251666666666665, 2.03375, 3.99725, 4.990083333333333, 3.0128333333333335, 2.4844166666666667, 1.378, 0.65975, 0, 0.001, 0.006916666666666667, 0.008166666666666666, 0, 0, 0, 0, 0, 0, 0, 0.004083333333333333, 0.010583333333333333, 0.011166666666666667, 1.261, 5.308833333333333, 6.604, 6.321166666666667, 6.488333333333333, 6.78425, 6.052083333333333, 2.5839166666666666, 0.529, 0.01616666666666667, 0.0055, 0, 0.0006666666666666666, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0024166666666666664, 0.0125, 0.7065, 5.835416666666667, 4.77025, 6.03925, 6.8445833333333335, 5.370333333333333, 4.490166666666667, 2.3506666666666667, 0.7650833333333333, 0.08583333333333333, 0.011454545454545455, 0, 0, 0.005666666666666667, 0, 0, 0, 0, 0, 0, 0, 0.0033333333333333335, 0.004083333333333333, 0.02033333333333333, 0.02316666666666667, 1.4106666666666667, 0.8588333333333333, 0.0015833333333333333, 0.006583333333333333, 0.010083333333333335, 0.3410833333333333, 2.9290833333333337, 1.1175833333333332, 0.48583333333333334, 0, 0, 0, 0.0006666666666666666, 0.017916666666666668, 0.004, 0, 0, 0.001, 0, 0, 0, 0.02358333333333333, 0.006416666666666667, 0.008166666666666666, 0.0031666666666666666, 0.009916666666666666, 2.7254166666666664, 1.83725, 2.63225, 2.2170833333333335, 0.529, 0, 0, 0, 0, 0, 0.0003333333333333333, 0, 0, 0.011416666666666665, 0.011083333333333334, 0, 0, 0, 0, 0.008333333333333333, 0.008818181818181819, 0.015333333333333334, 0.018857142857142857, 0.024833333333333332, 0.010888888888888889, 2.2174, 3.9214166666666666, 1.6248181818181817, 1.937, 1.789, 0.0195, 0.0143, 0, 0, 0.009, 0.018875]), - DATA('Netzbezug: 2,4 kWh', [0, 0.011916666666666666, 0.01633333333333333, 0.00609090909090909, 0.015333333333333334, 0.011666666666666665, 0.0024166666666666664, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.02425, 0.004416666666666667, 0.0035833333333333333, 0, 0, 0, 0.04441666666666667, 0, 0.013111111111111112, 0.001, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0011666666666666668, 0, 0, 0, 0.0015833333333333333, 0.013333333333333334, 0.020416666666666666, 0.01125, 0.019727272727272725, 0.012444444444444445, 0.009583333333333334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.007666666666666667, 0, 0.0023333333333333335, 0.0125, 0.01609090909090909, 0.02016666666666667, 0.014083333333333333, 0.006363636363636363, 0.01955555555555556, 0.04841666666666666, 0.011166666666666667, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.014222222222222221, 0.00225, 0, 0.0036666666666666666, 0.032916666666666664, 0.014666666666666666, 0.0135, 0.017363636363636362, 0.013333333333333334, 0.022083333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0009166666666666666, 0, 0.0021666666666666666, 0, 0, 0, 0.0005, 0.04841666666666666, 0, 0.005555555555555556, 0.02716666666666667, 0.017333333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0023333333333333335, 0.008333333333333333, 0.003, 0.015916666666666666, 0.00325, 0, 0.004333333333333333, 0.001, 0, 0, 0.019545454545454546, 0.0017777777777777776, 0.006416666666666667, 0.017666666666666667, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0058, 0.005625, 0, 0]), + DATA('Einspeisung: 119,7 kWh', [0.0023333333333333335, 0, 0, 0, 0, 0, 0, 0.014166666666666666, 0.02808333333333333, 0.9546666666666667, 4.150583333333333, 6.431333333333333, 5.737583333333333, 5.6714166666666666, 5.873333333333333, 5.049083333333333, 3.122, 1.0374166666666667, 0.22808333333333333, 0.02, 0, 0, 0, 0.008333333333333333, 0.0030833333333333333, 0.008333333333333333, 0, 0.007727272727272728, 0, 0, 0.00275, 0.013833333333333335, 0.017416666666666667, 0.006083333333333333, 0.5646666666666667, 2.2251666666666665, 2.03375, 3.99725, 4.990083333333333, 3.0128333333333335, 2.4844166666666667, 1.378, 0.65975, 0, 0.001, 0.006916666666666667, 0.008166666666666666, 0, 0, 0, 0, 0, 0, 0, 0.004083333333333333, 0.010583333333333333, 0.011166666666666667, 1.261, 5.308833333333333, 6.604, 6.321166666666667, 6.488333333333333, 6.78425, 6.052083333333333, 2.5839166666666666, 0.529, 0.01616666666666667, 0.0055, 0, 0.0006666666666666666, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0024166666666666664, 0.0125, 0.7065, 5.835416666666667, 4.77025, 6.03925, 6.8445833333333335, 5.370333333333333, 4.490166666666667, 2.3506666666666667, 0.7650833333333333, 0.08583333333333333, 0.011454545454545455, 0, 0, 0.005666666666666667, 0, 0, 0, 0, 0, 0, 0, 0.0033333333333333335, 0.004083333333333333, 0.02033333333333333, 0.02316666666666667, 1.4106666666666667, 0.8588333333333333, 0.0015833333333333333, 0.006583333333333333, 0.010083333333333335, 0.3410833333333333, 2.9290833333333337, 1.1175833333333332, 0.48583333333333334, 0, 0, 0, 0.0006666666666666666, 0.017916666666666668, 0.004, 0, 0, 0.001, 0, 0, 0, 0.02358333333333333, 0.006416666666666667, 0.008166666666666666, 0.0031666666666666666, 0.009916666666666666, 2.7254166666666664, 1.83725, 2.63225, 2.2170833333333335, 0.529, 0, 0, 0, 0, 0, 0.0003333333333333333, 0, 0, 0.011416666666666665, 0.011083333333333334, 0, 0, 0, 0, 0.008333333333333333, 0.008818181818181819, 0.015333333333333334, 0.018857142857142857, 0.024833333333333332, 0.010888888888888889, 2.2174, 3.9214166666666666, 1.6248181818181817, 1.937, 1.789, 0.0195, 0.0143, 0, 0, 0.009, 0.018875]), + DATA('Bezug: 2,4 kWh', [0, 0.011916666666666666, 0.01633333333333333, 0.00609090909090909, 0.015333333333333334, 0.011666666666666665, 0.0024166666666666664, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.02425, 0.004416666666666667, 0.0035833333333333333, 0, 0, 0, 0.04441666666666667, 0, 0.013111111111111112, 0.001, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0011666666666666668, 0, 0, 0, 0.0015833333333333333, 0.013333333333333334, 0.020416666666666666, 0.01125, 0.019727272727272725, 0.012444444444444445, 0.009583333333333334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.007666666666666667, 0, 0.0023333333333333335, 0.0125, 0.01609090909090909, 0.02016666666666667, 0.014083333333333333, 0.006363636363636363, 0.01955555555555556, 0.04841666666666666, 0.011166666666666667, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.014222222222222221, 0.00225, 0, 0.0036666666666666666, 0.032916666666666664, 0.014666666666666666, 0.0135, 0.017363636363636362, 0.013333333333333334, 0.022083333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0009166666666666666, 0, 0.0021666666666666666, 0, 0, 0, 0.0005, 0.04841666666666666, 0, 0.005555555555555556, 0.02716666666666667, 0.017333333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0023333333333333335, 0.008333333333333333, 0.003, 0.015916666666666666, 0.00325, 0, 0.004333333333333333, 0.001, 0, 0, 0.019545454545454546, 0.0017777777777777776, 0.006416666666666667, 0.017666666666666667, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0058, 0.005625, 0, 0]), DATA('Verbrauch: 76,7 kWh', [0.16022222222222224, 0.16516666666666666, 0.14991666666666667, 0.24245454545454548, 0.24866666666666665, 0.2679166666666667, 0.19658333333333333, 0.15775, 0.5408333333333334, 0.7640833333333333, 1.9163333333333332, 0.6645833333333334, 2.0044166666666667, 2.1950833333333333, 1.63125, 0.7880833333333334, 0.24116666666666667, 0.1095, 0.3621666666666667, 0.14041666666666666, 0.9824166666666666, 0.4598333333333333, 0.31808333333333333, 0.8699166666666667, 0.157, 0.14616666666666667, 0.17258333333333334, 0.12590909090909091, 0.19444444444444445, 0.22383333333333336, 0.37725, 0.7250833333333334, 0.385, 1.39075, 0.26275, 0.19433333333333333, 0.10516666666666667, 0.201, 0.30775, 0.19716666666666666, 0.20083333333333334, 0.4910833333333333, 0.398, 0.42825, 0.75675, 0.3935, 0.16933333333333334, 0.24841666666666665, 0.3354166666666667, 0.22233333333333336, 0.1825, 0.1720909090909091, 0.179, 0.2568333333333333, 0.3364166666666667, 0.8403333333333334, 0.4155, 0.6171666666666666, 0.7785, 0.5746666666666667, 1.53875, 0.6193333333333334, 0.99225, 0.43191666666666667, 0.92975, 1.0189166666666667, 0.22433333333333333, 0.6004166666666666, 0.4410833333333333, 0.8973333333333333, 0.5895, 0.24541666666666664, 0.13836363636363638, 0.21366666666666664, 0.18075, 0.16654545454545452, 0.2578888888888889, 0.2514166666666667, 0.5236666666666666, 1.2431666666666668, 0.7379166666666667, 0.9735833333333334, 0.35125, 2.5838333333333336, 1.7480833333333332, 1.2421666666666666, 2.35675, 1.5921666666666667, 1.19375, 0.17808333333333334, 0.24683333333333335, 0.6248181818181818, 0.47044444444444444, 0.9619166666666666, 0.20433333333333334, 0.6376666666666666, 0.14958333333333335, 0.19125, 0.14783333333333334, 0.208, 0.22866666666666666, 0.24891666666666665, 0.1505, 0.6745, 0.7685, 0.5545833333333333, 0.50325, 0.5148333333333334, 1.9893333333333332, 1.2161666666666668, 0.6651666666666667, 0.15025, 0.12625, 0.05316666666666667, 0.4963333333333333, 2.54575, 1.3246666666666667, 2.115, 0.6698333333333334, 0.15458333333333335, 0.15975, 0.13916666666666666, 0.12266666666666667, 0.2029090909090909, 0.23122222222222222, 0.533, 0.15675, 0.13625, 2.067, 0.7903333333333333, 0.9883333333333334, 0.44608333333333333, 0.2790833333333333, 1.8005, 0.8198333333333334, 2.60525, 1.83225, 2.1533333333333333, 1.072, 1.2043333333333333, 0.6051666666666666, 1.13675, 0.3330833333333333, 0.3438333333333333, 0.6486666666666666, 0.48025, 0.17116666666666666, 0.15381818181818183, 0.19722222222222222, 0.22858333333333333, 0.22016666666666665, 0.16758333333333333, 1.3263636363636362, 0.9056666666666666, 0.5432857142857144, 0.8975, 2.457222222222222, 1.1668, 1.3920833333333333, 3.111909090909091, 0.6785, 0.451, 1.089, 0.1713, 0.6919, 0.444625, 0.5696, 0.1315]), DATA('Ladezustand', History.WEEK.dataChannelWithValues.result.data['_sum/EssSoc']) ], @@ -73,8 +71,8 @@ describe('History EnergyMonitor', () => { DATA('Direktverbrauch: 5.808,7 kWh', [191.524, 214.083, 198.811, 196.842, 184.218, 201.167, 175.916, null, 347.243, 166.862, 176.461, 218.586, 229.496, 228.661, 211.608, 217.075, 177.422, 179.495, 200.029, 229.434, 229.765, null, 360.727, 171.324, 206.255, null, 442.327, 225.59, 227.751, null]), DATA('Beladung: 3.944,3 kWh', [113.476, 162.917, 150.189, 157.158, 149.782, 159.833, 155.084, null, 228.757, 128.138, 157.539, 59.414, 156.504, 107.339, 156.392, 158.925, 158.578, 121.505, 120.971, 154.566, 173.235, null, 204.273, 156.676, 143.745, null, 247.673, 157.41, 104.249, null]), DATA('Entladung: 3.394,4 kWh', [112.818, 126.532, 139.622, 133.212, 169.24, 98.705, 109.367, null, 204.267, 118.504, 121.261, 74.97, 144.175, 89.897, 141.582, 111.261, 122.274, 106.232, 139.405, 132.225, 143.86, null, 235.044, 63.914, 123.844, null, 242.102, 130.546, 59.571, null]), - DATA('Netzeinspeisung: 12.738 kWh', [603, 590, 551, 572, 69, 236, 626, null, 1003, 261, 518, 698, 640, 388, 471, 373, 373, 677, 286, 406, 249, null, 446, 369, 558, null, 776, 425, 574, null]), - DATA('Netzbezug: 773 kWh', [16, 6, 3, 3, 5, 48, 4, null, 5, 26, 17, 62, 8, 66, 13, 21, 4, 3, 18, 27, 29, null, 118, 85, 2, null, 72, 28, 84, null]), + DATA('Einspeisung: 12.738 kWh', [603, 590, 551, 572, 69, 236, 626, null, 1003, 261, 518, 698, 640, 388, 471, 373, 373, 677, 286, 406, 249, null, 446, 369, 558, null, 776, 425, 574, null]), + DATA('Bezug: 773 kWh', [16, 6, 3, 3, 5, 48, 4, null, 5, 26, 17, 62, 8, 66, 13, 21, 4, 3, 18, 27, 29, null, 118, 85, 2, null, 72, 28, 84, null]), DATA('Verbrauch: 9.976,1 kWh', [320.342, 346.615, 341.433, 333.054, 358.458, 347.872, 289.283, null, 556.51, 311.366, 314.722, 355.556, 381.671, 384.558, 366.19, 349.336, 303.696, 288.727, 357.434, 388.659, 402.625, null, 713.771, 320.238, 332.099, null, 756.429, 384.136, 371.322, null]) ], labels: LABELS(History.MONTH.energyPerPeriodChannelWithValues.result.timestamps), @@ -96,8 +94,8 @@ describe('History EnergyMonitor', () => { DATA('Direktverbrauch: 22.466,2 kWh', [1597.394, 2056.891, 3150.228, 3720.697, 5506.053, 5808.6720000000005, 546.405, null, null, null, null, null]), DATA('Beladung: 15.296,8 kWh', [294.606, 1673.109, 3337.772, 3074.303, 2495.947, 3944.328, 372.595, null, null, null, null, null]), DATA('Entladung: 12.898,2 kWh', [208.491, 1339.036, 2911.126, 2555.138, 2123.751, 3394.43, 335.402, null, null, null, null, null]), - DATA('Netzeinspeisung: 30.703 kWh', [20, 86, 677, 3657, 12839, 12738, 627, null, null, null, null, null]), - DATA('Netzbezug: 23.209 kWh', [9829, 4812, 2915, 2036, 2712, 773, 94, null, null, null, null, null]), + DATA('Einspeisung: 30.703 kWh', [20, 86, 677, 3657, 12839, 12738, 627, null, null, null, null, null]), + DATA('Bezug: 23.209 kWh', [9829, 4812, 2915, 2036, 2712, 773, 94, null, null, null, null, null]), DATA('Verbrauch: 58.573,4 kWh', [11634.885, 8207.927, 8976.354, 8311.835, 10341.804, 9976.102, 975.807, null, null, null, null, null]) ], labels: LABELS(History.YEAR.energyPerPeriodChannelWithValues.result.timestamps), @@ -131,8 +129,8 @@ describe('History EnergyMonitor', () => { data: [ DATA('Beladung: 15.296,8 kWh', [294.606, 1673.109, 3337.772, 3074.303, 2495.947, 3944.328, 372.595, null, null, null, null, null]), DATA('Entladung: 12.898,2 kWh', [208.491, 1339.036, 2911.126, 2555.138, 2123.751, 3394.43, 335.402, null, null, null, null, null]), - DATA('Netzeinspeisung: 30.703 kWh', [20, 86, 677, 3657, 12839, 12738, 627, null, null, null, null, null]), - DATA('Netzbezug: 23.209 kWh', [9829, 4812, 2915, 2036, 2712, 773, 94, null, null, null, null, null]), + DATA('Einspeisung: 30.703 kWh', [20, 86, 677, 3657, 12839, 12738, 627, null, null, null, null, null]), + DATA('Bezug: 23.209 kWh', [9829, 4812, 2915, 2036, 2712, 773, 94, null, null, null, null, null]), DATA('Verbrauch: 58.573,4 kWh', [11634.885, 8207.927, 8976.354, 8311.835, 10341.804, 9976.102, 975.807, null, null, null, null, null]) ], labels: LABELS(History.YEAR.energyPerPeriodChannelWithValues.result.timestamps), @@ -151,8 +149,8 @@ describe('History EnergyMonitor', () => { { datasets: { data: [ - DATA('Netzeinspeisung: 30.703 kWh', [20, 86, 677, 3657, 12839, 12738, 627, null, null, null, null, null]), - DATA('Netzbezug: 23.209 kWh', [9829, 4812, 2915, 2036, 2712, 773, 94, null, null, null, null, null]), + DATA('Einspeisung: 30.703 kWh', [20, 86, 677, 3657, 12839, 12738, 627, null, null, null, null, null]), + DATA('Bezug: 23.209 kWh', [9829, 4812, 2915, 2036, 2712, 773, 94, null, null, null, null, null]), DATA('Verbrauch: 58.573,4 kWh', [11634.885, 8207.927, 8976.354, 8311.835, 10341.804, 9976.102, 975.807, null, null, null, null, null]) ], labels: LABELS(History.YEAR.energyPerPeriodChannelWithValues.result.timestamps), diff --git a/ui/src/app/edge/history/common/energy/chart/chart.ts b/ui/src/app/edge/history/common/energy/chart/chart.ts index 590b397328f..cbcace9d046 100644 --- a/ui/src/app/edge/history/common/energy/chart/chart.ts +++ b/ui/src/app/edge/history/common/energy/chart/chart.ts @@ -15,9 +15,9 @@ export class ChartComponent extends AbstractHistoryChart { return ChartComponent.getChartData(this.config, this.chartType, this.translate); } - public static getChartData(config: EdgeConfig, chartType: 'line' | 'bar', translate: TranslateService): HistoryUtils.ChartData { + public static getChartData(config: EdgeConfig | null, chartType: 'line' | 'bar', translate: TranslateService): HistoryUtils.ChartData { let input: HistoryUtils.InputChannel[] = - config.widgets.classes.reduce((arr: HistoryUtils.InputChannel[], key) => { + config?.widgets.classes.reduce((arr: HistoryUtils.InputChannel[], key) => { let newObj = []; switch (key) { @@ -143,7 +143,7 @@ export class ChartComponent extends AbstractHistoryChart { // Sell to grid { - name: translate.instant('General.gridSell'), + name: translate.instant('General.gridSellAdvanced'), nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => { return energyValues.result.data['_sum/GridSellActiveEnergy']; }, @@ -157,7 +157,7 @@ export class ChartComponent extends AbstractHistoryChart { // Buy from Grid { - name: translate.instant('General.gridBuy'), + name: translate.instant('General.gridBuyAdvanced'), nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => { return energyValues.result.data['_sum/GridBuyActiveEnergy']; }, diff --git a/ui/src/app/edge/history/common/grid/chart/chart.constants.spec.ts b/ui/src/app/edge/history/common/grid/chart/chart.constants.spec.ts new file mode 100644 index 00000000000..3a76b796673 --- /dev/null +++ b/ui/src/app/edge/history/common/grid/chart/chart.constants.spec.ts @@ -0,0 +1,13 @@ +import { DummyConfig } from "src/app/shared/edge/edgeconfig.spec"; +import { OeTester } from "src/app/shared/genericComponents/shared/testing/common"; +import { OeChartTester } from "src/app/shared/genericComponents/shared/testing/tester"; +import { EdgeConfig } from "src/app/shared/shared"; +import { TestContext, removeFunctions } from "src/app/shared/test/utils.spec"; +import { ChartComponent } from "./chart"; + +export function expectView(config: EdgeConfig, testContext: TestContext, chartType: 'line' | 'bar', channels: OeTester.Types.Channels, view: OeChartTester.View, showPhases: boolean): void { + expect(removeFunctions(OeChartTester + .apply(ChartComponent + .getChartData(DummyConfig.convertDummyEdgeConfigToRealEdgeConfig(config), chartType, testContext.translate, showPhases), chartType, channels, testContext))) + .toEqual(removeFunctions(view)); +}; \ No newline at end of file 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 new file mode 100644 index 00000000000..471ae1803e5 --- /dev/null +++ b/ui/src/app/edge/history/common/grid/chart/chart.spec.ts @@ -0,0 +1,79 @@ +import { History } from "src/app/edge/history/common/energy/chart/channels.spec"; +import { DummyConfig, SOCOMEC_GRID_METER } from "src/app/shared/edge/edgeconfig.spec"; +import { OeTester } from "src/app/shared/genericComponents/shared/testing/common"; +import { sharedSetup, TestContext } from "src/app/shared/test/utils.spec"; + +import { DATA, LABELS } from "../../energy/chart/chart.constants.spec"; +import { expectView } from "./chart.constants.spec"; + +describe('History Grid', () => { + const defaultEMS = DummyConfig.from( + SOCOMEC_GRID_METER("meter0", "Netzzähler") + ); + + let TEST_CONTEXT: TestContext; + beforeEach(() => + TEST_CONTEXT = sharedSetup() + ); + + it('#getChartData()', () => { + { + // Line - Chart + expectView(defaultEMS, TEST_CONTEXT, 'line', History.DAY, + { + datasets: { + data: [ + DATA('Einspeisung: 15,6 kWh', [null, null, null, 0, 0, 0.006, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.004, 0, 0, 0, 0.004, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.005, 0, 0, 0, 0, 0, 0.001, 0.002, null, null, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.001, 0.004, 0, 0.004, 0, 0, 0, 0, 0.005, 0.013, 0.006, 0.004, 0.017, 0.015, 0.017, 0.011, 0, 0, 0, 0, 0.029, 0.015, 0.013, 0.019, 0.014, 0.007, 0.016, 0, 0.018, 0.022, 0, 0.012, 0.011, 0.007, 0, 0.033, 0.007, 0.003, 0.004, 0.011, 0, 0.038, 0, 0, 0.019, 0, 0.016, 0.014, 0.018, 0, 1.119, 3.453, 3.608, 3.941, 4.392, 3.786, 4.805, 4.688, 3.095, 2.32, 2.851, 3.058, 4.044, 5.011, 2.789, 6.53, 5.029, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]), + DATA('Bezug: 0,9 kWh', [null, null, null, 0.031, 0.018, 0, 0.02, 0.016, 0.015, 0.014, 0.009, 0.02, 0.025, 0.025, 0.025, 0.021, 0.012, 0.009, 0.01, 0.011, 0.005, 0.003, 0, 0.015, 0.018, 0.023, 0, 0, 0, 0.002, 0.002, 0.003, 0.015, 0.008, 0.022, 0.027, 0.016, 0.003, 0.002, 0, 0.028, 0.027, 0.017, 0.001, 0, 0, 0, null, null, null, null, 0.011, 0.01, 0.004, 0.006, 0.007, 0.018, 0.008, 0.012, 0.009, 0.004, 0.013, 0.015, 0.012, 0, 0, 0, 0.002, 0, 0.005, 0.001, 0.03, 0.062, 0, 0, 0, 0, 0, 0, 0, 0, 0.015, 0.005, 0.004, 0.007, 0, 0, 0, 0, 0, 0, 0, 0.005, 0, 0, 0, 0, 0, 0, 0.021, 0, 0, 0, 0, 0, 0.003, 0, 0.004, 0, 0, 0.032, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]) + ], + labels: LABELS(History.DAY.dataChannelWithValues.result.timestamps), + options: OeTester.ChartOptions.LINE_CHART_OPTIONS('hour') + } + }, false); + } + { + // Line - Chart + expectView(defaultEMS, TEST_CONTEXT, 'line', History.WEEK, + { + datasets: { + data: [ + DATA('Einspeisung: 119,7 kWh', [0.0023333333333333335, 0, 0, 0, 0, 0, 0, 0.014166666666666666, 0.02808333333333333, 0.9546666666666667, 4.150583333333333, 6.431333333333333, 5.737583333333333, 5.6714166666666666, 5.873333333333333, 5.049083333333333, 3.122, 1.0374166666666667, 0.22808333333333333, 0.02, 0, 0, 0, 0.008333333333333333, 0.0030833333333333333, 0.008333333333333333, 0, 0.007727272727272728, 0, 0, 0.00275, 0.013833333333333335, 0.017416666666666667, 0.006083333333333333, 0.5646666666666667, 2.2251666666666665, 2.03375, 3.99725, 4.990083333333333, 3.0128333333333335, 2.4844166666666667, 1.378, 0.65975, 0, 0.001, 0.006916666666666667, 0.008166666666666666, 0, 0, 0, 0, 0, 0, 0, 0.004083333333333333, 0.010583333333333333, 0.011166666666666667, 1.261, 5.308833333333333, 6.604, 6.321166666666667, 6.488333333333333, 6.78425, 6.052083333333333, 2.5839166666666666, 0.529, 0.01616666666666667, 0.0055, 0, 0.0006666666666666666, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0024166666666666664, 0.0125, 0.7065, 5.835416666666667, 4.77025, 6.03925, 6.8445833333333335, 5.370333333333333, 4.490166666666667, 2.3506666666666667, 0.7650833333333333, 0.08583333333333333, 0.011454545454545455, 0, 0, 0.005666666666666667, 0, 0, 0, 0, 0, 0, 0, 0.0033333333333333335, 0.004083333333333333, 0.02033333333333333, 0.02316666666666667, 1.4106666666666667, 0.8588333333333333, 0.0015833333333333333, 0.006583333333333333, 0.010083333333333335, 0.3410833333333333, 2.9290833333333337, 1.1175833333333332, 0.48583333333333334, 0, 0, 0, 0.0006666666666666666, 0.017916666666666668, 0.004, 0, 0, 0.001, 0, 0, 0, 0.02358333333333333, 0.006416666666666667, 0.008166666666666666, 0.0031666666666666666, 0.009916666666666666, 2.7254166666666664, 1.83725, 2.63225, 2.2170833333333335, 0.529, 0, 0, 0, 0, 0, 0.0003333333333333333, 0, 0, 0.011416666666666665, 0.011083333333333334, 0, 0, 0, 0, 0.008333333333333333, 0.008818181818181819, 0.015333333333333334, 0.018857142857142857, 0.024833333333333332, 0.010888888888888889, 2.2174, 3.9214166666666666, 1.6248181818181817, 1.937, 1.789, 0.0195, 0.0143, 0, 0, 0.009, 0.018875]), + DATA('Bezug: 2,4 kWh', [0, 0.011916666666666666, 0.01633333333333333, 0.00609090909090909, 0.015333333333333334, 0.011666666666666665, 0.0024166666666666664, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.02425, 0.004416666666666667, 0.0035833333333333333, 0, 0, 0, 0.04441666666666667, 0, 0.013111111111111112, 0.001, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0011666666666666668, 0, 0, 0, 0.0015833333333333333, 0.013333333333333334, 0.020416666666666666, 0.01125, 0.019727272727272725, 0.012444444444444445, 0.009583333333333334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.007666666666666667, 0, 0.0023333333333333335, 0.0125, 0.01609090909090909, 0.02016666666666667, 0.014083333333333333, 0.006363636363636363, 0.01955555555555556, 0.04841666666666666, 0.011166666666666667, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.014222222222222221, 0.00225, 0, 0.0036666666666666666, 0.032916666666666664, 0.014666666666666666, 0.0135, 0.017363636363636362, 0.013333333333333334, 0.022083333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0009166666666666666, 0, 0.0021666666666666666, 0, 0, 0, 0.0005, 0.04841666666666666, 0, 0.005555555555555556, 0.02716666666666667, 0.017333333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0023333333333333335, 0.008333333333333333, 0.003, 0.015916666666666666, 0.00325, 0, 0.004333333333333333, 0.001, 0, 0, 0.019545454545454546, 0.0017777777777777776, 0.006416666666666667, 0.017666666666666667, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0058, 0.005625, 0, 0]) + ], + labels: LABELS(History.WEEK.dataChannelWithValues.result.timestamps), + options: OeTester.ChartOptions.LINE_CHART_OPTIONS('day') + } + + }, false); + } + { + // Line - Chart + expectView(defaultEMS, TEST_CONTEXT, 'bar', History.MONTH, + { + datasets: { + data: [ + DATA('Einspeisung: 12.738 kWh', [603, 590, 551, 572, 69, 236, 626, null, 1003, 261, 518, 698, 640, 388, 471, 373, 373, 677, 286, 406, 249, null, 446, 369, 558, null, 776, 425, 574, null]), + DATA('Bezug: 773 kWh', [16, 6, 3, 3, 5, 48, 4, null, 5, 26, 17, 62, 8, 66, 13, 21, 4, 3, 18, 27, 29, null, 118, 85, 2, null, 72, 28, 84, null]) + ], + labels: LABELS(History.MONTH.energyPerPeriodChannelWithValues.result.timestamps), + options: OeTester.ChartOptions.BAR_CHART_OPTIONS('day') + } + + }, false); + } + { + // Line - Chart + expectView(defaultEMS, TEST_CONTEXT, 'bar', History.YEAR, + { + datasets: { + data: [ + DATA('Einspeisung: 30.703 kWh', [20, 86, 677, 3657, 12839, 12738, 627, null, null, null, null, null]), + DATA('Bezug: 23.209 kWh', [9829, 4812, 2915, 2036, 2712, 773, 94, null, null, null, null, null]) + ], + labels: LABELS(History.YEAR.energyPerPeriodChannelWithValues.result.timestamps), + options: OeTester.ChartOptions.BAR_CHART_OPTIONS('month') + } + }, false); + } + }); +}); \ No newline at end of file diff --git a/ui/src/app/edge/history/common/grid/chart/chart.ts b/ui/src/app/edge/history/common/grid/chart/chart.ts new file mode 100644 index 00000000000..40a24d3f99f --- /dev/null +++ b/ui/src/app/edge/history/common/grid/chart/chart.ts @@ -0,0 +1,106 @@ +import { Component } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AbstractHistoryChart } from 'src/app/shared/genericComponents/chart/abstracthistorychart'; +import { QueryHistoricTimeseriesEnergyResponse } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse'; +import { DefaultTypes } from 'src/app/shared/service/defaulttypes'; +import { ChartAxis, HistoryUtils, YAxisTitle } from 'src/app/shared/service/utils'; +import { ChannelAddress, EdgeConfig } from 'src/app/shared/shared'; + +@Component({ + selector: 'gridchart', + templateUrl: '../../../../../shared/genericComponents/chart/abstracthistorychart.html' +}) +export class ChartComponent extends AbstractHistoryChart { + + public override getChartData() { + return ChartComponent.getChartData(this.config, this.chartType, this.translate, this.showPhases); + } + + public static getChartData(config: EdgeConfig, chartType: 'line' | 'bar', translate: TranslateService, showPhases: boolean): HistoryUtils.ChartData { + + let input: DefaultTypes.History.InputChannel[] = [ + { + name: 'GridSell', + powerChannel: ChannelAddress.fromString('_sum/GridActivePower'), + energyChannel: ChannelAddress.fromString('_sum/GridSellActiveEnergy'), + ...(chartType === 'line' && { converter: HistoryUtils.ValueConverter.ONLY_NEGATIVE_AND_NEGATIVE_AS_POSITIVE }) + }, + { + name: 'GridBuy', + powerChannel: ChannelAddress.fromString('_sum/GridActivePower'), + energyChannel: ChannelAddress.fromString('_sum/GridBuyActiveEnergy'), + converter: HistoryUtils.ValueConverter.NEGATIVE_AS_ZERO + } + ]; + + if (showPhases) { + ['L1', 'L2', 'L3'].forEach(phase => { + input.push({ + name: 'GridActivePower' + phase, + powerChannel: ChannelAddress.fromString('_sum/GridActivePower' + phase) + }); + }); + } + + return { + input: input, + output: (data: DefaultTypes.History.ChannelData) => { + + let datasets: DefaultTypes.History.DisplayValues[] = [ + { + name: translate.instant('General.gridSellAdvanced'), + nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => { + return energyValues?.result.data['_sum/GridSellActiveEnergy'] ?? null; + }, + converter: () => { + return data['GridSell']; + }, + // TODO create Color class + color: 'rgba(0,0,200)', + stack: 1 + }, + + { + name: translate.instant('General.gridBuyAdvanced'), + nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => { + return energyValues?.result.data['_sum/GridBuyActiveEnergy'] ?? null; + }, + converter: () => { + return data['GridBuy']; + }, + color: 'rgb(0,0,0)', + stack: 0 + }]; + + + if (!showPhases) { + return datasets; + } + + ['L1', 'L2', 'L3'].forEach((phase, index) => { + datasets.push({ + name: 'Phase ' + phase, + nameSuffix: (energyValues: QueryHistoricTimeseriesEnergyResponse) => { + return energyValues?.result.data['_sum/GridActivePower' + phase]; + }, + converter: () => { + return data['GridActivePower' + phase] ?? null; + }, + color: AbstractHistoryChart.phaseColors[index], + stack: 3 + }); + }); + + return datasets; + }, + tooltip: { + formatNumber: '1.0-2' + }, + yAxes: [{ + unit: YAxisTitle.ENERGY, + position: 'left', + yAxisId: ChartAxis.LEFT + }] + }; + } +} \ No newline at end of file diff --git a/ui/src/app/edge/history/common/grid/flat/flat.html b/ui/src/app/edge/history/common/grid/flat/flat.html new file mode 100644 index 00000000000..77e9bee56f4 --- /dev/null +++ b/ui/src/app/edge/history/common/grid/flat/flat.html @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/ui/src/app/edge/history/common/grid/flat/flat.ts b/ui/src/app/edge/history/common/grid/flat/flat.ts new file mode 100644 index 00000000000..01cf0531d10 --- /dev/null +++ b/ui/src/app/edge/history/common/grid/flat/flat.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; +import { AbstractFlatWidget } from 'src/app/shared/genericComponents/flat/abstract-flat-widget'; + +@Component({ + selector: 'gridWidget', + templateUrl: './flat.html' +}) +export class FlatComponent extends AbstractFlatWidget { } \ No newline at end of file diff --git a/ui/src/app/edge/history/common/grid/grid.ts b/ui/src/app/edge/history/common/grid/grid.ts new file mode 100644 index 00000000000..2bf22e5619c --- /dev/null +++ b/ui/src/app/edge/history/common/grid/grid.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { SharedModule } from 'src/app/shared/shared.module'; + +import { ChartComponent } from './chart/chart'; +import { FlatComponent } from './flat/flat'; +import { OverviewComponent } from './overview/overview'; + +@NgModule({ + imports: [ + BrowserModule, + SharedModule + ], + entryComponents: [ + FlatComponent + ], + declarations: [ + FlatComponent, + ChartComponent, + OverviewComponent + ], + exports: [ + FlatComponent, + ChartComponent, + OverviewComponent + ] +}) +export class Common_Grid { } diff --git a/ui/src/app/edge/history/common/grid/overview/overview.html b/ui/src/app/edge/history/common/grid/overview/overview.html new file mode 100644 index 00000000000..73ac2d72a13 --- /dev/null +++ b/ui/src/app/edge/history/common/grid/overview/overview.html @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/ui/src/app/edge/history/common/grid/overview/overview.ts b/ui/src/app/edge/history/common/grid/overview/overview.ts new file mode 100644 index 00000000000..33e2bac67d0 --- /dev/null +++ b/ui/src/app/edge/history/common/grid/overview/overview.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +import { AbstractHistoryChartOverview } from 'src/app/shared/genericComponents/chart/abstractHistoryChartOverview'; + +@Component({ + templateUrl: './overview.html' +}) +export class OverviewComponent extends AbstractHistoryChartOverview { } \ No newline at end of file diff --git a/ui/src/app/edge/history/common/production/chart/productionMeterChart.ts b/ui/src/app/edge/history/common/production/chart/productionMeterChart.ts index a51aba4da33..c2ed43ad834 100644 --- a/ui/src/app/edge/history/common/production/chart/productionMeterChart.ts +++ b/ui/src/app/edge/history/common/production/chart/productionMeterChart.ts @@ -52,7 +52,7 @@ export class ProductionMeterChartComponent extends AbstractHistoryChart { converter: () => { return data['ActivePowerL' + i] ?? null; }, - color: this.phaseColors[i - 1] + color: AbstractHistoryChart.phaseColors[i - 1] }); } } diff --git a/ui/src/app/edge/history/common/production/chart/totalAcChart.ts b/ui/src/app/edge/history/common/production/chart/totalAcChart.ts index f1a7b6d3e5d..598a462b6a1 100644 --- a/ui/src/app/edge/history/common/production/chart/totalAcChart.ts +++ b/ui/src/app/edge/history/common/production/chart/totalAcChart.ts @@ -57,7 +57,7 @@ export class TotalAcChartComponent extends AbstractHistoryChart { } return data['ProductionAcActivePowerL' + i] ?? null; }, - color: 'rgb(' + this.phaseColors[i - 1] + ')' + color: 'rgb(' + AbstractHistoryChart.phaseColors[i - 1] + ')' }); } diff --git a/ui/src/app/edge/history/common/production/chart/totalChart.ts b/ui/src/app/edge/history/common/production/chart/totalChart.ts index ab92d3e5dd5..7a1449f5662 100644 --- a/ui/src/app/edge/history/common/production/chart/totalChart.ts +++ b/ui/src/app/edge/history/common/production/chart/totalChart.ts @@ -12,7 +12,7 @@ import { ChannelAddress } from '../../../../../shared/shared'; export class TotalChartComponent extends AbstractHistoryChart { protected override getChartData(): HistoryUtils.ChartData { - let productionMeterComponents = this.config.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter") + let productionMeterComponents = this.config?.getComponentsImplementingNature("io.openems.edge.meter.api.ElectricityMeter") .filter(component => this.config.isProducer(component)); let chargerComponents = this.config.getComponentsImplementingNature("io.openems.edge.ess.dccharger.api.EssDcCharger"); @@ -78,7 +78,6 @@ export class TotalChartComponent extends AbstractHistoryChart { }, color: 'rgb(0,152,204)', hiddenOnInit: true, - noStrokeThroughLegendIfHidden: false, stack: 2 }); @@ -108,7 +107,7 @@ export class TotalChartComponent extends AbstractHistoryChart { } return effectiveProduction; }, - color: 'rgb(' + this.phaseColors[i - 1] + ')', + color: 'rgb(' + AbstractHistoryChart.phaseColors[i - 1] + ')', stack: 3 }); } diff --git a/ui/src/app/edge/history/common/production/Production.ts b/ui/src/app/edge/history/common/production/production.ts similarity index 100% rename from ui/src/app/edge/history/common/production/Production.ts rename to ui/src/app/edge/history/common/production/production.ts diff --git a/ui/src/app/edge/history/grid/gridchartoverview/gridchartoverview.component.html b/ui/src/app/edge/history/grid/gridchartoverview/gridchartoverview.component.html deleted file mode 100644 index 0be05c502c9..00000000000 --- a/ui/src/app/edge/history/grid/gridchartoverview/gridchartoverview.component.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - General.grid - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - Edge.Index.Widgets.twoWayInfoGrid - - - - - -
-
\ No newline at end of file diff --git a/ui/src/app/edge/history/grid/gridchartoverview/gridchartoverview.component.ts b/ui/src/app/edge/history/grid/gridchartoverview/gridchartoverview.component.ts deleted file mode 100644 index 1b35ffeec9b..00000000000 --- a/ui/src/app/edge/history/grid/gridchartoverview/gridchartoverview.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { Edge, Service } from '../../../../shared/shared'; - -@Component({ - selector: GridChartOverviewComponent.SELECTOR, - templateUrl: './gridchartoverview.component.html' -}) -export class GridChartOverviewComponent implements OnInit { - - private static readonly SELECTOR = "grid-chart-overview"; - - public edge: Edge = null; - - public showPhases: boolean = false; - - constructor( - private route: ActivatedRoute, - public service: Service - ) { } - - ngOnInit() { - this.service.setCurrentComponent('', this.route).then(edge => { - this.edge = edge; - }); - } - - onNotifyPhases(showPhases: boolean): void { - this.showPhases = showPhases; - } -} \ No newline at end of file diff --git a/ui/src/app/edge/history/grid/widget.component.html b/ui/src/app/edge/history/grid/widget.component.html deleted file mode 100644 index 44bfb00fa07..00000000000 --- a/ui/src/app/edge/history/grid/widget.component.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - General.grid - - - - - - - - - - - -
General.gridBuyAdvanced - {{ data["_sum/GridBuyActiveEnergy"] | unitvalue:'kWh' }} -
General.gridSellAdvanced - {{ data["_sum/GridSellActiveEnergy"] | unitvalue:'kWh' }} -
- - - - - - -
 
-
-
-
-
\ No newline at end of file diff --git a/ui/src/app/edge/history/grid/widget.component.ts b/ui/src/app/edge/history/grid/widget.component.ts deleted file mode 100644 index b532062a535..00000000000 --- a/ui/src/app/edge/history/grid/widget.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { Cumulated } from 'src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse'; -import { DefaultTypes } from 'src/app/shared/service/defaulttypes'; - -import { ChannelAddress, Edge, EdgeConfig, Service } from '../../../shared/shared'; -import { AbstractHistoryWidget } from '../abstracthistorywidget'; - -@Component({ - selector: GridComponent.SELECTOR, - templateUrl: './widget.component.html' -}) -export class GridComponent extends AbstractHistoryWidget implements OnInit, OnChanges, OnDestroy { - - @Input() public period: DefaultTypes.HistoryPeriod; - - private static readonly SELECTOR = "gridWidget"; - - public data: Cumulated = null; - public edge: Edge = null; - - constructor( - public override service: Service, - private route: ActivatedRoute - ) { - super(service); - } - - ngOnInit() { - this.service.setCurrentComponent('', this.route).then(edge => { - this.edge = edge; - }); - } - - ngOnDestroy() { - this.unsubscribeWidgetRefresh(); - } - - ngOnChanges() { - this.updateValues(); - }; - - protected updateValues() { - this.service.getConfig().then(config => { - this.getChannelAddresses(this.edge, config).then(channels => { - this.service.queryEnergy(this.period.from, this.period.to, channels).then(response => { - this.data = response.result.data; - }).catch(() => { - this.data = null; - }); - }); - }); - } - - protected getChannelAddresses(edge: Edge, config: EdgeConfig): Promise { - return new Promise((resolve) => { - let channels: ChannelAddress[] = [ - new ChannelAddress('_sum', 'GridBuyActiveEnergy'), - new ChannelAddress('_sum', 'GridSellActiveEnergy') - ]; - resolve(channels); - }); - } -} \ No newline at end of file diff --git a/ui/src/app/edge/history/history.component.html b/ui/src/app/edge/history/history.component.html index 064adc2d25a..3ad5487528b 100644 --- a/ui/src/app/edge/history/history.component.html +++ b/ui/src/app/edge/history/history.component.html @@ -18,7 +18,7 @@ - + diff --git a/ui/src/app/edge/history/history.module.ts b/ui/src/app/edge/history/history.module.ts index 0684faa87f0..bc55788f4b8 100644 --- a/ui/src/app/edge/history/history.module.ts +++ b/ui/src/app/edge/history/history.module.ts @@ -7,10 +7,7 @@ import { ChannelthresholdTotalChartComponent } from './channelthreshold/totalcha import { ChannelthresholdWidgetComponent } from './channelthreshold/widget.component'; import { ChpSocChartComponent } from './chpsoc/chart.component'; import { ChpSocWidgetComponent } from './chpsoc/widget.component'; -import { Common_Autarchy } from './common/autarchy/Autarchy'; -import { CommonEnergyMonitor } from './common/energy/energy'; -import { Common_Production } from './common/production/Production'; -import { Common_Selfconsumption } from './common/selfconsumption/SelfConsumption'; +import { Common } from './common/common'; import { ConsumptionChartOverviewComponent } from './consumption/consumptionchartoverview/consumptionchartoverview.component'; import { ConsumptionEvcsChartComponent } from './consumption/evcschart.component'; import { ConsumptionMeterChartComponent } from './consumption/meterchart.component'; @@ -26,8 +23,6 @@ import { FixDigitalOutputSingleChartComponent } from './fixdigitaloutput/singlec import { FixDigitalOutputTotalChartComponent } from './fixdigitaloutput/totalchart.component'; import { FixDigitalOutputWidgetComponent } from './fixdigitaloutput/widget.component'; import { GridChartComponent } from './grid/chart.component'; -import { GridChartOverviewComponent } from './grid/gridchartoverview/gridchartoverview.component'; -import { GridComponent } from './grid/widget.component'; import { GridOptimizedChargeChartComponent } from './gridoptimizedcharge/chart.component'; import { GridOptimizedChargeChartOverviewComponent } from './gridoptimizedcharge/gridoptimizedchargechartoverview/gridoptimizedchargechartoverview.component'; import { SellToGridLimitChartComponent } from './gridoptimizedcharge/sellToGridLimitChart.component'; @@ -66,10 +61,7 @@ import { TimeOfUseTariffDischargeWidgetComponent } from './timeofusetariffdischa @NgModule({ imports: [ SharedModule, - Common_Autarchy, - Common_Production, - Common_Selfconsumption, - CommonEnergyMonitor + Common ], declarations: [ AsymmetricPeakshavingChartComponent, @@ -96,8 +88,6 @@ import { TimeOfUseTariffDischargeWidgetComponent } from './timeofusetariffdischa FixDigitalOutputTotalChartComponent, FixDigitalOutputWidgetComponent, GridChartComponent, - GridChartOverviewComponent, - GridComponent, GridOptimizedChargeChartComponent, GridOptimizedChargeChartOverviewComponent, GridOptimizedChargeWidgetComponent, 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 2a6b7e9aa47..d51b7535334 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 @@ -1,5 +1,5 @@ import { LINE_INFO } from "src/app/shared/edge/edgeconfig.spec"; -import { OeFormlyViewTester } from "src/app/shared/genericComponents/shared/tester"; +import { OeFormlyViewTester } from "src/app/shared/genericComponents/shared/testing/tester"; import { sharedSetup, TestContext } from "src/app/shared/test/utils.spec"; import { ModalComponent } from "./modal"; @@ -13,7 +13,7 @@ export function expectView(testContext: TestContext, viewContext: OeFormlyViewTe expect(generatedView).toEqual(view); }; -describe('Autarkie - Modal', () => { +describe('Autarchy - Modal', () => { let TEST_CONTEXT: TestContext; beforeEach(() => TEST_CONTEXT = sharedSetup()); diff --git a/ui/src/app/edge/live/common/consumption/modal/modal.constants.spec.ts b/ui/src/app/edge/live/common/consumption/modal/modal.constants.spec.ts index 21a84e524e6..6e5c1850f14 100644 --- a/ui/src/app/edge/live/common/consumption/modal/modal.constants.spec.ts +++ b/ui/src/app/edge/live/common/consumption/modal/modal.constants.spec.ts @@ -3,7 +3,7 @@ import { EdgeConfig } from "src/app/shared/shared"; import { TestContext } from "src/app/shared/test/utils.spec"; import { Role } from "src/app/shared/type/role"; -import { OeFormlyViewTester } from "../../../../../shared/genericComponents/shared/tester"; +import { OeFormlyViewTester } from "../../../../../shared/genericComponents/shared/testing/tester"; import { ModalComponent } from "./modal"; export function expectView(config: EdgeConfig, role: Role, viewContext: OeFormlyViewTester.Context, testContext: TestContext, view: OeFormlyViewTester.View): void { 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 4eac102530e..e7037931f24 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 @@ -1,6 +1,6 @@ import { CHANNEL_LINE, DummyConfig, EVCS_KEBA_KECONTACT, LINE_HORIZONTAL, LINE_INFO_PHASES_DE, SOCOMEC_CONSUMPTION_METER, VALUE_FROM_CHANNELS_LINE } from "src/app/shared/edge/edgeconfig.spec"; import { TextIndentation } from "src/app/shared/genericComponents/modal/modal-line/modal-line"; -import { OeFormlyViewTester } from "src/app/shared/genericComponents/shared/tester"; +import { OeFormlyViewTester } from "src/app/shared/genericComponents/shared/testing/tester"; import { sharedSetup } from "src/app/shared/test/utils.spec"; import { Role } from "src/app/shared/type/role"; diff --git a/ui/src/app/edge/live/common/grid/flat/flat.html b/ui/src/app/edge/live/common/grid/flat/flat.html index 26e4c068529..fde286628df 100644 --- a/ui/src/app/edge/live/common/grid/flat/flat.html +++ b/ui/src/app/edge/live/common/grid/flat/flat.html @@ -1,8 +1,8 @@ - - diff --git a/ui/src/app/edge/live/common/grid/modal/constants.spec.ts b/ui/src/app/edge/live/common/grid/modal/constants.spec.ts index 760d1f7e718..da2303f8aea 100644 --- a/ui/src/app/edge/live/common/grid/modal/constants.spec.ts +++ b/ui/src/app/edge/live/common/grid/modal/constants.spec.ts @@ -1,7 +1,7 @@ import { EdgeConfig } from "src/app/shared/shared"; import { TestContext } from "src/app/shared/test/utils.spec"; import { Role } from "src/app/shared/type/role"; -import { OeFormlyViewTester } from "../../../../../shared/genericComponents/shared/tester"; +import { OeFormlyViewTester } from "../../../../../shared/genericComponents/shared/testing/tester"; import { ModalComponent } from "./modal"; export function expectView(config: EdgeConfig, role: Role, viewContext: OeFormlyViewTester.Context, testContext: TestContext, view: OeFormlyViewTester.View): void { 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 788525b5f79..d33ab9afb3f 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 @@ -1,5 +1,5 @@ import { CHANNEL_LINE, DummyConfig, LINE_HORIZONTAL, LINE_INFO_PHASES_DE, PHASE_ADMIN, PHASE_GUEST, SOCOMEC_GRID_METER } from "src/app/shared/edge/edgeconfig.spec"; -import { OeFormlyViewTester } from "src/app/shared/genericComponents/shared/tester"; +import { OeFormlyViewTester } from "src/app/shared/genericComponents/shared/testing/tester"; import { GridMode } from "src/app/shared/shared"; import { sharedSetup, TestContext } from "src/app/shared/test/utils.spec"; import { Role } from "src/app/shared/type/role"; @@ -43,8 +43,8 @@ describe('Grid - Modal', () => { expectView(EMS, Role.ADMIN, VIEW_CONTEXT(), TEST_CONTEXT, { title: "Netz", lines: [ - CHANNEL_LINE("Bezug", "0 W"), CHANNEL_LINE("Einspeisung", "1.000 W"), + CHANNEL_LINE("Bezug", "0 W"), PHASE_ADMIN("Phase L1 Einspeisung", "230 V", "2,2 A", "500 W"), PHASE_ADMIN("Phase L2 Bezug", "-", "-", "1.500 W"), PHASE_ADMIN("Phase L3", "-", "-", "-"), @@ -57,8 +57,8 @@ describe('Grid - Modal', () => { expectView(EMS, Role.OWNER, VIEW_CONTEXT(), TEST_CONTEXT, { title: "Netz", lines: [ - CHANNEL_LINE("Bezug", "0 W"), CHANNEL_LINE("Einspeisung", "1.000 W"), + CHANNEL_LINE("Bezug", "0 W"), PHASE_GUEST("Phase L1 Einspeisung", "500 W"), PHASE_GUEST("Phase L2 Bezug", "1.500 W"), PHASE_GUEST("Phase L3", "-"), @@ -76,8 +76,8 @@ describe('Grid - Modal', () => { name: "Keine Netzverbindung!", value: "" }, - CHANNEL_LINE("Bezug", "0 W"), CHANNEL_LINE("Einspeisung", "1.000 W"), + CHANNEL_LINE("Bezug", "0 W"), PHASE_ADMIN("Phase L1 Einspeisung", "230 V", "2,2 A", "500 W"), PHASE_ADMIN("Phase L2 Bezug", "-", "-", "1.500 W"), PHASE_ADMIN("Phase L3", "-", "-", "-"), @@ -98,8 +98,8 @@ describe('Grid - Modal', () => { expectView(EMS, Role.ADMIN, VIEW_CONTEXT(), TEST_CONTEXT, { title: "Netz", lines: [ - CHANNEL_LINE("Bezug", "0 W"), CHANNEL_LINE("Einspeisung", "1.000 W"), + CHANNEL_LINE("Bezug", "0 W"), LINE_HORIZONTAL, CHANNEL_LINE("meter10", "-"), PHASE_ADMIN("Phase L1", "-", "-", "-"), @@ -119,8 +119,8 @@ describe('Grid - Modal', () => { expectView(EMS, Role.GUEST, VIEW_CONTEXT(), TEST_CONTEXT, { title: "Netz", lines: [ - CHANNEL_LINE("Bezug", "0 W"), CHANNEL_LINE("Einspeisung", "1.000 W"), + CHANNEL_LINE("Bezug", "0 W"), LINE_HORIZONTAL, CHANNEL_LINE("meter10", "-"), PHASE_GUEST("Phase L1", "-"), diff --git a/ui/src/app/edge/live/common/grid/modal/modal.ts b/ui/src/app/edge/live/common/grid/modal/modal.ts index 9f094b4130d..ae08b2488a6 100644 --- a/ui/src/app/edge/live/common/grid/modal/modal.ts +++ b/ui/src/app/edge/live/common/grid/modal/modal.ts @@ -32,17 +32,19 @@ export class ModalComponent extends AbstractFormlyComponent { // Sum Channels (if more than one meter) if (gridMeters.length > 1) { - lines.push({ - type: 'channel-line', - name: translate.instant("General.gridBuyAdvanced"), - channel: '_sum/GridActivePower', - converter: Converter.GRID_BUY_POWER_OR_ZERO - }, { - type: 'channel-line', - name: translate.instant("General.gridSellAdvanced"), - channel: '_sum/GridActivePower', - converter: Converter.GRID_SELL_POWER_OR_ZERO - }, { + lines.push( + { + type: 'channel-line', + name: translate.instant("General.gridSellAdvanced"), + channel: '_sum/GridActivePower', + converter: Converter.GRID_SELL_POWER_OR_ZERO + }, + { + type: 'channel-line', + name: translate.instant("General.gridBuyAdvanced"), + channel: '_sum/GridActivePower', + converter: Converter.GRID_BUY_POWER_OR_ZERO + }, { type: 'horizontal-line' }); } @@ -51,17 +53,19 @@ export class ModalComponent extends AbstractFormlyComponent { for (var meter of gridMeters) { if (gridMeters.length == 1) { // Two lines if there is only one meter (= same visualization as with Sum Channels) - lines.push({ - type: 'channel-line', - name: translate.instant("General.gridBuyAdvanced"), - channel: meter.id + '/ActivePower', - converter: Converter.GRID_BUY_POWER_OR_ZERO - }, { - type: 'channel-line', - name: translate.instant("General.gridSellAdvanced"), - channel: meter.id + '/ActivePower', - converter: Converter.GRID_SELL_POWER_OR_ZERO - }); + lines.push( + { + type: 'channel-line', + name: translate.instant("General.gridSellAdvanced"), + channel: meter.id + '/ActivePower', + converter: Converter.GRID_SELL_POWER_OR_ZERO + }, + { + type: 'channel-line', + name: translate.instant("General.gridBuyAdvanced"), + channel: meter.id + '/ActivePower', + converter: Converter.GRID_BUY_POWER_OR_ZERO + }); } else { // More than one meter? Show only one line per meter. 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 fec3c9a49b3..7a604cc1253 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 @@ -1,5 +1,5 @@ import { LINE_INFO } from "src/app/shared/edge/edgeconfig.spec"; -import { OeFormlyViewTester } from "src/app/shared/genericComponents/shared/tester"; +import { OeFormlyViewTester } from "src/app/shared/genericComponents/shared/testing/tester"; import { sharedSetup, TestContext } from "src/app/shared/test/utils.spec"; import { ModalComponent } from "./modal"; @@ -14,7 +14,7 @@ export function expectView(testContext: TestContext, viewContext: OeFormlyViewTe }; describe('SelfConsumption - Modal', () => { - let TEST_CONTEXT:TestContext; + let TEST_CONTEXT: TestContext; beforeEach(() => TEST_CONTEXT = sharedSetup()); it('generateView()', () => { diff --git a/ui/src/app/shared/edge/edgeconfig.spec.ts b/ui/src/app/shared/edge/edgeconfig.spec.ts index 751c35ebc45..ec8624ed038 100644 --- a/ui/src/app/shared/edge/edgeconfig.spec.ts +++ b/ui/src/app/shared/edge/edgeconfig.spec.ts @@ -1,6 +1,6 @@ import { SumState } from "src/app/index/shared/sumState"; import { TextIndentation } from "../genericComponents/modal/modal-line/modal-line"; -import { OeFormlyViewTester } from "../genericComponents/shared/tester"; +import { OeFormlyViewTester } from "../genericComponents/shared/testing/tester"; import { Role } from "../type/role"; import { Edge } from "./edge"; import { EdgeConfig } from "./edgeconfig"; diff --git a/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts b/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts index 4348f80e3f9..6404bd865d6 100644 --- a/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts +++ b/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts @@ -45,7 +45,7 @@ export abstract class AbstractHistoryChart implements OnInit { protected isDataExisting: boolean = true; protected config: EdgeConfig = null; protected errorResponse: JsonrpcResponseError | null = null; - protected readonly phaseColors: string[] = ['rgb(255,127,80)', 'rgb(0,0,255)', 'rgb(128,128,0)']; + protected static phaseColors: string[] = ['rgb(255,127,80)', 'rgb(0,0,255)', 'rgb(128,128,0)']; private legendOptions: { label: string, strokeThroughHidingStyle: boolean, hideLabelInLegend: boolean }[] = []; private channelData: { data: { [name: string]: number[] } } = { data: {} }; @@ -500,7 +500,6 @@ export abstract class AbstractHistoryChart implements OnInit { public static getOptions(chartObject: HistoryUtils.ChartData, chartType: 'line' | 'bar', service: Service, translate: TranslateService, legendOptions: { label: string, strokeThroughHidingStyle: boolean }[], channelData: { data: { [name: string]: number[] } }): ChartOptions { - let tooltipsLabel: string | null = null; let options = Utils.deepCopy(Utils.deepCopy(DEFAULT_TIME_CHART_OPTIONS_WITHOUT_PREDEFINED_Y_AXIS)); diff --git a/ui/src/app/shared/genericComponents/chart/chart.ts b/ui/src/app/shared/genericComponents/chart/chart.ts index 54a2d202d28..88bc3cb02cd 100644 --- a/ui/src/app/shared/genericComponents/chart/chart.ts +++ b/ui/src/app/shared/genericComponents/chart/chart.ts @@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, O import { ActivatedRoute } from "@angular/router"; import { ModalController, PopoverController } from "@ionic/angular"; import { TranslateService } from "@ngx-translate/core"; + import { ChartOptionsPopoverComponent } from "../../chartoptions/popover/popover.component"; import { DefaultTypes } from "../../service/defaulttypes"; import { Edge, Service } from "../../shared"; @@ -14,8 +15,8 @@ export class ChartComponent implements OnInit, OnChanges { public edge: Edge | null = null; @Input() public title: string = ''; - @Input() public showPhases: boolean; - @Input() public showTotal: boolean; + @Input() public showPhases: boolean | null = null; + @Input() public showTotal: boolean | null = null; @Output() public setShowPhases: EventEmitter = new EventEmitter(); @Output() public setShowTotal: EventEmitter = new EventEmitter(); @Input() public isPopoverNeeded: boolean = false; @@ -37,9 +38,12 @@ export class ChartComponent implements OnInit, OnChanges { this.service.setCurrentComponent('', this.route).then(edge => { this.edge = edge; }); + } + /** Run change detection explicitly after the change, to avoid expression changed after it was checked*/ ngOnChanges() { + this.ref.detectChanges(); this.checkIfPopoverNeeded(); } diff --git a/ui/src/app/shared/genericComponents/shared/testing/common.ts b/ui/src/app/shared/genericComponents/shared/testing/common.ts new file mode 100644 index 00000000000..5663c7c7a01 --- /dev/null +++ b/ui/src/app/shared/genericComponents/shared/testing/common.ts @@ -0,0 +1,188 @@ +import { TimeUnit } from "chart.js"; +import { OeChartTester } from "src/app/shared/genericComponents/shared/testing/tester"; +import { QueryHistoricTimeseriesDataResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesDataResponse"; +import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse"; +import { QueryHistoricTimeseriesEnergyResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyResponse"; + +export namespace OeTester { + + export namespace Types { + export type Channels = { + + /** Always one value for each channel from a {@link QueryHistoricTimeseriesEnergyResponse} */ + energyChannelWithValues: QueryHistoricTimeseriesEnergyResponse, + + /** data from a {@link QueryHistoricTimeseriesEnergyPerPeriodResponse} */ + energyPerPeriodChannelWithValues?: QueryHistoricTimeseriesEnergyPerPeriodResponse, + /** data from a {@link QueryHistoricTimeseriesDataResponse} */ + dataChannelWithValues?: QueryHistoricTimeseriesDataResponse + } + } + + export namespace ChartOptions { + export const LINE_CHART_OPTIONS = (period: string, title?: string): OeChartTester.Dataset.Option => ({ + type: 'option', + options: { + "maintainAspectRatio": false, + "legend": { + "labels": {}, + "position": "bottom" + }, + "elements": { + "point": { + "radius": 0, + "hitRadius": 0, + "hoverRadius": 0 + }, + "line": { + "borderWidth": 2, + "tension": 0.1 + }, + "rectangle": { + "borderWidth": 2 + } + }, + "hover": { + "mode": "point", + "intersect": true + }, + "scales": { + "yAxes": [ + { + "id": "left", + "position": "left", + "scaleLabel": { + "display": true, + "labelString": title ?? "kW", + "padding": 5, + "fontSize": 11 + }, + "gridLines": { + "display": true + }, + "ticks": { + "beginAtZero": false + } + } + ], + "xAxes": [ + { + "ticks": {}, + "stacked": false, + "type": "time", + "time": { + "minUnit": "hour", + "displayFormats": { + "millisecond": "SSS [ms]", + "second": "HH:mm:ss a", + "minute": "HH:mm", + "hour": "HH:[00]", + "day": "DD", + "week": "ll", + "month": "MM", + "quarter": "[Q]Q - YYYY", + "year": "YYYY" + }, + "unit": period as TimeUnit + }, + "bounds": "ticks" + } + ] + }, + "tooltips": { + "mode": "index", + "intersect": false, + "axis": "x", + "callbacks": {} + }, + "responsive": true + } + }); + export const BAR_CHART_OPTIONS = (period: string, title?: string): OeChartTester.Dataset.Option => ({ + type: 'option', + options: { + "maintainAspectRatio": false, + "legend": { + "labels": {}, + "position": "bottom" + }, + "elements": { + "point": { + "radius": 0, + "hitRadius": 0, + "hoverRadius": 0 + }, + "line": { + "borderWidth": 2, + "tension": 0.1 + }, + "rectangle": { + "borderWidth": 2 + } + }, + "hover": { + "mode": "point", + "intersect": true + }, + "scales": { + "yAxes": [ + { + "id": "left", + "position": "left", + "scaleLabel": { + "display": true, + "labelString": title ?? "kWh", + "padding": 5, + "fontSize": 11 + }, + "gridLines": { + "display": true + }, + "ticks": { + "beginAtZero": false + }, + "stacked": true + } + ], + "xAxes": [ + { + "ticks": { + "maxTicksLimit": 12, + "source": "data" + }, + "stacked": true, + "type": "time", + "time": { + "minUnit": "hour", + "displayFormats": { + "millisecond": "SSS [ms]", + "second": "HH:mm:ss a", + "minute": "HH:mm", + "hour": "HH:[00]", + "day": "DD", + "week": "ll", + "month": "MM", + "quarter": "[Q]Q - YYYY", + "year": "YYYY" + }, + "unit": period as TimeUnit + }, + "offset": true, + "bounds": "ticks" + } + ] + }, + "tooltips": { + "mode": "x", + "intersect": false, + "axis": "x", + "callbacks": {} + }, + "responsive": true + } + }); + } + + + +} \ No newline at end of file diff --git a/ui/src/app/shared/genericComponents/shared/tester.ts b/ui/src/app/shared/genericComponents/shared/testing/tester.ts similarity index 91% rename from ui/src/app/shared/genericComponents/shared/tester.ts rename to ui/src/app/shared/genericComponents/shared/testing/tester.ts index 4d3cadafd80..00ab93df840 100644 --- a/ui/src/app/shared/genericComponents/shared/tester.ts +++ b/ui/src/app/shared/genericComponents/shared/testing/tester.ts @@ -1,16 +1,16 @@ import { ChartDataSets } from "chart.js"; -import { History } from "src/app/edge/history/common/energy/chart/channels.spec"; import { ChartOptions } from "src/app/edge/history/shared"; - -import { QueryHistoricTimeseriesDataResponse } from "../../jsonrpc/response/queryHistoricTimeseriesDataResponse"; -import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from "../../jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse"; -import { HistoryUtils } from "../../service/utils"; -import { CurrentData } from "../../shared"; -import { TestContext } from "../../test/utils.spec"; -import { AbstractHistoryChart } from "../chart/abstracthistorychart"; -import { TextIndentation } from "../modal/modal-line/modal-line"; -import { Converter } from "./converter"; -import { OeFormlyField, OeFormlyView } from "./oe-formly-component"; +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 { CurrentData } from "src/app/shared/shared"; +import { TestContext } from "src/app/shared/test/utils.spec"; + +import { AbstractHistoryChart } from "../../chart/abstracthistorychart"; +import { TextIndentation } from "../../modal/modal-line/modal-line"; +import { Converter } from "../converter"; +import { OeFormlyField, OeFormlyView } from "../oe-formly-component"; +import { OeTester } from "./common"; export class OeFormlyViewTester { @@ -222,7 +222,7 @@ export namespace OeChartTester { export class OeChartTester { - public static apply(chartData: HistoryUtils.ChartData, chartType: 'line' | 'bar', channels: History.OeChannels, testContext: TestContext): OeChartTester.View { + public static apply(chartData: HistoryUtils.ChartData, chartType: 'line' | 'bar', channels: OeTester.Types.Channels, testContext: TestContext): OeChartTester.View { let channelData = OeChartTester.getChannelDataByCharttype(chartType, channels); @@ -291,7 +291,7 @@ export class OeChartTester { * @param channels the channels * @returns dataset options */ - public static convertChartDataToOptions(chartData: HistoryUtils.ChartData, chartType: 'line' | 'bar', testContext: TestContext, channels: History.OeChannels): OeChartTester.Dataset.Option { + public static convertChartDataToOptions(chartData: HistoryUtils.ChartData, chartType: 'line' | 'bar', testContext: TestContext, channels: OeTester.Types.Channels): OeChartTester.Dataset.Option { let channelData: QueryHistoricTimeseriesDataResponse | QueryHistoricTimeseriesEnergyPerPeriodResponse = OeChartTester.getChannelDataByCharttype(chartType, channels); let displayValues = chartData.output(channelData.result.data); @@ -309,7 +309,7 @@ export class OeChartTester { }; } - private static getChannelDataByCharttype(chartType: 'line' | 'bar', channels: History.OeChannels): QueryHistoricTimeseriesEnergyPerPeriodResponse | QueryHistoricTimeseriesDataResponse { + private static getChannelDataByCharttype(chartType: 'line' | 'bar', channels: OeTester.Types.Channels): QueryHistoricTimeseriesEnergyPerPeriodResponse | QueryHistoricTimeseriesDataResponse { switch (chartType) { case 'line': return channels.dataChannelWithValues; diff --git a/ui/src/app/shared/service/defaulttypes.ts b/ui/src/app/shared/service/defaulttypes.ts index 25f29fcb354..20649da8552 100644 --- a/ui/src/app/shared/service/defaulttypes.ts +++ b/ui/src/app/shared/service/defaulttypes.ts @@ -1,5 +1,7 @@ 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'; export module DefaultTypes { @@ -91,6 +93,57 @@ export module DefaultTypes { export enum PeriodString { DAY = 'day', WEEK = 'week', MONTH = 'month', YEAR = 'year', CUSTOM = 'custom' }; + export namespace History { + + export enum YAxisTitle { + PERCENTAGE, + ENERGY + } + export type InputChannel = { + + /** Must be unique, is used as identifier in {@link ChartData.input} */ + name: string, + powerChannel: ChannelAddress, + energyChannel?: ChannelAddress + + /** Choose between predefined converters */ + converter?: (value: number) => number | null, + } + export type DisplayValues = { + name: string, + /** suffix to the name */ + nameSuffix?: (energyValues: QueryHistoricTimeseriesEnergyResponse) => number | string, + /** Convert the values to be displayed in Chart */ + converter: () => number[], + /** If dataset should be hidden on Init */ + hiddenOnInit?: boolean, + /** default: true, stroke through label for hidden dataset */ + noStrokeThroughLegendIfHidden?: boolean, + /** color in rgb-Format */ + color: string, + /** the stack for barChart */ + stack?: number, + } + + export type ChannelData = { + [name: string]: number[] + } + + export type ChartData = { + /** Input Channels that need to be queried from the database */ + input: InputChannel[], + /** Output Channels that will be shown in the chart */ + output: (data: ChannelData) => DisplayValues[], + tooltip: { + /** Format of Number displayed */ + formatNumber: string, + afterTitle?: string + }, + /** Name to be displayed on the left y-axis, also the unit to be displayed in tooltips and legend */ + unit: YAxisTitle, + } + } + export class HistoryPeriod { constructor( diff --git a/ui/src/app/shared/service/utils.ts b/ui/src/app/shared/service/utils.ts index dad82bc7111..38226fac0fd 100644 --- a/ui/src/app/shared/service/utils.ts +++ b/ui/src/app/shared/service/utils.ts @@ -696,5 +696,12 @@ export namespace HistoryUtils { return Math.abs(Math.min(0, value)); } }; + export const ONLY_NEGATIVE_AND_NEGATIVE_AS_POSITIVE = (value: number) => { + if (value < 0) { + return Math.abs(value); + } else { + return 0; + } + }; } -} \ No newline at end of file +} From 1e95d4d9fc48daa63587bbf484871dfda11737b0 Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Wed, 1 Nov 2023 16:42:21 +0100 Subject: [PATCH 25/27] FEMS Backport (#2419) * KACO PV-Inverter: calculate production energy manually (#857) Sometimes data from SunSpec was not reliable (even after fixing SunSpec scalefactors). * GoodWe 20/30 Chargers: avoid multiple HIGH Priority mobus tasks (#859) Set goodwe charger channels by the abstract goodwe component to avoid multiple HIGH Priority mobus tasks for Goodwe20/30 Chargers. TwoStringCharger is no longer a ModbusComponent * UI: Utils, tests and translations * fix subtractSafely of not checking for undefined * explicitely checking for undefined and null * add unittest for utils * UI: Fix chart wrong scaling when firstSetupProtocol is too close to end of month * Backport FEMS Backend - Fix compatibility with and require Odoo 16 (separate Odoo module will be updated independently) - Fix possible NPEs - Fix starting of Websocket servers (avoid event race conditions) - Add COMPONENT_IDs to backend components - Fix Websocket handshake case-insensitive - Improve general performance - AVoid excessive logs - Add GenericSystemLog to Metadata: record system execute and update - Improve InfluxDB Aggregated data handling - Allow multiple InfluxDB servers for different periods (defined by start-/enddate) - InfluxDB: only allow Channel-Addresses in standardized format * Websocket Api Controller: cleanup getEdgeRequest & JUnit tests * Cleanup getEdgeRequest to use the static method to generate the metadata for the local edge * added tests for getEdgeRequest/getEdgesRequest Edge * AppCenter: add check for peakshaving to not be compatible with home - added invert boolean for error messages - add test for peakshaving & CheckHome - update translations * AppCenter: separated AppConfiguration into tasks This change is a prerequisite for automatically adding channels to the persistence predictor for TimeOfUseTariff Apps. - separated AppConfiguration into tasks - added Builder for AppConfiguration - changed AppConfiguration to a record - moved AggregateTasks into separate folder - added simple(& automatical) order for tasks - add tests for all AggregateTasks - moved validation to AggregateTasks to validate their Task configuration - changed AppValidateWorker to a (OSGi-)Component - added OnlyIfThrowing interface * Add ENTSO-E App --------- Co-authored-by: Michael Grill <59126309+michaelgrill@users.noreply.github.com> Co-authored-by: Sebastian Asen <47855186+sebastianasen@users.noreply.github.com> Co-authored-by: Lukas Rieger <73471197+lukasrgr@users.noreply.github.com> Co-authored-by: Stefan Feilmeier <3515268+sfeilmeier@users.noreply.github.com> --- .gitpod.Dockerfile | 2 +- .gitpod.yml | 6 +- .../alerting/handler/OfflineEdgeHandler.java | 3 +- .../Backend2BackendWebsocket.java | 27 +- .../openems/backend/b2bwebsocket/OnOpen.java | 2 +- .../backend/b2bwebsocket/WebsocketServer.java | 2 +- .../openems/backend/common/metadata/Edge.java | 130 +- .../backend/common/metadata/Metadata.java | 38 + .../backend/common/test/DummyEventAdmin.java | 39 + .../backend/common/test/DummyMetadata.java | 29 +- .../backend/common/metadata/EdgeTest.java | 88 + .../edgewebsocket/EdgeWebsocketImpl.java | 9 +- .../backend/edgewebsocket/OnNotification.java | 17 + .../openems/backend/edgewebsocket/OnOpen.java | 39 +- .../edgewebsocket/WebsocketServer.java | 36 +- .../backend/metadata/dummy/MetadataDummy.java | 15 +- .../backend/metadata/file/MetadataFile.java | 15 +- .../openems/backend/metadata/odoo/Field.java | 3 +- .../backend/metadata/odoo/MetadataOdoo.java | 17 +- .../metadata/odoo/postgres/PgEdgeHandler.java | 58 + .../aggregatedinflux/AggregatedInflux.java | 28 +- .../aggregatedinflux/AllowedChannels.java | 46 +- .../backend/timedata/influx/Config.java | 6 + .../influx/FieldTypeConflictHandler.java | 7 - .../backend/timedata/influx/TimeFilter.java | 80 + .../timedata/influx/TimedataInfluxDb.java | 148 +- .../timedata/influx/TimeFilterTest.java | 41 + .../timedata/influx/TimedataInfluxDbTest.java | 34 + .../uiwebsocket/impl/OnNotification.java | 2 +- .../backend/uiwebsocket/impl/OnRequest.java | 96 +- .../uiwebsocket/impl/UiWebsocketImpl.java | 40 +- .../uiwebsocket/impl/WebsocketServer.java | 2 +- .../backend/uiwebsocket/impl/WsData.java | 16 +- .../io/openems/common/OpenemsConstants.java | 15 + .../notification/LogMessageNotification.java | 2 +- .../jsonrpc/response/GetEdgeResponse.java | 2 +- .../jsonrpc/response/GetEdgesResponse.java | 2 +- .../io/openems/common/websocket/OnOpen.java | 35 - .../common/websocket/OnRequestHandler.java | 2 +- .../common/websocket/WebsocketUtils.java | 8 +- .../openems/common/websocket/OnOpenTest.java | 58 - .../home/BatteryFeneconHomeHardwareType.java | 2 +- .../controller/api/websocket/OnRequest.java | 32 +- .../edge/controller/api/websocket/Utils.java | 15 +- .../api/websocket/OnRequestTest.java | 60 + .../edge/app/api/ModbusTcpApiReadOnly.java | 7 +- .../edge/app/api/ModbusTcpApiReadWrite.java | 7 +- .../src/io/openems/edge/app/api/MqttApi.java | 5 +- .../edge/app/api/RestJsonApiReadOnly.java | 7 +- .../edge/app/api/RestJsonApiReadWrite.java | 7 +- .../openems/edge/app/ess/FixActivePower.java | 7 +- .../edge/app/ess/PrepareBatteryExtension.java | 6 +- .../openems/edge/app/evcs/AlpitronicEvcs.java | 25 +- .../io/openems/edge/app/evcs/DezonyEvcs.java | 14 +- .../io/openems/edge/app/evcs/EvcsCluster.java | 5 +- .../openems/edge/app/evcs/HardyBarthEvcs.java | 21 +- .../openems/edge/app/evcs/IesKeywattEvcs.java | 14 +- .../io/openems/edge/app/evcs/KebaEvcs.java | 22 +- .../edge/app/evcs/WebastoNextEvcs.java | 14 +- .../edge/app/evcs/WebastoUniteEvcs.java | 14 +- .../edge/app/hardware/KMtronic8Channel.java | 17 +- .../edge/app/heat/CombinedHeatAndPower.java | 10 +- .../io/openems/edge/app/heat/HeatPump.java | 10 +- .../openems/edge/app/heat/HeatingElement.java | 10 +- .../app/integratedsystem/FeneconHome.java | 7 +- .../app/integratedsystem/FeneconHome20.java | 7 +- .../app/integratedsystem/FeneconHome30.java | 7 +- .../app/loadcontrol/ManualRelayControl.java | 5 +- .../app/loadcontrol/ThresholdControl.java | 5 +- .../edge/app/meter/CarloGavazziMeter.java | 8 +- .../openems/edge/app/meter/JanitzaMeter.java | 5 +- .../io/openems/edge/app/meter/KdkMeter.java | 5 +- .../edge/app/meter/MicrocareSdm630Meter.java | 5 +- .../openems/edge/app/meter/SocomecMeter.java | 5 +- .../edge/app/peakshaving/PeakShaving.java | 13 +- .../peakshaving/PhaseAccuratePeakShaving.java | 13 +- .../app/pvinverter/FroniusPvInverter.java | 5 +- .../edge/app/pvinverter/KacoPvInverter.java | 5 +- .../edge/app/pvinverter/KostalPvInverter.java | 5 +- .../edge/app/pvinverter/SmaPvInverter.java | 6 +- .../app/pvinverter/SolarEdgePvInverter.java | 5 +- .../GridOptimizedCharge.java | 6 +- .../SelfConsumptionOptimization.java | 5 +- .../app/timeofusetariff/AwattarHourly.java | 21 +- .../edge/app/timeofusetariff/EntsoE.java | 196 ++ .../timeofusetariff/StromdaoCorrently.java | 22 +- .../edge/app/timeofusetariff/Tibber.java | 23 +- .../core/appmanager/AbstractOpenemsApp.java | 303 --- .../AbstractOpenemsAppWithProps.java | 15 +- .../core/appmanager/AppConfiguration.java | 192 +- .../edge/core/appmanager/AppManagerImpl.java | 24 +- .../core/appmanager/AppManagerUtilImpl.java | 12 +- .../core/appmanager/AppValidateWorker.java | 142 +- .../core/appmanager/ComponentUtilImpl.java | 1 + .../edge/core/appmanager/OpenemsApp.java | 7 - .../core/appmanager/ResolveDependencies.java | 4 +- .../edge/core/appmanager/ThrowingOnlyIf.java | 24 + .../appmanager/dependency/AggregateTask.java | 70 - .../dependency/AppConfigValidator.java | 211 ++ .../dependency/AppManagerAppHelperImpl.java | 189 +- .../appmanager/dependency/DependencyUtil.java | 2 +- .../SchedulerAggregateTaskImpl.java | 75 - .../dependency/StaticIpAggregateTaskImpl.java | 81 - .../edge/core/appmanager/dependency/Task.java | 7 + .../core/appmanager/dependency/Tasks.java | 85 + .../aggregatetask/AggregateTask.java | 77 + .../aggregatetask/ComponentAggregateTask.java | 23 + .../ComponentAggregateTaskImpl.java | 81 +- .../aggregatetask/ComponentConfiguration.java | 14 + .../aggregatetask/SchedulerAggregateTask.java | 5 + .../SchedulerAggregateTaskImpl.java | 150 ++ .../aggregatetask/SchedulerConfiguration.java | 12 + .../aggregatetask/StaticIpAggregateTask.java | 5 + .../StaticIpAggregateTaskImpl.java | 146 ++ .../aggregatetask/StaticIpConfiguration.java | 14 + .../validator/CheckAppsNotInstalled.java | 5 + .../validator/CheckCardinality.java | 5 + .../core/appmanager/validator/CheckHome.java | 23 +- .../core/appmanager/validator/CheckHost.java | 5 + .../CheckNoComponentInstalledOfFactoryId.java | 5 + .../appmanager/validator/CheckRelayCount.java | 5 + .../core/appmanager/validator/Checkable.java | 10 +- .../appmanager/validator/ValidatorConfig.java | 14 + .../appmanager/validator/ValidatorImpl.java | 17 +- .../validator/translation_de.properties | 3 +- .../validator/translation_en.properties | 3 +- .../openems/edge/app/TestADependencyToC.java | 4 +- .../openems/edge/app/TestBDependencyToC.java | 4 +- .../test/io/openems/edge/app/TestC.java | 2 +- .../io/openems/edge/app/TestMultipleIds.java | 5 +- .../integratedsystem/TestFeneconHome20.java | 1 + .../integratedsystem/TestFeneconHome30.java | 1 + .../AppManagerAppHelperImplTest.java | 33 + .../AppManagerImpSynchronizationTest.java | 20 +- .../core/appmanager/AppManagerImplTest.java | 6 +- .../core/appmanager/AppManagerTestBundle.java | 87 +- .../io/openems/edge/core/appmanager/Apps.java | 24 + .../appmanager/DummyAppManagerAppHelper.java | 64 +- .../edge/core/appmanager/DummyValidator.java | 9 +- .../core/appmanager/TestTranslations.java | 8 + .../ComponentAggregateTaskImplTest.java | 261 +++ .../SchedulerAggregateTaskImplTest.java | 145 ++ .../StaticIpAggregateTaskImplTest.java | 41 + .../appmanager/validator/CheckHomeTest.java | 91 + .../GoodWeBatteryInverterImpl.java | 18 +- .../edge/goodwe/charger/twostring/Config.java | 11 - .../twostring/GoodWeChargerTwoString.java | 65 +- .../twostring/GoodWeChargerTwoStringImpl.java | 165 +- .../edge/goodwe/charger/twostring/PvPort.java | 41 +- .../edge/goodwe/common/AbstractGoodWe.java | 143 +- .../io/openems/edge/goodwe/common/GoodWe.java | 49 + .../GoodWeBatteryInverterImplTest.java | 327 ++- .../GoodWeChargerTwoStringImplTest.java | 4 - .../goodwe/charger/twostring/MyConfig.java | 27 - .../charger/twostring/RuleOfThreeTest.java | 10 +- .../bnd.bnd | 3 +- .../PvInverterKacoBlueplanetImpl.java | 28 +- .../sunspec/AbstractSunSpecPvInverter.java | 19 +- .../openems/shared/influxdb/DbDataUtils.java | 4 + .../shared/influxdb/proxy/InfluxQlProxy.java | 43 +- ui/package-lock.json | 1877 ++++++++++------- .../history/common/energy/chart/chart.spec.ts | 1 + .../systemupdate/executeSystemUpdate.ts | 3 +- .../index/overview/overview.component.html | 5 +- .../chart/abstracthistorychart.ts | 10 +- ui/src/app/shared/service/utils.spec.ts | 13 + ui/src/app/shared/service/utils.ts | 18 +- ui/src/assets/i18n/de.json | 2 +- ui/src/assets/i18n/en.json | 2 +- ui/src/global.scss | 8 + 170 files changed, 5377 insertions(+), 2351 deletions(-) create mode 100644 io.openems.backend.common/src/io/openems/backend/common/test/DummyEventAdmin.java create mode 100644 io.openems.backend.common/test/io/openems/backend/common/metadata/EdgeTest.java create mode 100644 io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/TimeFilter.java create mode 100644 io.openems.backend.timedata.influx/test/io/openems/backend/timedata/influx/TimeFilterTest.java create mode 100644 io.openems.backend.timedata.influx/test/io/openems/backend/timedata/influx/TimedataInfluxDbTest.java rename {io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket => io.openems.common/src/io/openems/common}/jsonrpc/notification/LogMessageNotification.java (96%) delete mode 100644 io.openems.common/test/io/openems/common/websocket/OnOpenTest.java create mode 100644 io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/OnRequestTest.java create mode 100644 io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/ThrowingOnlyIf.java delete mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AggregateTask.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppConfigValidator.java delete mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/SchedulerAggregateTaskImpl.java delete mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/StaticIpAggregateTaskImpl.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Task.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Tasks.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/AggregateTask.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTask.java rename io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/{ => aggregatetask}/ComponentAggregateTaskImpl.java (75%) create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentConfiguration.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTask.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImpl.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerConfiguration.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTask.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImpl.java create mode 100644 io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpConfiguration.java create mode 100644 io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImplTest.java create mode 100644 io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImplTest.java create mode 100644 io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImplTest.java create mode 100644 io.openems.edge.core/test/io/openems/edge/core/appmanager/validator/CheckHomeTest.java create mode 100644 ui/src/app/shared/service/utils.spec.ts diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index 98c86e541bb..e637e7fecdb 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -12,7 +12,7 @@ ENV TRIGGER_REBUILD 4 RUN npm install -g @angular/cli # Install odoo -ENV ODOO_VERSION 15.0 +ENV ODOO_VERSION 16.0 ENV ODOO_RELEASE latest RUN curl -o odoo.deb -sSL http://nightly.odoo.com/${ODOO_VERSION}/nightly/deb/odoo_${ODOO_VERSION}.${ODOO_RELEASE}_all.deb \ && sudo apt-get update \ diff --git a/.gitpod.yml b/.gitpod.yml index cf8fa573f26..54481a73d07 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -26,9 +26,9 @@ tasks: cd /workspace/odoo mkdir -p addons-available addons-enabled cd addons-available - git clone --depth=1 -b 15.0 https://github.com/OCA/partner-contact - git clone --depth=1 -b 15.0 https://github.com/OCA/web.git - git clone --depth=1 -b 15.0 https://github.com/OpenEMS/odoo-openems.git + git clone --depth=1 -b 16.0 https://github.com/OCA/partner-contact + git clone --depth=1 -b 16.0 https://github.com/OCA/web.git + git clone --depth=1 -b 16.0 https://github.com/OpenEMS/odoo-openems.git cd ../addons-enabled ln -s ../addons-available/partner-contact/partner_firstname ln -s ../addons-available/web/web_m2x_options diff --git a/io.openems.backend.alerting/src/io/openems/backend/alerting/handler/OfflineEdgeHandler.java b/io.openems.backend.alerting/src/io/openems/backend/alerting/handler/OfflineEdgeHandler.java index e7aa1a77304..277398a52aa 100644 --- a/io.openems.backend.alerting/src/io/openems/backend/alerting/handler/OfflineEdgeHandler.java +++ b/io.openems.backend.alerting/src/io/openems/backend/alerting/handler/OfflineEdgeHandler.java @@ -194,7 +194,8 @@ protected void tryRemoveEdge(Edge edge) { protected void tryAddEdge(Edge edge) { if (this.isValidEdge(edge)) { var msg = this.getEdgeMessage(edge); - if (msg != null) { + var msgScheduler = this.msgScheduler; + if (msg != null && msgScheduler != null) { this.msgScheduler.schedule(msg); } } diff --git a/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/Backend2BackendWebsocket.java b/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/Backend2BackendWebsocket.java index 7c637a952da..02e8f452e24 100644 --- a/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/Backend2BackendWebsocket.java +++ b/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/Backend2BackendWebsocket.java @@ -24,7 +24,6 @@ import io.openems.backend.common.metadata.Metadata; import io.openems.backend.common.timedata.TimedataManager; import io.openems.common.utils.ThreadPoolUtils; -import io.openems.common.websocket.AbstractWebsocketServer.DebugMode; @Designate(ocd = Config.class, factory = true) @Component(// @@ -37,6 +36,8 @@ }) public class Backend2BackendWebsocket extends AbstractOpenemsBackendComponent implements EventHandler { + private static final String COMPONENT_ID = "b2bwebsocket0"; + public static final int DEFAULT_PORT = 8076; protected final ScheduledExecutorService executor = Executors.newScheduledThreadPool(10, @@ -64,6 +65,10 @@ public Backend2BackendWebsocket() { @Activate private void activate(Config config) { this.config = config; + + if (this.metadata.isInitialized()) { + this.startServer(); + } } @Deactivate @@ -74,14 +79,13 @@ private void deactivate() { /** * Create and start new server. - * - * @param port the port - * @param poolSize number of threads dedicated to handle the tasks - * @param debugMode activate a regular debug log about the state of the tasks */ - private synchronized void startServer(int port, int poolSize, DebugMode debugMode) { - this.server = new WebsocketServer(this, this.getName(), port, poolSize, debugMode); - this.server.start(); + private synchronized void startServer() { + if (this.server == null) { + this.server = new WebsocketServer(this, this.getName(), this.config.port(), this.config.poolSize(), + this.config.debugMode()); + this.server.start(); + } } /** @@ -112,8 +116,13 @@ protected void logError(Logger log, String message) { public void handleEvent(Event event) { switch (event.getTopic()) { case Metadata.Events.AFTER_IS_INITIALIZED: - this.startServer(this.config.port(), this.config.poolSize(), this.config.debugMode()); + this.startServer(); break; } } + + public String getId() { + return COMPONENT_ID; + } + } diff --git a/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/OnOpen.java b/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/OnOpen.java index 651f84f8d51..0eaa5d3ccc0 100644 --- a/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/OnOpen.java +++ b/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/OnOpen.java @@ -27,7 +27,7 @@ public void run(WebSocket ws, JsonObject handshake) throws OpenemsNamedException try { // Read "Authorization" header for Simple HTTP authentication. Source: // https://stackoverflow.com/questions/16000517/how-to-get-password-from-http-basic-authentication - final var authorization = JsonUtils.getAsString(handshake, "Authorization"); + final var authorization = JsonUtils.getAsString(handshake, "authorization"); if (authorization == null || !authorization.toLowerCase().startsWith("basic")) { throw OpenemsError.COMMON_AUTHENTICATION_FAILED.exception(); } diff --git a/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/WebsocketServer.java b/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/WebsocketServer.java index 690398fde27..5b2f849f2b0 100644 --- a/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/WebsocketServer.java +++ b/io.openems.backend.b2bwebsocket/src/io/openems/backend/b2bwebsocket/WebsocketServer.java @@ -27,7 +27,7 @@ public WebsocketServer(Backend2BackendWebsocket parent, String name, int port, i var data = TreeBasedTable.create(); var now = Instant.now().toEpochMilli(); ThreadPoolUtils.debugMetrics(executor).forEach((key, value) -> { - data.put(now, "b2bwebsocket/" + key, new JsonPrimitive(value)); + data.put(now, parent.getId() + "/" + key, new JsonPrimitive(value)); }); parent.timedataManager.write("backend0", new TimestampedDataNotification(data)); }); diff --git a/io.openems.backend.common/src/io/openems/backend/common/metadata/Edge.java b/io.openems.backend.common/src/io/openems/backend/common/metadata/Edge.java index db0f9beadfc..1108c6108f7 100644 --- a/io.openems.backend.common/src/io/openems/backend/common/metadata/Edge.java +++ b/io.openems.backend.common/src/io/openems/backend/common/metadata/Edge.java @@ -1,9 +1,10 @@ package io.openems.backend.common.metadata; -import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,9 +24,9 @@ public class Edge { private final String id; private String comment; - private SemanticVersion version; - private String producttype; - private ZonedDateTime lastmessage; + private final AtomicReference version = new AtomicReference<>(SemanticVersion.ZERO); + private final AtomicReference producttype = new AtomicReference<>(""); + private final AtomicReference lastmessage = new AtomicReference<>(null); private boolean isOnline = false; private final List user; @@ -34,12 +35,14 @@ public Edge(Metadata parent, String id, String comment, String version, String p ZonedDateTime lastmessage) { this.id = id; this.comment = comment; - this.version = SemanticVersion.fromStringOrZero(version); - this.producttype = producttype; - this.lastmessage = lastmessage; this.parent = parent; this.user = new ArrayList<>(); + + // Avoid initial events + this.setProducttype(producttype, false); + this.setVersion(SemanticVersion.fromStringOrZero(version), false); + this.setLastmessage(lastmessage, false); } public String getId() { @@ -66,10 +69,10 @@ public JsonObject toJsonObject() { return JsonUtils.buildJsonObject() // .addProperty("id", this.id) // .addProperty("comment", this.comment) // - .addProperty("version", this.version.toString()) // - .addProperty("producttype", this.producttype) // + .addProperty("version", this.version.get().toString()) // + .addProperty("producttype", this.producttype.get()) // .addProperty("online", this.isOnline) // - .addPropertyIfNotNull("lastmessage", this.lastmessage) // + .addPropertyIfNotNull("lastmessage", this.lastmessage.get()) // .build(); } @@ -110,36 +113,34 @@ public synchronized void setOnline(boolean isOnline) { } /** - * Sets the Last-Message-Timestamp to now() and calls the - * setLastmessage-Listeners. + * Sets the Last-Message-Timestamp to now() (truncated to Minutes) and emits a + * ON_SET_LASTMESSAGE event; but only max one event per Minute. */ - public synchronized void setLastmessage() { - this.setLastmessage(ZonedDateTime.now(ZoneOffset.UTC)); + public void setLastmessage() { + this.setLastmessage(ZonedDateTime.now()); } /** - * Sets the Last-Message-Timestamp and calls the setLastmessage-Listeners. + * Sets the Last-Message-Timestamp (truncated to Minutes) and emits a + * ON_SET_LASTMESSAGE event; but only max one event per Minute. * * @param timestamp the Last-Message-Timestamp */ - public synchronized void setLastmessage(ZonedDateTime timestamp) { + public void setLastmessage(ZonedDateTime timestamp) { this.setLastmessage(timestamp, true); } - /** - * Sets the Last-Message-Timestamp. - * - * @param timestamp the Last-Message-Timestamp - * @param callListeners whether to call the setLastmessage-Listeners - */ - public synchronized void setLastmessage(ZonedDateTime timestamp, boolean callListeners) { - if (callListeners) { + private void setLastmessage(ZonedDateTime timestamp, boolean emitEvent) { + if (timestamp == null) { + return; + } + timestamp = timestamp.truncatedTo(ChronoUnit.MINUTES); + var previousTimestamp = this.lastmessage.getAndSet(timestamp); + if (emitEvent && (previousTimestamp == null || previousTimestamp.isBefore(timestamp))) { EventBuilder.from(this.parent.getEventAdmin(), Events.ON_SET_LASTMESSAGE) // .addArg(Events.OnSetLastmessage.EDGE, this) // - .send(); // + .send(); } - var now = timestamp; - this.lastmessage = now; } /** @@ -148,43 +149,37 @@ public synchronized void setLastmessage(ZonedDateTime timestamp, boolean callLis * @return Last-Message-Timestamp in UTC Timezone */ public ZonedDateTime getLastmessage() { - return this.lastmessage; + return this.lastmessage.get(); } /* * Version */ public SemanticVersion getVersion() { - return this.version; + return this.version.get(); } /** - * Sets the version and calls the SetVersion-Listeners. + * Sets the version and emits a ON_SET_VERSION event. * * @param version the version */ - public synchronized void setVersion(SemanticVersion version) { + public void setVersion(SemanticVersion version) { this.setVersion(version, true); } - /** - * Sets the version. - * - * @param version the version - * @param callListeners whether to call the SetVersion-Listeners - */ - public synchronized void setVersion(SemanticVersion version, boolean callListeners) { - if (!Objects.equal(this.version, version)) { // on change - if (callListeners) { - this.log.info( - "Edge [" + this.getId() + "]: Update version from [" + this.version + "] to [" + version + "]"); - - EventBuilder.from(this.parent.getEventAdmin(), Events.ON_SET_VERSION) // - .addArg(Events.OnSetVersion.EDGE, this) // - .addArg(Events.OnSetVersion.VERSION, version) // - .send(); // - } - this.version = version; + private void setVersion(SemanticVersion version, boolean emitEvent) { + if (version == null) { + version = SemanticVersion.ZERO; + } + var oldVersion = this.version.getAndSet(version); + if (emitEvent && !Objects.equal(oldVersion, version)) { // on change + this.log.info("Edge [" + this.getId() + "]: Update version from [" + oldVersion + "] to [" + version + "]"); + + EventBuilder.from(this.parent.getEventAdmin(), Events.ON_SET_VERSION) // + .addArg(Events.OnSetVersion.EDGE, this) // + .addArg(Events.OnSetVersion.VERSION, version) // + .send(); } } @@ -192,36 +187,31 @@ public synchronized void setVersion(SemanticVersion version, boolean callListene * Producttype */ public String getProducttype() { - return this.producttype; + return this.producttype.get(); } /** - * Sets the Producttype and calls the SetProducttype-Listeners. + * Sets the Producttype and emits a ON_SET_PRODUCTTYPE event. * * @param producttype the Producttype */ - public synchronized void setProducttype(String producttype) { + public void setProducttype(String producttype) { this.setProducttype(producttype, true); } - /** - * Sets the Producttype. - * - * @param producttype the Producttype - * @param callListeners whether to call the SetProducttype-Listeners - */ - public synchronized void setProducttype(String producttype, boolean callListeners) { - if (!Objects.equal(this.producttype, producttype)) { // on change - if (callListeners) { - this.log.info("Edge [" + this.getId() + "]: Update Product-Type to [" + producttype + "]. It was [" - + this.producttype + "]"); - - EventBuilder.from(this.parent.getEventAdmin(), Events.ON_SET_PRODUCTTYPE) // - .addArg(Events.OnSetProducttype.EDGE, this) // - .addArg(Events.OnSetProducttype.PRODUCTTYPE, producttype) // - .send(); // - } - this.producttype = producttype; + private void setProducttype(String producttype, boolean emitEvent) { + if (producttype == null) { + producttype = ""; + } + var oldProducttype = this.producttype.getAndSet(producttype); + if (emitEvent && !Objects.equal(oldProducttype, producttype)) { // on change + this.log.info("Edge [" + this.getId() + "]: Update Product-Type from [" + oldProducttype + "] to [" + + producttype + "]"); + + EventBuilder.from(this.parent.getEventAdmin(), Events.ON_SET_PRODUCTTYPE) // + .addArg(Events.OnSetProducttype.EDGE, this) // + .addArg(Events.OnSetProducttype.PRODUCTTYPE, producttype) // + .send(); } } diff --git a/io.openems.backend.common/src/io/openems/backend/common/metadata/Metadata.java b/io.openems.backend.common/src/io/openems/backend/common/metadata/Metadata.java index 4803f4f87e9..3a9ebbd01f8 100644 --- a/io.openems.backend.common/src/io/openems/backend/common/metadata/Metadata.java +++ b/io.openems.backend.common/src/io/openems/backend/common/metadata/Metadata.java @@ -365,4 +365,42 @@ public List getPageDevice(User user, PaginationOptions paginationO */ public EdgeMetadata getEdgeMetadataForUser(User user, String edgeId) throws OpenemsNamedException; + public interface GenericSystemLog { + + /** + * Gets the edgeId of the target log. + * + * @return the edgeId + */ + public String edgeId(); + + /** + * Gets the user which triggered the log. + * + * @return the user + */ + public User user(); + + /** + * Gets a short string which represents the whole log. + * + * @return the teaser string + */ + public String teaser(); + + /** + * Gets a map of values of the log. + * + * @return the map + */ + public Map getValues(); + } + + /** + * Handles a Systemlog-Message. + * + * @param systemLog the log + */ + public void logGenericSystemLog(GenericSystemLog systemLog); + } diff --git a/io.openems.backend.common/src/io/openems/backend/common/test/DummyEventAdmin.java b/io.openems.backend.common/src/io/openems/backend/common/test/DummyEventAdmin.java new file mode 100644 index 00000000000..2dfd65bf50e --- /dev/null +++ b/io.openems.backend.common/src/io/openems/backend/common/test/DummyEventAdmin.java @@ -0,0 +1,39 @@ +package io.openems.backend.common.test; + +import java.util.function.Consumer; +import java.util.function.Function; + +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; + +public class DummyEventAdmin implements EventAdmin { + + private final Function eventFilter; + private final Consumer onEvent; + + public DummyEventAdmin(Consumer onEvent) { + this.eventFilter = (e) -> true; + this.onEvent = onEvent; + } + + public DummyEventAdmin(Function eventFilter, Consumer onEvent) { + this.eventFilter = eventFilter; + this.onEvent = onEvent; + } + + @Override + public void postEvent(Event event) { + this.handleEvent(event); + } + + @Override + public void sendEvent(Event event) { + this.handleEvent(event); + } + + private void handleEvent(Event event) { + if (this.eventFilter.apply(event)) { + this.onEvent.accept(event); + } + } +} diff --git a/io.openems.backend.common/src/io/openems/backend/common/test/DummyMetadata.java b/io.openems.backend.common/src/io/openems/backend/common/test/DummyMetadata.java index 2ba43c2c546..84c83b0bb93 100644 --- a/io.openems.backend.common/src/io/openems/backend/common/test/DummyMetadata.java +++ b/io.openems.backend.common/src/io/openems/backend/common/test/DummyMetadata.java @@ -4,7 +4,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; import com.google.gson.JsonObject; @@ -22,6 +25,21 @@ import io.openems.common.session.Language; public class DummyMetadata implements Metadata { + + private final DummyEventAdmin eventAdmin; + + public DummyMetadata() { + this.eventAdmin = null; + } + + public DummyMetadata(Consumer event) { + this.eventAdmin = new DummyEventAdmin(event); + } + + public DummyMetadata(Function eventFilter, Consumer event) { + this.eventAdmin = new DummyEventAdmin(eventFilter, event); + } + @Override public boolean isInitialized() { return false; @@ -124,7 +142,11 @@ public void updateUserLanguage(User user, Language language) throws OpenemsNamed @Override public EventAdmin getEventAdmin() { - throw new UnsupportedOperationException("Unsupported by Dummy Class"); + if (this.eventAdmin == null) { + throw new UnsupportedOperationException("Unsupported by Dummy Class"); + } else { + return this.eventAdmin; + } } @Override @@ -148,4 +170,9 @@ public EdgeMetadata getEdgeMetadataForUser(User user, String edgeId) throws Open throw new UnsupportedOperationException("Unsupported by Dummy Class"); } + @Override + public void logGenericSystemLog(GenericSystemLog systemLog) { + throw new UnsupportedOperationException("Unsupported by Dummy Class"); + } + } diff --git a/io.openems.backend.common/test/io/openems/backend/common/metadata/EdgeTest.java b/io.openems.backend.common/test/io/openems/backend/common/metadata/EdgeTest.java new file mode 100644 index 00000000000..5ec0a381822 --- /dev/null +++ b/io.openems.backend.common/test/io/openems/backend/common/metadata/EdgeTest.java @@ -0,0 +1,88 @@ +package io.openems.backend.common.metadata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.openems.backend.common.test.DummyMetadata; +import io.openems.common.types.SemanticVersion; + +public class EdgeTest { + + @Test + public void testSetLastmessage() { + AtomicBoolean event = new AtomicBoolean(false); + var metadata = new DummyMetadata(e -> e.getTopic() == Edge.Events.ON_SET_LASTMESSAGE, e -> event.set(true)); + + var time = ZonedDateTime.of(2023, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")); + + var sut = new Edge(metadata, null, null, null, null, time); + + // Initial, no event + assertEquals(ZonedDateTime.of(2023, 1, 2, 3, 4, 0, 0, ZoneId.of("UTC")), sut.getLastmessage()); + + // Lastmessage unchanged; no event + time = ZonedDateTime.of(2023, 1, 2, 3, 4, 8, 9, ZoneId.of("UTC")); + assertFalse(event.get()); + assertEquals(ZonedDateTime.of(2023, 1, 2, 3, 4, 0, 0, ZoneId.of("UTC")), sut.getLastmessage()); + + // Lastmessage earlier; no event + time = ZonedDateTime.of(2023, 1, 1, 1, 1, 1, 1, ZoneId.of("UTC")); + assertFalse(event.get()); + assertEquals(ZonedDateTime.of(2023, 1, 2, 3, 4, 0, 0, ZoneId.of("UTC")), sut.getLastmessage()); + + // Lastmessage changed + event + sut.setLastmessage(ZonedDateTime.of(2023, 1, 2, 3, 5, 8, 9, ZoneId.of("UTC"))); + assertTrue(event.getAndSet(false)); + assertEquals(ZonedDateTime.of(2023, 1, 2, 3, 5, 0, 0, ZoneId.of("UTC")), sut.getLastmessage()); + } + + @Test + public void testSetVersion() { + AtomicBoolean event = new AtomicBoolean(false); + var metadata = new DummyMetadata(e -> e.getTopic() == Edge.Events.ON_SET_VERSION, e -> event.set(true)); + + var sut = new Edge(metadata, null, null, null, null, null); + + // Initial, no event + assertFalse(event.get()); + assertEquals("0.0.0", sut.getVersion().toString()); + + // Unchanged, no event + sut.setVersion(null); + assertFalse(event.get()); + + // Version changed + event + sut.setVersion(new SemanticVersion(1, 2, 3)); + assertTrue(event.getAndSet(false)); + assertEquals("1.2.3", sut.getVersion().toString()); + } + + @Test + public void testSetProducttype() { + AtomicBoolean event = new AtomicBoolean(false); + var metadata = new DummyMetadata(e -> e.getTopic() == Edge.Events.ON_SET_PRODUCTTYPE, e -> event.set(true)); + + var sut = new Edge(metadata, null, null, null, null, null); + + // Initial, no event + assertFalse(event.get()); + assertEquals("", sut.getProducttype().toString()); + + // Unchanged, no event + sut.setProducttype(null); + assertFalse(event.get()); + + // Changed + event + sut.setProducttype("HW01A"); + assertTrue(event.getAndSet(false)); + assertEquals("HW01A", sut.getProducttype().toString()); + } + +} diff --git a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/EdgeWebsocketImpl.java b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/EdgeWebsocketImpl.java index caedf00d8a3..18265a6a36a 100644 --- a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/EdgeWebsocketImpl.java +++ b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/EdgeWebsocketImpl.java @@ -67,7 +67,7 @@ public class EdgeWebsocketImpl extends AbstractOpenemsBackendComponent implements EdgeWebsocket, EventHandler { private static final String EDGE_ID = "backend0"; - private static final String COMPONENT_ID = "edgewebsocket"; + private static final String COMPONENT_ID = "edgewebsocket0"; private final Logger log = LoggerFactory.getLogger(EdgeWebsocketImpl.class); private final SystemLogHandler systemLogHandler; @@ -102,7 +102,7 @@ private void activate(Config config) { this.debugLogExecutor.scheduleWithFixedDelay(() -> { var data = TreeBasedTable.create(); var now = Instant.now().toEpochMilli(); - data.put(now, COMPONENT_ID + "/Connections", + data.put(now, this.getId() + "/Connections", new JsonPrimitive(this.server != null ? this.server.getConnections().size() : 0)); this.timedataManager.write(EDGE_ID, new TimestampedDataNotification(data)); }, 10, 10, TimeUnit.SECONDS); @@ -301,4 +301,9 @@ public SortedMap getChannelValues(String edgeId, } return result; } + + public String getId() { + return COMPONENT_ID; + } + } diff --git a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/OnNotification.java b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/OnNotification.java index b528209268b..7812290fd6b 100644 --- a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/OnNotification.java +++ b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/OnNotification.java @@ -19,6 +19,7 @@ import io.openems.common.jsonrpc.notification.AggregatedDataNotification; import io.openems.common.jsonrpc.notification.EdgeConfigNotification; import io.openems.common.jsonrpc.notification.EdgeRpcNotification; +import io.openems.common.jsonrpc.notification.LogMessageNotification; import io.openems.common.jsonrpc.notification.ResendDataNotification; import io.openems.common.jsonrpc.notification.SystemLogNotification; import io.openems.common.jsonrpc.notification.TimestampedDataNotification; @@ -63,6 +64,8 @@ public void run(WebSocket ws, JsonrpcNotification notification) throws OpenemsNa this.handleResendDataNotification(ResendDataNotification.from(notification), wsData); case SystemLogNotification.METHOD -> this.handleSystemLogNotification(SystemLogNotification.from(notification), wsData); + case LogMessageNotification.METHOD -> + this.handleLogMessageNotification(LogMessageNotification.from(notification), wsData); default -> this.parent.logWarn(this.log, edgeId, "Unhandled Notification: " + notification); } } @@ -168,4 +171,18 @@ private void handleSystemLogNotification(SystemLogNotification message, WsData w var edgeId = wsData.assertEdgeId(message); this.parent.handleSystemLogNotification(edgeId, message); } + + /** + * Handles a {@link LogMessageNotification}. Logs given message from request. + * + * @param notification the {@link LogMessageNotification} + * @param wsData the WebSocket attachment + */ + private void handleLogMessageNotification(LogMessageNotification notification, WsData wsData) + throws OpenemsNamedException { + this.parent.logInfo(this.log, "Edge [" + wsData.getEdgeId().orElse("NOT AUTHENTICATED") + "] " // + + notification.level.getName() + "-Message: " // + + notification.msg); + } + } diff --git a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/OnOpen.java b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/OnOpen.java index c05507d0405..e92aed90a5f 100644 --- a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/OnOpen.java +++ b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/OnOpen.java @@ -26,24 +26,21 @@ public void run(WebSocket ws, JsonObject handshake) { // get apikey from handshake var apikeyOpt = JsonUtils.getAsOptionalString(handshake, "apikey"); if (!apikeyOpt.isPresent()) { - throw new OpenemsException("Apikey is missing in handshake. " // - + "Remote [" + ws.getRemoteSocketAddress() + "]"); + throw new OpenemsException("Apikey is missing in handshake"); } apikey = apikeyOpt.get().trim(); // get edgeId for apikey var edgeIdOpt = this.parent.metadata.getEdgeIdForApikey(apikey); if (!edgeIdOpt.isPresent()) { - throw new OpenemsException("Unable to authenticate this Apikey. " // - + "Remote [" + ws.getRemoteSocketAddress() + "]"); + throw new OpenemsException("Unable to authenticate this Apikey"); } var edgeId = edgeIdOpt.get(); // get metadata for Edge var edgeOpt = this.parent.metadata.getEdge(edgeId); if (!edgeOpt.isPresent()) { - throw new OpenemsException("Unable to get metadata for Edge [" + edgeId + "]. " // - + "Remote [" + ws.getRemoteSocketAddress() + "]"); + throw new OpenemsException("Unable to get metadata for Edge [" + edgeId + "]"); } var edge = edgeOpt.get(); @@ -58,17 +55,43 @@ public void run(WebSocket ws, JsonObject handshake) { // close websocket ws.closeConnection(CloseFrame.REFUSE, "Connection to backend failed. " // + "Apikey [" + apikey + "]. " // - + "Remote [" + ws.getRemoteSocketAddress() + "] " // + + "Remote [" + parseRemoteIdentifier(ws, handshake) + "] " // + "Error: " + e.getMessage()); } else { // close websocket ws.closeConnection(CloseFrame.TRY_AGAIN_LATER, "Connection to backend failed. Metadata is not yet initialized. " // + "Apikey [" + apikey + "]. " // - + "Remote [" + ws.getRemoteSocketAddress() + "] " // + + "Remote [" + parseRemoteIdentifier(ws, handshake) + "] " // + "Error: " + e.getMessage()); } } } + /** + * Parses a identifier for the Remote from the handshake. + * + *

+ * Tries to use the headers "Forwarded", "X-Forwarded-For" or "X-Real-IP". Falls + * back to `ws.getRemoteSocketAddress()`. See https://serverfault.com/a/920060 + * + * @param ws the {@link WebSocket} + * @param handshake the Handshake + * @return an identifier String + */ + private static String parseRemoteIdentifier(WebSocket ws, JsonObject handshake) { + for (var key : REMOTE_IDENTIFICATION_HEADERS) { + var value = JsonUtils.getAsOptionalString(handshake, + key.toLowerCase() /* handshake keys are all lower case */); + if (value.isPresent()) { + return value.get(); + } + } + // fallback + return ws.getRemoteSocketAddress().toString(); + } + + private static final String[] REMOTE_IDENTIFICATION_HEADERS = new String[] { // + "Forwarded", "X-Forwarded-For", "X-Real-IP" }; + } diff --git a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/WebsocketServer.java b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/WebsocketServer.java index ac7397e9367..9d05a9e7fa9 100644 --- a/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/WebsocketServer.java +++ b/io.openems.backend.edgewebsocket/src/io/openems/backend/edgewebsocket/WebsocketServer.java @@ -6,7 +6,12 @@ import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Collectors; import org.java_websocket.WebSocket; import org.slf4j.Logger; @@ -20,6 +25,7 @@ import io.openems.common.jsonrpc.base.JsonrpcMessage; import io.openems.common.jsonrpc.notification.SystemLogNotification; import io.openems.common.jsonrpc.notification.TimestampedDataNotification; +import io.openems.common.types.ChannelAddress; import io.openems.common.types.SystemLog; import io.openems.common.utils.JsonUtils; import io.openems.common.utils.ThreadPoolUtils; @@ -40,7 +46,7 @@ public WebsocketServer(EdgeWebsocketImpl parent, String name, int port, int pool var data = TreeBasedTable.create(); var now = Instant.now().toEpochMilli(); ThreadPoolUtils.debugMetrics(executor).forEach((key, value) -> { - data.put(now, "edgewebsocket/" + key, new JsonPrimitive(value)); + data.put(now, parent.getId() + "/" + key, new JsonPrimitive(value)); }); parent.timedataManager.write("backend0", new TimestampedDataNotification(data)); }); @@ -149,4 +155,32 @@ protected void logWarn(Logger log, String message) { protected void logError(Logger log, String message) { this.parent.logError(log, message); } + + /** + * Gets the current cached date of the given edge and given channels. + * + * @param edgeId the id of the edge + * @param channels the channels + * @return the date + */ + public SortedMap getCurrentDataFromEdgeCache(String edgeId, + Set channels) { + record Pair(A a, B b) { + } + + return this.getConnections().stream() // + .map(WebSocket::getAttachment) // + .filter(Objects::nonNull) // + .map(WsData.class::cast) // + .filter(t -> t.getEdgeId().map(id -> id.equals(edgeId)).orElse(false)) // + .map(w -> w.edgeCache) // + .>mapMulti((cache, consumer) -> { + channels.stream() // + .forEach(t -> { + consumer.accept(new Pair<>(t, cache.getChannelValue(t.toString()))); + }); + }) // + .collect(Collectors.toMap(Pair::a, Pair::b, (t, u) -> u, TreeMap::new)); + } + } diff --git a/io.openems.backend.metadata.dummy/src/io/openems/backend/metadata/dummy/MetadataDummy.java b/io.openems.backend.metadata.dummy/src/io/openems/backend/metadata/dummy/MetadataDummy.java index 5b539965ada..b775cbc93a1 100644 --- a/io.openems.backend.metadata.dummy/src/io/openems/backend/metadata/dummy/MetadataDummy.java +++ b/io.openems.backend.metadata.dummy/src/io/openems/backend/metadata/dummy/MetadataDummy.java @@ -1,5 +1,7 @@ package io.openems.backend.metadata.dummy; +import static java.util.stream.Collectors.joining; + import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -322,7 +324,7 @@ public List getPageDevice(User user, PaginationOptions paginationO Role.ADMIN, // myEdge.isOnline(), // myEdge.getLastmessage(), // - null, // + null, // firstSetupProtocol Level.OK); }).toList(); } @@ -343,9 +345,18 @@ public EdgeMetadata getEdgeMetadataForUser(User user, String edgeId) throws Open Role.ADMIN, // edge.isOnline(), // edge.getLastmessage(), // - null, // + null, // firstSetupProtocol Level.OK // ); } + @Override + public void logGenericSystemLog(GenericSystemLog systemLog) { + this.logInfo(this.log, + "%s on %s executed %s [%s]".formatted(systemLog.user().getId(), systemLog.edgeId(), systemLog.teaser(), + systemLog.getValues().entrySet().stream() // + .map(t -> t.getKey() + "=" + t.getValue()) // + .collect(joining(", ")))); + } + } diff --git a/io.openems.backend.metadata.file/src/io/openems/backend/metadata/file/MetadataFile.java b/io.openems.backend.metadata.file/src/io/openems/backend/metadata/file/MetadataFile.java index 04deb993c6c..968b56f1574 100644 --- a/io.openems.backend.metadata.file/src/io/openems/backend/metadata/file/MetadataFile.java +++ b/io.openems.backend.metadata.file/src/io/openems/backend/metadata/file/MetadataFile.java @@ -1,5 +1,7 @@ package io.openems.backend.metadata.file; +import static java.util.stream.Collectors.joining; + import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; @@ -353,7 +355,7 @@ public List getPageDevice(User user, PaginationOptions paginationO Role.ADMIN, // myEdge.isOnline(), // myEdge.getLastmessage(), // - null, // + null, // firstSetupProtocol Level.OK); }).toList(); } @@ -374,9 +376,18 @@ public EdgeMetadata getEdgeMetadataForUser(User user, String edgeId) throws Open Role.ADMIN, // edge.isOnline(), // edge.getLastmessage(), // - null, // + null, // firstSetupProtocol Level.OK // ); } + @Override + public void logGenericSystemLog(GenericSystemLog systemLog) { + this.logInfo(this.log, + "%s on %s executed %s [%s]".formatted(systemLog.user().getId(), systemLog.edgeId(), systemLog.teaser(), + systemLog.getValues().entrySet().stream() // + .map(t -> t.getKey() + "=" + t.getValue()) // + .collect(joining(", ")))); + } + } diff --git a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/Field.java b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/Field.java index 2f5d04c1872..fd99e5ed873 100644 --- a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/Field.java +++ b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/Field.java @@ -513,7 +513,7 @@ public enum StockProductionLot implements Field { SERIAL_NUMBER("name", true), // PRODUCT("product_id", true); - public static final String ODOO_MODEL = "stock.production.lot"; + public static final String ODOO_MODEL = "stock.lot"; public static final String ODOO_TABLE = StockProductionLot.ODOO_MODEL.replace(".", "_"); private static final class StaticFields { @@ -522,7 +522,6 @@ private static final class StaticFields { private final int queryIndex; private final String id; - /** * Holds information if this Field should be queried from and written to * Database. diff --git a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/MetadataOdoo.java b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/MetadataOdoo.java index edcf70ee164..58b67e5990a 100644 --- a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/MetadataOdoo.java +++ b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/MetadataOdoo.java @@ -355,6 +355,18 @@ public void handleEvent(Event event) { } } + @Override + public void logGenericSystemLog(GenericSystemLog systemLog) { + this.executor.execute(() -> { + try { + final var edge = (MyEdge) this.getEdgeOrError(systemLog.edgeId()); + this.postgresHandler.edge.insertGenericSystemLog(edge.getOdooId(), systemLog); + } catch (SQLException | OpenemsNamedException e) { + this.logWarn(this.log, "Unable to insert "); + } + }); + } + private void onSetConfigEvent(EventReader reader) { this.executor.execute(() -> { var edge = (MyEdge) reader.getProperty(Edge.Events.OnSetConfig.EDGE); @@ -372,7 +384,10 @@ private void onSetConfigEvent(EventReader reader) { var diff = EdgeConfigDiff.diff(newConfig, oldConfig); if (diff.isDifferent()) { // Update "EdgeConfigUpdate" - this.logInfo(this.log, "Edge [" + edge.getId() + "]. Update config: " + diff.toString()); + var diffString = diff.toString(); + if (!diffString.isBlank()) { + this.logInfo(this.log, "Edge [" + edge.getId() + "]. Update config: " + diff.toString()); + } try { this.postgresHandler.edge.insertEdgeConfigUpdate(edge.getOdooId(), diff); diff --git a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/postgres/PgEdgeHandler.java b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/postgres/PgEdgeHandler.java index 35ed744446c..98b4047c35e 100644 --- a/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/postgres/PgEdgeHandler.java +++ b/io.openems.backend.metadata.odoo/src/io/openems/backend/metadata/odoo/postgres/PgEdgeHandler.java @@ -10,6 +10,7 @@ import com.zaxxer.hikari.HikariDataSource; +import io.openems.backend.common.metadata.Metadata.GenericSystemLog; import io.openems.backend.metadata.odoo.Field; import io.openems.backend.metadata.odoo.Field.EdgeConfigUpdate; import io.openems.backend.metadata.odoo.Field.EdgeDevice; @@ -110,6 +111,63 @@ public void insertEdgeConfigUpdate(int odooId, EdgeConfigDiff diff) throws SQLEx } } + /** + * Inserts an {@link GenericSystemLog} for an Edge-ID. + * + * @param odooId the Odoo-ID + * @param systemLog the {@link GenericSystemLog} + * @throws OpenemsNamedException on error + * @throws SQLException on error + */ + public void insertGenericSystemLog(int odooId, GenericSystemLog systemLog) + throws SQLException, OpenemsNamedException { + try (var con = this.dataSource.getConnection(); // + var pst = con.prepareStatement(new StringBuilder() // + .append("INSERT INTO ").append(EdgeConfigUpdate.ODOO_TABLE) // + .append(" (create_date") // + .append(", ").append(EdgeConfigUpdate.DEVICE_ID.id()) // + .append(", ").append(EdgeConfigUpdate.TEASER.id()) // + .append(", ").append(EdgeConfigUpdate.DETAILS.id()) // + .append(") VALUES(?, ?, ?, ?)") // + .toString())) { + pst.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now(ZoneOffset.UTC))); + pst.setInt(2, odooId); + pst.setString(3, systemLog.teaser()); + pst.setString(4, createHtml(systemLog)); + pst.execute(); + } + } + + private static String createHtml(GenericSystemLog systemLog) { + final var header = new StringBuilder(); + final var content = new StringBuilder(); + + header.append(""" + \ + """); + content.append(""); + for (var entry : systemLog.getValues().entrySet()) { + header.append("".formatted(entry.getKey())); + content.append("".formatted(entry.getValue())); + } + header.append(""); + content.append("".formatted(systemLog.user().getId(), systemLog.user().getName())); + + header.append(""" + + + + """); + content.append(""); + + return new StringBuilder() // + .append(header) // + .append(content) // + .append("
%s%sExecuted By%s: %s
") // + .toString(); + } + /** * Updates the ProductType for an Edge-ID. * diff --git a/io.openems.backend.timedata.aggregatedinflux/src/io/openems/backend/timedata/aggregatedinflux/AggregatedInflux.java b/io.openems.backend.timedata.aggregatedinflux/src/io/openems/backend/timedata/aggregatedinflux/AggregatedInflux.java index cb425cc893c..941aa0fc897 100644 --- a/io.openems.backend.timedata.aggregatedinflux/src/io/openems/backend/timedata/aggregatedinflux/AggregatedInflux.java +++ b/io.openems.backend.timedata.aggregatedinflux/src/io/openems/backend/timedata/aggregatedinflux/AggregatedInflux.java @@ -254,13 +254,18 @@ private void writeNotificationData(String edgeId, AbstractDataNotification notif final var timestamp = dataEntry.getKey(); final var timestampSeconds = timestamp / 1_000; - record AddValuesToPoint(String channel, JsonElement value, long timestamp) { + record AddValuesToPoint(String channel, JsonElement value) { } + // update available-since values + channelPerType.getOrDefault(ChannelType.AVG, emptyList()).forEach(entry -> { + this.setMissingAvailableSince(influxEdgeId, entry.getKey(), timestampSeconds); + }); + channelPerType.getOrDefault(ChannelType.MAX, emptyList()).forEach(entry -> { + this.setMissingAvailableSince(influxEdgeId, entry.getKey(), timestampSeconds - 86400 /* 1 day */); + }); + BiConsumer addEntryToPoint = (entry, point) -> { - if (!this.hasAvailableSince(influxEdgeId, entry.channel())) { - this.setAvailableSince(influxEdgeId, entry.channel(), entry.timestamp()); - } AllowedChannels.addWithSpecificChannelType(point, entry.channel(), entry.value()); }; @@ -270,8 +275,8 @@ record AddValuesToPoint(String channel, JsonElement value, long timestamp) { .time(timestampSeconds, WritePrecision.S); channelPerType.getOrDefault(ChannelType.AVG, emptyList()).stream() // - .forEach(entry -> addEntryToPoint - .accept(new AddValuesToPoint(entry.getKey(), entry.getValue(), timestampSeconds), point)); + .forEach(entry -> addEntryToPoint.accept(new AddValuesToPoint(entry.getKey(), entry.getValue()), + point)); this.influxConnector.write(point, this.writeParametersAvgPoints); for (final var measurementEntry : this.getDayChangeMeasurements(timestamp).entrySet()) { @@ -286,13 +291,20 @@ record AddValuesToPoint(String channel, JsonElement value, long timestamp) { .time(truncatedTimestamp, WritePrecision.S); channelPerType.getOrDefault(ChannelType.MAX, emptyList()).stream() // - .forEach(entry -> addEntryToPoint.accept( - new AddValuesToPoint(entry.getKey(), entry.getValue(), truncatedTimestamp), maxPoint)); + .forEach(entry -> addEntryToPoint.accept(new AddValuesToPoint(entry.getKey(), entry.getValue()), + maxPoint)); this.influxConnector.write(maxPoint, this.writeParametersMaxPoints); } } } + private final void setMissingAvailableSince(int influxEdgeId, String channel, long timestmap) { + if (this.hasAvailableSince(influxEdgeId, channel)) { + return; + } + this.setAvailableSince(influxEdgeId, channel, timestmap); + } + private Map getDayChangeMeasurements(long timestamp) { final var instant = Instant.ofEpochMilli(timestamp); return this.zoneToMeasurement.entrySet().stream() // diff --git a/io.openems.backend.timedata.aggregatedinflux/src/io/openems/backend/timedata/aggregatedinflux/AllowedChannels.java b/io.openems.backend.timedata.aggregatedinflux/src/io/openems/backend/timedata/aggregatedinflux/AllowedChannels.java index c76eda5460d..bd11967ec23 100644 --- a/io.openems.backend.timedata.aggregatedinflux/src/io/openems/backend/timedata/aggregatedinflux/AllowedChannels.java +++ b/io.openems.backend.timedata.aggregatedinflux/src/io/openems/backend/timedata/aggregatedinflux/AllowedChannels.java @@ -38,22 +38,24 @@ private AllowedChannels() { .put("_sum/ConsumptionActivePowerL1", DataType.LONG) // .put("_sum/ConsumptionActivePowerL2", DataType.LONG) // .put("_sum/ConsumptionActivePowerL3", DataType.LONG) // - .putAll(multiChannels("evcs", 0, 9, "ActualPower", DataType.LONG)) // - .putAll(multiChannels("io", 0, 9, "Relay", 1, 9, DataType.LONG)) // - .putAll(multiChannels("meter", 0, 9, "ActivePower", DataType.LONG)) // - .putAll(multiChannels("meter", 0, 9, "ActivePowerL", 1, 4, DataType.LONG)) // - .put("ctrlGridOptimizedCharge0/_PropertyMaximumSellToGridPower", DataType.LONG) // - .put("ctrlGridOptimizedCharge0/DelayChargeMaximumChargeLimit", DataType.LONG) // - .put("ctrlGridOptimizedCharge0/SellToGridLimitMinimumChargeLimit", DataType.LONG) // - .put("ctrlIoHeatPump0/RegularStateTime", DataType.LONG) // - .put("ctrlIoHeatPump0/RecommendationStateTime", DataType.LONG) // - .put("ctrlIoHeatPump0/ForceOnStateTime", DataType.LONG) // - .put("ctrlIoHeatPump0/LockStateTime", DataType.LONG) // + .put("_sum/UnmanagedConsumptionActivePower", DataType.LONG) // + .putAll(multiChannels("io", 0, 10, "Relay", 1, 9, DataType.LONG)) // .put("ctrlIoHeatPump0/Status", DataType.LONG) // + .putAll(multiChannels("ess", 0, 17, "Soc", DataType.LONG)) // + .putAll(multiChannels("ess", 0, 17, "ActivePower", DataType.LONG)) // .put("ctrlIoHeatingElement0/Level", DataType.LONG) // - .put("ess0/Soc", DataType.LONG) // - .put("ess0/ActivePower", DataType.LONG) // + .put("ctrlGridOptimizedCharge0/DelayChargeMaximumChargeLimit", DataType.LONG) // + .putAll(multiChannels("charger", 0, 10, "ActualPower", DataType.LONG)) // + .put("ctrlEmergencyCapacityReserve0/ActualReserveSoc", DataType.LONG) // + .put("ctrlGridOptimizedCharge0/_PropertyMaximumSellToGridPower", DataType.LONG) // + .putAll(multiChannels("meter", 0, 10, "ActivePower", DataType.LONG)) // + .putAll(multiChannels("meter", 0, 10, "ActivePowerL", 1, 4, DataType.LONG)) // + .putAll(multiChannels("pvInverter", 0, 10, "ActivePower", DataType.LONG)) // .put("_sum/EssDischargePower", DataType.LONG) // used for xlsx export + .put("ctrlGridOptimizedCharge0/SellToGridLimitMinimumChargeLimit", DataType.LONG) // + .put("ctrlEssTimeOfUseTariff0/QuarterlyPrices", DataType.DOUBLE) // + .put("ctrlEssTimeOfUseTariff0/StateMachine", DataType.LONG) // + .putAll(multiChannels("evcs", 0, 10, "ChargePower", DataType.LONG)) // .build(); ALLOWED_CUMULATED_CHANNELS = ImmutableMap.builder() // @@ -67,12 +69,15 @@ private AllowedChannels() { .put("_sum/GridBuyActiveEnergy", DataType.LONG) // .put("_sum/EssActiveChargeEnergy", DataType.LONG) // .put("_sum/EssActiveDischargeEnergy", DataType.LONG) // - .putAll(multiChannels("charger", 0, 5, "ActualEnergy", DataType.LONG)) // - .putAll(multiChannels("evcs", 0, 9, "ActiveConsumptionEnergy", DataType.LONG)) // - .putAll(multiChannels("io", 0, 9, "ActiveProductionEnergy", DataType.LONG)) // - .putAll(multiChannels("meter", 0, 9, "ActiveProductionEnergy", DataType.LONG)) // - .putAll(multiChannels("pvInverter", 0, 9, "ActiveProductionEnergy", DataType.LONG)) // .put("ctrlEssTimeOfUseTariffDischarge0/DelayedTime", DataType.LONG) // + .put("ctrlEssTimeOfUseTariff0/DelayedTime", DataType.LONG) // + .put("ctrlEssTimeOfUseTariff0/ChargedTime", DataType.LONG) // + .putAll(multiChannels("evcs", 0, 10, "ActiveConsumptionEnergy", DataType.LONG)) // + .putAll(multiChannels("meter", 0, 10, "ActiveProductionEnergy", DataType.LONG)) // + .putAll(multiChannels("meter", 0, 10, "ActiveConsumptionEnergy", DataType.LONG)) // + .putAll(multiChannels("io", 0, 10, "ActiveProductionEnergy", DataType.LONG)) // + .putAll(multiChannels("pvInverter", 0, 10, "ActiveProductionEnergy", DataType.LONG)) // + .putAll(multiChannels("charger", 0, 10, "ActualEnergy", DataType.LONG)) // .put("ctrlGridOptimizedCharge0/AvoidLowChargingTime", DataType.LONG) // .put("ctrlGridOptimizedCharge0/NoLimitationTime", DataType.LONG) // .put("ctrlGridOptimizedCharge0/SellToGridLimitTime", DataType.LONG) // @@ -80,6 +85,11 @@ private AllowedChannels() { .put("ctrlIoHeatingElement0/Level1CumulatedTime", DataType.LONG) // .put("ctrlIoHeatingElement0/Level2CumulatedTime", DataType.LONG) // .put("ctrlIoHeatingElement0/Level3CumulatedTime", DataType.LONG) // + .put("ctrlChpSoc0/CumulatedActiveTime", DataType.LONG) // + .put("ctrlFixActivePower0/CumulatedActiveTime", DataType.LONG) // + .putAll(multiChannels("ctrlChannelThreshold", 0, 5, "CumulatedActiveTime", DataType.LONG)) // + .putAll(multiChannels("ctrlIoChannelSingleThreshold", 0, 5, "CumulatedActiveTime", DataType.LONG)) // + .putAll(multiChannels("ctrlIoFixDigitalOutput", 0, 5, "CumulatedActiveTime", DataType.LONG)) // .put("ctrlIoHeatPump0/RegularStateTime", DataType.LONG) // .put("ctrlIoHeatPump0/RecommendationStateTime", DataType.LONG) // .put("ctrlIoHeatPump0/ForceOnStateTime", DataType.LONG) // diff --git a/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/Config.java b/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/Config.java index a5acec96dac..5bcef3e9e7e 100644 --- a/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/Config.java +++ b/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/Config.java @@ -13,6 +13,12 @@ @AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component") String id() default "timedata0"; + @AttributeDefinition(name = "Startdate", description = "for example: 2023-12-30; optional", required = false) + String startDate(); + + @AttributeDefinition(name = "Enddate", description = "for example: 2023-12-31; optional", required = false) + String endDate(); + @AttributeDefinition(name = "Query language", description = "Query language Flux or InfluxQL") QueryLanguageConfig queryLanguage() default QueryLanguageConfig.INFLUX_QL; diff --git a/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/FieldTypeConflictHandler.java b/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/FieldTypeConflictHandler.java index d64828bf2f6..39bd1b07e67 100644 --- a/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/FieldTypeConflictHandler.java +++ b/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/FieldTypeConflictHandler.java @@ -26,13 +26,6 @@ public class FieldTypeConflictHandler { public FieldTypeConflictHandler(TimedataInfluxDb parent) { this.parent = parent; - this.initializePredefinedHandlers(); - } - - /** - * Add some already known Handlers. - */ - private void initializePredefinedHandlers() { } /** diff --git a/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/TimeFilter.java b/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/TimeFilter.java new file mode 100644 index 00000000000..8162e9e58d4 --- /dev/null +++ b/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/TimeFilter.java @@ -0,0 +1,80 @@ +package io.openems.backend.timedata.influx; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.stream.Stream; + +import io.openems.common.utils.DateUtils; + +public class TimeFilter { + + private final long start; // [ms] + private final long end; // [ms] + + /** + * Build a {@link TimeFilter} from String configurations. + * + * @param startDate a string Start-Date in + * {@link DateTimeFormatter#ISO_LOCAL_DATE} format + * @param endDate a string End-Date in + * {@link DateTimeFormatter#ISO_LOCAL_DATE} format + * @return a {@link TimeFilter} + */ + public static TimeFilter from(String startDate, String endDate) { + final long start; + { + var tmp = DateUtils.parseLocalDateOrNull(startDate); + if (tmp == null) { + start = -1; + } else { + start = tmp.atStartOfDay(ZoneId.of("UTC")).toEpochSecond() * 1000; + } + } + + final long end; + { + var tmp = DateUtils.parseLocalDateOrNull(endDate); + if (tmp == null) { + end = -1; + } else { + end = tmp.atStartOfDay(ZoneId.of("UTC")).plusDays(1).toEpochSecond() * 1000; + } + } + + return new TimeFilter(start, end); + } + + private TimeFilter(long start, long end) { + this.start = start; + this.end = end; + } + + /** + * Tests the given {@link ZonedDateTime}s for validity. + * + * @param times {@link ZonedDateTime}s to validate + * @return true if valid + */ + public boolean isValid(ZonedDateTime... times) { + return Stream.of(times) // + .allMatch(t -> this.isValid(t.toEpochSecond() * 1000)); + } + + /** + * Tests the given timestamp for validity. + * + * @param timestampMs timestamp in milliseconds + * @return true if valid + */ + public boolean isValid(long timestampMs) { + if (this.start != -1 && timestampMs < this.start) { + return false; + } + if (this.end != -1 && timestampMs > this.end) { + return false; + } + return true; + } + +} diff --git a/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/TimedataInfluxDb.java b/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/TimedataInfluxDb.java index 32c21a65cc5..4f57f2c2f61 100644 --- a/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/TimedataInfluxDb.java +++ b/io.openems.backend.timedata.influx/src/io/openems/backend/timedata/influx/TimedataInfluxDb.java @@ -5,7 +5,9 @@ import java.util.Optional; import java.util.Set; import java.util.SortedMap; -import java.util.function.Function; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.regex.Pattern; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -43,13 +45,10 @@ import io.openems.common.utils.StringUtils; import io.openems.shared.influxdb.InfluxConnector; -@Designate(ocd = Config.class, factory = false) +@Designate(ocd = Config.class, factory = true) @Component(// name = "Timedata.InfluxDB", // configurationPolicy = ConfigurationPolicy.REQUIRE, // - property = { // - "service.ranking:Integer=1" // ranking order (highest first) - }, // immediate = true // ) @EventTopics({ // @@ -65,6 +64,7 @@ public class TimedataInfluxDb extends AbstractOpenemsBackendComponent implements private Config config; private InfluxConnector influxConnector = null; + private TimeFilter timeFilter; // edgeId, channelIds which are timestamped channels private final Multimap timestampedChannelsForEdge = HashMultimap.create(); @@ -77,6 +77,7 @@ public TimedataInfluxDb() { @Activate private void activate(Config config) throws OpenemsException, IllegalArgumentException { this.config = config; + this.timeFilter = TimeFilter.from(config.startDate(), config.endDate()); this.logInfo(this.log, "Activate [" // + "url=" + config.url() + ";"// @@ -127,22 +128,14 @@ public void write(String edgeId, TimestampedDataNotification notification) { return; } - // parse the numeric EdgeId - try { - int influxEdgeId = InfluxConnector.parseNumberFromName(edgeId); - - // Write data to default location - this.writeData(// - influxEdgeId, // - notification, // - channel -> { - this.timestampedChannelsForEdge.put(influxEdgeId, channel); - return true; - }); - } catch (OpenemsException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + // Write data to default location + this.writeData(// + edgeId, // + notification, // + (influxEdgeId, channel) -> { + this.timestampedChannelsForEdge.put(influxEdgeId, channel); + return true; + }); } @Override @@ -151,17 +144,11 @@ public void write(String edgeId, AggregatedDataNotification notification) { return; } - try { - int influxEdgeId = InfluxConnector.parseNumberFromName(edgeId); - - // Write data to default location - this.writeData(// - influxEdgeId, // - notification, // - channel -> !this.isTimestampedChannel(influxEdgeId, channel)); - } catch (OpenemsException e) { - e.printStackTrace(); - } + // Write data to default location + this.writeData(// + edgeId, // + notification, // + (influxEdgeId, channel) -> !this.isTimestampedChannel(influxEdgeId, channel)); } @Override @@ -182,17 +169,25 @@ private boolean isTimestampedChannel(int edgeId, String channel) { /** * Actually writes the data to InfluxDB. * - * @param influxEdgeId the unique, numeric identifier of the Edge + * @param edgeId the unique identifier of the Edge * @param notification the {@link AbstractDataNotification} * @param shouldWriteValue the function which determines if the value should be * written * @throws OpenemsException on error */ private void writeData(// - int influxEdgeId, // + String edgeId, // AbstractDataNotification notification, // - Function shouldWriteValue // + BiFunction shouldWriteValue // ) { + final int influxEdgeId; + try { + influxEdgeId = InfluxConnector.parseNumberFromName(edgeId); + } catch (OpenemsException e) { + this.logWarn(this.log, "Unable to parse numeric Influx Edge-ID [" + edgeId + "] :" + e.getMessage()); + return; + } + final var data = notification.getData(); var dataEntries = data.rowMap().entrySet(); if (dataEntries.isEmpty()) { @@ -208,13 +203,19 @@ private void writeData(// } var timestamp = dataEntry.getKey(); + + if (!this.timeFilter.isValid(timestamp)) { + // timestamp is not within the TimeFilter + continue; + } + // this builds an InfluxDB record ("point") for a given timestamp var point = Point // .measurement(this.config.measurement()) // .addTag(OpenemsOEM.INFLUXDB_TAG, String.valueOf(influxEdgeId)) // .time(timestamp, WritePrecision.MS); for (var channelEntry : channelEntries) { - if (!shouldWriteValue.apply(channelEntry.getKey())) { + if (!shouldWriteValue.apply(influxEdgeId, channelEntry.getKey())) { continue; } this.addValue(// @@ -222,7 +223,7 @@ private void writeData(// channelEntry.getKey(), // channelEntry.getValue()); } - + this.influxConnector.write(point); } } @@ -231,6 +232,10 @@ private void writeData(// public SortedMap> queryHistoricData(String edgeId, ZonedDateTime fromDate, ZonedDateTime toDate, Set channels, Resolution resolution) throws OpenemsNamedException { + if (!this.timeFilter.isValid(fromDate, toDate)) { + return null; + } + // parse the numeric EdgeId Optional influxEdgeId = Optional.of(InfluxConnector.parseNumberFromName(edgeId)); @@ -241,6 +246,10 @@ public SortedMap> queryHis @Override public SortedMap queryHistoricEnergy(String edgeId, ZonedDateTime fromDate, ZonedDateTime toDate, Set channels) throws OpenemsNamedException { + if (!this.timeFilter.isValid(fromDate, toDate)) { + return null; + } + // parse the numeric EdgeId Optional influxEdgeId = Optional.of(InfluxConnector.parseNumberFromName(edgeId)); return this.influxConnector.queryHistoricEnergy(influxEdgeId, fromDate, toDate, channels, @@ -251,6 +260,10 @@ public SortedMap queryHistoricEnergy(String edgeId, public SortedMap> queryHistoricEnergyPerPeriod(String edgeId, ZonedDateTime fromDate, ZonedDateTime toDate, Set channels, Resolution resolution) throws OpenemsNamedException { + if (!this.timeFilter.isValid(fromDate, toDate)) { + return null; + } + // parse the numeric EdgeId Optional influxEdgeId = Optional.of(InfluxConnector.parseNumberFromName(edgeId)); return this.influxConnector.queryHistoricEnergyPerPeriod(influxEdgeId, fromDate, toDate, channels, resolution, @@ -265,8 +278,10 @@ public SortedMap> queryHis * @param element the value */ private void addValue(Point builder, String field, JsonElement element) { - if (element == null || element.isJsonNull() || this.specialCaseFieldHandling(builder, field, element)) { - // already handled by special case handling + if (element == null || element.isJsonNull() // + || !isAllowed(field) // Channel-Address is not allowed/blacklisted + // already handled by special case handling + || this.specialCaseFieldHandling(builder, field, element)) { return; } @@ -366,4 +381,59 @@ protected void logWarn(Logger log, String message) { public String id() { return this.config.id(); } + + private static final Predicate SUNSPEC_PATTERN = // + Pattern.compile("^S[0-9]+[A-Z][a-zA-Z0-9]*$").asPredicate(); + + /** + * Pattern for Component-IDs. + * + *

+ * Either: + * + *

    + *
  • starts with lower case letter + *
  • contains only ASCII letters and numbers + *
  • ends with a number + *
+ * + *

+ * Or: + *

    + *
  • starts with underscore (by convention for singleton Components) + *
  • continues with lower case letter + *
  • contains only ASCII letters and numbers + *
  • ends with a letter + *
+ */ + // TODO move to io.openems.common and validate pattern on Edge + private static final Predicate COMPONENT_ID_PATTERN = // + Pattern.compile("^([a-z][a-zA-Z0-9]+[0-9]+|_[a-z][a-zA-Z0-9]+[a-zA-Z])$").asPredicate(); + + protected static boolean isAllowed(String channelAddress) { + if (channelAddress == null) { + return false; + } + + var c = channelAddress.split("/"); + if (c.length != 2) { + return false; + } + + // Valid Component-ID + var componentId = c[0]; + if (!COMPONENT_ID_PATTERN.test(componentId)) { + return false; + } + + // Valid Channel-ID + var channelId = c[1]; + if (SUNSPEC_PATTERN.test(channelId)) { + // SunSpec Channels + return false; + } + + return true; + } + } diff --git a/io.openems.backend.timedata.influx/test/io/openems/backend/timedata/influx/TimeFilterTest.java b/io.openems.backend.timedata.influx/test/io/openems/backend/timedata/influx/TimeFilterTest.java new file mode 100644 index 00000000000..5dd96bac324 --- /dev/null +++ b/io.openems.backend.timedata.influx/test/io/openems/backend/timedata/influx/TimeFilterTest.java @@ -0,0 +1,41 @@ +package io.openems.backend.timedata.influx; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import org.junit.Test; + +public class TimeFilterTest { + + // Sunday, 1. January 2023 00:00:00 + private static final long T_01_01_2023 = 1672531200000L; + + private static final ZonedDateTime Z_01_01_1970 = ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")); + private static final ZonedDateTime Z_01_01_2023 = ZonedDateTime.of(2023, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")); + + @Test + public void testTimestamp() { + assertTrue(TimeFilter.from(null, null).isValid(0)); + + assertFalse(TimeFilter.from("2000-01-30", null).isValid(0)); + assertTrue(TimeFilter.from("2000-01-30", null).isValid(T_01_01_2023)); + + assertFalse(TimeFilter.from(null, "2000-01-30").isValid(T_01_01_2023)); + assertTrue(TimeFilter.from(null, "2024-01-30").isValid(T_01_01_2023)); + } + + @Test + public void testZonedDateTime() { + assertTrue(TimeFilter.from(null, null).isValid(Z_01_01_1970, Z_01_01_2023)); + + assertFalse(TimeFilter.from("2000-01-30", null).isValid(Z_01_01_1970)); + assertTrue(TimeFilter.from("2000-01-30", null).isValid(Z_01_01_2023)); + + assertFalse(TimeFilter.from(null, "2000-01-30").isValid(Z_01_01_2023)); + assertTrue(TimeFilter.from(null, "2024-01-30").isValid(Z_01_01_2023)); + } + +} diff --git a/io.openems.backend.timedata.influx/test/io/openems/backend/timedata/influx/TimedataInfluxDbTest.java b/io.openems.backend.timedata.influx/test/io/openems/backend/timedata/influx/TimedataInfluxDbTest.java new file mode 100644 index 00000000000..707f19f9340 --- /dev/null +++ b/io.openems.backend.timedata.influx/test/io/openems/backend/timedata/influx/TimedataInfluxDbTest.java @@ -0,0 +1,34 @@ +package io.openems.backend.timedata.influx; + +import static io.openems.backend.timedata.influx.TimedataInfluxDb.isAllowed; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class TimedataInfluxDbTest { + + @Test + public void testIsAllowed() { + assertFalse(isAllowed(null)); + assertFalse(isAllowed("invalid")); + assertFalse(isAllowed("in/va/lid")); + + // Channel-ID + assertTrue(isAllowed("ess0/ActivePower")); + assertTrue(isAllowed("_sum/EssActivePower")); + assertTrue(isAllowed("_cycle/MeasuredCycleTime")); + + assertFalse(isAllowed("ess/ActivePower")); + assertFalse(isAllowed("Ess1/ActivePower")); + assertFalse(isAllowed("cycle/MeasuredCycleTime")); + assertFalse(isAllowed("_cycle1/MeasuredCycleTime")); + assertFalse(isAllowed("My Heat-Pump/Status")); + assertFalse(isAllowed("äöü/Status")); + + // SunSpec + assertFalse(isAllowed("pvInverter0/S1Evt")); + assertFalse(isAllowed("pvInverter0/S111A")); + } + +} diff --git a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/OnNotification.java b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/OnNotification.java index 458012d7d35..43063567657 100644 --- a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/OnNotification.java +++ b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/OnNotification.java @@ -5,9 +5,9 @@ import org.slf4j.LoggerFactory; import io.openems.backend.common.metadata.User; -import io.openems.backend.uiwebsocket.jsonrpc.notification.LogMessageNotification; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.jsonrpc.base.JsonrpcNotification; +import io.openems.common.jsonrpc.notification.LogMessageNotification; public class OnNotification implements io.openems.common.websocket.OnNotification { diff --git a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/OnRequest.java b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/OnRequest.java index e1d2caee4e9..b2ac0abe429 100644 --- a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/OnRequest.java +++ b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/OnRequest.java @@ -1,7 +1,9 @@ package io.openems.backend.uiwebsocket.impl; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -21,6 +23,7 @@ import io.openems.backend.common.jsonrpc.response.GetUserAlertingConfigsResponse; import io.openems.backend.common.jsonrpc.response.GetUserInformationResponse; import io.openems.backend.common.metadata.AlertingSetting; +import io.openems.backend.common.metadata.Metadata.GenericSystemLog; import io.openems.backend.common.metadata.User; import io.openems.common.exceptions.OpenemsError; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; @@ -30,6 +33,7 @@ import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.common.jsonrpc.request.AuthenticateWithPasswordRequest; import io.openems.common.jsonrpc.request.AuthenticateWithTokenRequest; +import io.openems.common.jsonrpc.request.ComponentJsonApiRequest; import io.openems.common.jsonrpc.request.EdgeRpcRequest; import io.openems.common.jsonrpc.request.GetEdgeRequest; import io.openems.common.jsonrpc.request.GetEdgesRequest; @@ -223,21 +227,42 @@ private CompletableFuture handleEdgeRpcRequest(WsData wsData, U var request = edgeRpcRequest.getPayload(); user.assertEdgeRoleIsAtLeast(EdgeRpcRequest.METHOD, edgeId, Role.GUEST); - CompletableFuture resultFuture; - switch (request.getMethod()) { - - case SubscribeChannelsRequest.METHOD: - resultFuture = this.handleSubscribeChannelsRequest(wsData, edgeId, user, - SubscribeChannelsRequest.from(request)); - break; - - case SubscribeSystemLogRequest.METHOD: - resultFuture = this.handleSubscribeSystemLogRequest(wsData, edgeId, user, - SubscribeSystemLogRequest.from(request)); - break; + CompletableFuture resultFuture = switch (request.getMethod()) { + case SubscribeChannelsRequest.METHOD -> + this.handleSubscribeChannelsRequest(wsData, edgeId, user, SubscribeChannelsRequest.from(request)); + case SubscribeSystemLogRequest.METHOD -> + this.handleSubscribeSystemLogRequest(wsData, edgeId, user, SubscribeSystemLogRequest.from(request)); + case ComponentJsonApiRequest.METHOD -> { + final var componentRequest = ComponentJsonApiRequest.from(request); + if (!"_host".equals(componentRequest.getComponentId())) { + yield null; + } + switch (componentRequest.getPayload().getMethod()) { + case "executeSystemCommand" -> { + final var executeSystemCommandRequest = componentRequest.getPayload(); + final var p = executeSystemCommandRequest.getParams(); + this.parent.metadata.logGenericSystemLog(new LogSystemExecuteCommend(edgeId, user, // + JsonUtils.getAsString(p, "command"), // + JsonUtils.getAsOptionalBoolean(p, "sudo").orElse(null), // + JsonUtils.getAsOptionalString(p, "username").orElse(null), // + JsonUtils.getAsOptionalString(p, "password").orElse(null), // + JsonUtils.getAsOptionalBoolean(p, "runInBackground").orElse(null) // + )); + } + case "executeSystemUpdate" -> { + this.parent.metadata.logGenericSystemLog(new LogUpdateSystem(edgeId, user)); + } + } - default: + yield null; + } + default -> { // unable to handle; try generic handler + yield null; + } + }; + + if (resultFuture == null) { return null; } @@ -256,6 +281,51 @@ private CompletableFuture handleEdgeRpcRequest(WsData wsData, U return result; } + private record LogSystemExecuteCommend(// + String edgeId, // non-null + User user, // non-null + String command, // non-null + Boolean sudo, // null-able + String username, // null-able + String password, // null-able + Boolean runInBackground // null-able + ) implements GenericSystemLog { + + @Override + public String teaser() { + return "Systemcommand: " + this.command; + } + + @Override + public Map getValues() { + return Map.of(// + "Sudo", Boolean.toString(Optional.ofNullable(this.sudo()).orElse(false)), // + "Command", this.command(), // + "Username", this.username(), // + "Password", this.password() == null || this.password().isEmpty() ? "[NOT_SET]" : "[SET]", // + "Run in Background", Boolean.toString(Optional.ofNullable(this.runInBackground()).orElse(false)) // + ); + } + + } + + private record LogUpdateSystem(// + String edgeId, // non-null + User user // non-null + ) implements GenericSystemLog { + + @Override + public String teaser() { + return "Systemupdate"; + } + + @Override + public Map getValues() { + return Map.of(); + } + + } + /** * Handles a {@link SubscribeChannelsRequest}. * diff --git a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/UiWebsocketImpl.java b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/UiWebsocketImpl.java index bb90a69ed35..c936874d3c3 100644 --- a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/UiWebsocketImpl.java +++ b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/UiWebsocketImpl.java @@ -41,7 +41,6 @@ import io.openems.common.jsonrpc.base.JsonrpcResponseSuccess; import io.openems.common.jsonrpc.notification.TimestampedDataNotification; import io.openems.common.utils.ThreadPoolUtils; -import io.openems.common.websocket.AbstractWebsocketServer.DebugMode; @Designate(ocd = Config.class, factory = false) @Component(// @@ -55,7 +54,7 @@ public class UiWebsocketImpl extends AbstractOpenemsBackendComponent implements UiWebsocket, EventHandler { private static final String EDGE_ID = "backend0"; - private static final String COMPONENT_ID = "uiwebsocket"; + private static final String COMPONENT_ID = "uiwebsocket0"; private final ScheduledExecutorService debugLogExecutor = Executors.newSingleThreadScheduledExecutor(); @@ -89,6 +88,10 @@ private void activate(Config config) { new JsonPrimitive(this.server != null ? this.server.getConnections().size() : 0)); this.timedataManager.write(EDGE_ID, new TimestampedDataNotification(data)); }, 10, 10, TimeUnit.SECONDS); + + if (this.metadata.isInitialized()) { + this.startServer(); + } } @Deactivate @@ -99,23 +102,23 @@ private void deactivate() { /** * Create and start new server. - * - * @param port the port - * @param poolSize number of threads dedicated to handle the tasks - * @param debugMode activate a regular debug log about the state of the tasks */ - private synchronized void startServer(int port, int poolSize, DebugMode debugMode) { - this.server = new WebsocketServer(this, "Ui.Websocket", port, poolSize, debugMode); - this.server.start(); + private synchronized void startServer() { + if (this.server == null) { + this.server = new WebsocketServer(this, this.getName(), this.config.port(), this.config.poolSize(), + this.config.debugMode()); + this.server.start(); + } } /** * Stop existing websocket server. */ private synchronized void stopServer() { - if (this.server != null) { - this.server.stop(); + if (this.server == null) { + return; } + this.server.stop(); } @Override @@ -148,6 +151,9 @@ public CompletableFuture send(UUID websocketId, JsonrpcR @Override public void sendBroadcast(String edgeId, JsonrpcNotification notification) throws OpenemsNamedException { + if (this.server == null) { + return; + } var wsDatas = this.getWsDatasForEdgeId(edgeId); OpenemsNamedException exception = null; for (WsData wsData : wsDatas) { @@ -224,13 +230,16 @@ private List getWsDatasForEdgeId(String edgeId) { public void handleEvent(Event event) { switch (event.getTopic()) { case Metadata.Events.AFTER_IS_INITIALIZED: - this.startServer(this.config.port(), this.config.poolSize(), this.config.debugMode()); + this.startServer(); break; } } @Override public void sendSubscribedChannels(String edgeId, EdgeCache edgeCache) { + if (this.server == null) { + return; + } var connections = this.server.getConnections(); for (var websocket : connections) { WsData wsData = websocket.getAttachment(); @@ -249,7 +258,7 @@ public void sendSubscribedChannels(String edgeId, EdgeCache edgeCache) { * @return the {@link User} * @throws OpenemsNamedException if User is not authenticated */ - public User assertUser(WsData wsData, AbstractJsonrpcRequest request) throws OpenemsNamedException { + protected User assertUser(WsData wsData, AbstractJsonrpcRequest request) throws OpenemsNamedException { var userIdOpt = wsData.getUserId(); if (!userIdOpt.isPresent()) { throw OpenemsError.COMMON_USER_NOT_AUTHENTICATED @@ -262,4 +271,9 @@ public User assertUser(WsData wsData, AbstractJsonrpcRequest request) throws Ope } return userOpt.get(); } + + public String getId() { + return COMPONENT_ID; + } + } diff --git a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WebsocketServer.java b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WebsocketServer.java index 2ebdea3695a..c3d36bfd958 100644 --- a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WebsocketServer.java +++ b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WebsocketServer.java @@ -34,7 +34,7 @@ public WebsocketServer(UiWebsocketImpl parent, String name, int port, int poolSi var data = TreeBasedTable.create(); var now = Instant.now().toEpochMilli(); ThreadPoolUtils.debugMetrics(executor).forEach((key, value) -> { - data.put(now, "uiwebsocket/" + key, new JsonPrimitive(value)); + data.put(now, parent.getId() + "/" + key, new JsonPrimitive(value)); }); parent.timedataManager.write("backend0", new TimestampedDataNotification(data)); }); diff --git a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WsData.java b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WsData.java index 869be69c0d9..885e71cb4d6 100644 --- a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WsData.java +++ b/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/impl/WsData.java @@ -28,9 +28,13 @@ public class WsData extends io.openems.common.websocket.WsData { private static class SubscribedChannels { + private static final Logger LOG = LoggerFactory.getLogger(SubscribedChannels.class); + private int lastRequestCount = Integer.MIN_VALUE; private final Map> subscribedChannels = new HashMap<>(); + private Set currentDataMissingChannelValues = Collections.emptySet(); + /** * Applies a SubscribeChannelsRequest. * @@ -56,11 +60,15 @@ public Map getChannelValues(String edgeId, EdgeCache edgeCa return Collections.emptyMap(); } - var result = new HashMap(subscribedChannels.size()); - for (var channel : subscribedChannels) { - result.put(channel, edgeCache.getChannelValue(channel)); + var result = edgeCache.getChannelValues(subscribedChannels); + if (!result.b().isEmpty()) { + if (!result.b().equals(this.currentDataMissingChannelValues)) { + LOG.info("[" + edgeId + "] Channels missing in Current-Data: [" + String.join(", ", result.b()) + + "]"); + } + this.currentDataMissingChannelValues = result.b(); } - return result; + return result.a(); } protected void dispose() { diff --git a/io.openems.common/src/io/openems/common/OpenemsConstants.java b/io.openems.common/src/io/openems/common/OpenemsConstants.java index dc9c9939415..59d1325b7b4 100644 --- a/io.openems.common/src/io/openems/common/OpenemsConstants.java +++ b/io.openems.common/src/io/openems/common/OpenemsConstants.java @@ -57,6 +57,21 @@ public class OpenemsConstants { OpenemsConstants.VERSION_PATCH, // OpenemsConstants.VERSION_STRING); + /** + * The version development branch. + */ + public static final String VERSION_DEV_BRANCH = ""; + + /** + * The version development commit hash. + */ + public static final String VERSION_DEV_COMMIT = ""; + + /** + * The version development build time. + */ + public static final String VERSION_DEV_BUILD_TIME = ""; + /** * The manufacturer of the device that is running OpenEMS. * diff --git a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/jsonrpc/notification/LogMessageNotification.java b/io.openems.common/src/io/openems/common/jsonrpc/notification/LogMessageNotification.java similarity index 96% rename from io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/jsonrpc/notification/LogMessageNotification.java rename to io.openems.common/src/io/openems/common/jsonrpc/notification/LogMessageNotification.java index 2e3b8808d8b..268b80cbcf5 100644 --- a/io.openems.backend.uiwebsocket/src/io/openems/backend/uiwebsocket/jsonrpc/notification/LogMessageNotification.java +++ b/io.openems.common/src/io/openems/common/jsonrpc/notification/LogMessageNotification.java @@ -1,4 +1,4 @@ -package io.openems.backend.uiwebsocket.jsonrpc.notification; +package io.openems.common.jsonrpc.notification; import java.lang.System.Logger.Level; diff --git a/io.openems.common/src/io/openems/common/jsonrpc/response/GetEdgeResponse.java b/io.openems.common/src/io/openems/common/jsonrpc/response/GetEdgeResponse.java index b3460f96989..4996680ef64 100644 --- a/io.openems.common/src/io/openems/common/jsonrpc/response/GetEdgeResponse.java +++ b/io.openems.common/src/io/openems/common/jsonrpc/response/GetEdgeResponse.java @@ -24,7 +24,7 @@ */ public class GetEdgeResponse extends JsonrpcResponseSuccess { - private final EdgeMetadata edgeMetadata; + public final EdgeMetadata edgeMetadata; public GetEdgeResponse(UUID id, EdgeMetadata edgeMetadata) { super(id); diff --git a/io.openems.common/src/io/openems/common/jsonrpc/response/GetEdgesResponse.java b/io.openems.common/src/io/openems/common/jsonrpc/response/GetEdgesResponse.java index 5e54a92144d..85abf272563 100644 --- a/io.openems.common/src/io/openems/common/jsonrpc/response/GetEdgesResponse.java +++ b/io.openems.common/src/io/openems/common/jsonrpc/response/GetEdgesResponse.java @@ -80,7 +80,7 @@ protected JsonObject toJsonObject() { } } - private final List edgeMetadata; + public final List edgeMetadata; public GetEdgesResponse(UUID id, List edgeMetadata) { super(id); diff --git a/io.openems.common/src/io/openems/common/websocket/OnOpen.java b/io.openems.common/src/io/openems/common/websocket/OnOpen.java index 3851d7e4524..b7e0870704d 100644 --- a/io.openems.common/src/io/openems/common/websocket/OnOpen.java +++ b/io.openems.common/src/io/openems/common/websocket/OnOpen.java @@ -1,15 +1,10 @@ package io.openems.common.websocket; -import java.util.Map.Entry; -import java.util.Optional; - import org.java_websocket.WebSocket; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.utils.JsonUtils; @FunctionalInterface public interface OnOpen { @@ -23,34 +18,4 @@ public interface OnOpen { */ public void run(WebSocket ws, JsonObject handshake) throws OpenemsNamedException; - /** - * Get field from the 'cookie' field in the handshake. - * - *

- * Per specification - * all variants of 'cookie' are accepted. - * - * @param handshake the Handshake - * @param fieldname the field name - * @return value as optional - */ - public static Optional getFieldFromHandshakeCookie(JsonObject handshake, String fieldname) { - for (Entry entry : handshake.entrySet()) { - if (entry.getKey().equalsIgnoreCase("cookie")) { - var cookieOpt = JsonUtils.getAsOptionalString(entry.getValue()); - if (cookieOpt.isPresent()) { - for (String cookieVariable : cookieOpt.get().split("; ")) { - var keyValue = cookieVariable.split("="); - if (keyValue.length == 2) { - if (keyValue[0].equals(fieldname)) { - return Optional.ofNullable(keyValue[1]); - } - } - } - } - } - } - return Optional.empty(); - } } diff --git a/io.openems.common/src/io/openems/common/websocket/OnRequestHandler.java b/io.openems.common/src/io/openems/common/websocket/OnRequestHandler.java index 8d85ca97f97..0e4cf55c695 100644 --- a/io.openems.common/src/io/openems/common/websocket/OnRequestHandler.java +++ b/io.openems.common/src/io/openems/common/websocket/OnRequestHandler.java @@ -93,7 +93,7 @@ private void handleException(Throwable t) { log // .append("for Request ") // - .append(StringUtils.toShortString(JsonrpcUtils.simplifyJsonrpcMessage(this.request), 100)); // + .append(StringUtils.toShortString(JsonrpcUtils.simplifyJsonrpcMessage(this.request), 200)); // this.parent.logWarn(this.log, log.toString()); // Get JSON-RPC Response Error diff --git a/io.openems.common/src/io/openems/common/websocket/WebsocketUtils.java b/io.openems.common/src/io/openems/common/websocket/WebsocketUtils.java index dc9a9271841..a60902b90bc 100644 --- a/io.openems.common/src/io/openems/common/websocket/WebsocketUtils.java +++ b/io.openems.common/src/io/openems/common/websocket/WebsocketUtils.java @@ -9,6 +9,12 @@ public class WebsocketUtils { /** * Converts a Handshake to a JsonObject. + * + *

+ * NOTE: Per specification + * "Field names are case-insensitive". Because of this fields are converted to + * lower-case. * * @param handshake the {@link Handshakedata} * @return the converted {@link JsonObject} @@ -17,7 +23,7 @@ public static JsonObject handshakeToJsonObject(Handshakedata handshake) { var j = new JsonObject(); for (var iter = handshake.iterateHttpFields(); iter.hasNext();) { var field = iter.next(); - j.addProperty(field, handshake.getFieldValue(field)); + j.addProperty(field.toLowerCase(), handshake.getFieldValue(field)); } return j; } diff --git a/io.openems.common/test/io/openems/common/websocket/OnOpenTest.java b/io.openems.common/test/io/openems/common/websocket/OnOpenTest.java deleted file mode 100644 index 144775b6796..00000000000 --- a/io.openems.common/test/io/openems/common/websocket/OnOpenTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.openems.common.websocket; - -import java.util.Optional; - -import org.junit.Assert; -import org.junit.Test; - -import com.google.gson.JsonObject; - -import io.openems.common.utils.JsonUtils; - -public class OnOpenTest { - - private static JsonObject createJsonObject(String key, String value) { - return JsonUtils.buildJsonObject() // - .addProperty(key, value) // - .build(); - } - - @Test - public void testGetFieldFromHandshakeCookie() { - Assert.assertEquals(Optional.of("bar"), // - OnOpen.getFieldFromHandshakeCookie(// - OnOpenTest.createJsonObject("cookie", "foo=bar"), // - "foo")); - - Assert.assertEquals(Optional.of("bar"), // - OnOpen.getFieldFromHandshakeCookie(// - OnOpenTest.createJsonObject("COOKIE", "foo=bar"), // - "foo")); - - Assert.assertEquals(Optional.of("bar"), // - OnOpen.getFieldFromHandshakeCookie(// - OnOpenTest.createJsonObject("cOOkIe", "foo=bar"), // - "foo")); - - Assert.assertEquals(Optional.empty(), // - OnOpen.getFieldFromHandshakeCookie(// - OnOpenTest.createJsonObject("spooky", "foo=bar"), // - "foo")); - - Assert.assertEquals(Optional.empty(), // - OnOpen.getFieldFromHandshakeCookie(// - OnOpenTest.createJsonObject("cookie", "foobar"), // - "foo")); - - Assert.assertEquals(Optional.empty(), // - OnOpen.getFieldFromHandshakeCookie(// - OnOpenTest.createJsonObject("cookie", "foo=bar"), // - "bar")); - - Assert.assertEquals(Optional.empty(), // - OnOpen.getFieldFromHandshakeCookie(// - JsonUtils.buildJsonObject().add("cookie", new JsonObject()).build(), // - "bar")); - } - -} diff --git a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeHardwareType.java b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeHardwareType.java index ccb3f0380c7..15782f7b432 100644 --- a/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeHardwareType.java +++ b/io.openems.edge.battery.fenecon.home/src/io/openems/edge/battery/fenecon/home/BatteryFeneconHomeHardwareType.java @@ -6,7 +6,7 @@ public enum BatteryFeneconHomeHardwareType implements OptionsEnum { BATTERY_52(52, "Fenecon Home Battery 52Ah", 2200, 42, 49, 14, 3, new FeneconHomeBatteryProtection52()), // - BATTERY_64(64, "Fenecon Home Battery 64,4Ah", 2650, 40.6f, 49.7f, 14, 5, new FeneconHomeBatteryProtection64()); // + BATTERY_64(64, "Fenecon Home Battery 64,4Ah", 2800, 40.6f, 49.7f, 14, 5, new FeneconHomeBatteryProtection64()); // /** * Defaults to {@link #BATTERY_52} to avoid detection failure with old firmware diff --git a/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/OnRequest.java b/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/OnRequest.java index 968844054cf..a2c7cde6f6f 100644 --- a/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/OnRequest.java +++ b/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/OnRequest.java @@ -1,6 +1,6 @@ package io.openems.edge.controller.api.websocket; -import java.time.ZonedDateTime; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -12,7 +12,6 @@ import com.google.gson.JsonElement; -import io.openems.common.OpenemsConstants; import io.openems.common.exceptions.OpenemsError; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; @@ -42,7 +41,6 @@ import io.openems.common.jsonrpc.response.EdgeRpcResponse; import io.openems.common.jsonrpc.response.GetEdgeResponse; import io.openems.common.jsonrpc.response.GetEdgesResponse; -import io.openems.common.jsonrpc.response.GetEdgesResponse.EdgeMetadata; import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesDataResponse; import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesEnergyPerPeriodResponse; import io.openems.common.jsonrpc.response.QueryHistoricTimeseriesEnergyResponse; @@ -89,10 +87,10 @@ public CompletableFuture run(WebSocket ws, Jso return this.handleEdgeRpcRequest(wsData, user, EdgeRpcRequest.from(request)); case GetEdgesRequest.METHOD: - return this.handleGetEdgesRequest(user, GetEdgesRequest.from(request)); + return handleGetEdgesRequest(user, GetEdgesRequest.from(request)); case GetEdgeRequest.METHOD: - return this.handleGetEdgeRequest(user, GetEdgeRequest.from(request)); + return handleGetEdgeRequest(user, GetEdgeRequest.from(request)); case SubscribeEdgesRequest.METHOD: return this.handleSubscribeEdgesReqeust(user, SubscribeEdgesRequest.from(request)); @@ -256,7 +254,7 @@ private CompletableFuture handleAuthentication(WsData ws this.parent.logInfo(this.log, "User [" + user.getId() + ":" + user.getName() + "] connected."); return CompletableFuture.completedFuture(new AuthenticateResponse(requestId, token, user, - Utils.getEdgeMetadata(user.getRole()), Language.DEFAULT)); + List.of(Utils.getEdgeMetadata(user.getRole())), Language.DEFAULT)); } wsData.unsetUser(); throw OpenemsError.COMMON_AUTHENTICATION_FAILED.exception(); @@ -504,9 +502,10 @@ private CompletableFuture handleSubscribeSystemLogReques * @return the {@link GetEdgesResponse} Response Future * @throws OpenemsNamedException on error */ - private CompletableFuture handleGetEdgesRequest(User user, GetEdgesRequest request) { + protected static CompletableFuture handleGetEdgesRequest(User user, + GetEdgesRequest request) { return CompletableFuture.completedFuture(// - new GetEdgesResponse(request.getId(), Utils.getEdgeMetadata(user.getGlobalRole()))); + new GetEdgesResponse(request.getId(), List.of(Utils.getEdgeMetadata(user.getGlobalRole())))); } /** @@ -516,22 +515,9 @@ private CompletableFuture handleGetEdgesRequest(User use * @param request the {@link GetEdgeRequest} * @return the {@link GetEdgeResponse} Response Future */ - private CompletableFuture handleGetEdgeRequest(User user, GetEdgeRequest request) { + protected static CompletableFuture handleGetEdgeRequest(User user, GetEdgeRequest request) { return CompletableFuture.completedFuture(// - new GetEdgeResponse(request.id, // - new EdgeMetadata(// - ControllerApiWebsocket.EDGE_ID, // - ControllerApiWebsocket.EDGE_COMMENT, // - ControllerApiWebsocket.EDGE_PRODUCT_TYPE, // - OpenemsConstants.VERSION, // - user.getGlobalRole(), // - true, // - ZonedDateTime.now(), // - ZonedDateTime.now(), // - ControllerApiWebsocket.SUM_STATE // - ) // - ) // - ); + new GetEdgeResponse(request.id, Utils.getEdgeMetadata(user.getGlobalRole()))); } /** diff --git a/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/Utils.java b/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/Utils.java index d78b02109bc..02105b3736a 100644 --- a/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/Utils.java +++ b/io.openems.edge.controller.api.websocket/src/io/openems/edge/controller/api/websocket/Utils.java @@ -1,9 +1,6 @@ package io.openems.edge.controller.api.websocket; import java.time.ZonedDateTime; -import java.util.List; - -import com.google.common.collect.Lists; import io.openems.common.OpenemsConstants; import io.openems.common.jsonrpc.response.GetEdgesResponse.EdgeMetadata; @@ -15,20 +12,20 @@ public class Utils { * Gets the EdgeMetadata for one Edge. * * @param role the {@link Role} for this Edge - * @return a list of {@link EdgeMetadata}s + * @return the {@link EdgeMetadata} */ - public static List getEdgeMetadata(Role role) { - return Lists.newArrayList(new EdgeMetadata(// + public static EdgeMetadata getEdgeMetadata(Role role) { + return new EdgeMetadata(// ControllerApiWebsocket.EDGE_ID, // Edge-ID ControllerApiWebsocket.EDGE_COMMENT, // Comment ControllerApiWebsocket.EDGE_PRODUCT_TYPE, // Product-Type OpenemsConstants.VERSION, // Version role, // Role true, // Is Online - ZonedDateTime.now(), // now - null, // + ZonedDateTime.now(), // lastMessage + null, // firstSetupProtocol ControllerApiWebsocket.SUM_STATE // - )); + ); } } diff --git a/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/OnRequestTest.java b/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/OnRequestTest.java new file mode 100644 index 00000000000..49a4519afbb --- /dev/null +++ b/io.openems.edge.controller.api.websocket/test/io/openems/edge/controller/api/websocket/OnRequestTest.java @@ -0,0 +1,60 @@ +package io.openems.edge.controller.api.websocket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import io.openems.common.OpenemsConstants; +import io.openems.common.jsonrpc.base.GenericJsonrpcRequest; +import io.openems.common.jsonrpc.request.GetEdgeRequest; +import io.openems.common.jsonrpc.request.GetEdgesRequest; +import io.openems.common.jsonrpc.response.GetEdgesResponse.EdgeMetadata; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.test.DummyUser; +import io.openems.edge.common.user.User; + +public class OnRequestTest { + + private final User user = new DummyUser("id", "password", Language.DEFAULT, Role.ADMIN); + + @Test + public void testHandleGetEdgesRequest() throws Exception { + final var response = OnRequest.handleGetEdgesRequest(this.user, + GetEdgesRequest.from(new GenericJsonrpcRequest(GetEdgesRequest.METHOD, JsonUtils.buildJsonObject() // + .addProperty("page", 0) // + .build()))) + .get(); + + final var edges = response.edgeMetadata; + assertEquals(1, edges.size()); + + this.validateLocalEdgeMetadata(edges.get(0)); + } + + @Test + public void testHandleGetEdgeRequest() throws Exception { + final var response = OnRequest.handleGetEdgeRequest(this.user, + GetEdgeRequest.from(new GenericJsonrpcRequest(GetEdgeRequest.METHOD, JsonUtils.buildJsonObject() // + .addProperty("edgeId", ControllerApiWebsocket.EDGE_ID) // + .build()))) + .get(); + + this.validateLocalEdgeMetadata(response.edgeMetadata); + } + + private void validateLocalEdgeMetadata(EdgeMetadata edge) { + assertEquals(ControllerApiWebsocket.EDGE_ID, edge.id()); + assertEquals(ControllerApiWebsocket.EDGE_COMMENT, edge.comment()); + assertEquals(ControllerApiWebsocket.EDGE_PRODUCT_TYPE, edge.producttype()); + assertEquals(ControllerApiWebsocket.SUM_STATE, edge.sumState()); + assertEquals(OpenemsConstants.VERSION, edge.version()); + assertTrue(edge.isOnline()); + assertEquals(this.user.getGlobalRole(), edge.role()); + assertNull(edge.firstSetupProtocol()); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java index 1f7b67ba2fc..176863e7c2b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadOnly.java @@ -37,6 +37,7 @@ import io.openems.edge.core.appmanager.OpenemsAppCategory; import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a App for ReadOnly Modbus/TCP Api. @@ -131,7 +132,7 @@ public OpenemsAppCardinality getCardinality() { protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { return (t, p, l) -> { if (!this.getBoolean(p, Property.ACTIVE)) { - return new AppConfiguration(); + return AppConfiguration.empty(); } var controllerId = this.getId(t, p, Property.CONTROLLER_ID); @@ -144,7 +145,9 @@ protected ThrowingTriFunction, L .add("component.ids", componentIds) // .build())); - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java index 50b5ab76a68..39ad2e04817 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/ModbusTcpApiReadWrite.java @@ -36,6 +36,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; /** @@ -188,7 +189,11 @@ protected ThrowingTriFunction, L .build()) // ); - return new AppConfiguration(components, schedulerIds, null, dependencies); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(schedulerIds)) // + .addDependencies(dependencies) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/MqttApi.java b/io.openems.edge.core/src/io/openems/edge/app/api/MqttApi.java index 54abb880741..c1e2a7712d7 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/MqttApi.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/MqttApi.java @@ -32,6 +32,7 @@ import io.openems.edge.core.appmanager.OpenemsAppCardinality; import io.openems.edge.core.appmanager.OpenemsAppCategory; import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; /** @@ -153,7 +154,9 @@ protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { return (t, p, l) -> { if (!this.getBoolean(p, Property.ACTIVE)) { - return new AppConfiguration(); + return AppConfiguration.empty(); } var controllerId = this.getId(t, p, Property.CONTROLLER_ID); @@ -141,7 +142,9 @@ protected ThrowingTriFunction, L .build()) // ); - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadWrite.java b/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadWrite.java index 28571454f88..9016f640745 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadWrite.java +++ b/io.openems.edge.core/src/io/openems/edge/app/api/RestJsonApiReadWrite.java @@ -33,6 +33,7 @@ import io.openems.edge.core.appmanager.OpenemsAppCategory; import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; /** @@ -149,7 +150,11 @@ protected ThrowingTriFunction, L "ctrlEmergencyCapacityReserve0", // "ctrlGridOptimizedCharge0" // ); - - return new AppConfiguration(components, schedulerIds); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(schedulerIds)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/ess/PrepareBatteryExtension.java b/io.openems.edge.core/src/io/openems/edge/app/ess/PrepareBatteryExtension.java index 8f339c4e3f5..09a410b9af6 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/ess/PrepareBatteryExtension.java +++ b/io.openems.edge.core/src/io/openems/edge/app/ess/PrepareBatteryExtension.java @@ -37,6 +37,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; /** @@ -162,7 +163,10 @@ protected ThrowingTriFunction, L "ctrlBalancing0" // ); - return new AppConfiguration(components, schedulerIds); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(schedulerIds)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/AlpitronicEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/AlpitronicEvcs.java index d4711ee495b..9f294c87ced 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/AlpitronicEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/AlpitronicEvcs.java @@ -18,7 +18,6 @@ import org.osgi.service.component.annotations.Reference; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; @@ -48,6 +47,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.Case; import io.openems.edge.core.appmanager.formly.DefaultValueOptions; import io.openems.edge.core.appmanager.formly.Exp; @@ -274,19 +274,16 @@ protected ThrowingTriFunction b.addTask(Tasks.staticIp(new InterfaceConfiguration("eth0") // + // range from 192.168.1.96 - 192.168.1.111 + .addIp("Evcs", "192.168.1.97/28")))) // + .addDependencies(EvcsCluster.dependency(t, this.componentManager, this.componentUtil, + maxHardwarePowerPerPhase, addedEvcsIds.stream().toArray(String[]::new))) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/DezonyEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/DezonyEvcs.java index d9fb85b42cc..03cd56cde8a 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/DezonyEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/DezonyEvcs.java @@ -40,6 +40,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a dezony IQ evcs App. @@ -143,13 +144,12 @@ protected ThrowingTriFunction, L .build())// ); - return new AppConfiguration(// - components, // - Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0"), // - null, // - EvcsCluster.dependency(t, this.componentManager, this.componentUtil, maxHardwarePowerPerPhase, - evcsId) // - ); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(ctrlEvcsId, "ctrlBalancing0")) // + .addDependencies(EvcsCluster.dependency(t, this.componentManager, this.componentUtil, + maxHardwarePowerPerPhase, evcsId)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java index f72a5bf9287..0ca9958782c 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/EvcsCluster.java @@ -55,6 +55,7 @@ import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration.AppDependencyConfig; import io.openems.edge.core.appmanager.dependency.DependencyUtil; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; /** @@ -158,7 +159,9 @@ protected ThrowingTriFunction, L .build()) // ); - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java index 80e957ac45b..bdbbf4626b2 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/HardyBarthEvcs.java @@ -50,6 +50,7 @@ import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.Case; import io.openems.edge.core.appmanager.formly.DefaultValueOptions; import io.openems.edge.core.appmanager.formly.Exp; @@ -352,18 +353,16 @@ OpenemsNamedException> appPropertyConfigurationFactory() { maxHardwarePowerPerPhase, removeIds, evcsId); } - final var ips = Lists.newArrayList(// - new InterfaceConfiguration("eth0") // - .addIp("Evcs", "192.168.25.10/24") // - ); - schedulerIds.add("ctrlBalancing0"); - return new AppConfiguration(// - components, // - schedulerIds, // - ip.startsWith("192.168.25.") ? ips : null, // - clusterDependency // - ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(schedulerIds)) // + .throwingOnlyIf(ip.startsWith("192.168.25."), + b -> b.addTask(Tasks.staticIp(new InterfaceConfiguration("eth0") // + .addIp("Evcs", "192.168.25.10/24")))) // + .addDependencies(clusterDependency) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java index db71aec8305..f210318ca5f 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/IesKeywattEvcs.java @@ -38,6 +38,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; /** @@ -149,13 +150,12 @@ protected ThrowingTriFunction, L .build())// ); - return new AppConfiguration(// - components, // - Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0"), // - null, // - EvcsCluster.dependency(t, this.componentManager, this.componentUtil, maxHardwarePowerPerPhase, - evcsId) // - ); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(ctrlEvcsId, "ctrlBalancing0")) // + .addDependencies(EvcsCluster.dependency(t, this.componentManager, this.componentUtil, maxHardwarePowerPerPhase, + evcsId)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java index d082b46e052..586b8666506 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/KebaEvcs.java @@ -39,6 +39,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a Keba evcs App. @@ -136,18 +137,15 @@ protected ThrowingTriFunction, L .build())// ); - var ips = Lists.newArrayList(// - new InterfaceConfiguration("eth0") // - .addIp("Evcs", "192.168.25.10/24") // - ); - - return new AppConfiguration(// - components, // - Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0"), // - ip.startsWith("192.168.25.") ? ips : null, // - EvcsCluster.dependency(t, this.componentManager, this.componentUtil, maxHardwarePowerPerPhase, - evcsId) // - ); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(ctrlEvcsId, "ctrlBalancing0")) // + .throwingOnlyIf(ip.startsWith("192.168.25."), + b -> b.addTask(Tasks.staticIp(new InterfaceConfiguration("eth0") // + .addIp("Evcs", "192.168.25.10/24")))) // + .addDependencies(EvcsCluster.dependency(t, this.componentManager, this.componentUtil, + maxHardwarePowerPerPhase, evcsId)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoNextEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoNextEvcs.java index d6df7b7fd66..de00b79b123 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoNextEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoNextEvcs.java @@ -41,6 +41,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a Webasto Next evcs App. @@ -156,13 +157,12 @@ protected ThrowingTriFunction, L .build()) // ); - return new AppConfiguration(// - components, // - Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0"), // - null, // - EvcsCluster.dependency(t, this.componentManager, this.componentUtil, maxHardwarePowerPerPhase, - evcsId) // - ); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(ctrlEvcsId, "ctrlBalancing0")) // + .addDependencies(EvcsCluster.dependency(t, this.componentManager, this.componentUtil, + maxHardwarePowerPerPhase, evcsId)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoUniteEvcs.java b/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoUniteEvcs.java index e781332e983..c08ccbe1d71 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoUniteEvcs.java +++ b/io.openems.edge.core/src/io/openems/edge/app/evcs/WebastoUniteEvcs.java @@ -41,6 +41,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a Webasto Unite evcs App. @@ -156,13 +157,12 @@ protected ThrowingTriFunction, L .build()) // ); - return new AppConfiguration(// - components, // - Lists.newArrayList(ctrlEvcsId, "ctrlBalancing0"), // - null, // - EvcsCluster.dependency(t, this.componentManager, this.componentUtil, maxHardwarePowerPerPhase, - evcsId) // - ); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(ctrlEvcsId, "ctrlBalancing0")) // + .addDependencies(EvcsCluster.dependency(t, this.componentManager, this.componentUtil, + maxHardwarePowerPerPhase, evcsId)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java b/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java index 1932d92f04b..1665046364b 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java +++ b/io.openems.edge.core/src/io/openems/edge/app/hardware/KMtronic8Channel.java @@ -36,6 +36,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a App for KMtronic 8-Channel Relay. @@ -127,16 +128,12 @@ protected ThrowingTriFunction, L .build())// ); - final var ips = Lists.newArrayList(// - new InterfaceConfiguration("eth0") // - .addIp("Relay", "192.168.1.198/28") // - ); - - return new AppConfiguration(// - comp, // - null, // - ip.startsWith("192.168.1.") ? ips : null // - ); + return AppConfiguration.create() // + .addTask(Tasks.component(comp)) // + .throwingOnlyIf(ip.startsWith("192.168.1."), + b -> b.addTask(Tasks.staticIp(new InterfaceConfiguration("eth0") // + .addIp("Relay", "192.168.1.198/28")))) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java b/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java index 9974030ca51..d9689b7b72d 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/CombinedHeatAndPower.java @@ -44,6 +44,7 @@ import io.openems.edge.core.appmanager.Type.Parameter.BundleProvider; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; import io.openems.edge.core.appmanager.dependency.DependencyUtil; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** @@ -152,7 +153,9 @@ protected ThrowingTriFunction, L if (instanceIdOfRelay == null) { // relay may be created but not as a app - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); } var dependencies = Lists.newArrayList(new DependencyDeclaration("RELAY", // @@ -165,7 +168,10 @@ protected ThrowingTriFunction, L .setSpecificInstanceId(instanceIdOfRelay) // .build())); - return new AppConfiguration(components, null, null, dependencies); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addDependencies(dependencies) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java index 3ec9607b394..cf80e8d1547 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatPump.java @@ -43,6 +43,7 @@ import io.openems.edge.core.appmanager.Type.Parameter.BundleProvider; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; import io.openems.edge.core.appmanager.dependency.DependencyUtil; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** @@ -147,7 +148,9 @@ protected ThrowingTriFunction, L if (appIdOfRelay == null) { // relay may be created but not as an app - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); } final var dependencies = List.of(new DependencyDeclaration("RELAY", // @@ -160,7 +163,10 @@ protected ThrowingTriFunction, L .setSpecificInstanceId(appIdOfRelay) // .build())); - return new AppConfiguration(components, null, null, dependencies); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addDependencies(dependencies) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java index 33b23dc89ec..0f7abbe5a69 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java +++ b/io.openems.edge.core/src/io/openems/edge/app/heat/HeatingElement.java @@ -48,6 +48,7 @@ import io.openems.edge.core.appmanager.Type.Parameter.BundleProvider; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; import io.openems.edge.core.appmanager.dependency.DependencyUtil; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; import io.openems.edge.core.appmanager.validator.ValidatorConfig; @@ -190,7 +191,9 @@ protected ThrowingTriFunction, L if (appIdOfRelay == null) { // relay may be created but not as a app - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); } final var dependencies = Lists.newArrayList(new DependencyDeclaration("RELAY", // @@ -204,7 +207,10 @@ protected ThrowingTriFunction, L .build()) // ); - return new AppConfiguration(components, null, null, dependencies); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addDependencies(dependencies) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java index b15d04696dc..af004f25d21 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome.java @@ -45,6 +45,7 @@ import io.openems.edge.core.appmanager.OpenemsAppPermissions; import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.Exp; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; import io.openems.edge.core.appmanager.formly.enums.InputType; @@ -378,7 +379,11 @@ AppConfiguration, OpenemsNamedException> appConfigurationFactory() { dependencies.add(acType.getDependency(modbusIdExternal)); } - return new AppConfiguration(components, schedulerExecutionOrder, null, dependencies); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(schedulerExecutionOrder)) // + .addDependencies(dependencies) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java index 22c546600be..5ea2fb18123 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java +++ b/io.openems.edge.core/src/io/openems/edge/app/integratedsystem/FeneconHome20.java @@ -71,6 +71,7 @@ import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.Exp; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; @@ -277,7 +278,11 @@ protected ThrowingTriFunction, L .addProperty("type", type) // .build())); - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/KdkMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/KdkMeter.java index db8afccdd5d..7a5c954a516 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/KdkMeter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/KdkMeter.java @@ -40,6 +40,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a App for a Kdk meter. @@ -142,7 +143,9 @@ protected ThrowingTriFunction, L .build()) // ); - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/MicrocareSdm630Meter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/MicrocareSdm630Meter.java index 8b8f579e0d5..1e52ae9c544 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/MicrocareSdm630Meter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/MicrocareSdm630Meter.java @@ -40,6 +40,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a Microcare SDM630 meter App. @@ -147,7 +148,9 @@ protected ThrowingTriFunction, L .build()) // ); - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java b/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java index 957e3173d01..79ea53eaf88 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/meter/SocomecMeter.java @@ -39,6 +39,7 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a App for a Socomec meter. @@ -140,7 +141,9 @@ protected ThrowingTriFunction, L .build()) // ); - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PeakShaving.java b/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PeakShaving.java index 763c9c3e9a3..bbd22c223fc 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PeakShaving.java +++ b/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PeakShaving.java @@ -35,6 +35,9 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.validator.Checkables; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a symmetric peak shaving app. @@ -163,10 +166,18 @@ protected ThrowingTriFunction, L .addProperty("rechargePower", rechargePower) // .build())); - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } + @Override + protected ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setCompatibleCheckableConfigs(Checkables.checkHome().invert()); + } + @Override protected Property[] propertyValues() { return Property.values(); diff --git a/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PhaseAccuratePeakShaving.java b/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PhaseAccuratePeakShaving.java index 306dc49e3ac..366c481fd44 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PhaseAccuratePeakShaving.java +++ b/io.openems.edge.core/src/io/openems/edge/app/peakshaving/PhaseAccuratePeakShaving.java @@ -36,6 +36,9 @@ import io.openems.edge.core.appmanager.Type; import io.openems.edge.core.appmanager.Type.Parameter; import io.openems.edge.core.appmanager.Type.Parameter.BundleParameter; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.validator.Checkables; +import io.openems.edge.core.appmanager.validator.ValidatorConfig; /** * Describes a asymmetric peak shaving app. @@ -165,10 +168,18 @@ protected ThrowingTriFunction, L .addProperty("rechargePower", rechargePower) // .build())); - return new AppConfiguration(components); + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } + @Override + protected ValidatorConfig.Builder getValidateBuilder() { + return ValidatorConfig.create() // + .setCompatibleCheckableConfigs(Checkables.checkHome().invert()); + } + @Override protected Property[] propertyValues() { return Property.values(); diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/FroniusPvInverter.java b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/FroniusPvInverter.java index 9da6347aa4a..df735a235a5 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/FroniusPvInverter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/FroniusPvInverter.java @@ -26,6 +26,7 @@ import io.openems.edge.core.appmanager.Nameable; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a App for Fronius PV-Inverter. @@ -87,7 +88,9 @@ protected ThrowingTriFunction, L b -> b.addProperty("modbusUnitId", modbusUnitId) // .addProperty("phase", phase), null); - return new AppConfiguration(components); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SolarEdgePvInverter.java b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SolarEdgePvInverter.java index 7134987434b..0b84c694992 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SolarEdgePvInverter.java +++ b/io.openems.edge.core/src/io/openems/edge/app/pvinverter/SolarEdgePvInverter.java @@ -24,6 +24,7 @@ import io.openems.edge.core.appmanager.Nameable; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Describes a App for SolarEdge PV-Inverter. @@ -81,7 +82,9 @@ protected ThrowingTriFunction, L final var alias = this.getString(p, l, Property.ALIAS); - var components = TimeOfUseProps.getComponents(t, ctrlEssTimeOfUseTariffId, alias, "TimeOfUseTariff.Awattar", - this.getName(l), timeOfUseTariffProviderId, null); - - return new AppConfiguration(components, Lists.newArrayList(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")); + var components = Lists.newArrayList(// + new EdgeConfig.Component(ctrlEssTimeOfUseTariffId, alias, + "Controller.Ess.Time-Of-Use-Tariff", JsonUtils.buildJsonObject() // + .addProperty("ess.id", "ess0") // + .build()), // + new EdgeConfig.Component(timeOfUseTariffProviderId, this.getName(l), "TimeOfUseTariff.Awattar", + JsonUtils.buildJsonObject() // + .build())// + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java new file mode 100644 index 00000000000..ffaca7888c9 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java @@ -0,0 +1,196 @@ +package io.openems.edge.app.timeofusetariff; + +import java.util.Map; +import java.util.function.Function; + +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Reference; + +import com.google.common.collect.Lists; +import com.google.gson.JsonElement; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.function.ThrowingTriFunction; +import io.openems.common.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.app.common.props.CommonProps; +import io.openems.edge.app.enums.OptionsFactory; +import io.openems.edge.app.enums.TranslatableEnum; +import io.openems.edge.app.timeofusetariff.EntsoE.Property; +import io.openems.edge.common.component.ComponentManager; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AbstractOpenemsAppWithProps; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppDef; +import io.openems.edge.core.appmanager.AppDescriptor; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.Nameable; +import io.openems.edge.core.appmanager.OpenemsApp; +import io.openems.edge.core.appmanager.OpenemsAppCardinality; +import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; + +/** + * Describes a App for ENTSO-E. + * + *

+  {
+    "appId":"App.TimeOfUseTariff.ENTSO-E",
+    "alias":"ENTSO-E",
+    "instanceId": UUID,
+    "image": base64,
+    "properties":{
+    	"CTRL_ESS_TIME_OF_USE_TARIFF_ID": "ctrlEssTimeOfUseTariff0",
+    	"TIME_OF_USE_TARIFF_PROVIDER_ID": "timeOfUseTariff0",
+    	"BIDDING_ZONE": {@link BiddingZone},
+    	"CONTROL_MODE": {@link ControlMode}
+    },
+    "appDescriptor": {
+    	"websiteUrl": {@link AppDescriptor#getWebsiteUrl()}
+    }
+  }
+ * 
+ */ +@org.osgi.service.component.annotations.Component(name = "App.TimeOfUseTariff.ENTSO-E") +public class EntsoE extends AbstractOpenemsAppWithProps + implements OpenemsApp { + // TODO provide image in folder + + public static enum Property implements Type, Nameable { + // Component-IDs + CTRL_ESS_TIME_OF_USE_TARIFF_ID(AppDef.componentId("ctrlEssTimeOfUseTariff0")), // + TIME_OF_USE_TARIFF_PROVIDER_ID(AppDef.componentId("timeOfUseTariff0")), // + + // Properties + ALIAS(CommonProps.alias()), // + // TODO make this an Enum + BIDDING_ZONE(AppDef.of(EntsoE.class)// + .setTranslatedLabelWithAppPrefix(".biddingZone.label") // + .setTranslatedDescriptionWithAppPrefix(".biddingZone.description") // + .setField(JsonFormlyUtil::buildSelectFromNameable, (app, property, l, parameter, field) -> { + field.setOptions(BiddingZone.optionsFactory(), l); + field.isRequired(true); + })); + + private final AppDef def; + + private Property(AppDef def) { + this.def = def; + } + + @Override + public Property self() { + return this; + } + + @Override + public AppDef def() { + return this.def; + } + + @Override + public Function, Type.Parameter.BundleParameter> getParamter() { + return Type.Parameter.functionOf(AbstractOpenemsApp::getTranslationBundle); + } + } + + @Activate + public EntsoE(@Reference ComponentManager componentManager, ComponentContext context, + @Reference ConfigurationAdmin cm, @Reference ComponentUtil componentUtil) { + super(componentManager, context, cm, componentUtil); + } + + @Override + protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appPropertyConfigurationFactory() { + return (t, p, l) -> { + final var ctrlEssTimeOfUseTariffId = this.getId(t, p, Property.CTRL_ESS_TIME_OF_USE_TARIFF_ID); + final var timeOfUseTariffProviderId = this.getId(t, p, Property.TIME_OF_USE_TARIFF_PROVIDER_ID); + + final var alias = this.getString(p, l, Property.ALIAS); + final var biddingZone = this.getString(p, l, Property.BIDDING_ZONE); + + var components = Lists.newArrayList(// + new EdgeConfig.Component(ctrlEssTimeOfUseTariffId, alias, "Controller.Ess.Time-Of-Use-Tariff", + JsonUtils.buildJsonObject() // + .addProperty("ess.id", "ess0") // + .build()), // + new EdgeConfig.Component(timeOfUseTariffProviderId, this.getName(l), "TimeOfUseTariff.ENTSO-E", + JsonUtils.buildJsonObject() // + .build())// + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")) // + .build(); + }; + } + + @Override + public AppDescriptor getAppDescriptor() { + return AppDescriptor.create() // + .setWebsiteUrl("https://fenecon.de/fenecon-fems/fems-app-zeitvariabler-stromtarif/") // + .build(); + } + + @Override + public OpenemsAppCategory[] getCategories() { + return new OpenemsAppCategory[] { OpenemsAppCategory.TIME_OF_USE_TARIFF }; + } + + @Override + protected Property[] propertyValues() { + return Property.values(); + } + + @Override + public OpenemsAppCardinality getCardinality() { + return OpenemsAppCardinality.SINGLE_IN_CATEGORY; + } + + @Override + protected EntsoE getApp() { + return this; + } + + public enum BiddingZone implements TranslatableEnum { + GERMANY("germany"), // + AUSTRIA("austria"), // + SWEDEN_SE1("sweden_se1"), // + SWEDEN_SE2("sweden_se2"), // + SWEDEN_SE3("sweden_se3"), // + SWEDEN_SE4("sweden_se4"), // + ; + + private static final String TRANSLATION_PREFIX = "App.TimeOfUseTariff.ENTSO-E.biddingZone.option."; + + private final String translationKey; + + private BiddingZone(String translationKey) { + this.translationKey = TRANSLATION_PREFIX + translationKey; + } + + @Override + public final String getTranslation(Language l) { + final var bundle = AbstractOpenemsApp.getTranslationBundle(l); + return TranslationUtil.getTranslation(bundle, this.translationKey); + } + + /** + * Creates a {@link OptionsFactory} of this enum. + * + * @return the {@link OptionsFactory} + */ + public static final OptionsFactory optionsFactory() { + return OptionsFactory.of(values()); + } + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java index 3244ea30e6e..1ceec22a60c 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/StromdaoCorrently.java @@ -14,6 +14,8 @@ import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.function.ThrowingTriFunction; import io.openems.common.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; import io.openems.edge.app.common.props.CommonProps; import io.openems.edge.app.timeofusetariff.StromdaoCorrently.Property; import io.openems.edge.common.component.ComponentManager; @@ -29,6 +31,7 @@ import io.openems.edge.core.appmanager.OpenemsAppCardinality; import io.openems.edge.core.appmanager.OpenemsAppCategory; import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; import io.openems.edge.core.appmanager.formly.enums.InputType; @@ -109,10 +112,21 @@ protected ThrowingTriFunction, L final var alias = this.getString(p, l, Property.ALIAS); final var zipCode = this.getString(p, l, Property.ZIP_CODE); - var comp = TimeOfUseProps.getComponents(t, ctrlEssTimeOfUseTariffId, alias, "TimeOfUseTariff.Corrently", - this.getName(l), timeOfUseTariffProviderId, b -> b.addPropertyIfNotNull("zipcode", zipCode)); - - return new AppConfiguration(comp, Lists.newArrayList(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")); + var components = Lists.newArrayList(// + new EdgeConfig.Component(ctrlEssTimeOfUseTariffId, alias, "Controller.Ess.Time-Of-Use-Tariff", + JsonUtils.buildJsonObject() // + .addProperty("ess.id", "ess0") // + .addPropertyIfNotNull("zipcode", zipCode) // + .build()), // + new EdgeConfig.Component(timeOfUseTariffProviderId, this.getName(l), "TimeOfUseTariff.Corrently", + JsonUtils.buildJsonObject() // + .build())// + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java index f0944c4561b..d7ea3c4cd8d 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/Tibber.java @@ -17,6 +17,8 @@ import io.openems.common.exceptions.OpenemsException; import io.openems.common.function.ThrowingTriFunction; import io.openems.common.session.Language; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; import io.openems.edge.app.common.props.CommonProps; import io.openems.edge.app.timeofusetariff.Tibber.Property; import io.openems.edge.common.component.ComponentManager; @@ -32,6 +34,7 @@ import io.openems.edge.core.appmanager.OpenemsAppCardinality; import io.openems.edge.core.appmanager.OpenemsAppCategory; import io.openems.edge.core.appmanager.Type; +import io.openems.edge.core.appmanager.dependency.Tasks; import io.openems.edge.core.appmanager.formly.JsonFormlyUtil; /** @@ -116,11 +119,21 @@ protected ThrowingTriFunction, L throw new OpenemsException("Access Token is required!"); } - var comp = TimeOfUseProps.getComponents(t, ctrlEssTimeOfUseTariffId, alias, "TimeOfUseTariff.Tibber", - this.getName(l), timeOfUseTariffProviderId, - b -> b.addPropertyIfNotNull("accessToken", accessToken)); - - return new AppConfiguration(comp, Lists.newArrayList(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")); + var components = Lists.newArrayList(// + new EdgeConfig.Component(ctrlEssTimeOfUseTariffId, alias, "Controller.Ess.Time-Of-Use-Tariff", + JsonUtils.buildJsonObject() // + .addProperty("ess.id", "ess0") // + .addPropertyIfNotNull("accessToken", accessToken) // + .build()), // + new EdgeConfig.Component(timeOfUseTariffProviderId, this.getName(l), "TimeOfUseTariff.Corrently", + JsonUtils.buildJsonObject() // + .build())// + ); + + return AppConfiguration.create() // + .addTask(Tasks.component(components)) // + .addTask(Tasks.scheduler(ctrlEssTimeOfUseTariffId, "ctrlBalancing0")) // + .build(); }; } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsApp.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsApp.java index 915d1fcea9b..8c8b953db9c 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsApp.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsApp.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.ResourceBundle; @@ -27,13 +26,10 @@ import io.openems.common.function.ThrowingFunction; import io.openems.common.function.ThrowingTriFunction; import io.openems.common.session.Language; -import io.openems.common.types.EdgeConfig; import io.openems.common.types.EdgeConfig.Component; import io.openems.common.utils.JsonUtils; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.user.User; -import io.openems.edge.core.appmanager.dependency.Dependency; -import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; import io.openems.edge.core.appmanager.validator.CheckCardinality; import io.openems.edge.core.appmanager.validator.Checkable; import io.openems.edge.core.appmanager.validator.ValidatorConfig; @@ -68,21 +64,6 @@ protected AbstractOpenemsApp(ComponentManager componentManager, ComponentContext AppConfiguration, // OpenemsNamedException> appPropertyConfigurationFactory(); - protected final void assertCheckables(ConfigurationTarget t, Checkable... checkables) throws OpenemsNamedException { - if (!t.isAddOrUpdate()) { - return; - } - final List errors = new ArrayList<>(); - for (Checkable checkable : checkables) { - if (!checkable.check()) { - errors.add(checkable.getErrorMessage(Language.DEFAULT)); - } - } - if (!errors.isEmpty()) { - throw new OpenemsException(errors.stream().collect(Collectors.joining(";"))); - } - } - /** * Gets the {@link AppConfiguration} for the given properties. * @@ -175,44 +156,6 @@ protected String getId(ConfigurationTarget t, Map map, PR return this.getValueOrDefault(map, p, defaultId); } - /** - * Validate the App configuration. - * - * @param jProperties a JsonObject holding the App properties - * @param dependecies the dependencies of the current instance - * @return a list of validation errors. Empty list says 'no errors' - */ - protected List getValidationErrors(JsonObject jProperties, List dependecies) { - final var errors = new ArrayList(); - - final var properties = this.convertToMap(errors, jProperties); - final var appConfiguration = this.configuration(errors, ConfigurationTarget.VALIDATE, null, properties); - if (appConfiguration == null) { - return errors; - } - - final var edgeConfig = this.componentManager.getEdgeConfig(); - - this.validateComponentConfigurations(errors, edgeConfig, appConfiguration); - this.validateScheduler(errors, edgeConfig, appConfiguration); - - try { - var appManager = (AppManagerImpl) this.componentManager.getComponent(AppManager.SINGLETON_COMPONENT_ID); - this.validateDependecies(errors, dependecies, appConfiguration.dependencies, appManager); - } catch (OpenemsNamedException e) { - // AppManager not found - errors.add("No AppManager reachable!"); - } - - // TODO remove 'if' if it works on windows - // changing network settings only works on linux - if (!System.getProperty("os.name").startsWith("Windows")) { - this.validateIps(errors, edgeConfig, appConfiguration); - } - - return errors; - } - @Override public final ValidatorConfig getValidatorConfig() { Map properties = new TreeMap<>(); @@ -279,230 +222,6 @@ public boolean hasProperty(String property) { return null; } - @Override - public void validate(OpenemsAppInstance instance) throws OpenemsNamedException { - var errors = this.getValidationErrors(instance.properties, instance.dependencies); - if (!errors.isEmpty()) { - var error = errors.stream().collect(Collectors.joining("|")); - throw new OpenemsException(error); - } - } - - /** - * Compare actual and expected Components. - * - * @param errors a collection of validation errors - * @param actualEdgeConfig the currently active {@link EdgeConfig} - * @param expectedAppConfiguration the expected {@link AppConfiguration} - */ - private void validateComponentConfigurations(ArrayList errors, EdgeConfig actualEdgeConfig, - AppConfiguration expectedAppConfiguration) { - var missingComponents = new ArrayList(); - for (var expectedComponent : expectedAppConfiguration.components) { - var componentId = expectedComponent.getId(); - - // Get Actual Component Configuration - Component actualComponent; - var tempFoundComponent = actualEdgeConfig.getComponent(componentId); - if (tempFoundComponent.isEmpty()) { - missingComponents.add(expectedComponent); - continue; - } else { - actualComponent = tempFoundComponent.get(); - } - // ALIAS should not be validated because it can be different depending on the - // language - ComponentUtilImpl.isSameConfigurationWithoutAlias(errors, expectedComponent, actualComponent); - } - - if (!missingComponents.isEmpty()) { - errors.add("Missing Component" // - + (missingComponents.size() > 1 ? "s" : "") + ":" // - + missingComponents.stream() // - .map(c -> c.getId() + "[" + c.getFactoryId() + "]") // - .collect(Collectors.joining(","))); - } - } - - private void validateIps(ArrayList errors, EdgeConfig actualEdgeConfig, - AppConfiguration expectedAppConfiguration) { - if (expectedAppConfiguration.ips.isEmpty()) { - return; - } - - try { - var interfaces = this.componentUtil.getInterfaces(); - expectedAppConfiguration.ips.stream() // - .forEach(i -> { - var existingInterface = interfaces.stream() // - .filter(t -> t.getName().equals(i.interfaceName)) // - .findFirst().orElse(null); - - if (existingInterface == null) { - errors.add("Interface '" + i.interfaceName + "' not found."); - return; - } - - var missingIps = i.getIps().stream() // - .filter(ip -> { - if (existingInterface.getAddresses().getValue().stream() // - .anyMatch(existingIp -> existingIp.isInSameNetwork(ip))) { - return false; - } - return true; - }).collect(Collectors.toList()); - - if (missingIps.isEmpty()) { - return; - } - errors.add("Address '" - + missingIps.stream().map(t -> t.toString()).collect(Collectors.joining(", ")) + "' " - + (missingIps.size() > 1 ? "are" : "is") + " not added on " + i.interfaceName); - }); - } catch (NullPointerException | IllegalStateException | OpenemsNamedException e) { - errors.add("Can not validate host config!"); - errors.add(e.getMessage()); - } - } - - /** - * Validates the execution order in the Scheduler. - * - * @param errors a collection of validation errors - * @param actualEdgeConfig the currently active {@link EdgeConfig} - * @param expectedAppConfiguration the expected {@link AppConfiguration} - */ - private void validateScheduler(ArrayList errors, EdgeConfig actualEdgeConfig, - AppConfiguration expectedAppConfiguration) { - if (expectedAppConfiguration.schedulerExecutionOrder.isEmpty()) { - return; - } - - // Prepare Queue - var controllers = new LinkedList<>(this.componentUtil.removeIdsWhichNotExist( - expectedAppConfiguration.schedulerExecutionOrder, expectedAppConfiguration.components)); - - if (controllers.isEmpty()) { - return; - } - - List schedulerIds; - try { - schedulerIds = this.componentUtil.getSchedulerIds(); - } catch (OpenemsNamedException e) { - errors.add(e.getMessage()); - return; - } - - var nextControllerId = controllers.poll(); - - // Remove found Controllers from Queue in order - for (var controllerId : schedulerIds) { - if (controllerId.equals(nextControllerId)) { - nextControllerId = controllers.poll(); - } - } - if (nextControllerId != null) { - errors.add("Controller [" + nextControllerId + "] is not/wrongly configured in Scheduler"); - } - } - - private void validateDependecies(List errors, List configDependencies, - List neededDependencies, AppManagerImpl appManager) { - - // find dependencies that are not in config - var notRegisteredDependencies = neededDependencies.stream().filter( - t -> configDependencies == null || !configDependencies.stream().anyMatch(o -> o.key.equals(t.key))) - .collect(Collectors.toList()); - - // check if exactly one app is available of the needed appId - for (var dependency : notRegisteredDependencies) { - List minErrors = null; - for (var appConfig : dependency.appConfigs) { - var appConfigErrors = new LinkedList(); - if (appConfig.specificInstanceId != null) { - try { - final var instance = appManager.findInstanceByIdOrError(appConfig.specificInstanceId); - final var app = appManager.findAppById(instance.appId); - final var props = app.map(a -> { - try { - return AbstractOpenemsApp.fillUpProperties(a, instance.properties); - } catch (UnsupportedOperationException e) { - return instance.properties; - } - }).orElse(instance.properties); - checkProperties(errors, props, appConfig, dependency.key); - } catch (OpenemsNamedException e) { - appConfigErrors.add(e.getMessage()); - } - } else { - - var list = appManager.getInstantiatedApps().stream().filter(t -> t.appId.equals(appConfig.appId)) - .collect(Collectors.toList()); - if (list.size() != 1) { - errors.add("Missing dependency with Key[" + dependency.key + "] needed App[" + appConfig.appId - + "]"); - } else { - checkProperties(errors, list.get(0).properties, appConfig, dependency.key); - } - } - - if (minErrors == null || minErrors.size() > appConfigErrors.size()) { - minErrors = appConfigErrors; - } - } - - errors.addAll(minErrors); - } - - if (configDependencies == null) { - return; - } - // check if dependency apps are available - for (var dependency : configDependencies) { - final OpenemsAppInstance appInstance; - try { - appInstance = appManager.findInstanceByIdOrError(dependency.instanceId); - } catch (OpenemsNamedException e) { - errors.add(e.getMessage()); - continue; - } - var dd = neededDependencies.stream().filter(d -> d.key.equals(dependency.key)).findAny(); - if (dd.isEmpty()) { - errors.add("Can not get DependencyDeclaration of Dependency[" + dependency.key + "]"); - continue; - } - - // get app config - var appConfig = dd.get().appConfigs.stream() // - .filter(c -> c.specificInstanceId != null) // - .filter(c -> c.specificInstanceId.equals(appInstance.instanceId)).findAny(); - - if (appConfig.isEmpty()) { - appConfig = dd.get().appConfigs.stream() // - .filter(c -> c.appId != null) // - .filter(c -> c.appId.equals(appInstance.appId)).findAny(); - - if (appConfig.isEmpty()) { - errors.add("Can not get DependencyAppConfig of Dependency[" + dependency.key + "]"); - continue; - } - } - - var copy = appInstance.properties.deepCopy(); - try { - final var app = appManager.findAppByIdOrError(appInstance.appId); - copy = AbstractOpenemsApp.fillUpProperties(app, appInstance.properties); - } catch (OpenemsNamedException e) { - errors.add(e.getMessage()); - } catch (UnsupportedOperationException e) { - // get props not supported - } - // when available check properties - checkProperties(errors, copy, appConfig.get(), dependency.key); - } - } - /** * Creates a copy of the original configuration and fills up properties which * are binded bidirectional. @@ -538,28 +257,6 @@ public static JsonObject fillUpProperties(// return copy; } - private static final void checkProperties(List errors, JsonObject actualAppProperties, - DependencyDeclaration.AppDependencyConfig appDependencyConfig, String dependecyKey) { - if (appDependencyConfig == null) { - errors.add("SubApp with Key[" + dependecyKey + "] not found!"); - return; - } - - for (var property : appDependencyConfig.properties.entrySet()) { - var actualValue = actualAppProperties.get(property.getKey()); - if (actualValue == null) { - errors.add("Value for Key[" + property.getKey() + "] not found!"); - continue; - } - var actual = actualValue.toString().replace("\"", ""); - var needed = property.getValue().toString().replace("\"", ""); - if (!actual.equals(needed)) { - errors.add("Value for Key[" + property.getKey() + "] does not match: expected[" + needed + "] actual[" - + actual + "] !"); - } - } - } - @Override public OpenemsAppPermissions getAppPermissions() { return OpenemsAppPermissions.create().build(); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsAppWithProps.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsAppWithProps.java index 398b38b5c40..fa7043d8378 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsAppWithProps.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AbstractOpenemsAppWithProps.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -24,14 +23,13 @@ import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.Type.GetParameterValues; -import io.openems.edge.core.appmanager.dependency.Dependency; import io.openems.edge.core.appmanager.flag.Flag; import io.openems.edge.core.appmanager.flag.Flags; public abstract class AbstractOpenemsAppWithProps, // PROPERTY extends Type & Nameable, // - PARAMETER // + PARAMETER // > extends AbstractOpenemsApp implements OpenemsApp { protected AbstractOpenemsAppWithProps(ComponentManager componentManager, ComponentContext componentContext, @@ -203,17 +201,6 @@ public AppConfiguration getAppConfiguration(// ); } - @Override - protected List getValidationErrors(// - final JsonObject jProperties, // - final List dependecies // - ) { - return super.getValidationErrors(// - AbstractOpenemsApp.fillUpProperties(this, jProperties), // - dependecies // - ); - } - private Function mapDefaultValue(// final PROPERTY property, // final PARAMETER parameter // diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppConfiguration.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppConfiguration.java index d074921dde9..365a896f6ec 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppConfiguration.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppConfiguration.java @@ -1,43 +1,187 @@ package io.openems.edge.core.appmanager; +import static java.util.Collections.emptyList; + import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; -import io.openems.common.types.EdgeConfig.Component; +import io.openems.common.types.EdgeConfig; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.Task; +import io.openems.edge.core.appmanager.dependency.Tasks; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.ComponentAggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.ComponentConfiguration; + +public record AppConfiguration(List> tasks, List dependencies) { + + public static final class AppConfigurationBuilder implements Self, + OnlyIf, ThrowingOnlyIf { + + public final List> tasks = new ArrayList<>(); + public final List dependencies = new ArrayList<>(); + + private AppConfigurationBuilder() { + super(); + } + + /** + * Adds a {@link Task} to the configuration. + * + *

+ * Create tasks with the static methods in the {@link Tasks} class + * + * @param task the task to add + * @return this + */ + public AppConfigurationBuilder addTask(Task task) { + this.tasks.add(task); + return this; + } + + /** + * Adds a single dependency to the {@link AppConfiguration}. + * + * @param dependencyDeclaration the dependency to add + * @return this + */ + public AppConfigurationBuilder addDependency(DependencyDeclaration dependencyDeclaration) { + this.dependencies.add(dependencyDeclaration); + return this; + } + + /** + * Adds the dependencies to the {@link AppConfiguration}. + * + * @param dependencyDeclaration the dependencies to add + * @return this + */ + public AppConfigurationBuilder addDependencies(DependencyDeclaration... dependencyDeclaration) { + Stream.of(dependencyDeclaration).forEach(this.dependencies::add); + return this; + } -public class AppConfiguration { - // the components the app needs - public final List components; - // the execute order in the scheduler of the components - public final List schedulerExecutionOrder; - // the static ips in the Network configuration to access different networks - public final List ips; + /** + * Adds the dependencies to the {@link AppConfiguration}. + * + * @param dependencyDeclaration the dependencies to add + * @return this + */ + public AppConfigurationBuilder addDependencies(Collection dependencyDeclaration) { + this.dependencies.addAll(dependencyDeclaration); + return this; + } - public final List dependencies; + public final AppConfiguration build() { + return new AppConfiguration(// + Collections.unmodifiableList(this.tasks), // + Collections.unmodifiableList(this.dependencies) // + ); + } - public AppConfiguration() { - this(null); + @Override + public AppConfigurationBuilder self() { + return this; + } + + } + + /** + * Creates a builder for creating an {@link AppConfiguration}. + * + * @return the builder + */ + public static AppConfigurationBuilder create() { + return new AppConfigurationBuilder(); + } + + /** + * Creates an empty {@link AppConfiguration}. + * + * @return the configuration + */ + public static AppConfiguration empty() { + return create().build(); + } + + public List getComponents() { + return this.map(ComponentAggregateTask.class, ComponentConfiguration::components) // + .orElse(emptyList()); } - public AppConfiguration(List components) { - this(components, null); + /** + * Gets the configuration for the given task class. + * + *

+ * e. g. if {@link ComponentAggregateTask} is given which is a + * {@link AggregateTask} of {@link ComponentConfiguration} then this + * configuration is returned if defined in the {@link AppConfiguration} else + * null. + * + * @param the type of the configuration + * @param the type of the {@link AggregateTask} + * @param clazz the {@link Class} of the {@link AggregateTask} + * @return the found configuration or null if not defined + */ + @SuppressWarnings("unchecked") + public > C getConfiguration(Class clazz) { + return (C) this.tasks.stream() // + .filter(t -> t.aggregateTaskClass().isAssignableFrom(clazz)) // + .findAny() // + .map(Task::configuration) // + .orElse(null); } - public AppConfiguration(List components, List schedulerExecutionOrder) { - this(components, schedulerExecutionOrder, null); + private , L> Optional map(// + final Class clazz, // + final Function mapper // + ) { + return Optional.ofNullable(this.getConfiguration(clazz)) // + .map(mapper); // } - public AppConfiguration(List components, List schedulerExecutionOrder, - List ips) { - this(components, schedulerExecutionOrder, ips, null); + /** + * Flat maps a attribute from a configuration to a single list. + * + * @param the type of the Configuration + * @param the type of the {@link AggregateTask} + * @param the type of the list + * @param configs the configurations to map + * @param clazz the {@link Class} of the Task + * @param mapper the mapper from the configuration to the type of the list + * @return the list with all instances + */ + public static , L> Stream flatMap(// + final List configs, // + final Class clazz, // + final Function> mapper // + ) { + return Optional.ofNullable(configs) // + .map(c -> { + return c.stream() // + .map(t -> t.getConfiguration(clazz)) // + .filter(Objects::nonNull) // + .map(mapper) // + .flatMap(Collection::stream); + }).orElse(Stream.empty()); } - public AppConfiguration(List components, List schedulerExecutionOrder, - List ips, List dependencies) { - this.components = components != null ? components : new ArrayList<>(); - this.schedulerExecutionOrder = schedulerExecutionOrder != null ? schedulerExecutionOrder : new ArrayList<>(); - this.ips = ips != null ? ips : new ArrayList<>(); - this.dependencies = dependencies != null ? dependencies : new ArrayList<>(); + /** + * Collects all components from the {@link AppConfiguration AppConfigurations} + * and returns them. + * + * @param configs the {@link AppConfiguration AppConfigurations} to get the + * {@link EdgeConfig.Component Components} from + * @return the {@link EdgeConfig.Component Components} + */ + public static List getComponentsFromConfigs(List configs) { + return flatMap(configs, ComponentAggregateTask.class, ComponentConfiguration::components).toList(); } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java index 92a08eca324..a8b53f68b3b 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerImpl.java @@ -20,8 +20,6 @@ import java.util.stream.Collectors; import org.osgi.service.cm.ConfigurationAdmin; -import org.osgi.service.cm.ConfigurationEvent; -import org.osgi.service.cm.ConfigurationListener; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.ComponentServiceObjects; import org.osgi.service.component.annotations.Activate; @@ -77,12 +75,12 @@ property = { // "enabled=true" // }) -public class AppManagerImpl extends AbstractOpenemsComponent - implements AppManager, OpenemsComponent, JsonApi, ConfigurationListener { +public class AppManagerImpl extends AbstractOpenemsComponent implements AppManager, OpenemsComponent, JsonApi { private final Logger log = LoggerFactory.getLogger(this.getClass()); - private final AppValidateWorker worker; + @Reference + private AppValidateWorker appValidateWorker; private final AppInstallWorker appInstallWorker; private final AppSynchronizeWorker appSynchronizeWorker; @@ -124,7 +122,6 @@ public AppManagerImpl() { OpenemsComponent.ChannelId.values(), // AppManager.ChannelId.values() // ); - this.worker = new AppValidateWorker(this); this.appInstallWorker = new AppInstallWorker(this); this.appSynchronizeWorker = new AppSynchronizeWorker(this); } @@ -134,7 +131,7 @@ protected void activate(ComponentContext componentContext, Config config) { super.activate(componentContext, SINGLETON_COMPONENT_ID, SINGLETON_SERVICE_PID, true); this.applyConfig(config); - this.worker.activate(this.id()); + this.appValidateWorker.setConfig(new AppValidateWorker.Config(this::_setDefectiveApp)); this.appInstallWorker.activate(this.id()); this.appSynchronizeWorker.activate(this.id()); @@ -154,12 +151,11 @@ protected void modified(ComponentContext componentContext, Config config) throws super.modified(componentContext, SINGLETON_COMPONENT_ID, SINGLETON_SERVICE_PID, true); this.applyConfig(config); - this.worker.modified(this.id()); this.appInstallWorker.modified(this.id()); this.appSynchronizeWorker.modified(this.id()); this.appInstallWorker.setKeyForFreeApps(config.keyForFreeApps()); - this.worker.triggerNextRun(); + this.appValidateWorker.triggerNextRun(); if (OpenemsComponent.validateSingleton(this.cm, SINGLETON_SERVICE_PID, SINGLETON_COMPONENT_ID)) { return; @@ -170,7 +166,6 @@ protected void modified(ComponentContext componentContext, Config config) throws @Deactivate protected void deactivate() { super.deactivate(); - this.worker.deactivate(); this.appInstallWorker.deactivate(); this.appSynchronizeWorker.deactivate(); } @@ -302,11 +297,6 @@ private void applyConfig(Config config) { } } - @Override - public void configurationEvent(ConfigurationEvent event) { - this.worker.configurationEvent(event); - } - /** * Gets a filter for excluding instances. * @@ -420,7 +410,7 @@ public boolean hasNext() { @Override public String debugLog() { - final var workerLog = this.worker.debugLog(); + final var workerLog = this.appValidateWorker.debugLog(); if (workerLog == null && !this.waitingForModified) { return null; } @@ -548,7 +538,7 @@ public CompletableFuture handleAddAppInstanceRequest(Us // actually install the app return appHelper.installApp(user, tmpInstance, openemsApp); }); - + instance = installedValues.rootInstance; warnings.addAll(installedValues.warnings); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtilImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtilImpl.java index bb001e09b66..55f57e778fc 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtilImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppManagerUtilImpl.java @@ -1,5 +1,7 @@ package io.openems.edge.core.appmanager; +import static java.util.Collections.emptyList; + import java.util.List; import java.util.Optional; import java.util.UUID; @@ -27,17 +29,21 @@ public AppManagerUtilImpl(@Reference ComponentManager componentManager) { @Override public List getInstantiatedApps() { - return this.getAppManagerImpl().getInstantiatedApps(); + return Optional.ofNullable(this.getAppManagerImpl()) // + .map(AppManagerImpl::getInstantiatedApps) // + .orElse(emptyList()); } @Override public Optional findAppById(String id) { - return this.getAppManagerImpl().findAppById(id); + return Optional.ofNullable(this.getAppManagerImpl()) // + .flatMap(t -> t.findAppById(id)); } @Override public Optional findInstanceById(UUID id) { - return this.getAppManagerImpl().findInstanceById(id); + return Optional.ofNullable(this.getAppManagerImpl()) // + .flatMap(t -> t.findInstanceById(id)); } @Override diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppValidateWorker.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppValidateWorker.java index 0f34bc90511..6b84c36da21 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppValidateWorker.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/AppValidateWorker.java @@ -1,16 +1,26 @@ package io.openems.edge.core.appmanager; +import static java.util.stream.Collectors.groupingBy; + import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; +import java.util.HashSet; import java.util.Map; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.osgi.service.cm.ConfigurationEvent; import org.osgi.service.cm.ConfigurationListener; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; + +import com.google.common.collect.Sets; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.worker.AbstractWorker; +import io.openems.edge.core.appmanager.dependency.AppConfigValidator; /** * This Worker constantly validates:. @@ -20,7 +30,14 @@ * OpenEMS Components, IP addresses, Scheduler settings, etc. * */ -public class AppValidateWorker extends AbstractWorker { +@Component(// + service = { ConfigurationListener.class, AppValidateWorker.class } // +) +public class AppValidateWorker extends AbstractWorker implements ConfigurationListener { + + public record Config(Consumer setDefectiveApp) { + + } /* * For INITIAL_CYCLES cycles the distance between two checks is @@ -35,8 +52,6 @@ public class AppValidateWorker extends AbstractWorker { private static final int INITIAL_CYCLE_TIME = 10_000; // in ms private static final int REGULAR_CYCLE_TIME = 60 * 60_000; // in ms - private final AppManagerImpl parent; - /** * Map from App to defect details. */ @@ -44,42 +59,41 @@ public class AppValidateWorker extends AbstractWorker { private int cycleCountDown = AppValidateWorker.INITIAL_CYCLES; - public AppValidateWorker(AppManagerImpl parent) { - this.parent = parent; - } + @Reference + private AppManagerUtil appManagerUtil; - /** - * Called by {@link ConfigurationListener}. - * - * @param event a {@link ConfigurationEvent} - */ - protected void configurationEvent(ConfigurationEvent event) { - // trigger immediate validation on configuration event - this.triggerNextRun(); - } + @Reference + private AppConfigValidator validator; - /** - * Called by parent debugLog. - * - * @return a debug log String or null - */ - protected String debugLog() { - var defectiveApps = this.defectiveApps.entrySet().stream() // - .map(e -> e.getKey() + "[" + e.getValue() + "]") // - .collect(Collectors.joining(" ")); + private Config config; - if (defectiveApps.isEmpty()) { - return null; + @Activate + private void activate() { + this.activate(this.getClass().getSimpleName()); + } - } - return defectiveApps; + @Override + @Deactivate + public void deactivate() { + super.deactivate(); + } + + @Override + public void configurationEvent(ConfigurationEvent event) { + this.triggerNextRun(); } @Override protected void forever() { this.validateApps(); - this.parent._setDefectiveApp(!this.defectiveApps.isEmpty()); + final var config = this.config; + if (config == null) { + return; + } + if (config.setDefectiveApp != null) { + config.setDefectiveApp.accept(!this.defectiveApps.isEmpty()); + } } @Override @@ -98,23 +112,25 @@ public void triggerNextRun() { super.triggerNextRun(); } + public void setConfig(Config config) { + this.config = config; + } + /** - * Validates all Apps. - * - *

- * 'protected' so that it can be used in a JUnit test. + * Called by parent debugLog. * - * @param app the {@link OpenemsApp} - * @param instance the App instance + * @return a debug log String or null */ - protected void validateApp(OpenemsApp app, OpenemsAppInstance instance) { - // Found correct OpenemsApp -> validate - var key = app.getAppId(); - try { - app.validate(instance); - } catch (OpenemsNamedException e1) { - this.defectiveApps.put(key, e1.getMessage()); + protected String debugLog() { + var defectiveApps = this.defectiveApps.entrySet().stream() // + .map(e -> e.getKey() + "[" + e.getValue() + "]") // + .collect(Collectors.joining(" ")); + + if (defectiveApps.isEmpty()) { + return null; + } + return defectiveApps; } /** @@ -124,24 +140,38 @@ protected void validateApp(OpenemsApp app, OpenemsAppInstance instance) { * 'protected' so that it can be used in a JUnit test. */ protected void validateApps() { - var instances = new ArrayList<>(this.parent.instantiatedApps); - for (OpenemsApp app : this.parent.availableApps) { - this.defectiveApps.remove(app.getAppId()); - var removingInstances = new LinkedList<>(); - for (var instantiatedApp : instances) { - if (app.getAppId().equals(instantiatedApp.appId)) { - this.validateApp(app, instantiatedApp); - removingInstances.add(instantiatedApp); + final var unknownApps = new HashSet(); + final var handledKeys = Sets.newHashSet("UNKNOWNAPPS"); + for (var entry : this.appManagerUtil.getInstantiatedApps().stream() // + .collect(groupingBy(t -> t.appId)).entrySet()) { + final var appId = entry.getKey(); + handledKeys.add(appId); + final var app = this.appManagerUtil.findAppById(appId); + if (app.isEmpty()) { + unknownApps.add(appId); + continue; + } + + final var errorsOfApp = new ArrayList(); + for (var instance : entry.getValue()) { + try { + this.validator.validate(instance); + } catch (OpenemsNamedException e) { + errorsOfApp.add(e.getMessage()); } } - instances.removeAll(removingInstances); + if (errorsOfApp.isEmpty()) { + this.defectiveApps.remove(appId); + } else { + this.defectiveApps.put(appId, String.join("; ", errorsOfApp)); + } } - final var unknownApps = "UNKNOWAPPS"; - if (!instances.isEmpty()) { - this.defectiveApps.put(unknownApps, instances.stream().map(t -> t.appId).collect(Collectors.joining("|"))); + if (!unknownApps.isEmpty()) { + this.defectiveApps.put("UNKNOWNAPPS", String.join("|", unknownApps)); } else { - this.defectiveApps.remove(unknownApps); + this.defectiveApps.remove("UNKNOWNAPPS"); } + this.defectiveApps.keySet().removeIf(t -> !handledKeys.contains(t)); } } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtilImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtilImpl.java index 7489b4a118c..6df853bfc6c 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtilImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ComponentUtilImpl.java @@ -693,6 +693,7 @@ public List insertSchedulerOrder(List actualOrder, List return new ArrayList<>(insertOrder); } var order = new ArrayList<>(actualOrder); + insertOrder = new ArrayList(insertOrder); Collections.reverse(insertOrder); var index = actualOrder.size(); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsApp.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsApp.java index 4e731d1a48d..2fe0f327ac6 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsApp.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/OpenemsApp.java @@ -121,13 +121,6 @@ public default Flag[] flags() { return new Flag[] {}; } - /** - * Validate the {@link OpenemsApp}. - * - * @param instance the app instance - */ - public void validate(OpenemsAppInstance instance) throws OpenemsNamedException; - public static final String FALLBACK_IMAGE = """ data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY5\ 1AAABhWlDQ1BJQ0MgUHJvZmlsZQAAKM+VkT1Iw1AUhU9TpVIqgu0g4pChOlkQFXGUKBbBQmkrtOpg8tI/aNKQpLg4Cq4FB38Wqw\ diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ResolveDependencies.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ResolveDependencies.java index b46163eaf5d..e7222904914 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ResolveDependencies.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ResolveDependencies.java @@ -65,7 +65,7 @@ public static void resolveDependencies(User user, AppManagerImpl appManagerImpl, Language.DEFAULT); // check if instance should have dependencies - if (configuration.dependencies == null || configuration.dependencies.isEmpty()) { + if (configuration.dependencies() == null || configuration.dependencies().isEmpty()) { if (instance.dependencies != null && !instance.dependencies.isEmpty()) { LOG.info(String.format("Instance %s has unnecessary dependencies!", instance.instanceId)); } @@ -73,7 +73,7 @@ public static void resolveDependencies(User user, AppManagerImpl appManagerImpl, } // remove satisfied dependencies - for (var dependency : configuration.dependencies) { + for (var dependency : configuration.dependencies()) { // dependency exists if (instance.dependencies != null && instance.dependencies.stream() // .anyMatch(t -> t.key.equals(dependency.key))) { diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/ThrowingOnlyIf.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ThrowingOnlyIf.java new file mode 100644 index 00000000000..5c2a648e160 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/ThrowingOnlyIf.java @@ -0,0 +1,24 @@ +package io.openems.edge.core.appmanager; + +import io.openems.common.function.ThrowingConsumer; + +public interface ThrowingOnlyIf> extends Self { + + /** + * Executes the consumer only if the statement is true. + * + * @param the type of the exception + * @param statement the statement to determine if the consumer should get + * executed + * @param consumer the consumer to execute if the statement is true + * @return this + * @throws E if the consumer throws the specified exception + */ + public default T throwingOnlyIf(boolean statement, ThrowingConsumer consumer) throws E { + if (statement) { + consumer.accept(this.self()); + } + return this.self(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AggregateTask.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AggregateTask.java deleted file mode 100644 index eafd5ae6ece..00000000000 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AggregateTask.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.openems.edge.core.appmanager.dependency; - -import java.util.List; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.types.EdgeConfig; -import io.openems.edge.common.user.User; -import io.openems.edge.core.appmanager.AppConfiguration; - -public interface AggregateTask { - - /** - * Aggregates the given instance. - * - * @param instance the {@link AppConfiguration} of the instance - * @param oldConfig the old configuration of the instance - */ - public void aggregate(AppConfiguration instance, AppConfiguration oldConfig); - - /** - * e. g. creates components that were aggregated by the instances and my also - * delete unused components. - * - * @param user the executing user - * @param otherAppConfigurations the other existing {@link AppConfiguration}s - * @throws OpenemsNamedException on error - */ - public void create(User user, List otherAppConfigurations) throws OpenemsNamedException; - - /** - * e. g. deletes components that were aggregated. - * - * @param user the executing user - * @param otherAppConfigurations the other existing {@link AppConfiguration}s - * @throws OpenemsNamedException on error - */ - public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException; - - /** - * Resets the task. - */ - public void reset(); - - public static interface ComponentAggregateTask extends AggregateTask { - - /** - * Gets the Components that were created. - * - * @return the created {@link EdgeConfig.Component} - */ - public List getCreatedComponents(); - - /** - * Gets the Components that were deleted. - * - * @return the id's of the deleted {@link EdgeConfig.Component} - */ - public List getDeletedComponents(); - - } - - public static interface SchedulerAggregateTask extends AggregateTask { - - } - - public static interface StaticIpAggregateTask extends AggregateTask { - - } - -} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppConfigValidator.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppConfigValidator.java new file mode 100644 index 00000000000..75fba915fd3 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppConfigValidator.java @@ -0,0 +1,211 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; + +import com.google.gson.JsonObject; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.exceptions.OpenemsException; +import io.openems.common.session.Language; +import io.openems.edge.core.appmanager.AbstractOpenemsApp; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.AppManagerUtil; +import io.openems.edge.core.appmanager.ConfigurationTarget; +import io.openems.edge.core.appmanager.OpenemsAppInstance; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; + +@Component(service = AppConfigValidator.class) +public class AppConfigValidator { + + @Reference + private AppManagerUtil appManagerUtil; + + @Reference(// + cardinality = ReferenceCardinality.MULTIPLE, // + policy = ReferencePolicy.DYNAMIC, // + policyOption = ReferencePolicyOption.GREEDY // + ) + private volatile List> tasks; + + public AppConfigValidator() { + } + + @Activate + private void activate() { + + } + + /** + * Validates the expected configuration of an app the actual configuration on + * the system. + * + * @param instance the instance to validate + * @throws OpenemsNamedException on error + */ + public void validate(OpenemsAppInstance instance) throws OpenemsNamedException { + final var configuration = this.appManagerUtil.getAppConfiguration(ConfigurationTarget.VALIDATE, instance, + Language.DEFAULT); + + final var errors = new ArrayList(); + for (var task : configuration.tasks()) { + final var aggregateTask = this.findTaskByClass(task.aggregateTaskClass()); + if (aggregateTask == null) { + errors.add("Missing AggregateTask to validate " + task.aggregateTaskClass().getCanonicalName()); + continue; + } + + validate(aggregateTask, errors, configuration, task.configuration()); + } + + this.validateDependecies(errors, instance.dependencies, configuration.dependencies()); + + if (!errors.isEmpty()) { + throw new OpenemsException(String.join("|", errors)); + } + } + + @SuppressWarnings("unchecked") + private static void validate(AggregateTask aggregateTask, List errors, + AppConfiguration appConfiguration, Object configuration) { + aggregateTask.validate(errors, appConfiguration, (T) configuration); + } + + private AggregateTask findTaskByClass(Class> clazz) { + return this.tasks.stream() // + .filter(t -> clazz.isAssignableFrom(t.getClass())) // + .findAny() // + .orElse(null); + } + + private void validateDependecies(// + List errors, // + List configDependencies, // + List neededDependencies // + ) { + // find dependencies that are not in config + var notRegisteredDependencies = neededDependencies.stream().filter( + t -> configDependencies == null || !configDependencies.stream().anyMatch(o -> o.key.equals(t.key))) + .toList(); + + // check if exactly one app is available of the needed appId + for (var dependency : notRegisteredDependencies) { + List minErrors = null; + for (var appConfig : dependency.appConfigs) { + var appConfigErrors = new LinkedList(); + if (appConfig.specificInstanceId != null) { + try { + final var instance = this.appManagerUtil.findInstanceByIdOrError(appConfig.specificInstanceId); + final var app = this.appManagerUtil.findAppById(instance.appId); + final var props = app.map(a -> { + try { + return AbstractOpenemsApp.fillUpProperties(a, instance.properties); + } catch (UnsupportedOperationException e) { + return instance.properties; + } + }).orElse(instance.properties); + checkProperties(errors, props, appConfig, dependency.key); + } catch (OpenemsNamedException e) { + appConfigErrors.add(e.getMessage()); + } + } else { + var list = this.appManagerUtil.getInstantiatedAppsOfApp(appConfig.appId); + if (list.size() != 1) { + errors.add("Missing dependency with Key[" + dependency.key + "] needed App[" + appConfig.appId + + "]"); + } else { + checkProperties(errors, list.get(0).properties, appConfig, dependency.key); + } + } + + if (minErrors == null || minErrors.size() > appConfigErrors.size()) { + minErrors = appConfigErrors; + } + } + + errors.addAll(minErrors); + } + + if (configDependencies == null) { + return; + } + // check if dependency apps are available + for (var dependency : configDependencies) { + final OpenemsAppInstance appInstance; + try { + appInstance = this.appManagerUtil.findInstanceByIdOrError(dependency.instanceId); + } catch (OpenemsNamedException e) { + errors.add(e.getMessage()); + continue; + } + final var dependencyDeclaration = neededDependencies.stream() // + .filter(d -> d.key.equals(dependency.key)) // + .findAny().orElse(null); + if (dependencyDeclaration == null) { + errors.add("Can not get DependencyDeclaration of Dependency[" + dependency.key + "]"); + continue; + } + + // get app config + var appConfig = dependencyDeclaration.appConfigs.stream() // + .filter(c -> c.specificInstanceId != null) // + .filter(c -> c.specificInstanceId.equals(appInstance.instanceId)) // + .findAny(); + + if (appConfig.isEmpty()) { + appConfig = dependencyDeclaration.appConfigs.stream() // + .filter(c -> c.appId != null) // + .filter(c -> c.appId.equals(appInstance.appId)) // + .findAny(); + + if (appConfig.isEmpty()) { + errors.add("Can not get DependencyAppConfig of Dependency[" + dependency.key + "]"); + continue; + } + } + + var copy = appInstance.properties.deepCopy(); + try { + final var app = this.appManagerUtil.findAppByIdOrError(appInstance.appId); + copy = AbstractOpenemsApp.fillUpProperties(app, appInstance.properties); + } catch (OpenemsNamedException e) { + errors.add(e.getMessage()); + } catch (UnsupportedOperationException e) { + // get props not supported + } + // when available check properties + checkProperties(errors, copy, appConfig.get(), dependency.key); + } + } + + private static final void checkProperties(List errors, JsonObject actualAppProperties, + DependencyDeclaration.AppDependencyConfig appDependencyConfig, String dependecyKey) { + if (appDependencyConfig == null) { + errors.add("SubApp with Key[" + dependecyKey + "] not found!"); + return; + } + + for (var property : appDependencyConfig.properties.entrySet()) { + var actualValue = actualAppProperties.get(property.getKey()); + if (actualValue == null) { + errors.add("Value for Key[" + property.getKey() + "] not found!"); + continue; + } + var actual = actualValue.toString().replace("\"", ""); + var needed = property.getValue().toString().replace("\"", ""); + if (!actual.equals(needed)) { + errors.add("Value for Key[" + property.getKey() + "] does not match: expected[" + needed + "] actual[" + + actual + "] !"); + } + } + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java index a810150f805..6cb81cdb867 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/AppManagerAppHelperImpl.java @@ -1,5 +1,7 @@ package io.openems.edge.core.appmanager.dependency; +import static java.util.stream.Collectors.joining; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -19,6 +21,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -49,11 +52,11 @@ import io.openems.edge.core.appmanager.ComponentUtil; import io.openems.edge.core.appmanager.ComponentUtilImpl; import io.openems.edge.core.appmanager.ConfigurationTarget; -import io.openems.edge.core.appmanager.InterfaceConfiguration; import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppInstance; import io.openems.edge.core.appmanager.TranslationUtil; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration.AppDependencyConfig; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; import io.openems.edge.core.appmanager.validator.Validator; @Component(// @@ -83,12 +86,7 @@ public class AppManagerAppHelperImpl implements AppManagerAppHelper { private final Validator validator; - // tasks - private final AggregateTask.ComponentAggregateTask componentsTask; - private final AggregateTask.SchedulerAggregateTask schedulerTask; - private final AggregateTask.StaticIpAggregateTask staticIpTask; - - private final AggregateTask[] tasks; + private final List> tasks = new ArrayList<>(); private TemporaryApps temporaryApps; @@ -96,18 +94,25 @@ public class AppManagerAppHelperImpl implements AppManagerAppHelper { public AppManagerAppHelperImpl(// @Reference ComponentManager componentManager, // @Reference ComponentUtil componentUtil, // - @Reference Validator validator, // - @Reference AggregateTask.ComponentAggregateTask componentsTask, // - @Reference AggregateTask.SchedulerAggregateTask schedulerTask, // - @Reference AggregateTask.StaticIpAggregateTask staticIpTask // + @Reference Validator validator // ) { this.componentManager = componentManager; this.componentUtil = componentUtil; this.validator = validator; - this.componentsTask = componentsTask; - this.schedulerTask = schedulerTask; - this.staticIpTask = staticIpTask; - this.tasks = new AggregateTask[] { componentsTask, schedulerTask, staticIpTask }; + } + + @Reference(// + cardinality = ReferenceCardinality.MULTIPLE, // + policy = ReferencePolicy.DYNAMIC, // + policyOption = ReferencePolicyOption.GREEDY // + ) + private void bindAggregateTask(AggregateTask task) { + insert(this.tasks, task); + } + + @SuppressWarnings("unused") + private void unbindAggregateTask(AggregateTask task) { + this.tasks.remove(task); } @Override @@ -164,23 +169,17 @@ private UpdateValues usingTemporaryApps(User user, ThrowingSupplier(); final var language = user == null ? null : user.getLanguage(); - final var bundle = getTranslationBundle(language); // execute all tasks - Lists.newArrayList(// - Map.entry(this.componentsTask, "canNotUpdateComponents"), // - // needs to run after component task to get the components which were created - Map.entry(this.schedulerTask, "canNotUpdateScheduler"), // - Map.entry(this.staticIpTask, "canNotUpdateStaticIps")) // - .forEach(entry -> { - try { - entry.getKey().create(user, otherAppConfigs); - } catch (OpenemsNamedException e) { - final var errorMessage = TranslationUtil.getTranslation(bundle, entry.getValue()); - this.log.error(errorMessage, e); - errors.add(errorMessage); - } - }); + for (var task : this.tasks) { + try { + task.create(user, otherAppConfigs); + } catch (OpenemsNamedException e) { + final var errorMessage = task.getGeneralFailMessage(language); + this.log.error(errorMessage, e); + errors.add(errorMessage); + } + } if (!errors.isEmpty()) { throw new OpenemsException(errors.stream().collect(Collectors.joining("|"))); @@ -206,7 +205,7 @@ private UpdateValues updateAppInternal(final User user, OpenemsAppInstance oldIn var references = this.getAppsWithReferenceTo(oldInstance.instanceId); references.removeAll(this.temporaryApps.currentlyDeletingApps()); for (var entry : this.getAppManagerImpl().appConfigs(references, null)) { - for (var dependencieDeclaration : entry.getValue().dependencies) { + for (var dependencieDeclaration : entry.getValue().dependencies()) { var dd = entry.getKey().dependencies.stream() .filter(d -> d.instanceId.equals(oldInstance.instanceId)) @@ -273,7 +272,7 @@ private UpdateValues updateAppInternal(final User user, OpenemsAppInstance oldIn List dependencies = new ArrayList<>(dependencieInstances.size()); var removeKeys = new LinkedList(); for (var dependency : dependencieInstances.entrySet()) { - if (!dc.config.dependencies.stream().anyMatch(t -> t.equals(dependency.getKey().sub))) { + if (!dc.config.dependencies().stream().anyMatch(t -> t.equals(dependency.getKey().sub))) { continue; } removeKeys.add(dependency.getKey()); @@ -429,7 +428,7 @@ private UpdateValues updateAppInternal(final User user, OpenemsAppInstance oldIn } var newConfig = this.getNewAppConfigWithReplacedIds(dc.app, oldInstanceOfCurrentApp, - newAppInstance, AppManagerAppHelperImpl.getComponentsFromConfigs(otherAppConfigs), + newAppInstance, AppConfiguration.getComponentsFromConfigs(otherAppConfigs), language); this.aggregateAllTasks(newConfig, oldConfig); @@ -525,8 +524,7 @@ private UpdateValues updateAppInternal(final User user, OpenemsAppInstance oldIn } var newAppConfig = this.getNewAppConfigWithReplacedIds(dc.app, oldAppConfig.instance, - newAppInstance, AppManagerAppHelperImpl.getComponentsFromConfigs(otherAppConfigs), - language); + newAppInstance, AppConfiguration.getComponentsFromConfigs(otherAppConfigs), language); this.aggregateAllTasks(newAppConfig, oldAppConfig.config); } catch (OpenemsNamedException e) { @@ -709,8 +707,8 @@ private DependencyDeclaration.AppDependencyConfig getAppDependencyConfig(Openems private final DependencyDeclaration getNeededDependencyTo(OpenemsAppInstance instance, String appId, UUID instanceId) { try { - var neededDependencies = this.appManagerUtil.getAppConfiguration(ConfigurationTarget.UPDATE, instance, - null).dependencies; + var neededDependencies = this.appManagerUtil.getAppConfiguration(ConfigurationTarget.UPDATE, instance, null) + .dependencies(); if (neededDependencies == null || neededDependencies.isEmpty()) { return null; } @@ -802,7 +800,7 @@ private UpdateValues deleteAppInternal(User user, OpenemsAppInstance instance) t return false; } - var dependencyDeclaration = config.dependencies.stream() + var dependencyDeclaration = config.dependencies().stream() .filter(dd -> dd.key.equals(dependency.get().key)).findFirst(); if (dependencyDeclaration.isEmpty()) { @@ -930,16 +928,50 @@ private List getAppsWithReferenceTo(List .collect(Collectors.toList()); } + @SuppressWarnings("unchecked") private final void aggregateAllTasks(AppConfiguration instance, AppConfiguration oldInstance) { + Function>> notExistingTask = config -> { + return Optional.ofNullable(config) // + .map(c -> c.tasks().stream() // + .filter(t -> Stream.of(this.tasks) // + .anyMatch(ot -> ot.getClass().isAssignableFrom(t.aggregateTaskClass()))) // + .collect(Collectors.toList()) // + ).orElse(Lists.newArrayList()); + }; + + var notAbleToExecuteTasks = notExistingTask.apply(instance); + notAbleToExecuteTasks.addAll(notExistingTask.apply(oldInstance)); + if (!notAbleToExecuteTasks.isEmpty()) { + // TODO maybe throw exception and return + this.log.warn("Unable to find Task implementations for " + notAbleToExecuteTasks.stream() // + .map(t -> t.aggregateTaskClass().getSimpleName()) // + .collect(joining(", "))); + } + for (var task : this.tasks) { - task.aggregate(instance, oldInstance); + var newConfiguration = Optional.ofNullable(instance) // + .map(c -> c.getConfiguration(task.getClass())) // + .orElse(null); + + var oldConfiguration = Optional.ofNullable(oldInstance) // + .map(c -> c.getConfiguration(task.getClass())) // + .orElse(null); + + if (newConfiguration == null && oldConfiguration == null) { + continue; + } + + this.aggregateTask(task, newConfiguration, oldConfiguration); } } + @SuppressWarnings("unchecked") + private void aggregateTask(AggregateTask aggregateTask, Object newConfig, Object oldConfig) { + aggregateTask.aggregate((T) newConfig, (T) oldConfig); + } + private void resetTasks() { - for (var task : this.tasks) { - task.reset(); - } + this.tasks.forEach(AggregateTask::reset); } /** @@ -959,7 +991,7 @@ private final boolean isAllowedToDelete(OpenemsAppInstance instance, UUID... ign if (!dependency.instanceId.equals(instance.instanceId)) { continue; } - var declaration = entry.getValue().dependencies.stream().filter(dd -> dd.key.equals(dependency.key)) + var declaration = entry.getValue().dependencies().stream().filter(dd -> dd.key.equals(dependency.key)) .findAny(); // declaration not found for dependency @@ -995,26 +1027,6 @@ protected void checkStatus(OpenemsApp openemsApp, Language language) throws Open throw new OpenemsException("Status '" + status.name() + "' is not implemented."); } - protected static List getComponentsFromConfigs(List configs) { - return mapAppConfiguration(configs, c -> c.components); - } - - protected static List getSchedulerIdsFromConfigs(List configs) { - return mapAppConfiguration(configs, c -> c.schedulerExecutionOrder); - } - - protected static List getStaticIpsFromConfigs(List configs) { - return mapAppConfiguration(configs, c -> c.ips); - } - - private static List mapAppConfiguration(List configs, - Function> mapper) { - return configs.stream() // - .map(mapper) // - .flatMap(l -> l.stream()) // - .collect(Collectors.toList()); - } - /** * Finds the needed app for a {@link DependencyDeclaration}. * @@ -1125,7 +1137,7 @@ private DependencyConfig foreachDependency(// if (oldConfig == null) { final var comps = this.getAppManagerImpl() .getOtherAppConfigurations(alreadyIteratedInstances.stream().toArray(UUID[]::new)) // - .stream().flatMap(c -> c.components.stream()).collect(Collectors.toList()); + .stream().flatMap(c -> c.getComponents().stream()).collect(Collectors.toList()); OpenemsAppInstance a = null; if (sub != null && appConfig.specificInstanceId != null) { a = this.getInstance(appConfig.specificInstanceId); @@ -1151,7 +1163,7 @@ private DependencyConfig foreachDependency(// skipIds.add(newInstanceId); final var comps = this.getAppManagerImpl() .getOtherAppConfigurations(skipIds.stream().toArray(UUID[]::new)) // - .stream().flatMap(c -> c.components.stream()).collect(Collectors.toList()); + .stream().flatMap(c -> c.getComponents().stream()).collect(Collectors.toList()); config = this.getNewAppConfigWithReplacedIds(app, oldConfig.instance, // new OpenemsAppInstance(app.getAppId(), appConfig.alias, newInstanceId, newProps, null), // comps, l); @@ -1168,7 +1180,7 @@ private DependencyConfig foreachDependency(// var dependencies = new LinkedList(); if (includeResult == IncludeApp.INCLUDE_WITH_DEPENDENCIES) { - for (var dependency : config.dependencies) { + for (var dependency : config.dependencies()) { var nextAppConfig = determineDependencyConfig.apply(dependency.appConfigs); if (nextAppConfig == null) { // can not determine one out of many configs @@ -1383,7 +1395,7 @@ private DependencyConfig foreachExistingDependency(OpenemsAppInstance instance, if (alreadyIteratedApps.contains(dependencyApp)) { continue; } - var subApp = config.dependencies.stream().filter(t -> t.key.equals(dependency.key)).findFirst() + var subApp = config.dependencies().stream().filter(t -> t.key.equals(dependency.key)).findFirst() .get(); var dependencyConfig = this.foreachExistingDependency(dependencyApp, target, consumer, instance, subApp, l, alreadyIteratedApps, includeInstance); @@ -1434,7 +1446,7 @@ private final List getReplaceableComponentIds(OpenemsApp app, Jso Map defaultIdToCurrentId = new HashMap<>(); // remove already set ids - for (var component : config.components) { + for (var component : config.getComponents()) { String removeKey = null; for (var entry : copy.entrySet()) { var id = JsonUtils.getAsOptionalString(entry.getValue()).orElse(null); @@ -1454,12 +1466,12 @@ private final List getReplaceableComponentIds(OpenemsApp app, Jso config = app.getAppConfiguration(ConfigurationTarget.TEST, copy, null); - for (var comp : config.components) { + for (var comp : config.getComponents()) { copy.addProperty(comp.getId(), prefix); } var configWithNewIds = app.getAppConfiguration(ConfigurationTarget.TEST, copy, null); Map replaceableComponentIds = new HashMap<>(); - for (var comp : configWithNewIds.components) { + for (var comp : configWithNewIds.getComponents()) { if (comp.getId().startsWith(prefix)) { // "METER_ID:meter0" var raw = comp.getId().substring(prefix.length()); @@ -1542,7 +1554,7 @@ private AppConfiguration getNewAppConfigWithReplacedIds(// var newAppConfig = this.appManagerUtil.getAppConfiguration(target, app, newAppInstance.alias, propertiesCopy, language); - final var orderedComponents = ComponentUtilImpl.order(newAppConfig.components); + final var orderedComponents = ComponentUtilImpl.order(newAppConfig.getComponents()); final var iterator = new ArrayList<>(orderedComponents).iterator(); for (int i = 0; iterator.hasNext(); i++) { final var comp = iterator.next(); @@ -1591,7 +1603,7 @@ private AppConfiguration getNewAppConfigWithReplacedIds(// for (var entry : this.getAppManagerImpl().appConfigs( this.temporaryApps.currentlyCreatingModifiedApps(), AppManagerImpl.excludingInstanceIds(newAppInstance.instanceId))) { - foundComponent = entry.getValue().components.stream() + foundComponent = entry.getValue().getComponents().stream() .filter(t -> t.getId().equals(comp.getId())).findFirst().orElse(null); if (foundComponent != null) { break; @@ -1681,7 +1693,13 @@ private final AppManagerImpl getAppManagerImpl() { return (AppManagerImpl) appManagerImpl; } - private static ResourceBundle getTranslationBundle(Language language) { + /** + * Gets the {@link ResourceBundle} based on the given {@link Language}. + * + * @param language the {@link Language} of the translations + * @return the {@link ResourceBundle} + */ + public static ResourceBundle getTranslationBundle(Language language) { if (language == null) { language = Language.DEFAULT; } @@ -1708,4 +1726,29 @@ public TemporaryApps getTemporaryApps() { .orElse(null); } + /** + * Inserts a task into a existing list of tasks. The task gets inserted at a + * position which suits their + * {@link AggregateTask.AggregateTaskExecuteConstraints}. + * + * @param tasks the existing task list + * @param task the task to insert + */ + public static void insert(List> tasks, AggregateTask task) { + // TODO detect circular constraints + var insertIndex = tasks.size(); + + final var iterator = tasks.listIterator(); + while (iterator.hasNext()) { + final var i = iterator.nextIndex(); + final var t = iterator.next(); + if (t.getExecuteConstraints().runAfter().stream() // + .anyMatch(e -> e.isAssignableFrom(task.getClass()))) { + insertIndex = i; + } + } + + tasks.add(insertIndex, task); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyUtil.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyUtil.java index bae01298115..d29de120956 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyUtil.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/DependencyUtil.java @@ -110,7 +110,7 @@ public final OpenemsAppInstance getAppWhichHasComponentInternal(// instances.addAll(appHelper.getTemporaryApps().currentlyCreatingApps()); } for (var entry : appManagerImpl.appConfigs(instances, null)) { - if (entry.getValue().components.stream().anyMatch(c -> c.getId().equals(componentId))) { + if (entry.getValue().getComponents().stream().anyMatch(c -> c.getId().equals(componentId))) { this.setCurrentlyRunning(false); return entry.getKey(); } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/SchedulerAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/SchedulerAggregateTaskImpl.java deleted file mode 100644 index 161f79fd718..00000000000 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/SchedulerAggregateTaskImpl.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.openems.edge.core.appmanager.dependency; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.edge.common.user.User; -import io.openems.edge.core.appmanager.AppConfiguration; -import io.openems.edge.core.appmanager.ComponentUtil; - -@Component -public class SchedulerAggregateTaskImpl implements AggregateTask, AggregateTask.SchedulerAggregateTask { - - private final AggregateTask.ComponentAggregateTask aggregateTask; - private final ComponentUtil componentUtil; - - private List order; - private List removeIds; - - @Activate - public SchedulerAggregateTaskImpl(@Reference AggregateTask.ComponentAggregateTask aggregateTask, - @Reference ComponentUtil componentUtil) { - this.aggregateTask = aggregateTask; - this.componentUtil = componentUtil; - } - - @Override - public void reset() { - this.order = new LinkedList<>(); - this.removeIds = new LinkedList<>(); - } - - @Override - public void aggregate(AppConfiguration instance, AppConfiguration oldConfig) { - if (instance != null) { - this.order = this.componentUtil.insertSchedulerOrder(this.order, instance.schedulerExecutionOrder); - } - if (oldConfig != null) { - var schedulerIdDiff = new ArrayList<>(oldConfig.schedulerExecutionOrder); - if (instance != null) { - schedulerIdDiff.removeAll(instance.schedulerExecutionOrder); - } - this.removeIds.addAll(schedulerIdDiff); - } - } - - @Override - public void create(User user, List otherAppConfigurations) throws OpenemsNamedException { - this.order = this.componentUtil.insertSchedulerOrder(this.componentUtil.getSchedulerIds(), this.order); - this.componentUtil.updateScheduler(user, this.order, this.aggregateTask.getCreatedComponents()); - - this.delete(user, otherAppConfigurations); - } - - /** - * removes id's from the scheduler that were aggregated. - * - * @param user the executing user - * @param otherAppConfigurations the other existing {@link AppConfiguration}s - * @throws OpenemsNamedException on error - */ - @Override - public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException { - this.removeIds.removeAll(AppManagerAppHelperImpl.getSchedulerIdsFromConfigs(otherAppConfigurations)); - this.removeIds.addAll(this.aggregateTask.getDeletedComponents()); - - this.componentUtil.removeIdsInSchedulerIfExisting(user, this.removeIds); - } - -} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/StaticIpAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/StaticIpAggregateTaskImpl.java deleted file mode 100644 index b4aba8735c3..00000000000 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/StaticIpAggregateTaskImpl.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.openems.edge.core.appmanager.dependency; - -import java.util.LinkedList; -import java.util.List; - -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.edge.common.user.User; -import io.openems.edge.core.appmanager.AppConfiguration; -import io.openems.edge.core.appmanager.ComponentUtil; -import io.openems.edge.core.appmanager.InterfaceConfiguration; - -@Component -public class StaticIpAggregateTaskImpl implements AggregateTask, AggregateTask.StaticIpAggregateTask { - - /** - * Setting ip configuration only works on Linux devices. - */ - private final boolean isWindows = System.getProperty("os.name").startsWith("Windows"); - - private final ComponentUtil componentUtil; - - private List ips; - private List ips2Delete; - - @Activate - public StaticIpAggregateTaskImpl(@Reference ComponentUtil componentUtil) { - this.componentUtil = componentUtil; - } - - @Override - public void reset() { - this.ips = new LinkedList<>(); - this.ips2Delete = new LinkedList<>(); - } - - @Override - public void aggregate(AppConfiguration instance, AppConfiguration oldConfig) { - if (this.isWindows) { - return; - } - if (instance != null) { - this.ips.addAll(instance.ips); - } - if (oldConfig != null) { - this.ips2Delete.addAll(oldConfig.ips); - } - } - - @Override - public void create(User user, List otherAppConfigurations) throws OpenemsNamedException { - this.execute(user, otherAppConfigurations, this.ips, this.ips2Delete); - } - - @Override - public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException { - this.execute(user, otherAppConfigurations, null, this.ips2Delete); - } - - private void execute(// - final User user, // - final List otherAppConfigurations, // - final List ips, // - final List ipsToDelete // - ) throws OpenemsNamedException { - if (this.isWindows) { - return; - } - InterfaceConfiguration.removeDuplicatedIps(ipsToDelete, // - AppManagerAppHelperImpl.getStaticIpsFromConfigs(otherAppConfigurations)); - this.componentUtil.updateHosts(// - user, // - ips == null ? null : InterfaceConfiguration.summarize(ips), // - InterfaceConfiguration.summarize(ipsToDelete) // - ); - } - -} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Task.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Task.java new file mode 100644 index 00000000000..c2b5e9eb238 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Task.java @@ -0,0 +1,7 @@ +package io.openems.edge.core.appmanager.dependency; + +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; + +public record Task(Class> aggregateTaskClass, T configuration) { + +} \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Tasks.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Tasks.java new file mode 100644 index 00000000000..46d277877f8 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/Tasks.java @@ -0,0 +1,85 @@ +package io.openems.edge.core.appmanager.dependency; + +import java.util.List; + +import io.openems.common.types.EdgeConfig; +import io.openems.edge.core.appmanager.InterfaceConfiguration; +import io.openems.edge.core.appmanager.OpenemsAppInstance; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.ComponentAggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.ComponentConfiguration; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerAggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerConfiguration; +import io.openems.edge.core.appmanager.dependency.aggregatetask.StaticIpAggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.StaticIpConfiguration; + +public class Tasks { + + /** + * Creates a {@link Task} for setting the {@link ComponentConfiguration}. + * + * @param components the components to create or update + * @return the {@link Task} + */ + public static Task component(List components) { + return createTask(ComponentAggregateTask.class, new ComponentConfiguration(components)); + } + + /** + * Creates a {@link Task} for setting the {@link ComponentConfiguration}. + * + * @param components the components to create or update + * @return the {@link Task} + */ + public static Task component(EdgeConfig.Component... components) { + return createTask(ComponentAggregateTask.class, new ComponentConfiguration(components)); + } + + /** + * Creates a {@link Task} for setting the {@link StaticIpConfiguration}. + * + * @param interfaceConfiguration the {@link InterfaceConfiguration} to set + * @return the {@link Task} + */ + public static Task staticIp(List interfaceConfiguration) { + return createTask(StaticIpAggregateTask.class, new StaticIpConfiguration(interfaceConfiguration)); + } + + /** + * Creates a {@link Task} for setting the {@link StaticIpConfiguration}. + * + * @param interfaceConfiguration the {@link InterfaceConfiguration} to set + * @return the {@link Task} + */ + public static Task staticIp(InterfaceConfiguration... interfaceConfiguration) { + return createTask(StaticIpAggregateTask.class, new StaticIpConfiguration(interfaceConfiguration)); + } + + /** + * Creates a Task for setting the {@link SchedulerConfiguration}. + * + * @param componentOrder the order of the components in the scheduler + * @return the {@link Task} to run when creating the {@link OpenemsAppInstance} + */ + public static Task scheduler(List componentOrder) { + return createTask(SchedulerAggregateTask.class, new SchedulerConfiguration(componentOrder)); + } + + /** + * Creates a Task for setting the {@link SchedulerConfiguration}. + * + * @param componentOrder the order of the components in the scheduler + * @return the {@link Task} to run when creating the {@link OpenemsAppInstance} + */ + public static Task scheduler(String... componentOrder) { + return createTask(SchedulerAggregateTask.class, new SchedulerConfiguration(componentOrder)); + } + + private static > Task createTask(Class clazz, C configuration) { + return new Task<>(// + clazz, // + configuration // + ); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/AggregateTask.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/AggregateTask.java new file mode 100644 index 00000000000..b5a42dd4944 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/AggregateTask.java @@ -0,0 +1,77 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import static java.util.Collections.emptySet; + +import java.util.List; +import java.util.Set; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.session.Language; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.AppConfiguration; + +public interface AggregateTask { + + public static record AggregateTaskExecuteConstraints(// + /** + * Tasks which need to run before this task. + */ + Set>> runAfter // + ) { + + } + + /** + * Aggregates the given instance. + * + * @param currentConfiguration the {@link AppConfiguration} of the instance + * @param lastConfiguration the old configuration of the instance + */ + public void aggregate(T currentConfiguration, T lastConfiguration); + + /** + * e. g. creates components that were aggregated by the instances and my also + * delete unused components. + * + * @param user the executing user + * @param otherAppConfigurations the other existing {@link AppConfiguration}s + * @throws OpenemsNamedException on error + */ + public void create(User user, List otherAppConfigurations) throws OpenemsNamedException; + + /** + * e. g. deletes components that were aggregated. + * + * @param user the executing user + * @param otherAppConfigurations the other existing {@link AppConfiguration}s + * @throws OpenemsNamedException on error + */ + public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException; + + /** + * Validates the expected configuration. + * + * @param errors the errors that occur during the validation + * @param appConfiguration the whole configuration + * @param config the configuration to validate + */ + public void validate(List errors, AppConfiguration appConfiguration, T config); + + /** + * Gets a general message for the user if any operations fails. + * + * @param l the {@link Language} of the message + * @return the error message + */ + public String getGeneralFailMessage(Language l); + + public default AggregateTaskExecuteConstraints getExecuteConstraints() { + return new AggregateTaskExecuteConstraints(emptySet()); + } + + /** + * Resets the task. + */ + public void reset(); + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTask.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTask.java new file mode 100644 index 00000000000..be05bcb45cc --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTask.java @@ -0,0 +1,23 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import java.util.List; + +import io.openems.common.types.EdgeConfig; + +public interface ComponentAggregateTask extends AggregateTask { + + /** + * Gets the Components that were created. + * + * @return the created {@link EdgeConfig.Component} + */ + public List getCreatedComponents(); + + /** + * Gets the Components that were deleted. + * + * @return the id's of the deleted {@link EdgeConfig.Component} + */ + public List getDeletedComponents(); + +} \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/ComponentAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java similarity index 75% rename from io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/ComponentAggregateTaskImpl.java rename to io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java index 7f31b74e202..4ac9a8b6e3f 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/ComponentAggregateTaskImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImpl.java @@ -1,4 +1,4 @@ -package io.openems.edge.core.appmanager.dependency; +package io.openems.edge.core.appmanager.dependency.aggregatetask; import java.util.ArrayList; import java.util.Collections; @@ -9,22 +9,34 @@ import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; +import io.openems.common.exceptions.InvalidValueException; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.exceptions.OpenemsException; import io.openems.common.jsonrpc.request.CreateComponentConfigRequest; import io.openems.common.jsonrpc.request.DeleteComponentConfigRequest; import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest; import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest.Property; +import io.openems.common.session.Language; import io.openems.common.types.EdgeConfig; import io.openems.edge.common.component.ComponentManager; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.AppConfiguration; import io.openems.edge.core.appmanager.ComponentUtilImpl; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.dependency.AppManagerAppHelperImpl; import io.openems.edge.core.componentmanager.ComponentManagerImpl; -@Component -public class ComponentAggregateTaskImpl implements AggregateTask, AggregateTask.ComponentAggregateTask { +@Component(// + service = { // + AggregateTask.class, // + ComponentAggregateTask.class, // + ComponentAggregateTaskImpl.class // + }, // + scope = ServiceScope.SINGLETON // +) +public class ComponentAggregateTaskImpl implements ComponentAggregateTask { private final ComponentManager componentManager; @@ -48,17 +60,17 @@ public void reset() { } @Override - public void aggregate(AppConfiguration config, AppConfiguration oldConfig) { + public void aggregate(ComponentConfiguration config, ComponentConfiguration oldConfig) { if (config != null) { // remove duplicated components // TODO maybe error - this.components.removeIf(t -> config.components.stream().anyMatch(o -> t.getId().equals(o.getId()))); - this.components.addAll(config.components); + this.components.removeIf(t -> config.components().stream().anyMatch(o -> t.getId().equals(o.getId()))); + this.components.addAll(config.components()); } if (oldConfig != null) { - var componentDiff = new ArrayList<>(oldConfig.components); + var componentDiff = new ArrayList<>(oldConfig.components()); if (config != null) { - componentDiff.removeIf(t -> config.components.stream().anyMatch(c -> c.getId().equals(t.getId()))); + componentDiff.removeIf(t -> config.components().stream().anyMatch(c -> c.getId().equals(t.getId()))); } this.components2Delete.addAll(componentDiff); } @@ -66,8 +78,11 @@ public void aggregate(AppConfiguration config, AppConfiguration oldConfig) { @Override public void create(User user, List otherAppConfigurations) throws OpenemsNamedException { + if (!this.anyChanges()) { + return; + } var errors = new LinkedList(); - var otherAppComponents = AppManagerAppHelperImpl.getComponentsFromConfigs(otherAppConfigurations); + var otherAppComponents = AppConfiguration.getComponentsFromConfigs(otherAppConfigurations); // create components for (var comp : ComponentUtilImpl.order(this.components)) { /** @@ -149,8 +164,11 @@ public void create(User user, List otherAppConfigurations) thr */ @Override public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException { + if (!this.anyChanges()) { + return; + } List errors = new ArrayList<>(); - var notMyComponents = AppManagerAppHelperImpl.getComponentsFromConfigs(otherAppConfigurations); + var notMyComponents = AppConfiguration.getComponentsFromConfigs(otherAppConfigurations); for (var comp : this.components2Delete) { if (notMyComponents.stream().anyMatch(t -> t.getId().equals(comp.getId()))) { continue; @@ -180,6 +198,49 @@ public void delete(User user, List otherAppConfigurations) thr } } + @Override + public String getGeneralFailMessage(Language l) { + final var bundle = AppManagerAppHelperImpl.getTranslationBundle(l); + return TranslationUtil.getTranslation(bundle, "canNotUpdateComponents"); + } + + @Override + public void validate(// + final List errors, // + final AppConfiguration appConfiguration, // + final ComponentConfiguration config // + ) { + var actualEdgeConfig = this.componentManager.getEdgeConfig(); + + var missingComponents = new ArrayList(); + for (var expectedComponent : config.components()) { + var componentId = expectedComponent.getId(); + + // Get Actual Component Configuration + EdgeConfig.Component actualComponent; + try { + actualComponent = actualEdgeConfig.getComponentOrError(componentId); + } catch (InvalidValueException e) { + missingComponents.add(componentId); + continue; + } + // ALIAS should not be validated because it can be different depending on the + // language + ComponentUtilImpl.isSameConfigurationWithoutAlias(errors, expectedComponent, actualComponent); + } + + if (!missingComponents.isEmpty()) { + errors.add("Missing Component" // + + (missingComponents.size() > 1 ? "s" : "") + ":" // + + missingComponents.stream().collect(Collectors.joining(","))); + } + } + + private final boolean anyChanges() { + return !this.components.isEmpty() // + || !this.components2Delete.isEmpty(); + } + private void createComponent(User user, EdgeConfig.Component comp) throws OpenemsNamedException { List properties = comp.getProperties().entrySet().stream() .map(t -> new Property(t.getKey(), t.getValue())).collect(Collectors.toList()); diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentConfiguration.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentConfiguration.java new file mode 100644 index 00000000000..6af67e684e6 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentConfiguration.java @@ -0,0 +1,14 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import java.util.List; +import java.util.stream.Stream; + +import io.openems.common.types.EdgeConfig; + +public record ComponentConfiguration(List components) { + + public ComponentConfiguration(EdgeConfig.Component... components) { + this(Stream.of(components).toList()); + } + +} \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTask.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTask.java new file mode 100644 index 00000000000..7f86fa5afa9 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTask.java @@ -0,0 +1,5 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +public interface SchedulerAggregateTask extends AggregateTask { + +} \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImpl.java new file mode 100644 index 00000000000..1f768653eca --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImpl.java @@ -0,0 +1,150 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.session.Language; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.dependency.AppManagerAppHelperImpl; + +@Component(// + service = { // + AggregateTask.class, // + SchedulerAggregateTask.class, // + SchedulerAggregateTaskImpl.class // + }, // + scope = ServiceScope.SINGLETON // +) +public class SchedulerAggregateTaskImpl implements SchedulerAggregateTask { + + private final ComponentAggregateTask aggregateTask; + private final ComponentUtil componentUtil; + + private List order; + private List removeIds; + + @Activate + public SchedulerAggregateTaskImpl(// + @Reference ComponentAggregateTask aggregateTask, // + @Reference ComponentUtil componentUtil // + ) { + this.aggregateTask = aggregateTask; + this.componentUtil = componentUtil; + } + + @Override + public void reset() { + this.order = new LinkedList<>(); + this.removeIds = new LinkedList<>(); + } + + @Override + public void aggregate(// + final SchedulerConfiguration currentConfiguration, // + final SchedulerConfiguration lastConfiguration // + ) { + if (currentConfiguration != null) { + this.order = this.componentUtil.insertSchedulerOrder(this.order, currentConfiguration.componentOrder()); + } + if (lastConfiguration != null) { + var schedulerIdDiff = new ArrayList<>(lastConfiguration.componentOrder()); + if (currentConfiguration != null) { + schedulerIdDiff.removeAll(currentConfiguration.componentOrder()); + } + this.removeIds.addAll(schedulerIdDiff); + } + } + + @Override + public void create(User user, List otherAppConfigurations) throws OpenemsNamedException { + if (!this.anyChanges()) { + return; + } + this.order = this.componentUtil.insertSchedulerOrder(this.componentUtil.getSchedulerIds(), this.order); + this.componentUtil.updateScheduler(user, this.order, this.aggregateTask.getCreatedComponents()); + + this.delete(user, otherAppConfigurations); + } + + @Override + public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException { + if (!this.anyChanges()) { + return; + } + var otherIds = AppConfiguration + .flatMap(otherAppConfigurations, SchedulerAggregateTask.class, SchedulerConfiguration::componentOrder) + .toList(); + this.removeIds.removeAll(otherIds); + this.removeIds.addAll(this.aggregateTask.getDeletedComponents()); + + this.componentUtil.removeIdsInSchedulerIfExisting(user, this.removeIds); + } + + @Override + public String getGeneralFailMessage(Language l) { + final var bundle = AppManagerAppHelperImpl.getTranslationBundle(l); + return TranslationUtil.getTranslation(bundle, "canNotUpdateScheduler"); + } + + @Override + public AggregateTaskExecuteConstraints getExecuteConstraints() { + return new AggregateTaskExecuteConstraints(Set.of(// + // Needs to run after the AggregateTask.ComponentAggregateTask to also remove + // ids in the scheduler of components which got deleted + ComponentAggregateTask.class // + )); + } + + @Override + public void validate(List errors, AppConfiguration appConfiguration, SchedulerConfiguration configuration) { + if (configuration.componentOrder().isEmpty()) { + return; + } + + // Prepare Queue + var controllers = new LinkedList<>(this.componentUtil.removeIdsWhichNotExist(configuration.componentOrder(), + appConfiguration.getComponents())); + + if (controllers.isEmpty()) { + return; + } + + List schedulerIds; + try { + schedulerIds = this.componentUtil.getSchedulerIds(); + } catch (OpenemsNamedException e) { + errors.add(e.getMessage()); + return; + } + + var nextControllerId = controllers.poll(); + + // Remove found Controllers from Queue in order + for (var controllerId : schedulerIds) { + if (controllerId.equals(nextControllerId)) { + nextControllerId = controllers.poll(); + } + } + if (nextControllerId != null) { + errors.add("Controller [" + nextControllerId + "] is not/wrongly configured in Scheduler"); + } + } + + private boolean anyChanges() { + return !this.order.isEmpty() // + || !this.removeIds.isEmpty() // + || !this.aggregateTask.getDeletedComponents().isEmpty(); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerConfiguration.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerConfiguration.java new file mode 100644 index 00000000000..f30fbb95903 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerConfiguration.java @@ -0,0 +1,12 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import java.util.List; +import java.util.stream.Stream; + +public record SchedulerConfiguration(List componentOrder) { + + public SchedulerConfiguration(String... componentIds) { + this(Stream.of(componentIds).toList()); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTask.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTask.java new file mode 100644 index 00000000000..c3875b5b718 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTask.java @@ -0,0 +1,5 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +public interface StaticIpAggregateTask extends AggregateTask { + +} \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImpl.java new file mode 100644 index 00000000000..b01d6dde1a3 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImpl.java @@ -0,0 +1,146 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.session.Language; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.ComponentUtil; +import io.openems.edge.core.appmanager.InterfaceConfiguration; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.dependency.AppManagerAppHelperImpl; + +@Component(// + service = { // + AggregateTask.class, // + StaticIpAggregateTask.class, // + StaticIpAggregateTaskImpl.class // + }, // + scope = ServiceScope.SINGLETON // +) +public class StaticIpAggregateTaskImpl implements StaticIpAggregateTask { + + private final boolean isWindows = System.getProperty("os.name").startsWith("Windows"); + + private final ComponentUtil componentUtil; + + private List ips; + private List ips2Delete; + + @Activate + public StaticIpAggregateTaskImpl(@Reference ComponentUtil componentUtil) { + this.componentUtil = componentUtil; + } + + @Override + public void reset() { + this.ips = new LinkedList<>(); + this.ips2Delete = new LinkedList<>(); + } + + @Override + public void aggregate(StaticIpConfiguration instance, StaticIpConfiguration oldConfig) { + if (this.isWindows) { + return; + } + if (instance != null) { + this.ips.addAll(instance.interfaceConfiguration()); + } + if (oldConfig != null) { + this.ips2Delete.addAll(oldConfig.interfaceConfiguration()); + } + } + + @Override + public void create(User user, List otherAppConfigurations) throws OpenemsNamedException { + this.execute(user, otherAppConfigurations, this.ips, this.ips2Delete); + } + + @Override + public void delete(User user, List otherAppConfigurations) throws OpenemsNamedException { + this.execute(user, otherAppConfigurations, null, this.ips2Delete); + } + + private void execute(// + final User user, // + final List otherAppConfigurations, // + final List ips, // + final List ipsToDelete // + ) throws OpenemsNamedException { + if (!this.anyChanges()) { + return; + } + InterfaceConfiguration.removeDuplicatedIps(ipsToDelete, // + AppConfiguration + .flatMap(otherAppConfigurations, StaticIpAggregateTask.class, c -> c.interfaceConfiguration()) + .toList()); + this.componentUtil.updateHosts(// + user, // + ips == null ? null : InterfaceConfiguration.summarize(ips), // + InterfaceConfiguration.summarize(ipsToDelete) // + ); + } + + @Override + public String getGeneralFailMessage(Language l) { + final var bundle = AppManagerAppHelperImpl.getTranslationBundle(l); + return TranslationUtil.getTranslation(bundle, "canNotUpdateStaticIps"); + } + + @Override + public void validate(List errors, AppConfiguration appConfiguration, StaticIpConfiguration config) { + // setting ip configuration is not implemented for windows + if (this.isWindows) { + return; + } + + if (config.interfaceConfiguration().isEmpty()) { + return; + } + try { + var interfaces = this.componentUtil.getInterfaces(); + config.interfaceConfiguration().stream() // + .forEach(i -> { + var existingInterface = interfaces.stream() // + .filter(t -> t.getName().equals(i.interfaceName)) // + .findFirst().orElse(null); + + if (existingInterface == null) { + errors.add("Interface '" + i.interfaceName + "' not found."); + return; + } + + var missingIps = i.getIps().stream() // + .filter(ip -> { + if (existingInterface.getAddresses().getValue().stream() // + .anyMatch(existingIp -> existingIp.isInSameNetwork(ip))) { + return false; + } + return true; + }).collect(Collectors.toList()); + + if (missingIps.isEmpty()) { + return; + } + errors.add("Address '" + + missingIps.stream().map(t -> t.toString()).collect(Collectors.joining(", ")) + "' " + + (missingIps.size() > 1 ? "are" : "is") + " not added on " + i.interfaceName); + }); + } catch (OpenemsNamedException ex) { + ex.printStackTrace(); + } + } + + private final boolean anyChanges() { + return !this.isWindows && (this.ips.isEmpty() || this.ips2Delete.isEmpty()); + } + +} diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpConfiguration.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpConfiguration.java new file mode 100644 index 00000000000..661a0016d68 --- /dev/null +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpConfiguration.java @@ -0,0 +1,14 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import java.util.List; +import java.util.stream.Stream; + +import io.openems.edge.core.appmanager.InterfaceConfiguration; + +public record StaticIpConfiguration(List interfaceConfiguration) { + + public StaticIpConfiguration(InterfaceConfiguration... interfaceConfiguration) { + this(Stream.of(interfaceConfiguration).toList()); + } + +} \ No newline at end of file diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckAppsNotInstalled.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckAppsNotInstalled.java index 98c6ba68081..352f8c2433e 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckAppsNotInstalled.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckAppsNotInstalled.java @@ -80,4 +80,9 @@ public String getErrorMessage(Language language) { appNameStream.collect(Collectors.joining(", "))); } + @Override + public String getInvertedErrorMessage(Language language) { + throw new UnsupportedOperationException(); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCardinality.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCardinality.java index f609fcc8a1f..58508a84ac1 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCardinality.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckCardinality.java @@ -134,4 +134,9 @@ public String getErrorMessage(Language language) { return null; } + @Override + public String getInvertedErrorMessage(Language language) { + throw new UnsupportedOperationException(); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java index 263d408d670..8d58d508b08 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHome.java @@ -1,7 +1,5 @@ package io.openems.edge.core.appmanager.validator; -import java.util.TreeMap; - import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -24,9 +22,12 @@ public class CheckHome extends AbstractCheckable implements Checkable { private final Checkable checkAppsNotInstalled; @Activate - public CheckHome(@Reference ComponentManager componentManager, ComponentContext componentContext, + public CheckHome(// + @Reference ComponentManager componentManager, // + ComponentContext componentContext, // @Reference(target = "(" + OpenemsConstants.PROPERTY_OSGI_COMPONENT_NAME + "=" - + CheckAppsNotInstalled.COMPONENT_NAME + ")") Checkable checkAppsNotInstalled) { + + CheckAppsNotInstalled.COMPONENT_NAME + ")") Checkable checkAppsNotInstalled // + ) { super(componentContext); this.componentManager = componentManager; this.checkAppsNotInstalled = checkAppsNotInstalled; @@ -35,9 +36,12 @@ public CheckHome(@Reference ComponentManager componentManager, ComponentContext @Override public boolean check() { var batteries = this.componentManager.getEdgeConfig().getComponentsByFactory("Battery.Fenecon.Home"); - this.checkAppsNotInstalled.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // - .put("appIds", new String[] { "App.FENECON.Home" }) // - .build()); + this.checkAppsNotInstalled.setProperties(Checkables.checkAppsNotInstalled(// + "App.FENECON.Home", // + "App.FENECON.Home.20", // + "App.FENECON.Home.30" // + ).properties()); + // TODO remove check for batteries // not every home has the home app installed but if a batterie of an home is // installed its probably a home and so the app can be used. @@ -52,4 +56,9 @@ public String getErrorMessage(Language language) { return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckHome.Message"); } + @Override + public String getInvertedErrorMessage(Language language) { + return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckHome.Message.Inverted"); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHost.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHost.java index b86fb0a324a..42cf685be1c 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHost.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckHost.java @@ -77,4 +77,9 @@ public String getErrorMessage(Language language) { return AbstractCheckable.getTranslation(language, "Validator.Checkable.CheckHost.NotReachable", address); } + @Override + public String getInvertedErrorMessage(Language language) { + throw new UnsupportedOperationException(); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckNoComponentInstalledOfFactoryId.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckNoComponentInstalledOfFactoryId.java index 5411fbc15ec..047cf14d042 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckNoComponentInstalledOfFactoryId.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckNoComponentInstalledOfFactoryId.java @@ -46,4 +46,9 @@ public String getErrorMessage(Language language) { "Validator.Checkable.CheckNoComponentInstalledOfFactorieId.Message", this.factorieId); } + @Override + public String getInvertedErrorMessage(Language language) { + throw new UnsupportedOperationException(); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckRelayCount.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckRelayCount.java index 3aeeebdf01a..a4b8a7bd669 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckRelayCount.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/CheckRelayCount.java @@ -93,4 +93,9 @@ public String getErrorMessage(Language language) { return messageBuilder.toString(); } + @Override + public String getInvertedErrorMessage(Language language) { + throw new UnsupportedOperationException(); + } + } diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkable.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkable.java index 883dde7d8c7..26df5d14d9c 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkable.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/Checkable.java @@ -21,13 +21,21 @@ public interface Checkable { public boolean check(); /** - * Gets the error message if the check was incorrect completed. + * Gets the error message if the check was not successful. * * @param language the language of the message * @return the message */ public String getErrorMessage(Language language); + /** + * Gets the error message if the check was successful. + * + * @param language the language of the message + * @return the message + */ + public String getInvertedErrorMessage(Language language); + /** * Sets the properties. * diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java index 2e439a35cbd..8c7c05745a0 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorConfig.java @@ -60,6 +60,20 @@ public CheckableConfig(String checkableComponentName, Map properties) this(checkableComponentName, false, properties); } + /** + * Creates a new {@link CheckableConfig} with the current + * {@link CheckableConfig#invertResult} inverted. + * + * @return the new {@link CheckableConfig} + */ + public CheckableConfig invert() { + return new CheckableConfig(// + this.checkableComponentName(), // + !this.invertResult(), // + this.properties() // + ); + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java index ad7af5dab48..d8868d85b75 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/ValidatorImpl.java @@ -1,5 +1,7 @@ package io.openems.edge.core.appmanager.validator; +import static java.util.Collections.emptyList; + import java.util.ArrayList; import java.util.List; @@ -40,9 +42,9 @@ public ValidatorImpl() { public List getErrorMessages(List checkableConfigs, Language language, boolean returnImmediate) { if (checkableConfigs.isEmpty()) { - return new ArrayList<>(); + return emptyList(); } - var errorMessages = new ArrayList(checkableConfigs.size()); + final var errorMessages = new ArrayList(checkableConfigs.size()); for (var config : checkableConfigs) { // find the componentServiceObjects base on the given configuration name @@ -71,9 +73,14 @@ public List getErrorMessages(List checkableConfigs, Lan checkable.setProperties(config.properties()); var result = checkable.check(); if (result == config.invertResult()) { - var errorMessage = checkable.getErrorMessage(language); - if (config.invertResult()) { - errorMessage = "Invert[" + errorMessage + "]"; + String errorMessage; + try { + errorMessage = config.invertResult() ? checkable.getInvertedErrorMessage(language) + : checkable.getErrorMessage(language); + } catch (UnsupportedOperationException e) { + LOG.error("Missing implementation for getting " + (config.invertResult() ? "inverted " : "") + + "error message for check \"" + config.checkableComponentName() + "\"!", e); + errorMessage = "Check \"" + config.checkableComponentName() + "\" failed."; } errorMessages.add(errorMessage); if (returnImmediate) { diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties index 6b781991d16..b43cee111a9 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_de.properties @@ -3,7 +3,8 @@ Validator.Checkable.CheckAppsNotInstalled.Message = Es ist bereits eine App aus Validator.Checkable.CheckCardinality.Message.SingleInCategorie = Es ist bereits eine App aus derselben Kategorie "{0}" installiert. Validator.Checkable.CheckCardinality.Message.Single = Es ist bereits eine App "{0}" installiert. -Validator.Checkable.CheckHome.Message = Kein Home System installiert! +Validator.Checkable.CheckHome.Message = Diese App benötigt ein Home System. +Validator.Checkable.CheckHome.Message.Inverted = Diese App ist nicht für Home Systeme verfügbar. Validator.Checkable.CheckHost.NotReachable = Gerät mit der IP "{0}" ist nicht erreichbar! Validator.Checkable.CheckHost.WrongIp = IP "{0}" ist keine valide IP-Adresse! diff --git a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties index b9d9eae6147..aa0657e25cd 100644 --- a/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties +++ b/io.openems.edge.core/src/io/openems/edge/core/appmanager/validator/translation_en.properties @@ -3,7 +3,8 @@ Validator.Checkable.CheckAppsNotInstalled.Message = Apps with names {0} are inst Validator.Checkable.CheckCardinality.Message.SingleInCategorie = There is already an app of the same category "{0}" installed. Validator.Checkable.CheckCardinality.Message.Single = There is already an app "{0}" installed. -Validator.Checkable.CheckHome.Message = No Home system installed! +Validator.Checkable.CheckHome.Message = This App requires a Home System. +Validator.Checkable.CheckHome.Message.Inverted = This App is not available for Home systems. Validator.Checkable.CheckHost.NotReachable = Device with IP "{0}" is not reachable! Validator.Checkable.CheckHost.WrongIp = IP "{0}" is not a valid IP-Address! diff --git a/io.openems.edge.core/test/io/openems/edge/app/TestADependencyToC.java b/io.openems.edge.core/test/io/openems/edge/app/TestADependencyToC.java index 0dbf9b38671..ae0c088fed9 100644 --- a/io.openems.edge.core/test/io/openems/edge/app/TestADependencyToC.java +++ b/io.openems.edge.core/test/io/openems/edge/app/TestADependencyToC.java @@ -111,7 +111,9 @@ protected ThrowingTriFunction, Language, AppConfiguration, OpenemsNamedException> appConfigurationFactory() { - return (t, u, s) -> new AppConfiguration(); + return (t, u, s) -> AppConfiguration.empty(); } @Override diff --git a/io.openems.edge.core/test/io/openems/edge/app/TestMultipleIds.java b/io.openems.edge.core/test/io/openems/edge/app/TestMultipleIds.java index d7297236b42..26cc8fc0ec1 100644 --- a/io.openems.edge.core/test/io/openems/edge/app/TestMultipleIds.java +++ b/io.openems.edge.core/test/io/openems/edge/app/TestMultipleIds.java @@ -29,6 +29,7 @@ import io.openems.edge.core.appmanager.OpenemsApp; import io.openems.edge.core.appmanager.OpenemsAppCardinality; import io.openems.edge.core.appmanager.OpenemsAppCategory; +import io.openems.edge.core.appmanager.dependency.Tasks; /** * Test app for testing dependencies. @@ -97,7 +98,9 @@ protected ThrowingTriFunction t.appId.equals("App.FENECON.Home.20")).findAny().orElse(null); assertNotNull(homeInstance); + this.appManagerTestBundle.assertNoValidationErrors(); return homeInstance; } diff --git a/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome30.java b/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome30.java index e90a2e34eb3..88d1f632f93 100644 --- a/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome30.java +++ b/io.openems.edge.core/test/io/openems/edge/app/integratedsystem/TestFeneconHome30.java @@ -188,6 +188,7 @@ private final OpenemsAppInstance createFullHome() throws Exception { .filter(t -> t.appId.equals("App.FENECON.Home.30")).findAny().orElse(null); assertNotNull(homeInstance); + this.appManagerTestBundle.assertNoValidationErrors(); this.appManagerTestBundle.assertExactSchedulerOrder("Failed setting initial Home 30 Scheduler configuration", "ctrlPrepareBatteryExtension0", "ctrlEmergencyCapacityReserve0", "ctrlGridOptimizedCharge0", diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerAppHelperImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerAppHelperImplTest.java index 5c9604c10d4..4fa8a8db0c6 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerAppHelperImplTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerAppHelperImplTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ExecutionException; import org.junit.Before; @@ -19,7 +21,12 @@ import io.openems.edge.app.TestC; import io.openems.edge.common.test.DummyUser; import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.dependency.AppManagerAppHelperImpl; import io.openems.edge.core.appmanager.dependency.DependencyDeclaration; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.ComponentAggregateTaskImpl; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerAggregateTaskImpl; +import io.openems.edge.core.appmanager.dependency.aggregatetask.StaticIpAggregateTaskImpl; import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; import io.openems.edge.core.appmanager.jsonrpc.DeleteAppInstance; import io.openems.edge.core.appmanager.jsonrpc.UpdateAppInstance; @@ -405,4 +412,30 @@ private OpenemsAppInstance getAppByAppId(String appId) { .get(); } + @Test + public void testInsertAggregateTask() { + final var componentTask = new ComponentAggregateTaskImpl(null); + final var schedulerTask = new SchedulerAggregateTaskImpl(componentTask, null); + final var networkTask = new StaticIpAggregateTaskImpl(null); + + final var list = new ArrayList>(); + + AppManagerAppHelperImpl.insert(list, componentTask); + AppManagerAppHelperImpl.insert(list, networkTask); + AppManagerAppHelperImpl.insert(list, schedulerTask); + assertEquals(List.of(componentTask, networkTask, schedulerTask), list); + + list.clear(); + AppManagerAppHelperImpl.insert(list, schedulerTask); + AppManagerAppHelperImpl.insert(list, componentTask); + AppManagerAppHelperImpl.insert(list, networkTask); + assertEquals(List.of(componentTask, schedulerTask, networkTask), list); + + list.clear(); + AppManagerAppHelperImpl.insert(list, schedulerTask); + AppManagerAppHelperImpl.insert(list, networkTask); + AppManagerAppHelperImpl.insert(list, componentTask); + assertEquals(List.of(componentTask, schedulerTask, networkTask), list); + } + } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImpSynchronizationTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImpSynchronizationTest.java index a98e7a35d7c..95056824568 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImpSynchronizationTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImpSynchronizationTest.java @@ -18,6 +18,7 @@ import io.openems.common.session.Language; import io.openems.common.session.Role; import io.openems.common.utils.JsonUtils; +import io.openems.common.utils.ReflectionUtils; import io.openems.edge.app.evcs.KebaEvcs; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyComponentContext; @@ -26,9 +27,12 @@ import io.openems.edge.common.test.DummyUser; import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.AppManagerTestBundle.CheckablesBundle; +import io.openems.edge.core.appmanager.dependency.AppManagerAppHelper; import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; import io.openems.edge.core.appmanager.jsonrpc.DeleteAppInstance; +import io.openems.edge.core.appmanager.validator.CheckAppsNotInstalled; import io.openems.edge.core.appmanager.validator.CheckCardinality; +import io.openems.edge.core.appmanager.validator.CheckHome; import io.openems.edge.core.appmanager.validator.CheckRelayCount; public class AppManagerImpSynchronizationTest { @@ -40,6 +44,8 @@ public class AppManagerImpSynchronizationTest { @Before public void before() throws Exception { this.appManager = new AppManagerImpl(); + ReflectionUtils.setAttribute(AppManagerImpl.class, this.appManager, "appValidateWorker", + new AppValidateWorker()); assertTrue(this.appManager.lockModifyingApps.tryLock()); this.appManager.lockModifyingApps.unlock(); assertFalse(this.appManager.waitingForModified); @@ -68,19 +74,27 @@ public void before() throws Exception { final var appManagerUtil = new AppManagerUtilImpl(componentManager); final var validator = new DummyValidator(); - final var checkablesBundle = new CheckablesBundle( + final var checkablesBundle = new CheckablesBundle(// new CheckCardinality(this.appManager, appManagerUtil, AppManagerTestBundle.getComponentContext(CheckCardinality.COMPONENT_NAME)), // new CheckRelayCount(componentUtil, - AppManagerTestBundle.getComponentContext(CheckRelayCount.COMPONENT_NAME), null) // + AppManagerTestBundle.getComponentContext(CheckRelayCount.COMPONENT_NAME), null), // + new CheckAppsNotInstalled(this.appManager, + AppManagerTestBundle.getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME)), // + new CheckHome(this.appManager.componentManager, + AppManagerTestBundle.getComponentContext(CheckHome.COMPONENT_NAME), + new CheckAppsNotInstalled(this.appManager, + AppManagerTestBundle.getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME))) // ); validator.setCheckables(checkablesBundle.all()); + new ComponentTest(this.appManager) // .addReference("cm", cm) // .addReference("componentManager", componentManager) // .addReference("csoAppManagerAppHelper", - DummyAppManagerAppHelper.cso(componentManager, componentUtil, validator, appManagerUtil)) // + AppManagerTestBundle.cso(new DummyAppManagerAppHelper(componentManager, + componentUtil, validator, appManagerUtil))) // .addReference("validator", validator) // .addReference("backendUtil", new DummyAppCenterBackendUtil()) // .addReference("availableApps", Lists.newArrayList(// diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java index 1bc2123c213..a34744a95ab 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerImplTest.java @@ -327,7 +327,7 @@ public void testFindAppById() { @Test public void testCheckCardinalitySingle() throws Exception { - var checkable = this.appManagerTestBundle.checkablesBundle.checkCardinality; + var checkable = this.appManagerTestBundle.checkablesBundle.checkCardinality(); checkable.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("openemsApp", this.homeApp) // .build()); @@ -341,7 +341,7 @@ public void testCheckCardinalityMultiple() throws Exception { UUID.randomUUID(), JsonUtils.buildJsonObject().build(), null)); this.appManagerTestBundle.sut.instantiatedApps.add(new OpenemsAppInstance(this.kebaEvcsApp.getAppId(), "alias", UUID.randomUUID(), JsonUtils.buildJsonObject().build(), null)); - var checkable = this.appManagerTestBundle.checkablesBundle.checkCardinality; + var checkable = this.appManagerTestBundle.checkablesBundle.checkCardinality(); checkable.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("openemsApp", this.kebaEvcsApp) // .build()); @@ -353,7 +353,7 @@ public void testCheckCardinalityMultiple() throws Exception { public void testCheckCardinalitySingleInCategorie() throws Exception { this.appManagerTestBundle.sut.instantiatedApps.add(new OpenemsAppInstance(this.awattarApp.getAppId(), "alias", UUID.randomUUID(), JsonUtils.buildJsonObject().build(), null)); - var checkable = this.appManagerTestBundle.checkablesBundle.checkCardinality; + var checkable = this.appManagerTestBundle.checkablesBundle.checkCardinality(); checkable.setProperties(new ValidatorConfig.MapBuilder<>(new TreeMap()) // .put("openemsApp", this.stromdao) // .build()); diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java index 21fd528569a..a7f505b9096 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/AppManagerTestBundle.java @@ -14,9 +14,11 @@ import java.util.function.Function; import java.util.stream.Collectors; +import org.osgi.framework.ServiceReference; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentConstants; import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.ComponentServiceObjects; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Modified; @@ -39,12 +41,16 @@ import io.openems.edge.common.test.DummyComponentManager; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.dependency.AppConfigValidator; +import io.openems.edge.core.appmanager.dependency.AppManagerAppHelper; import io.openems.edge.core.appmanager.dependency.DependencyUtil; import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance.Request; import io.openems.edge.core.appmanager.jsonrpc.DeleteAppInstance; import io.openems.edge.core.appmanager.jsonrpc.UpdateAppInstance; +import io.openems.edge.core.appmanager.validator.CheckAppsNotInstalled; import io.openems.edge.core.appmanager.validator.CheckCardinality; +import io.openems.edge.core.appmanager.validator.CheckHome; import io.openems.edge.core.appmanager.validator.CheckRelayCount; import io.openems.edge.core.appmanager.validator.Checkable; import io.openems.edge.core.appmanager.validator.Validator; @@ -60,6 +66,8 @@ public class AppManagerTestBundle { public final AppManagerUtil appManagerUtil; public final AppCenterBackendUtil appCenterBackendUtil; + private final AppValidateWorker appValidateWorker; + public final CheckablesBundle checkablesBundle; public AppManagerTestBundle(JsonObject initialComponentConfig, MyConfig initialAppManagerConfig, @@ -209,17 +217,31 @@ private final void modifyWithCurrentConfig() throws OpenemsNamedException { this.checkablesBundle = new CheckablesBundle( new CheckCardinality(this.sut, this.appManagerUtil, getComponentContext(CheckCardinality.COMPONENT_NAME)), // - new CheckRelayCount(this.componentUtil, getComponentContext(CheckRelayCount.COMPONENT_NAME), null) // + new CheckRelayCount(this.componentUtil, getComponentContext(CheckRelayCount.COMPONENT_NAME), null), // + new CheckAppsNotInstalled(this.sut, getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME)), // + new CheckHome(this.componentManger, getComponentContext(CheckHome.COMPONENT_NAME), + new CheckAppsNotInstalled(this.sut, getComponentContext(CheckAppsNotInstalled.COMPONENT_NAME))) // ); var dummyValidator = new DummyValidator(); dummyValidator.setCheckables(this.checkablesBundle.all()); this.validator = dummyValidator; - final var csoAppManagerAppHelper = DummyAppManagerAppHelper.cso(this.componentManger, this.componentUtil, + final var appManagerAppHelper = new DummyAppManagerAppHelper(this.componentManger, this.componentUtil, this.validator, this.appManagerUtil); + final var csoAppManagerAppHelper = cso((AppManagerAppHelper) appManagerAppHelper); + + this.appValidateWorker = new AppValidateWorker(); + final var appConfigValidator = new AppConfigValidator(); + + ReflectionUtils.setAttribute(AppValidateWorker.class, this.appValidateWorker, "appManagerUtil", + this.appManagerUtil); + ReflectionUtils.setAttribute(AppValidateWorker.class, this.appValidateWorker, "validator", appConfigValidator); - final var appManagerAppHelper = csoAppManagerAppHelper.getService(); + ReflectionUtils.setAttribute(AppConfigValidator.class, appConfigValidator, "appManagerUtil", + this.appManagerUtil); + ReflectionUtils.setAttribute(AppConfigValidator.class, appConfigValidator, "tasks", + appManagerAppHelper.getTasks()); // use this so the appManagerAppHelper does not has to be a OpenemsComponent and // the attribute can still be private @@ -234,6 +256,7 @@ private final void modifyWithCurrentConfig() throws OpenemsNamedException { .addReference("componentManager", this.componentManger) // .addReference("csoAppManagerAppHelper", csoAppManagerAppHelper) // .addReference("validator", this.validator) // + .addReference("appValidateWorker", this.appValidateWorker) // .addReference("backendUtil", this.appCenterBackendUtil) // .addReference("availableApps", availableAppsSupplier.apply(this)) // .activate(initialAppManagerConfig); @@ -268,12 +291,11 @@ public OpenemsAppInstance findFirst(String appId) { * @throws Exception on error */ public void assertNoValidationErrors() throws Exception { - var worker = new AppValidateWorker(this.sut); - worker.validateApps(); + this.appValidateWorker.validateApps(); // should not have found defective Apps - if (!worker.defectiveApps.isEmpty()) { - throw new Exception(worker.defectiveApps.entrySet().stream() // + if (!this.appValidateWorker.defectiveApps.isEmpty()) { + throw new Exception(this.appValidateWorker.defectiveApps.entrySet().stream() // .map(e -> e.getKey() + "[" + e.getValue() + "]") // .collect(Collectors.joining("|"))); } @@ -367,15 +389,12 @@ public void assertComponentsExist(EdgeConfig.Component... components) throws Ope } } - public static class CheckablesBundle { - - public final CheckCardinality checkCardinality; - public final CheckRelayCount checkRelayCount; - - public CheckablesBundle(CheckCardinality checkCardinality, CheckRelayCount checkRelayCount) { - this.checkCardinality = checkCardinality; - this.checkRelayCount = checkRelayCount; - } + public record CheckablesBundle(// + CheckCardinality checkCardinality, // + CheckRelayCount checkRelayCount, // + CheckAppsNotInstalled checkAppsNotInstalled, // + CheckHome checkHome // + ) { /** * Gets all {@link Checkable}. @@ -383,8 +402,11 @@ public CheckablesBundle(CheckCardinality checkCardinality, CheckRelayCount check * @return the {@link Checkable} */ public final List all() { - return Lists.newArrayList(this.checkCardinality, // - this.checkRelayCount // + return Lists.newArrayList(// + this.checkCardinality(), // + this.checkRelayCount(), // + this.checkAppsNotInstalled(), // + this.checkHome() // ); } } @@ -483,4 +505,33 @@ public void afterInit(AppManagerImpl impl, ConfigurationAdmin cm) { } + /** + * Creates a {@link ComponentServiceObjects} of a service. + * + * @param the type of the service + * @param service the service + * @return the {@link ComponentServiceObjects} + */ + public static ComponentServiceObjects cso(T service) { + return new ComponentServiceObjects() { + + @Override + public T getService() { + return service; + } + + @Override + public void ungetService(T service) { + // empty for test + } + + @Override + public ServiceReference getServiceReference() { + // not needed for test + return null; + } + + }; + } + } diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java index 7f00b8bc02b..f087ab14ea8 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/Apps.java @@ -29,6 +29,8 @@ import io.openems.edge.app.integratedsystem.FeneconHome30; import io.openems.edge.app.meter.MicrocareSdm630Meter; import io.openems.edge.app.meter.SocomecMeter; +import io.openems.edge.app.peakshaving.PeakShaving; +import io.openems.edge.app.peakshaving.PhaseAccuratePeakShaving; import io.openems.edge.app.pvselfconsumption.GridOptimizedCharge; import io.openems.edge.app.pvselfconsumption.SelfConsumptionOptimization; import io.openems.edge.app.timeofusetariff.AwattarHourly; @@ -318,6 +320,28 @@ public static final MicrocareSdm630Meter microcareSdm630Meter(AppManagerTestBund "App.Meter.Microcare.Sdm630"); } + // PeakShaving + + /** + * Test method for creating a {@link PeakShaving}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final PeakShaving peakShaving(AppManagerTestBundle t) { + return app(t, PeakShaving::new, "App.PeakShaving.PeakShaving"); + } + + /** + * Test method for creating a {@link PhaseAccuratePeakShaving}. + * + * @param t the {@link AppManagerTestBundle} + * @return the {@link OpenemsApp} instance + */ + public static final PhaseAccuratePeakShaving phaseAccuratePeakShaving(AppManagerTestBundle t) { + return app(t, PhaseAccuratePeakShaving::new, "App.PeakShaving.PhaseAccuratePeakShaving"); + } + // ess-controller /** diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java index 40ed8743bdf..2d2abc34810 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyAppManagerAppHelper.java @@ -1,9 +1,7 @@ package io.openems.edge.core.appmanager; import java.lang.reflect.InvocationTargetException; - -import org.osgi.framework.ServiceReference; -import org.osgi.service.component.ComponentServiceObjects; +import java.util.List; import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.utils.ReflectionUtils; @@ -11,59 +9,20 @@ import io.openems.edge.common.user.User; import io.openems.edge.core.appmanager.dependency.AppManagerAppHelper; import io.openems.edge.core.appmanager.dependency.AppManagerAppHelperImpl; -import io.openems.edge.core.appmanager.dependency.ComponentAggregateTaskImpl; -import io.openems.edge.core.appmanager.dependency.SchedulerAggregateTaskImpl; -import io.openems.edge.core.appmanager.dependency.StaticIpAggregateTaskImpl; import io.openems.edge.core.appmanager.dependency.TemporaryApps; import io.openems.edge.core.appmanager.dependency.UpdateValues; +import io.openems.edge.core.appmanager.dependency.aggregatetask.AggregateTask; +import io.openems.edge.core.appmanager.dependency.aggregatetask.ComponentAggregateTaskImpl; +import io.openems.edge.core.appmanager.dependency.aggregatetask.SchedulerAggregateTaskImpl; +import io.openems.edge.core.appmanager.dependency.aggregatetask.StaticIpAggregateTaskImpl; import io.openems.edge.core.appmanager.validator.Validator; public class DummyAppManagerAppHelper implements AppManagerAppHelper { - /** - * Creates a {@link ComponentServiceObjects} of a {@link AppManagerAppHelper} - * with the given parameter. - * - * @param componentManager the {@link ComponentManager} - * @param componentUtil the {@link ComponentUtil} - * @param validator the {@link Validator} - * @param appManagerUtil the {@link AppManagerUtil} - * @return the {@link ComponentServiceObjects} - * @throws IllegalAccessException on reflection error - * @throws InvocationTargetException on reflection error - */ - public static ComponentServiceObjects cso(// - ComponentManager componentManager, // - ComponentUtil componentUtil, // - Validator validator, // - AppManagerUtil appManagerUtil // - ) throws IllegalAccessException, InvocationTargetException { - return new ComponentServiceObjects() { - - private final AppManagerAppHelper impl = new DummyAppManagerAppHelper(componentManager, componentUtil, - validator, appManagerUtil); - - @Override - public AppManagerAppHelper getService() { - return this.impl; - } - - @Override - public void ungetService(AppManagerAppHelper service) { - // empty for test - } - - @Override - public ServiceReference getServiceReference() { - // not needed for test - return null; - } - - }; - } - private final AppManagerAppHelperImpl impl; + private final List> tasks; + public DummyAppManagerAppHelper(// ComponentManager componentManager, // ComponentUtil componentUtil, // @@ -73,12 +32,17 @@ public DummyAppManagerAppHelper(// final var componentTask = new ComponentAggregateTaskImpl(componentManager); final var schedulerTask = new SchedulerAggregateTaskImpl(componentTask, componentUtil); final var staticIpTask = new StaticIpAggregateTaskImpl(componentUtil); - this.impl = new AppManagerAppHelperImpl(componentManager, componentUtil, validator, componentTask, - schedulerTask, staticIpTask); + this.tasks = List.of(staticIpTask, componentTask, schedulerTask); + this.impl = new AppManagerAppHelperImpl(componentManager, componentUtil, validator); + ReflectionUtils.setAttribute(AppManagerAppHelperImpl.class, this.impl, "tasks", this.tasks); ReflectionUtils.setAttribute(AppManagerAppHelperImpl.class, this.impl, "appManagerUtil", util); } + public List> getTasks() { + return this.tasks; + } + @Override public UpdateValues installApp(User user, OpenemsAppInstance instance, OpenemsApp app) throws OpenemsNamedException { diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java index 94a18a19c57..51b830a0404 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/DummyValidator.java @@ -19,8 +19,9 @@ public List getErrorMessages(List checkableConfigs, Lan for (var check : checkableConfigs) { var checkable = this.findCheckableByName(check.checkableComponentName()); checkable.setProperties(check.properties()); - if (!checkable.check()) { - errors.add(checkable.getErrorMessage(language)); + if (checkable.check() == check.invertResult()) { + errors.add(check.invertResult() ? checkable.getInvertedErrorMessage(language) + : checkable.getErrorMessage(language)); return errors; } @@ -29,7 +30,9 @@ public List getErrorMessages(List checkableConfigs, Lan } private Checkable findCheckableByName(String name) { - return this.checkables.stream().filter(c -> c.getComponentName().equals(name)).findAny().get(); + return this.checkables.stream() // + .filter(c -> c.getComponentName().equals(name)) // + .findAny().get(); } public void setCheckables(List checkables) { diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java index 6dd6fdc2d32..df498d5f430 100644 --- a/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/TestTranslations.java @@ -67,6 +67,14 @@ public void beforeEach() throws Exception { this.apps.add(new TestTranslation(Apps.socomecMeter(t), false, JsonUtils.buildJsonObject() // .addProperty("MODBUS_ID", "modbus0") // .build())); + this.apps.add(new TestTranslation(Apps.peakShaving(t), true, JsonUtils.buildJsonObject() // + .addProperty("ESS_ID", "ess0") // + .addProperty("METER_ID", "meter0") // + .build())); + this.apps.add(new TestTranslation(Apps.phaseAccuratePeakShaving(t), true, JsonUtils.buildJsonObject() // + .addProperty("ESS_ID", "ess0") // + .addProperty("METER_ID", "meter0") // + .build())); this.apps.add(new TestTranslation(Apps.fixActivePower(t), true, JsonUtils.buildJsonObject() // .addProperty("ESS_ID", "ess0") // .build())); diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImplTest.java new file mode 100644 index 00000000000..44745092e91 --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/ComponentAggregateTaskImplTest.java @@ -0,0 +1,261 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import static java.util.Collections.emptyList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.test.DummyUser; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.DummyPseudoComponentManager; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.dependency.Tasks; + +public class ComponentAggregateTaskImplTest { + + private final User user = new DummyUser("1", "password", Language.DEFAULT, Role.ADMIN); + + private ComponentAggregateTask task; + + private DummyPseudoComponentManager componentManager; + + @Before + public void setUp() throws Exception { + this.componentManager = new DummyPseudoComponentManager(); + this.task = new ComponentAggregateTaskImpl(this.componentManager); + this.task.reset(); + } + + @Test + public void testAggregate() { + final var config = new ComponentConfiguration(// + new EdgeConfig.Component("test0", "test", "Test.test", JsonUtils.buildJsonObject() // + .build()) // + ); + + // should be able to handle null values + this.task.aggregate(config, null); + this.task.aggregate(null, config); + this.task.aggregate(null, null); + this.task.aggregate(config, config); + } + + @Test + public void testCreate() throws Exception { + final var dummyComponentId = "test0"; + final var config = new ComponentConfiguration(// + new EdgeConfig.Component(dummyComponentId, "test", "Test.test", JsonUtils.buildJsonObject() // + .build()) // + ); + + this.task.aggregate(config, null); + this.task.create(this.user, emptyList()); + final var component = this.componentManager.getComponent(dummyComponentId); + assertNotNull(component); + } + + @Test + public void testCreateWithExistingComponentNotFromOtherAppSameConfig() throws Exception { + final var config = new ComponentConfiguration(// + new EdgeConfig.Component("test0", "test", "Test.test", JsonUtils.buildJsonObject() // + .build()) // + ); + assertEquals(0, this.componentManager.getAllComponents().size()); + + // create component + this.componentManager.addComponent(config.components().get(0)); + assertEquals(1, this.componentManager.getAllComponents().size()); + + // not failing even if component already exist + this.task.aggregate(config, null); + this.task.create(this.user, emptyList()); + + // not creating the same component twice + assertEquals(1, this.componentManager.getAllComponents().size()); + } + + @Test + public void testCreateWithExistingComponentNotFromOtherAppDifferentConfig() throws Exception { + final var dummyComponentId = "test0"; + final var config = new ComponentConfiguration(// + new EdgeConfig.Component(dummyComponentId, "test", "Test.test", JsonUtils.buildJsonObject() // + .addProperty("testProperty", "test_updated_value") // + .build()) // + ); + assertEquals(0, this.componentManager.getAllComponents().size()); + + // create component + this.componentManager.addComponent(new EdgeConfig.Component(dummyComponentId, "test", "Test.test", + JsonUtils.buildJsonObject() // + .addProperty("testProperty", "test_first_value") // + .build())); + assertEquals(1, this.componentManager.getAllComponents().size()); + + // not failing even if component already exist + this.task.aggregate(config, null); + this.task.create(this.user, emptyList()); + + // not creating the same component twice + assertEquals(1, this.componentManager.getAllComponents().size()); + final var component = this.componentManager.getComponent(dummyComponentId); + assertEquals("test_updated_value", component.getComponentContext().getProperties().get("testProperty")); + } + + @Test + public void testCreateWithExistingComponentFromOtherAppSameConfig() throws Exception { + final var config = new ComponentConfiguration(// + new EdgeConfig.Component("test0", "test", "Test.test", JsonUtils.buildJsonObject() // + .build()) // + ); + assertEquals(0, this.componentManager.getAllComponents().size()); + + // creating components of 1st config + this.task.aggregate(config, null); + this.task.create(this.user, emptyList()); + assertEquals(1, this.componentManager.getAllComponents().size()); + + this.task.reset(); + + // creating components of 2nd config with same properties + this.task.aggregate(config, null); + this.task.create(this.user, List.of(AppConfiguration.create() // + .addTask(Tasks.component(config.components())) // + .build())); + + // not creating the same component twice + assertEquals(1, this.componentManager.getAllComponents().size()); + } + + @Test(expected = OpenemsNamedException.class) + public void testCreateWithExistingComponentFromOtherAppDifferentConfig() throws Exception { + final var dummyComponentId = "test0"; + final var config = AppConfiguration.create() // + .addTask(Tasks.component(new EdgeConfig.Component(dummyComponentId, "test", "Test.test", + JsonUtils.buildJsonObject() // + .addProperty("testProperty", "test_first_value") // + .build()))) + .build(); + assertEquals(0, this.componentManager.getAllComponents().size()); + + // creating components of 1st config + this.task.aggregate(config.getConfiguration(ComponentAggregateTask.class), null); + this.task.create(this.user, emptyList()); + assertEquals(1, this.componentManager.getAllComponents().size()); + + // creating components of 2nd config with different properties + this.task.aggregate(new ComponentConfiguration(// + new EdgeConfig.Component(dummyComponentId, "test", "Test.test", JsonUtils.buildJsonObject() // + .addProperty("testProperty", "test_updated_value") // + .build()) // + ), null); + this.task.create(this.user, List.of(config)); + } + + @Test(expected = OpenemsNamedException.class) + public void testDelete() throws Exception { + final var dummyComponentId = "test0"; + try { + final var config = new ComponentConfiguration(// + new EdgeConfig.Component(dummyComponentId, "test", "Test.test", JsonUtils.buildJsonObject() // + .build()) // + ); + + this.componentManager.addComponent(config.components().get(0)); + + this.task.aggregate(null, config); + this.task.delete(this.user, emptyList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + this.componentManager.getComponent(dummyComponentId); + } + + @Test + public void testGetGeneralFailMessage() { + final var dt = TranslationUtil.enableDebugMode(); + + for (var l : Language.values()) { + this.task.getGeneralFailMessage(l); + } + assertTrue(dt.getMissingKeys().isEmpty()); + } + + @Test + public void testValidate() throws Exception { + final var config = AppConfiguration.create() // + .addTask(Tasks + .component(new EdgeConfig.Component("test0", "test", "Test.test", JsonUtils.buildJsonObject() // + .build()))) + .build(); + + this.task.aggregate(config.getConfiguration(ComponentAggregateTask.class), null); + this.task.create(this.user, emptyList()); + + final var errors = new ArrayList(); + this.task.validate(errors, config, config.getConfiguration(ComponentAggregateTask.class)); + assertTrue(String.join(", ", errors), errors.isEmpty()); + } + + @Test + public void testValidateDetectMissingComponent() throws Exception { + final var config = AppConfiguration.create() // + .addTask(Tasks + .component(new EdgeConfig.Component("test0", "test", "Test.test", JsonUtils.buildJsonObject() // + .build()))) + .build(); + + final var errors = new ArrayList(); + this.task.validate(errors, config, config.getConfiguration(ComponentAggregateTask.class)); + assertFalse("No errors while validating configuration", errors.isEmpty()); + } + + @Test + public void testGetCreatedComponents() throws Exception { + assertTrue(this.task.getCreatedComponents().isEmpty()); + final var dummyComponentId = "test0"; + final var config = new ComponentConfiguration(// + new EdgeConfig.Component(dummyComponentId, "test", "Test.test", JsonUtils.buildJsonObject() // + .build()) // + ); + + this.task.aggregate(config, null); + this.task.create(this.user, emptyList()); + + assertFalse(this.task.getCreatedComponents().isEmpty()); + assertEquals(1, this.task.getCreatedComponents().size()); + assertEquals(dummyComponentId, this.task.getCreatedComponents().get(0).getId()); + } + + @Test + public void testGetDeletedComponents() throws Exception { + assertTrue(this.task.getDeletedComponents().isEmpty()); + final var dummyComponentId = "test0"; + final var config = new ComponentConfiguration(// + new EdgeConfig.Component(dummyComponentId, "test", "Test.test", JsonUtils.buildJsonObject() // + .build()) // + ); + + this.componentManager.addComponent(config.components().get(0)); + + this.task.aggregate(null, config); + this.task.delete(this.user, emptyList()); + + assertFalse(this.task.getDeletedComponents().isEmpty()); + assertEquals(1, this.task.getDeletedComponents().size()); + assertEquals(dummyComponentId, this.task.getDeletedComponents().get(0)); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImplTest.java new file mode 100644 index 00000000000..e91388ac304 --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/SchedulerAggregateTaskImplTest.java @@ -0,0 +1,145 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import static io.openems.common.utils.JsonUtils.toJsonArray; +import static java.util.Collections.emptyList; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import org.junit.Before; +import org.junit.Test; + +import com.google.gson.JsonPrimitive; + +import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; +import io.openems.common.jsonrpc.request.UpdateComponentConfigRequest; +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.common.types.EdgeConfig; +import io.openems.common.utils.JsonUtils; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.common.test.DummyUser; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.AppConfiguration; +import io.openems.edge.core.appmanager.ComponentUtilImpl; +import io.openems.edge.core.appmanager.DummyPseudoComponentManager; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.dependency.Tasks; + +public class SchedulerAggregateTaskImplTest { + + private final User user = new DummyUser("1", "password", Language.DEFAULT, Role.ADMIN); + + private SchedulerAggregateTask task; + + private DummyPseudoComponentManager componentManager; + private DummyConfigurationAdmin cm; + private ComponentUtilImpl componentUtil; + private ComponentAggregateTask aggregateTask; + + @Before + public void setUp() throws Exception { + this.componentManager = new DummyPseudoComponentManager(); + this.cm = new DummyConfigurationAdmin(); + this.componentUtil = new ComponentUtilImpl(this.componentManager, this.cm); + this.componentManager.setConfigurationAdmin(this.cm); + this.aggregateTask = new ComponentAggregateTaskImpl(this.componentManager); + this.aggregateTask.reset(); + this.task = new SchedulerAggregateTaskImpl(this.aggregateTask, this.componentUtil); + this.task.reset(); + + this.componentManager.addComponent(new EdgeConfig.Component("scheduler0", "", "Scheduler.AllAlphabetically", + JsonUtils.buildJsonObject() // + .addProperty("enabled", true) // + .add("controllers.ids", JsonUtils.buildJsonArray() // + .build()) + .build())); + // create config for scheduler + this.cm.getOrCreateEmptyConfiguration( + this.componentManager.getEdgeConfig().getComponent("scheduler0").get().getPid()); + } + + @Test + public void testAggregate() { + final var config = new SchedulerConfiguration("test0", "test1"); + + this.task.aggregate(config, null); + this.task.aggregate(null, config); + this.task.aggregate(null, null); + this.task.aggregate(config, config); + } + + @Test + public void testCreate() throws Exception { + final var config = new SchedulerConfiguration("test0", "test1"); + this.componentManager.addComponent( + new EdgeConfig.Component("test0", "test", "Test.test", JsonUtils.buildJsonObject().build())); + this.componentManager.addComponent( + new EdgeConfig.Component("test1", "test", "Test.test", JsonUtils.buildJsonObject().build())); + + this.task.aggregate(config, null); + this.task.create(this.user, emptyList()); + this.assertExactSchedulerOrder("Ids in scheduler do not match!", config.componentOrder()); + } + + @Test + public void testDelete() throws Exception { + final var config = new SchedulerConfiguration("test0", "test1"); + this.setSchedulerIds(config.componentOrder()); + this.task.aggregate(null, config); + this.task.delete(this.user, emptyList()); + this.assertExactSchedulerOrder("Ids in scheduler got not removed!"); + } + + @Test + public void testGetGeneralFailMessage() { + final var dt = TranslationUtil.enableDebugMode(); + + for (var l : Language.values()) { + this.task.getGeneralFailMessage(l); + } + assertTrue(dt.getMissingKeys().isEmpty()); + } + + @Test + public void testValidate() throws Exception { + final var config = AppConfiguration.create() // + .addTask(Tasks.scheduler("test0", "test1")) // + .build(); + this.setSchedulerIds(config.getConfiguration(SchedulerAggregateTask.class).componentOrder()); + + final var errors = new ArrayList(); + this.task.validate(errors, config, config.getConfiguration(SchedulerAggregateTask.class)); + assertTrue(errors.isEmpty()); + } + + private void assertExactSchedulerOrder(String message, List orderIds) throws IOException { + this.assertExactSchedulerOrder(message, orderIds.toArray(String[]::new)); + } + + private void assertExactSchedulerOrder(String message, String... orderIds) throws IOException { + final var config = this.cm.getConfiguration( + this.componentManager.getEdgeConfig().getComponent("scheduler0").get().getPid(), null); + final var ids = (String[]) config.getProperties().get("controllers.ids"); + assertArrayEquals(message, orderIds, ids); + } + + private void setSchedulerIds(List ids) + throws OpenemsNamedException, InterruptedException, ExecutionException { + this.setSchedulerIds(ids.toArray(String[]::new)); + } + + private void setSchedulerIds(String... ids) throws OpenemsNamedException, InterruptedException, ExecutionException { + this.componentManager.handleJsonrpcRequest(this.user, new UpdateComponentConfigRequest("scheduler0", List.of(// + new UpdateComponentConfigRequest.Property("controllers.ids", Arrays.stream(ids) // + .map(JsonPrimitive::new) // + .collect(toJsonArray())) // + ))).get(); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImplTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImplTest.java new file mode 100644 index 00000000000..33ac7ce96ec --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/dependency/aggregatetask/StaticIpAggregateTaskImplTest.java @@ -0,0 +1,41 @@ +package io.openems.edge.core.appmanager.dependency.aggregatetask; + +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import io.openems.common.session.Language; +import io.openems.edge.common.test.DummyConfigurationAdmin; +import io.openems.edge.core.appmanager.ComponentUtilImpl; +import io.openems.edge.core.appmanager.DummyPseudoComponentManager; +import io.openems.edge.core.appmanager.TranslationUtil; + +public class StaticIpAggregateTaskImplTest { + + private StaticIpAggregateTask task; + private DummyPseudoComponentManager componentManager; + private DummyConfigurationAdmin cm; + private ComponentUtilImpl componentUtil; + + @Before + public void setUp() throws Exception { + this.componentManager = new DummyPseudoComponentManager(); + this.cm = new DummyConfigurationAdmin(); + this.componentUtil = new ComponentUtilImpl(this.componentManager, this.cm); + this.componentManager.setConfigurationAdmin(this.cm); + this.task = new StaticIpAggregateTaskImpl(this.componentUtil); + this.task.reset(); + } + + @Test + public void testGetGeneralFailMessage() { + final var dt = TranslationUtil.enableDebugMode(); + + for (var l : Language.values()) { + this.task.getGeneralFailMessage(l); + } + assertTrue(dt.getMissingKeys().isEmpty()); + } + +} diff --git a/io.openems.edge.core/test/io/openems/edge/core/appmanager/validator/CheckHomeTest.java b/io.openems.edge.core/test/io/openems/edge/core/appmanager/validator/CheckHomeTest.java new file mode 100644 index 00000000000..b399ee3537b --- /dev/null +++ b/io.openems.edge.core/test/io/openems/edge/core/appmanager/validator/CheckHomeTest.java @@ -0,0 +1,91 @@ +package io.openems.edge.core.appmanager.validator; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +import io.openems.common.session.Language; +import io.openems.common.session.Role; +import io.openems.edge.app.integratedsystem.TestFeneconHome; +import io.openems.edge.app.integratedsystem.TestFeneconHome20; +import io.openems.edge.app.integratedsystem.TestFeneconHome30; +import io.openems.edge.common.test.DummyUser; +import io.openems.edge.common.user.User; +import io.openems.edge.core.appmanager.AppManagerTestBundle; +import io.openems.edge.core.appmanager.AppManagerTestBundle.PseudoComponentManagerFactory; +import io.openems.edge.core.appmanager.Apps; +import io.openems.edge.core.appmanager.TranslationUtil; +import io.openems.edge.core.appmanager.jsonrpc.AddAppInstance; + +public class CheckHomeTest { + + private final User user = new DummyUser("1", "password", Language.DEFAULT, Role.ADMIN); + + private AppManagerTestBundle appManagerTestBundle; + + private CheckHome checkHome; + + @Before + public void setUp() throws Exception { + this.appManagerTestBundle = new AppManagerTestBundle(null, null, t -> { + return ImmutableList.of(// + Apps.feneconHome(t), // + Apps.feneconHome20(t), // + Apps.feneconHome30(t) // + ); + }, null, new PseudoComponentManagerFactory()); + this.checkHome = this.appManagerTestBundle.checkablesBundle.checkHome(); + } + + @Test + public void testCheck() { + final var checkHome = this.appManagerTestBundle.checkablesBundle.checkHome(); + assertFalse(checkHome.check()); + } + + @Test + public void testCheckWithInstalledHome10() throws Exception { + final var response = this.appManagerTestBundle.sut + .handleAddAppInstanceRequest(this.user, + new AddAppInstance.Request("App.FENECON.Home", "key", "alias", TestFeneconHome.fullSettings())) + .get(); + + assertTrue(response.warnings.isEmpty()); + assertTrue(this.checkHome.check()); + } + + @Test + public void testCheckWithInstalledHome20() throws Exception { + final var response = this.appManagerTestBundle.sut.handleAddAppInstanceRequest(this.user, + new AddAppInstance.Request("App.FENECON.Home.20", "key", "alias", TestFeneconHome20.fullSettings())) + .get(); + + assertTrue(response.warnings.isEmpty()); + assertTrue(this.checkHome.check()); + } + + @Test + public void testCheckWithInstalledHome30() throws Exception { + final var response = this.appManagerTestBundle.sut.handleAddAppInstanceRequest(this.user, + new AddAppInstance.Request("App.FENECON.Home.30", "key", "alias", TestFeneconHome30.fullSettings())) + .get(); + + assertTrue(response.warnings.isEmpty()); + assertTrue(this.checkHome.check()); + } + + @Test + public void testGetErrorMessage() { + final var dt = TranslationUtil.enableDebugMode(); + for (var l : Language.values()) { + this.checkHome.getErrorMessage(l); + this.checkHome.getInvertedErrorMessage(l); + } + assertTrue(dt.getMissingKeys().isEmpty()); + } + +} diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java index 8f2211d082e..2dc7c6aaa2a 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImpl.java @@ -13,6 +13,9 @@ import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventHandler; +import org.osgi.service.event.propertytypes.EventTopics; import org.osgi.service.metatype.annotations.Designate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +36,7 @@ import io.openems.edge.common.channel.IntegerWriteChannel; import io.openems.edge.common.channel.value.Value; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.startstop.StartStop; import io.openems.edge.common.startstop.StartStoppable; import io.openems.edge.common.sum.Sum; @@ -57,9 +61,12 @@ immediate = true, // configurationPolicy = ConfigurationPolicy.REQUIRE // ) +@EventTopics({ // + EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, // +}) public class GoodWeBatteryInverterImpl extends AbstractGoodWe implements GoodWeBatteryInverter, GoodWe, HybridManagedSymmetricBatteryInverter, - ManagedSymmetricBatteryInverter, SymmetricBatteryInverter, ModbusComponent, OpenemsComponent { + ManagedSymmetricBatteryInverter, SymmetricBatteryInverter, ModbusComponent, OpenemsComponent, EventHandler { // Fenecon Home Battery Static module min voltage, used to calculate battery // module number per tower @@ -144,6 +151,14 @@ protected void deactivate() { super.deactivate(); } + @Override + public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } + super.handleEvent(event); + } + /** * Apply the configuration on Activate and Modified. * @@ -518,5 +533,4 @@ public boolean isManaged() { public boolean isOffGridPossible() { return this.config.backupEnable().equals(EnableDisable.ENABLE); } - } diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/Config.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/Config.java index b2be70f8362..14288206959 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/Config.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/Config.java @@ -3,8 +3,6 @@ import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.ObjectClassDefinition; -import io.openems.edge.goodwe.GoodWeConstants; - @ObjectClassDefinition(// name = "GoodWe Charger Two-String", // description = "Implements a GoodWe PV string (for an MPPT of two strings).") @@ -28,14 +26,5 @@ @AttributeDefinition(name = "GoodWe ESS or Battery-Inverter target filter", description = "This is auto-generated by 'GoodWe ESS or Battery-Inverter'.") String essOrBatteryInverter_target() default "(enabled=true)"; - @AttributeDefinition(name = "Modbus-ID", description = "ID of Modbus bridge.") - String modbus_id() default "modbus0"; - - @AttributeDefinition(name = "Modbus Unit-ID", description = "The Unit-ID of the Modbus device.") - int modbusUnitId() default GoodWeConstants.DEFAULT_UNIT_ID; - - @AttributeDefinition(name = "Modbus target filter", description = "This is auto-generated by 'Modbus-ID'.") - String Modbus_target() default "(enabled=true)"; - String webconsole_configurationFactory_nameHint() default "GoodWe Charger Two-String [{id}]"; } diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoString.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoString.java index bdbf5beb458..adf81bff1cf 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoString.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoString.java @@ -1,19 +1,16 @@ package io.openems.edge.goodwe.charger.twostring; -import io.openems.common.channel.Unit; -import io.openems.common.types.OpenemsType; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + import io.openems.edge.common.channel.Doc; -import io.openems.edge.common.channel.IntegerReadChannel; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.ess.dccharger.api.EssDcCharger; +import io.openems.edge.goodwe.charger.GoodWeCharger; -public interface GoodWeChargerTwoString extends OpenemsComponent { +public interface GoodWeChargerTwoString extends OpenemsComponent, EssDcCharger, GoodWeCharger { public enum ChannelId implements io.openems.edge.common.channel.ChannelId { - - TOTAL_MPPT_POWER(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.WATT)), - TOTAL_MPPT_CURRENT(Doc.of(OpenemsType.INTEGER) // - .unit(Unit.MILLIAMPERE)), // ; private final Doc doc; @@ -29,20 +26,48 @@ public Doc doc() { } /** - * Gets the Channel for {@link ChannelId#TOTAL_MPPT_POWER}. - * - * @return the Channel + * Used PV port of the GoodWe inverter. + * + * @return Used PV port */ - public default IntegerReadChannel getTotalMpptPowerChannel() { - return this.channel(ChannelId.TOTAL_MPPT_POWER); - } + public PvPort pvPort(); /** - * Gets the Channel for {@link ChannelId#TOTAL_MPPT_CURRENT}. - * - * @return the Channel + * Calculate a value by rule of three. + * + *

+ * Solves proportions and calculate the unknown value. + * + *

+ * Assure that the unit of the divisor and relatedValue are the same. + * + * @param total total optional of the required unit + * @param divisor divisor of the known unit + * @param related related optional with the known unit + * @return the calculated result. Return null for empty parameters or zero + * divisor */ - public default IntegerReadChannel getTotalMpptCurrentChannel() { - return this.channel(ChannelId.TOTAL_MPPT_CURRENT); + public static Optional calculateByRuleOfThree(Optional total, Optional divisor, + Optional related) { + + var result = new AtomicReference(null); + total.ifPresent(totalValue -> { + divisor.ifPresent(divisorValue -> { + related.ifPresent(relatedValue -> { + if (divisorValue == 0) { + return; + } + + /* + * As the total power of the charger is sometimes less than the power of an + * individual string, the minimum is taken. + * + * TODO: Remove it if it has been fixed by GoodWe + */ + result.set(Math.round((totalValue * relatedValue) / divisorValue.floatValue())); + }); + }); + }); + return Optional.ofNullable(result.get()); } } \ No newline at end of file diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImpl.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImpl.java index 3e75279df65..99d8632e405 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImpl.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImpl.java @@ -1,9 +1,5 @@ package io.openems.edge.goodwe.charger.twostring; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; @@ -14,31 +10,26 @@ import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.event.Event; import org.osgi.service.event.EventHandler; import org.osgi.service.event.propertytypes.EventTopics; import org.osgi.service.metatype.annotations.Designate; import io.openems.common.channel.AccessMode; import io.openems.common.exceptions.OpenemsException; -import io.openems.edge.bridge.modbus.api.BridgeModbus; -import io.openems.edge.bridge.modbus.api.ElementToChannelConverter; import io.openems.edge.bridge.modbus.api.ModbusComponent; -import io.openems.edge.bridge.modbus.api.ModbusProtocol; -import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement; -import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask; -import io.openems.edge.common.channel.value.Value; +import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.modbusslave.ModbusSlave; import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable; import io.openems.edge.common.modbusslave.ModbusSlaveTable; -import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.ess.dccharger.api.EssDcCharger; -import io.openems.edge.goodwe.charger.AbstractGoodWeEtCharger; import io.openems.edge.goodwe.charger.GoodWeCharger; import io.openems.edge.goodwe.common.GoodWe; import io.openems.edge.timedata.api.Timedata; import io.openems.edge.timedata.api.TimedataProvider; +import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; @Designate(ocd = Config.class, factory = true) @Component(// @@ -49,8 +40,11 @@ @EventTopics({ // EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE // }) -public class GoodWeChargerTwoStringImpl extends AbstractGoodWeEtCharger implements GoodWeChargerTwoString, EssDcCharger, - GoodWeCharger, ModbusComponent, OpenemsComponent, EventHandler, TimedataProvider, ModbusSlave { +public class GoodWeChargerTwoStringImpl extends AbstractOpenemsComponent implements GoodWeChargerTwoString, + EssDcCharger, GoodWeCharger, OpenemsComponent, EventHandler, TimedataProvider, ModbusSlave { + + private final CalculateEnergyFromPower calculateActualEnergy = new CalculateEnergyFromPower(this, + EssDcCharger.ChannelId.ACTUAL_ENERGY); @Reference private ConfigurationAdmin cm; @@ -61,13 +55,7 @@ public class GoodWeChargerTwoStringImpl extends AbstractGoodWeEtCharger implemen @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) private volatile Timedata timedata = null; - private PvPort pvPort; - - @Override - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) - protected void setModbus(BridgeModbus modbus) { - super.setModbus(modbus); - } + private Config config; public GoodWeChargerTwoStringImpl() { super(// @@ -81,11 +69,8 @@ public GoodWeChargerTwoStringImpl() { @Activate private void activate(ComponentContext context, Config config) throws OpenemsException { - this.pvPort = config.pvPort(); - if (super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(), this.cm, - "Modbus", config.modbus_id())) { - return; - } + super.activate(context, config.id(), config.alias(), config.enabled()); + this.config = config; if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "essOrBatteryInverter", config.essOrBatteryInverter_id())) { @@ -103,104 +88,12 @@ protected void deactivate() { } @Override - protected ModbusProtocol defineModbusProtocol() throws OpenemsException { - final var mpptPowerAddress = this.pvPort.mpptPowerAddress; - final var mpptCurrentAddress = this.pvPort.mpptCurrentAddress; - final var pvStartAddress = this.pvPort.pvStartAddress; - final var modbusProtocol = new ModbusProtocol(this, // - - // Voltage & current of single MPPT string - new FC3ReadRegistersTask(pvStartAddress, Priority.HIGH, // - m(EssDcCharger.ChannelId.VOLTAGE, new UnsignedWordElement(pvStartAddress), - ElementToChannelConverter.SCALE_FACTOR_2), - m(EssDcCharger.ChannelId.CURRENT, new UnsignedWordElement(pvStartAddress + 1), - ElementToChannelConverter.SCALE_FACTOR_2)), - - // Total MPPT power - new FC3ReadRegistersTask(mpptPowerAddress, Priority.HIGH, // - m(GoodWeChargerTwoString.ChannelId.TOTAL_MPPT_POWER, - new UnsignedWordElement(mpptPowerAddress))), - - // Total MPPT current - new FC3ReadRegistersTask(mpptCurrentAddress, Priority.HIGH, // - m(GoodWeChargerTwoString.ChannelId.TOTAL_MPPT_CURRENT, - new UnsignedWordElement(mpptCurrentAddress), // - ElementToChannelConverter.SCALE_FACTOR_2)) - - ); - - // Calculate power of single MPPT string - this.addCalculateChannelListeners(); - - return modbusProtocol; - } - - /** - * Calculates required Channels from other existing Channels. - */ - private void addCalculateChannelListeners() { - - // Get actual Channels - var totalMpptPowerChannel = this.getTotalMpptPowerChannel(); - var totalMpptCurrentChannel = this.getTotalMpptCurrentChannel(); - var stringCurrentChannel = this.getCurrentChannel(); - - // Power Value from the total MPPT power and current values - final Consumer> calculatePower = ignore -> { - // TODO: Calculate based on the related string - this._setActualPower(// - calculateByRuleOfThree(// - totalMpptPowerChannel.getNextValue().asOptional(), // - totalMpptCurrentChannel.getNextValue().asOptional(), // - stringCurrentChannel.getNextValue().asOptional()) // - // If at least one value was present, the result should not be null. - .orElse(0) // - ); - }; - - // Add Listeners - totalMpptPowerChannel.onSetNextValue(calculatePower); - stringCurrentChannel.onSetNextValue(calculatePower); - totalMpptCurrentChannel.onSetNextValue(calculatePower); - } - - /** - * Calculate a value by rule of three. - * - *

- * Solves proportions and calculate the unknown value. - * - *

- * Assure that the unit of the divisor and relatedValue are the same. - * - * @param total total optional of the required unit - * @param divisor divisor of the known unit - * @param related related optional with the known unit - * @return the calculated result. Return null for empty parameters or zero - * divisor - */ - public static Optional calculateByRuleOfThree(Optional total, Optional divisor, - Optional related) { - - var result = new AtomicReference(null); - total.ifPresent(totalValue -> { - divisor.ifPresent(divisorValue -> { - related.ifPresent(relatedValue -> { - if (divisorValue == 0) { - return; - } - - /* - * As the total power of the charger is sometimes less than the power of an - * individual string, the minimum is taken. - * - * TODO: Remove it if it has been fixed by GoodWe - */ - result.set(Math.round((totalValue * relatedValue) / divisorValue.floatValue())); - }); - }); - }); - return Optional.ofNullable(result.get()); + public void handleEvent(Event event) { + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE: + this.calculateEnergy(); + break; + } } @Override @@ -218,13 +111,27 @@ public Timedata getTimedata() { } @Override - protected GoodWe getEssOrBatteryInverter() { - return this.essOrBatteryInverter; + public PvPort pvPort() { + return this.config.pvPort(); + } + + /** + * Calculate the Energy values from ActivePower. + */ + private void calculateEnergy() { + var actualPower = this.getActualPower().get(); + if (actualPower == null) { + // Not available + this.calculateActualEnergy.update(null); + } else if (actualPower > 0) { + this.calculateActualEnergy.update(actualPower); + } else { + this.calculateActualEnergy.update(0); + } } @Override - protected int getStartAddress() { - // Not used because the defineModbusProtocol is not generic by the start address - return 0; + public final String debugLog() { + return "L:" + this.getActualPower().asString(); } } diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/PvPort.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/PvPort.java index 1f094cd4f43..36edaca2fcc 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/PvPort.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/charger/twostring/PvPort.java @@ -1,26 +1,37 @@ package io.openems.edge.goodwe.charger.twostring; +import io.openems.edge.goodwe.common.GoodWe; + /** * Defines the PV-Port of a GoodWe Charger Two-String. */ public enum PvPort { - PV_1(35337, 35345, 35103, 35107), // - PV_2(35337, 35345, 35107, 35103), // - PV_3(35338, 35346, 35111, 35115), // - PV_4(35338, 35346, 35115, 35111), // - PV_5(35339, 35347, 35304, 35306), // - PV_6(35339, 35347, 35306, 35304); + PV_1(GoodWe.ChannelId.TWO_S_MPPT1_P, GoodWe.ChannelId.TWO_S_MPPT1_I, GoodWe.ChannelId.TWO_S_PV1_I, + GoodWe.ChannelId.TWO_S_PV2_I, GoodWe.ChannelId.TWO_S_PV1_V), + PV_2(GoodWe.ChannelId.TWO_S_MPPT1_P, GoodWe.ChannelId.TWO_S_MPPT1_I, GoodWe.ChannelId.TWO_S_PV2_I, + GoodWe.ChannelId.TWO_S_PV1_I, GoodWe.ChannelId.TWO_S_PV2_V), // + PV_3(GoodWe.ChannelId.TWO_S_MPPT2_P, GoodWe.ChannelId.TWO_S_MPPT2_I, GoodWe.ChannelId.TWO_S_PV3_I, + GoodWe.ChannelId.TWO_S_PV4_I, GoodWe.ChannelId.TWO_S_PV3_V), // + PV_4(GoodWe.ChannelId.TWO_S_MPPT2_P, GoodWe.ChannelId.TWO_S_MPPT2_I, GoodWe.ChannelId.TWO_S_PV4_I, + GoodWe.ChannelId.TWO_S_PV5_I, GoodWe.ChannelId.TWO_S_PV4_V), // + PV_5(GoodWe.ChannelId.TWO_S_MPPT3_P, GoodWe.ChannelId.TWO_S_MPPT3_I, GoodWe.ChannelId.TWO_S_PV5_I, + GoodWe.ChannelId.TWO_S_PV6_I, GoodWe.ChannelId.TWO_S_PV5_V), // + PV_6(GoodWe.ChannelId.TWO_S_MPPT3_P, GoodWe.ChannelId.TWO_S_MPPT3_I, GoodWe.ChannelId.TWO_S_PV6_I, + GoodWe.ChannelId.TWO_S_PV5_I, GoodWe.ChannelId.TWO_S_PV6_V); // - public final int mpptPowerAddress; - public final int mpptCurrentAddress; - public final int pvStartAddress; - public final int relatedPvStartAddress; + public final GoodWe.ChannelId mpptPowerChannelId; + public final GoodWe.ChannelId mpptCurrentChannelId; + public final GoodWe.ChannelId pvCurrentId; + public final GoodWe.ChannelId relatedPvCurrent; + public final GoodWe.ChannelId pvVoltageId; - private PvPort(int mpptPowerAddress, int mpptCurrentAddress, int pvStartAddress, int relatedPvStartAddress) { - this.mpptPowerAddress = mpptPowerAddress; - this.mpptCurrentAddress = mpptCurrentAddress; - this.pvStartAddress = pvStartAddress; - this.relatedPvStartAddress = relatedPvStartAddress; + private PvPort(GoodWe.ChannelId mpptPowerChannelId, GoodWe.ChannelId mpptCurrentChannelId, + GoodWe.ChannelId pvCurrentId, GoodWe.ChannelId relatedPvCurrent, GoodWe.ChannelId pvVoltageId) { + this.mpptPowerChannelId = mpptPowerChannelId; + this.mpptCurrentChannelId = mpptCurrentChannelId; + this.pvCurrentId = pvCurrentId; + this.relatedPvCurrent = relatedPvCurrent; + this.pvVoltageId = pvVoltageId; } } diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java index 4a5f51d0875..6ea15d2e10c 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/AbstractGoodWe.java @@ -12,6 +12,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,12 +42,14 @@ import io.openems.edge.common.channel.EnumReadChannel; import io.openems.edge.common.channel.IntegerReadChannel; import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.event.EdgeEventConstants; import io.openems.edge.common.sum.GridMode; import io.openems.edge.common.taskmanager.Priority; import io.openems.edge.common.type.TypeUtils; import io.openems.edge.ess.api.HybridEss; import io.openems.edge.ess.api.SymmetricEss; import io.openems.edge.goodwe.charger.GoodWeCharger; +import io.openems.edge.goodwe.charger.twostring.GoodWeChargerTwoString; import io.openems.edge.goodwe.common.enums.BatteryMode; import io.openems.edge.goodwe.common.enums.GoodWeHardwareType; import io.openems.edge.goodwe.common.enums.GoodWeType; @@ -53,7 +57,7 @@ import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; public abstract class AbstractGoodWe extends AbstractOpenemsModbusComponent - implements GoodWe, OpenemsComponent, TimedataProvider { + implements GoodWe, OpenemsComponent, TimedataProvider, EventHandler { private static final Logger LOG = LoggerFactory.getLogger(AbstractGoodWe.class); private static final Map DIAG_STATUS_H_STATES = Map.of(// @@ -1238,10 +1242,21 @@ protected final ModbusProtocol defineModbusProtocol() throws OpenemsException { ModbusUtils.readELementOnce(protocol, new StringWordElement(35003, 8), true) // .thenAccept(serialNr -> { var hardwareType = getHardwareTypeFromSerialNr(serialNr); - if (hardwareType.equals(GoodWeHardwareType.OTHER)) { - this.logWarn(this.log, "GoodWe Hardware Type not defined by Serial Nr.: " + serialNr); + + try { + switch (hardwareType) { + case GOODWE_20, GOODWE_29_9 -> this.handleMultipleStringChargers(protocol); + case GOODWE_10, UNDEFINED -> { + } + case OTHER -> + this.logWarn(this.log, "GoodWe Hardware Type not defined by Serial Nr.: " + serialNr); + } + + this._setGoodweHardwareType(hardwareType); + + } catch (OpenemsException e) { + this.logError(this.log, "Unable to add charger tasks for modbus protocol"); } - this._setGoodweHardwareType(hardwareType); }); // Handles different DSP versions @@ -1273,6 +1288,20 @@ protected final ModbusProtocol defineModbusProtocol() throws OpenemsException { return protocol; } + @Override + public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: + + // Set charger information for MPPTs having more than one PV e.g. + // GoodWeChargerTwoString. + this.setMultipleStringChannels(); + } + } + /** * Get GoodWe hardware version by serial number. * @@ -1300,6 +1329,112 @@ protected static GoodWeHardwareType getHardwareTypeFromSerialNr(String serialNr) .orElse(GoodWeHardwareType.OTHER); } + /** + * Handle multiple string chargers. + * + *

+ * For MPPT connectors e.g. two string on one MPPT the power information is + * spread over several registers that should be read as complete blocks. + * + * @param protocol current protocol + * @throws OpenemsException on error + */ + private void handleMultipleStringChargers(ModbusProtocol protocol) throws OpenemsException { + /* + * For two string charger the registers the power information is spread over + * several registers that should be read as complete blocks + */ + /* + * Block 1: PV1 - PV4 voltage & current + */ + protocol.addTask(// + + new FC3ReadRegistersTask(35103, Priority.HIGH, // + m(GoodWe.ChannelId.TWO_S_PV1_V, new UnsignedWordElement(35103), + ElementToChannelConverter.SCALE_FACTOR_2), + m(GoodWe.ChannelId.TWO_S_PV1_I, new UnsignedWordElement(35104), + ElementToChannelConverter.SCALE_FACTOR_2), + + // Power having wrong values for two-string charger + new DummyRegisterElement(35105, 35106), + + m(GoodWe.ChannelId.TWO_S_PV2_V, new UnsignedWordElement(35107), + ElementToChannelConverter.SCALE_FACTOR_2), + m(GoodWe.ChannelId.TWO_S_PV2_I, new UnsignedWordElement(35108), + ElementToChannelConverter.SCALE_FACTOR_2), + new DummyRegisterElement(35109, 35110), + m(GoodWe.ChannelId.TWO_S_PV3_V, new UnsignedWordElement(35111), + ElementToChannelConverter.SCALE_FACTOR_2), + m(GoodWe.ChannelId.TWO_S_PV3_I, new UnsignedWordElement(35112), + ElementToChannelConverter.SCALE_FACTOR_2), + new DummyRegisterElement(35113, 35114), + m(GoodWe.ChannelId.TWO_S_PV4_V, new UnsignedWordElement(35115), + ElementToChannelConverter.SCALE_FACTOR_2), + m(GoodWe.ChannelId.TWO_S_PV4_I, new UnsignedWordElement(35116), + ElementToChannelConverter.SCALE_FACTOR_2)) // + ); + + /* + * Block 2: PV5 - PV6 voltage & current (would continue till PV16) and MPPT + * total power and current values + */ + protocol.addTask(// + new FC3ReadRegistersTask(35304, Priority.HIGH, // + m(GoodWe.ChannelId.TWO_S_PV5_V, new UnsignedWordElement(35304), + ElementToChannelConverter.SCALE_FACTOR_2), + m(GoodWe.ChannelId.TWO_S_PV5_I, new UnsignedWordElement(35305), + ElementToChannelConverter.SCALE_FACTOR_2), + m(GoodWe.ChannelId.TWO_S_PV6_V, new UnsignedWordElement(35306), + ElementToChannelConverter.SCALE_FACTOR_2), + m(GoodWe.ChannelId.TWO_S_PV6_I, new UnsignedWordElement(35307), + ElementToChannelConverter.SCALE_FACTOR_2), // + new DummyRegisterElement(35308, 35336), + m(GoodWe.ChannelId.TWO_S_MPPT1_P, new UnsignedWordElement(35337)), + m(GoodWe.ChannelId.TWO_S_MPPT2_P, new UnsignedWordElement(35338)), + m(GoodWe.ChannelId.TWO_S_MPPT3_P, new UnsignedWordElement(35339)), + new DummyRegisterElement(35340, 35344), // Power MPPT4 - MPPT8 + m(GoodWe.ChannelId.TWO_S_MPPT1_I, new UnsignedWordElement(35345), // + ElementToChannelConverter.SCALE_FACTOR_2), + m(GoodWe.ChannelId.TWO_S_MPPT2_I, new UnsignedWordElement(35346), // + ElementToChannelConverter.SCALE_FACTOR_2), + m(GoodWe.ChannelId.TWO_S_MPPT3_I, new UnsignedWordElement(35347), // + ElementToChannelConverter.SCALE_FACTOR_2)) // + ); + } + + private void setMultipleStringChannels() { + + this.chargers.stream() // + .filter(GoodWeChargerTwoString.class::isInstance) // + .map(GoodWeChargerTwoString.class::cast) // + .forEach(charger -> { + var pvPort = charger.pvPort(); + + // Get actual Channels + IntegerReadChannel totalMpptPowerChannel = this.channel(pvPort.mpptPowerChannelId); + IntegerReadChannel totalMpptCurrentChannel = this.channel(pvPort.mpptCurrentChannelId); + IntegerReadChannel stringCurrentChannel = this.channel(pvPort.pvCurrentId); + IntegerReadChannel stringVoltageChannel = this.channel(pvPort.pvVoltageId); + + // Power value from the total MPPT power and current values + charger._setActualPower(// + GoodWeChargerTwoString.calculateByRuleOfThree(// + totalMpptPowerChannel.getNextValue().asOptional(), // + totalMpptCurrentChannel.getNextValue().asOptional(), // + stringCurrentChannel.getNextValue().asOptional()) // + // If at least one value was present, the result should not be null. + .orElse(0) // + ); + + /* + * TODO: Could also be achieved by using listeners for onSetNextValue in + * addCharger and removeCharger. + */ + charger._setCurrent(stringCurrentChannel.getNextValue().get()); + charger._setVoltage(stringVoltageChannel.getNextValue().get()); + }); + } + private void handleDspVersion7(ModbusProtocol protocol) throws OpenemsException { protocol.addTask(// new FC3ReadRegistersTask(47519, Priority.LOW, // diff --git a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java index d699d7ec5f1..87721d37ad5 100644 --- a/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java +++ b/io.openems.edge.goodwe/src/io/openems/edge/goodwe/common/GoodWe.java @@ -100,6 +100,55 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId TOTAL_INV_POWER(Doc.of(OpenemsType.INTEGER) // .unit(Unit.WATT)), // + /* + * Channels for multiple String charger in one MPPT. Channels only set and used + * for GoodWe 20/30 + * + * MPPT1 + */ + TWO_S_MPPT1_P(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT)), + TWO_S_MPPT1_I(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIAMPERE)), // + TWO_S_PV1_V(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT)), // + TWO_S_PV1_I(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIAMPERE)), // + TWO_S_PV2_V(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT)), // + TWO_S_PV2_I(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIAMPERE)), // + /* + * MPPT2 + */ + TWO_S_MPPT2_P(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT)), + TWO_S_MPPT2_I(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIAMPERE)), // + TWO_S_PV3_V(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT)), // + TWO_S_PV3_I(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIAMPERE)), // + TWO_S_PV4_V(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT)), // + TWO_S_PV4_I(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIAMPERE)), // + /* + * MPPT3 + */ + TWO_S_MPPT3_P(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.WATT)), + TWO_S_MPPT3_I(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIAMPERE)), // + TWO_S_PV5_V(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT)), // + TWO_S_PV5_I(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIAMPERE)), // + TWO_S_PV6_V(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.VOLT)), // + TWO_S_PV6_I(Doc.of(OpenemsType.INTEGER) // + .unit(Unit.MILLIAMPERE)), // + /** * Total Active Power Of Inverter. * diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImplTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImplTest.java index d1685c96392..9aeacc78673 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImplTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/batteryinverter/GoodWeBatteryInverterImplTest.java @@ -13,6 +13,8 @@ import io.openems.edge.ess.test.DummyPower; import io.openems.edge.goodwe.GoodWeConstants; import io.openems.edge.goodwe.charger.singlestring.GoodWeChargerPv1; +import io.openems.edge.goodwe.charger.twostring.GoodWeChargerTwoStringImpl; +import io.openems.edge.goodwe.charger.twostring.PvPort; import io.openems.edge.goodwe.common.enums.ControlMode; import io.openems.edge.goodwe.common.enums.EmsPowerMode; import io.openems.edge.goodwe.common.enums.EnableDisable; @@ -26,13 +28,17 @@ public class GoodWeBatteryInverterImplTest { private static final String BATTERY_ID = "battery0"; private static final String BATTERY_INVERTER_ID = "batteryInverter0"; private static final String CHARGER_ID = "charger0"; + private static final String CHARGER_2_ID = "charger1"; + private static final String CHARGER_3_ID = "charger2"; + private static final String CHARGER_4_ID = "charger3"; + private static final String CHARGER_5_ID = "charger4"; + private static final String CHARGER_6_ID = "charger5"; private static final String SUM_ID = "_sum"; private static final Battery BATTERY = new DummyBattery(BATTERY_ID); private static final ChannelAddress EMS_POWER_MODE = new ChannelAddress(BATTERY_INVERTER_ID, "EmsPowerMode"); private static final ChannelAddress EMS_POWER_SET = new ChannelAddress(BATTERY_INVERTER_ID, "EmsPowerSet"); - private static final ChannelAddress ACTUAL_POWER = new ChannelAddress(CHARGER_ID, "ActualPower"); private static final ChannelAddress METER_COMMUNICATE_STATUS = new ChannelAddress(BATTERY_INVERTER_ID, "MeterCommunicateStatus"); private static final ChannelAddress MAX_AC_IMPORT = new ChannelAddress(BATTERY_INVERTER_ID, "MaxAcImport"); @@ -48,6 +54,44 @@ public class GoodWeBatteryInverterImplTest { private static final ChannelAddress MAX_APPARENT_POWER = new ChannelAddress(BATTERY_INVERTER_ID, "MaxApparentPower"); + private static final ChannelAddress CHARGER_ACTUAL_POWER = new ChannelAddress(CHARGER_ID, "ActualPower"); + private static final ChannelAddress CHARGER_VOLTAGE = new ChannelAddress(CHARGER_ID, "Voltage"); + private static final ChannelAddress CHARGER_CURRENT = new ChannelAddress(CHARGER_ID, "Current"); + private static final ChannelAddress CHARGER_2_ACTUAL_POWER = new ChannelAddress(CHARGER_2_ID, "ActualPower"); + private static final ChannelAddress CHARGER_2_VOLTAGE = new ChannelAddress(CHARGER_2_ID, "Voltage"); + private static final ChannelAddress CHARGER_2_CURRENT = new ChannelAddress(CHARGER_2_ID, "Current"); + private static final ChannelAddress CHARGER_3_ACTUAL_POWER = new ChannelAddress(CHARGER_3_ID, "ActualPower"); + private static final ChannelAddress CHARGER_3_VOLTAGE = new ChannelAddress(CHARGER_3_ID, "Voltage"); + private static final ChannelAddress CHARGER_3_CURRENT = new ChannelAddress(CHARGER_3_ID, "Current"); + private static final ChannelAddress CHARGER_4_ACTUAL_POWER = new ChannelAddress(CHARGER_4_ID, "ActualPower"); + private static final ChannelAddress CHARGER_4_VOLTAGE = new ChannelAddress(CHARGER_4_ID, "Voltage"); + private static final ChannelAddress CHARGER_4_CURRENT = new ChannelAddress(CHARGER_4_ID, "Current"); + private static final ChannelAddress CHARGER_5_ACTUAL_POWER = new ChannelAddress(CHARGER_5_ID, "ActualPower"); + private static final ChannelAddress CHARGER_5_VOLTAGE = new ChannelAddress(CHARGER_5_ID, "Voltage"); + private static final ChannelAddress CHARGER_5_CURRENT = new ChannelAddress(CHARGER_5_ID, "Current"); + private static final ChannelAddress CHARGER_6_ACTUAL_POWER = new ChannelAddress(CHARGER_6_ID, "ActualPower"); + private static final ChannelAddress CHARGER_6_VOLTAGE = new ChannelAddress(CHARGER_6_ID, "Voltage"); + private static final ChannelAddress CHARGER_6_CURRENT = new ChannelAddress(CHARGER_6_ID, "Current"); + + private static final ChannelAddress TWO_S_MPPT1_P = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSMppt1P"); + private static final ChannelAddress TWO_S_MPPT1_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSMppt1I"); + private static final ChannelAddress TWO_S_MPPT2_P = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSMppt2P"); + private static final ChannelAddress TWO_S_MPPT2_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSMppt2I"); + private static final ChannelAddress TWO_S_MPPT3_P = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSMppt3P"); + private static final ChannelAddress TWO_S_MPPT3_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSMppt3I"); + private static final ChannelAddress TWO_S_PV1_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv1I"); + private static final ChannelAddress TWO_S_PV1_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv1V"); + private static final ChannelAddress TWO_S_PV2_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv2I"); + private static final ChannelAddress TWO_S_PV2_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv2V"); + private static final ChannelAddress TWO_S_PV3_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv3I"); + private static final ChannelAddress TWO_S_PV3_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv3V"); + private static final ChannelAddress TWO_S_PV4_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv4I"); + private static final ChannelAddress TWO_S_PV4_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv4V"); + private static final ChannelAddress TWO_S_PV5_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv5I"); + private static final ChannelAddress TWO_S_PV5_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv5V"); + private static final ChannelAddress TWO_S_PV6_I = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv6I"); + private static final ChannelAddress TWO_S_PV6_V = new ChannelAddress(BATTERY_INVERTER_ID, "TwoSPv6V"); + @Test public void testEt() throws Exception { var charger = new GoodWeChargerPv1(); @@ -86,7 +130,7 @@ public void testEt() throws Exception { .input(ACTIVE_POWER, 0) // .input(MAX_AC_IMPORT, 0) // .input(MAX_AC_EXPORT, 0) // - .input(ACTUAL_POWER, 2000) // + .input(CHARGER_ACTUAL_POWER, 2000) // .onExecuteWriteCallbacks(() -> { ess.run(BATTERY, 1000, 0); }) // @@ -225,7 +269,7 @@ public void testEmsPowerModeAutoWithSurplus() throws Exception { .build()) // .next(new TestCase() // .input(METER_COMMUNICATE_STATUS, MeterCommunicateStatus.OK) // - .input(ACTUAL_POWER, 10000) // + .input(CHARGER_ACTUAL_POWER, 10000) // .input(CHARGE_MAX_CURRENT, 20).onExecuteWriteCallbacks(() -> { ess.run(BATTERY, 10000, 0); }) // @@ -385,4 +429,281 @@ public void testAcCalculation() throws Exception { .output(MAX_AC_EXPORT, 325)); } + @Test + public void testTwoStringCharger() throws Exception { + var ess = new GoodWeBatteryInverterImpl(); + var charger1 = new GoodWeChargerTwoStringImpl(); + var charger2 = new GoodWeChargerTwoStringImpl(); + var charger3 = new GoodWeChargerTwoStringImpl(); + var charger4 = new GoodWeChargerTwoStringImpl(); + var charger5 = new GoodWeChargerTwoStringImpl(); + var charger6 = new GoodWeChargerTwoStringImpl(); + + new ComponentTest(charger1) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("essOrBatteryInverter", ess) // + .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // + .setId(CHARGER_ID) // + .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setPvPort(PvPort.PV_1) // + .build()); + + new ComponentTest(charger2) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("essOrBatteryInverter", ess) // + .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // + .setId(CHARGER_2_ID) // + .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setPvPort(PvPort.PV_2) // + .build()); + + new ComponentTest(charger3) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("essOrBatteryInverter", ess) // + .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // + .setId(CHARGER_3_ID) // + .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setPvPort(PvPort.PV_3) // + .build()); + + new ComponentTest(charger4) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("essOrBatteryInverter", ess) // + .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // + .setId(CHARGER_4_ID) // + .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setPvPort(PvPort.PV_4) // + .build()); + + new ComponentTest(charger5) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("essOrBatteryInverter", ess) // + .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // + .setId(CHARGER_5_ID) // + .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setPvPort(PvPort.PV_5) // + .build()); + + new ComponentTest(charger6) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("essOrBatteryInverter", ess) // + .activate(io.openems.edge.goodwe.charger.twostring.MyConfig.create() // + .setId(CHARGER_6_ID) // + .setBatteryInverterId(BATTERY_INVERTER_ID) // + .setPvPort(PvPort.PV_6) // + .build()); + + ess.addCharger(charger1); + ess.addCharger(charger2); + new ComponentTest(ess) // + .addReference("power", new DummyPower()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("sum", new DummySum()) // + .addComponent(charger1) // + .addComponent(charger2) // + .addComponent(BATTERY) // + .activate(MyConfig.create() // + .setId(BATTERY_INVERTER_ID) // + .setModbusId(MODBUS_ID) // + .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setSafetyCountry(SafetyCountry.GERMANY) // + .setMpptForShadowEnable(EnableDisable.ENABLE) // + .setBackupEnable(EnableDisable.ENABLE) // + .setFeedPowerEnable(EnableDisable.ENABLE) // + .setFeedPowerPara(3000) // + .setFeedInPowerSettings(FeedInPowerSettings.PU_ENABLE_CURVE) // + .setControlMode(ControlMode.SMART) // + .build()) // + .next(new TestCase() // + .input(TWO_S_MPPT1_I, 20) // + .input(TWO_S_MPPT1_P, 2000) // + .input(TWO_S_PV1_I, 10) // + .input(TWO_S_PV2_I, 10) // + .input(TWO_S_PV1_V, 240) // + .input(TWO_S_PV2_V, 240) // + + // Values applied in the next cycle + .output(CHARGER_ACTUAL_POWER, 0) // + .output(CHARGER_2_ACTUAL_POWER, 0) // + .output(CHARGER_CURRENT, null) // + .output(CHARGER_2_CURRENT, null) // + .output(CHARGER_VOLTAGE, null) // + .output(CHARGER_2_VOLTAGE, null)) // + .next(new TestCase() // + .output(CHARGER_ACTUAL_POWER, 1000) // + .output(CHARGER_2_ACTUAL_POWER, 1000) // + .output(CHARGER_CURRENT, 10) // + .output(CHARGER_2_CURRENT, 10) // + .output(CHARGER_VOLTAGE, 240) // + .output(CHARGER_2_VOLTAGE, 240)) // + + // Chargers with different current values + .next(new TestCase() // + .input(TWO_S_MPPT1_I, 20) // + .input(TWO_S_MPPT1_P, 2000) // + .input(TWO_S_PV1_I, 5) // + .input(TWO_S_PV2_I, 15) // + .output(CHARGER_ACTUAL_POWER, 1000) // + .output(CHARGER_2_ACTUAL_POWER, 1000)) // + .next(new TestCase() // + .output(CHARGER_ACTUAL_POWER, 500) // + .output(CHARGER_2_ACTUAL_POWER, 1500)) // + + .next(new TestCase() // + .input(TWO_S_MPPT1_I, 20) // + .input(TWO_S_MPPT1_P, 2000) // + .input(TWO_S_PV1_I, 20) // + .input(TWO_S_PV2_I, 0) // + .output(CHARGER_ACTUAL_POWER, 500) // + .output(CHARGER_2_ACTUAL_POWER, 1500)) // + .next(new TestCase() // + .output(CHARGER_ACTUAL_POWER, 2000) // + .output(CHARGER_2_ACTUAL_POWER, 0) // + ); + + /* + * Test MPPT 2 - PV3 & PV4 + */ + ess.addCharger(charger3); + ess.addCharger(charger4); + new ComponentTest(ess) // + .addReference("power", new DummyPower()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("sum", new DummySum()) // + .addComponent(charger3) // + .addComponent(charger4) // + .addComponent(BATTERY) // + .activate(MyConfig.create() // + .setId(BATTERY_INVERTER_ID) // + .setModbusId(MODBUS_ID) // + .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setSafetyCountry(SafetyCountry.GERMANY) // + .setMpptForShadowEnable(EnableDisable.ENABLE) // + .setBackupEnable(EnableDisable.ENABLE) // + .setFeedPowerEnable(EnableDisable.ENABLE) // + .setFeedPowerPara(3000) // + .setFeedInPowerSettings(FeedInPowerSettings.PU_ENABLE_CURVE) // + .setControlMode(ControlMode.SMART) // + .build()) // + .next(new TestCase() // + .input(TWO_S_MPPT2_I, 20) // + .input(TWO_S_MPPT2_P, 2000) // + .input(TWO_S_PV3_I, 10) // + .input(TWO_S_PV4_I, 10) // + .input(TWO_S_PV3_V, 240) // + .input(TWO_S_PV4_V, 240) // + + // Values applied in the next cycle + .output(CHARGER_3_ACTUAL_POWER, 0) // + .output(CHARGER_4_ACTUAL_POWER, 0) // + .output(CHARGER_3_CURRENT, null) // + .output(CHARGER_4_CURRENT, null) // + .output(CHARGER_3_VOLTAGE, null) // + .output(CHARGER_4_VOLTAGE, null)) // + .next(new TestCase() // + .output(CHARGER_3_ACTUAL_POWER, 1000) // + .output(CHARGER_4_ACTUAL_POWER, 1000) // + .output(CHARGER_3_CURRENT, 10) // + .output(CHARGER_4_CURRENT, 10) // + .output(CHARGER_3_VOLTAGE, 240) // + .output(CHARGER_4_VOLTAGE, 240)) // + + // Chargers with different current values + .next(new TestCase() // + .input(TWO_S_MPPT2_I, 20) // + .input(TWO_S_MPPT2_P, 2000) // + .input(TWO_S_PV3_I, 5) // + .input(TWO_S_PV4_I, 15) // + .output(CHARGER_3_ACTUAL_POWER, 1000) // + .output(CHARGER_4_ACTUAL_POWER, 1000)) // + .next(new TestCase() // + .output(CHARGER_3_ACTUAL_POWER, 500) // + .output(CHARGER_4_ACTUAL_POWER, 1500)) // + + .next(new TestCase() // + .input(TWO_S_MPPT2_I, 20) // + .input(TWO_S_MPPT2_P, 2000) // + .input(TWO_S_PV3_I, 20) // + .input(TWO_S_PV4_I, 0) // + .output(CHARGER_3_ACTUAL_POWER, 500) // + .output(CHARGER_4_ACTUAL_POWER, 1500)) // + .next(new TestCase() // + .output(CHARGER_3_ACTUAL_POWER, 2000) // + .output(CHARGER_4_ACTUAL_POWER, 0) // + ); + + /* + * Test MPPT 3 - PV5 & PV6 + */ + ess.addCharger(charger5); + ess.addCharger(charger6); + new ComponentTest(ess) // + .addReference("power", new DummyPower()) // + .addReference("cm", new DummyConfigurationAdmin()) // + .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // + .addReference("sum", new DummySum()) // + .addComponent(charger5) // + .addComponent(charger6) // + .addComponent(BATTERY) // + .activate(MyConfig.create() // + .setId(BATTERY_INVERTER_ID) // + .setModbusId(MODBUS_ID) // + .setModbusUnitId(GoodWeConstants.DEFAULT_UNIT_ID) // + .setSafetyCountry(SafetyCountry.GERMANY) // + .setMpptForShadowEnable(EnableDisable.ENABLE) // + .setBackupEnable(EnableDisable.ENABLE) // + .setFeedPowerEnable(EnableDisable.ENABLE) // + .setFeedPowerPara(3000) // + .setFeedInPowerSettings(FeedInPowerSettings.PU_ENABLE_CURVE) // + .setControlMode(ControlMode.SMART) // + .build()) // + .next(new TestCase() // + .input(TWO_S_MPPT3_I, 20) // + .input(TWO_S_MPPT3_P, 2000) // + .input(TWO_S_PV5_I, 10) // + .input(TWO_S_PV6_I, 10) // + .input(TWO_S_PV5_V, 240) // + .input(TWO_S_PV6_V, 240) // + + // Values applied in the next cycle + .output(CHARGER_5_ACTUAL_POWER, 0) // + .output(CHARGER_6_ACTUAL_POWER, 0) // + .output(CHARGER_5_CURRENT, null) // + .output(CHARGER_6_CURRENT, null) // + .output(CHARGER_5_VOLTAGE, null) // + .output(CHARGER_6_VOLTAGE, null)) // + .next(new TestCase() // + .output(CHARGER_5_ACTUAL_POWER, 1000) // + .output(CHARGER_6_ACTUAL_POWER, 1000) // + .output(CHARGER_5_CURRENT, 10) // + .output(CHARGER_6_CURRENT, 10) // + .output(CHARGER_5_VOLTAGE, 240) // + .output(CHARGER_6_VOLTAGE, 240)) // + + // Chargers with different current values + .next(new TestCase() // + .input(TWO_S_MPPT3_I, 20) // + .input(TWO_S_MPPT3_P, 2000) // + .input(TWO_S_PV5_I, 5) // + .input(TWO_S_PV6_I, 15) // + .output(CHARGER_5_ACTUAL_POWER, 1000) // + .output(CHARGER_6_ACTUAL_POWER, 1000)) // + .next(new TestCase() // + .output(CHARGER_5_ACTUAL_POWER, 500) // + .output(CHARGER_6_ACTUAL_POWER, 1500)) // + + .next(new TestCase() // + .input(TWO_S_MPPT3_I, 20) // + .input(TWO_S_MPPT3_P, 2000) // + .input(TWO_S_PV5_I, 20) // + .input(TWO_S_PV6_I, 0) // + .output(CHARGER_5_ACTUAL_POWER, 500) // + .output(CHARGER_6_ACTUAL_POWER, 1500)) // + .next(new TestCase() // + .output(CHARGER_5_ACTUAL_POWER, 2000) // + .output(CHARGER_6_ACTUAL_POWER, 0) // + ); + } } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImplTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImplTest.java index 5ef847bba79..75841a8ca30 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImplTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/GoodWeChargerTwoStringImplTest.java @@ -2,14 +2,12 @@ import org.junit.Test; -import io.openems.edge.bridge.modbus.test.DummyModbusBridge; import io.openems.edge.common.test.ComponentTest; import io.openems.edge.common.test.DummyConfigurationAdmin; import io.openems.edge.goodwe.ess.GoodWeEssImpl; public class GoodWeChargerTwoStringImplTest { - private static final String MODBUS_ID = "modbus0"; private static final String ESS_ID = "ess0"; private static final String CHARGER_ID = "charger0"; @@ -18,11 +16,9 @@ public void test() throws Exception { new ComponentTest(new GoodWeChargerTwoStringImpl()) // .addReference("cm", new DummyConfigurationAdmin()) // .addReference("essOrBatteryInverter", new GoodWeEssImpl()) // - .addReference("setModbus", new DummyModbusBridge(MODBUS_ID)) // .activate(MyConfig.create() // .setId(CHARGER_ID) // .setBatteryInverterId(ESS_ID) // - .setModbusId(MODBUS_ID) // .setPvPort(PvPort.PV_1) // .build()); } diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/MyConfig.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/MyConfig.java index f8e5537ebe7..15f3ff0ee75 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/MyConfig.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/MyConfig.java @@ -9,8 +9,6 @@ public class MyConfig extends AbstractComponentConfig implements Config { public static class Builder { private String id; private String essOrBatteryInverter; - private String modbusId; - private int modbusUnitId; private PvPort pvPort; private Builder() { @@ -26,16 +24,6 @@ public Builder setBatteryInverterId(String essOrBatteryInverter) { return this; } - public Builder setModbusId(String modbusId) { - this.modbusId = modbusId; - return this; - } - - public Builder setModbusUnitId(int modbusUnitId) { - this.modbusUnitId = modbusUnitId; - return this; - } - public Builder setPvPort(PvPort pvPort) { this.pvPort = pvPort; return this; @@ -67,21 +55,6 @@ private MyConfig(Builder builder) { this.builder = builder; } - @Override - public int modbusUnitId() { - return this.builder.modbusUnitId; - } - - @Override - public String modbus_id() { - return this.builder.modbusId; - } - - @Override - public String Modbus_target() { - return ConfigUtils.generateReferenceTargetFilter(this.id(), this.modbus_id()); - } - @Override public String essOrBatteryInverter_id() { return this.builder.essOrBatteryInverter; diff --git a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/RuleOfThreeTest.java b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/RuleOfThreeTest.java index a73e90257e1..5298ac5f23b 100644 --- a/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/RuleOfThreeTest.java +++ b/io.openems.edge.goodwe/test/io/openems/edge/goodwe/charger/twostring/RuleOfThreeTest.java @@ -20,7 +20,7 @@ public void calculateByRuleOfThreeTest() { final var value1 = Optional.of(17); Integer expectedResult1 = 1_063; - Integer result = GoodWeChargerTwoStringImpl.calculateByRuleOfThree(totalValue, divisor, value1).orElse(0); + Integer result = GoodWeChargerTwoString.calculateByRuleOfThree(totalValue, divisor, value1).orElse(0); assertEquals(expectedResult1, result); /* @@ -37,7 +37,7 @@ public void calculateByRuleOfThreeTest() { * other. */ Integer expectedResult2 = totalValue.get() - expectedResult1 + 1; // - Integer result2 = GoodWeChargerTwoStringImpl.calculateByRuleOfThree(totalValue, divisor, value2).orElse(0); + Integer result2 = GoodWeChargerTwoString.calculateByRuleOfThree(totalValue, divisor, value2).orElse(0); assertEquals(expectedResult2, result2); @@ -49,7 +49,7 @@ public void calculateByRuleOfThreeWithZeroDivisorTest() { final var divisor = Optional.of(0); final var value = Optional.of(10); - var resultIsPresent = GoodWeChargerTwoStringImpl.calculateByRuleOfThree(totalValue, divisor, value).isPresent(); + var resultIsPresent = GoodWeChargerTwoString.calculateByRuleOfThree(totalValue, divisor, value).isPresent(); assertFalse(resultIsPresent); } @@ -61,7 +61,7 @@ public void calculateByRuleOfThreeWithGreaterResultTest() { final var value = Optional.of(40); Integer expectedResult = 2000; - Integer result = GoodWeChargerTwoStringImpl.calculateByRuleOfThree(totalValue, divisor, value).orElse(0); + Integer result = GoodWeChargerTwoString.calculateByRuleOfThree(totalValue, divisor, value).orElse(0); assertEquals(expectedResult, result); } @@ -73,7 +73,7 @@ public void calculateByRuleOfThreeWithEmptyValuesTest() { final Optional value = Optional.empty(); Integer expectedResult = 0; - Integer result = GoodWeChargerTwoStringImpl.calculateByRuleOfThree(totalValue, divisor, value).orElse(0); + Integer result = GoodWeChargerTwoString.calculateByRuleOfThree(totalValue, divisor, value).orElse(0); assertEquals(expectedResult, result); } diff --git a/io.openems.edge.pvinverter.kaco.blueplanet/bnd.bnd b/io.openems.edge.pvinverter.kaco.blueplanet/bnd.bnd index c47a5b5e4cb..71a6a400eec 100644 --- a/io.openems.edge.pvinverter.kaco.blueplanet/bnd.bnd +++ b/io.openems.edge.pvinverter.kaco.blueplanet/bnd.bnd @@ -10,7 +10,8 @@ Bundle-Version: 1.0.0.${tstamp} io.openems.edge.common,\ io.openems.edge.meter.api,\ io.openems.edge.pvinverter.api,\ - io.openems.edge.pvinverter.sunspec + io.openems.edge.pvinverter.sunspec,\ + io.openems.edge.timedata.api -testpath: \ ${testpath},\ diff --git a/io.openems.edge.pvinverter.kaco.blueplanet/src/io/openems/edge/pvinverter/kaco/blueplanet/PvInverterKacoBlueplanetImpl.java b/io.openems.edge.pvinverter.kaco.blueplanet/src/io/openems/edge/pvinverter/kaco/blueplanet/PvInverterKacoBlueplanetImpl.java index ffa35002cb9..68959240db1 100644 --- a/io.openems.edge.pvinverter.kaco.blueplanet/src/io/openems/edge/pvinverter/kaco/blueplanet/PvInverterKacoBlueplanetImpl.java +++ b/io.openems.edge.pvinverter.kaco.blueplanet/src/io/openems/edge/pvinverter/kaco/blueplanet/PvInverterKacoBlueplanetImpl.java @@ -35,6 +35,9 @@ import io.openems.edge.pvinverter.sunspec.AbstractSunSpecPvInverter; import io.openems.edge.pvinverter.sunspec.Phase; import io.openems.edge.pvinverter.sunspec.SunSpecPvInverter; +import io.openems.edge.timedata.api.Timedata; +import io.openems.edge.timedata.api.TimedataProvider; +import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower; @Designate(ocd = Config.class, factory = true) @Component(// @@ -45,11 +48,12 @@ "type=PRODUCTION" // }) @EventTopics({ // + EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE, // EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE // }) public class PvInverterKacoBlueplanetImpl extends AbstractSunSpecPvInverter implements PvInverterKacoBlueplanet, SunSpecPvInverter, ManagedSymmetricPvInverter, ModbusComponent, - ElectricityMeter, OpenemsComponent, EventHandler, ModbusSlave { + ElectricityMeter, OpenemsComponent, EventHandler, ModbusSlave, TimedataProvider { private static final Map ACTIVE_MODELS = ImmutableMap.builder() .put(DefaultSunSpecModel.S_1, Priority.LOW) // from 40002 @@ -73,9 +77,15 @@ public class PvInverterKacoBlueplanetImpl extends AbstractSunSpecPvInverter private static final int READ_FROM_MODBUS_BLOCK = 1; + private final CalculateEnergyFromPower calculateProductionEnergy = new CalculateEnergyFromPower(this, + ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY); + @Reference private ConfigurationAdmin cm; + @Reference(policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) + private volatile Timedata timedata; + @Override @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY) protected void setModbus(BridgeModbus modbus) { @@ -85,6 +95,7 @@ protected void setModbus(BridgeModbus modbus) { public PvInverterKacoBlueplanetImpl() throws OpenemsException { super(// ACTIVE_MODELS, // + true /* enable manuel calculation of ActiveProductionEnergy */, // OpenemsComponent.ChannelId.values(), // ModbusComponent.ChannelId.values(), // ElectricityMeter.ChannelId.values(), // @@ -110,7 +121,14 @@ protected void deactivate() { @Override public void handleEvent(Event event) { - super.handleEvent(event); + if (!this.isEnabled()) { + return; + } + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE -> + this.calculateProductionEnergy.update(this.getActivePower().get()); + case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE -> super.handleEvent(event); + } } @Override @@ -120,4 +138,10 @@ public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) { ElectricityMeter.getModbusSlaveNatureTable(accessMode), // ManagedSymmetricPvInverter.getModbusSlaveNatureTable(accessMode)); } + + @Override + public Timedata getTimedata() { + return this.timedata; + } + } diff --git a/io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/AbstractSunSpecPvInverter.java b/io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/AbstractSunSpecPvInverter.java index e1d1bbc73c8..19226d37020 100644 --- a/io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/AbstractSunSpecPvInverter.java +++ b/io.openems.edge.pvinverter.sunspec/src/io/openems/edge/pvinverter/sunspec/AbstractSunSpecPvInverter.java @@ -69,11 +69,20 @@ private InverterType(DefaultSunSpecModel... blocks) { private boolean readOnly; private Phase phase; private InverterType inverterType = null; + private final boolean calculateActiveProductionEnergyManually; public AbstractSunSpecPvInverter(Map activeModels, io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) throws OpenemsException { + this(activeModels, false, firstInitialChannelIds, furtherInitialChannelIds); + } + + public AbstractSunSpecPvInverter(Map activeModels, + boolean calculateActiveProductionEnergyManually, + io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds, + io.openems.edge.common.channel.ChannelId[]... furtherInitialChannelIds) throws OpenemsException { super(activeModels, firstInitialChannelIds, furtherInitialChannelIds); + this.calculateActiveProductionEnergyManually = calculateActiveProductionEnergyManually; this._setActiveConsumptionEnergy(0); // Automatically calculate sum values from L1/L2/L3 @@ -228,10 +237,12 @@ protected void onSunSpecInitializationCompleted() { } } - this.mapFirstPointToChannel(// - ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, // - DIRECT_1_TO_1, // - S111.WH, S112.WH, S113.WH, S101.WH, S102.WH, S103.WH); + if (!this.calculateActiveProductionEnergyManually) { + this.mapFirstPointToChannel(// + ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, // + DIRECT_1_TO_1, // + S111.WH, S112.WH, S113.WH, S101.WH, S102.WH, S103.WH); + } this.mapFirstPointToChannel(// ManagedSymmetricPvInverter.ChannelId.MAX_APPARENT_POWER, // DIRECT_1_TO_1, // diff --git a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/DbDataUtils.java b/io.openems.shared.influxdb/src/io/openems/shared/influxdb/DbDataUtils.java index f8615160dd2..f25c81cc1dd 100644 --- a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/DbDataUtils.java +++ b/io.openems.shared.influxdb/src/io/openems/shared/influxdb/DbDataUtils.java @@ -41,6 +41,10 @@ public static SortedMap> n ZonedDateTime fromDate, // ZonedDateTime toDate // ) { + if (table == null) { + return null; + } + // currently only works for days and months otherwise just return the table if (resolution.getUnit() != ChronoUnit.DAYS // && resolution.getUnit() != ChronoUnit.MONTHS) { diff --git a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/proxy/InfluxQlProxy.java b/io.openems.shared.influxdb/src/io/openems/shared/influxdb/proxy/InfluxQlProxy.java index 6b4cae89cfd..2d9d04234a0 100644 --- a/io.openems.shared.influxdb/src/io/openems/shared/influxdb/proxy/InfluxQlProxy.java +++ b/io.openems.shared.influxdb/src/io/openems/shared/influxdb/proxy/InfluxQlProxy.java @@ -83,13 +83,8 @@ public SortedMap queryHistoricEnergySingleValueInDa (t, u) -> u, TreeMap::new)); } - var channelsWithoutOldValues = firstResult.entrySet().stream() // - .filter(t -> t.getValue().first().isJsonNull() && !t.getValue().second().isJsonNull()) // - .map(Entry::getKey) // - .collect(Collectors.toSet()); - final var beforeValues = this.queryFirstValueBefore(bucket, influxConnection, measurement, influxEdgeId, - fromDate, channelsWithoutOldValues); + fromDate, channels); return mergeEnergyValues(firstResult, beforeValues); } @@ -304,8 +299,7 @@ protected String buildHistoricEnergyQuerySingleValueInDay(// // Prepare query string var b = new StringBuilder("SELECT ") // .append(channels.stream() // - .map(c -> "LAST(\"" + c.toString() + "\") AS \"LAST(" + c.toString() + ")\"" + ", FIRST(\"" // - + c.toString() + "\") AS \"FIRST(" + c.toString() + ")\"") // + .map(c -> "LAST(\"" + c.toString() + "\") AS \"LAST(" + c.toString() + ")\"") // .collect(Collectors.joining(", "))) // .append(" FROM ") // .append(measurement) // @@ -568,8 +562,13 @@ private static SortedMap> var timestampInstant = Instant .ofEpochMilli(Long.parseLong((String) t.second().getValueByKey("time"))); var zonedDateTime = ZonedDateTime.ofInstant(timestampInstant, fromDate.getZone()); - if (resolution.getUnit() == ChronoUnit.MONTHS) { - zonedDateTime = zonedDateTime.withDayOfMonth(1); + if (resolution.getUnit() == ChronoUnit.MONTHS && zonedDateTime.isAfter(fromDate)) { + if (zonedDateTime.getMonthValue() == fromDate.getMonthValue() // + && zonedDateTime.getYear() == fromDate.getYear()) { + zonedDateTime = fromDate; + } else { + zonedDateTime = zonedDateTime.withDayOfMonth(1); + } } return zonedDateTime.truncatedTo(DurationUnit.ofDays(1)); }, TreeMap::new, Collectors.toMap(Pair::first, r -> { @@ -611,7 +610,7 @@ private static JsonElement convertToJsonElement(Object valueObj) { return new JsonPrimitive(valueObj.toString()); } - private static SortedMap> convertHistoricEnergyResultSingleValueInDay(// + private static SortedMap convertHistoricEnergyResultSingleValueInDay(// InfluxQLQueryResult queryResult, // Optional influxEdgeId, // Set channels // @@ -629,12 +628,8 @@ private static SortedMap> convert .collect(Collectors.toMap(Pair::first, t -> { final var channel = t.first(); final var record = t.second(); - var first = record.getValueByKey("FIRST(" + channel.toString() + ")"); final var last = record.getValueByKey("LAST(" + channel.toString() + ")"); - if (Objects.equals(first, last)) { - first = null; - } - return new Pair(convertToJsonElement(first), convertToJsonElement(last)); + return convertToJsonElement(last); }, (t, u) -> u, TreeMap::new)); } @@ -786,22 +781,16 @@ record -> Long.parseLong(// } private static SortedMap mergeEnergyValues(// - SortedMap> firstResult, // + SortedMap firstResult, // SortedMap beforeValues // ) { return firstResult.entrySet().stream() // .collect(Collectors.toMap(Entry::getKey, t -> { final var channel = t.getKey(); - final var pair = t.getValue(); - - if (pair.second().isJsonNull()) { - return JsonNull.INSTANCE; - } - var first = t.getValue().first(); - var last = t.getValue().second(); - if (first.isJsonNull() && beforeValues != null) { - first = beforeValues.get(channel); - } + var first = Optional.ofNullable(beforeValues) // + .map(m -> m.get(channel)) // + .orElse(JsonNull.INSTANCE); + var last = t.getValue(); if (first == null || first.isJsonNull()) { return last; } diff --git a/ui/package-lock.json b/ui/package-lock.json index 2b5deaf97b5..7ac5df7d9ad 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -5361,21 +5361,21 @@ } }, "node_modules/@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -5383,6 +5383,12 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -5396,12 +5402,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2023.10.0-SNAPSHOT", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, "node_modules/@ionic/angular": { "version": "6.7.5", "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-6.7.5.tgz", @@ -6674,6 +6674,15 @@ "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==", "dev": true }, + "node_modules/@types/node-forge": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.8.tgz", + "integrity": "sha512-vGXshY9vim9CJjrpcS5raqSjEfKlJcWy2HNdgUasR66fAnVEYarrf1ULV4nfvpC1nZq/moA9qyqBcu83x+Jlrg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/q": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", @@ -7341,6 +7350,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@webassemblyjs/ast": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -7552,9 +7567,9 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -7920,6 +7935,20 @@ "deep-equal": "^2.0.5" } }, + "node_modules/aria-query/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/aria-query/node_modules/deep-equal": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", @@ -7985,22 +8014,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aria-query/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/aria-query/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/aria-query/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8257,22 +8277,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-buffer-byte-length/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/array-buffer-byte-length/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/array-buffer-byte-length/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8366,6 +8379,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-includes/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-includes/node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -8435,11 +8462,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-includes/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "node_modules/array-includes/node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/array-includes/node_modules/function.prototype.name": { "version": "1.1.6", @@ -8459,16 +8494,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-includes/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/array-includes/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8695,6 +8727,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.flat/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat/node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -8764,11 +8810,28 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "node_modules/array.prototype.flat/node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array.prototype.flat/node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } }, "node_modules/array.prototype.flat/node_modules/function.prototype.name": { "version": "1.1.6", @@ -8788,16 +8851,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/array.prototype.flat/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9006,6 +9066,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.flatmap/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flatmap/node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -9075,11 +9149,28 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flatmap/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "node_modules/array.prototype.flatmap/node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array.prototype.flatmap/node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } }, "node_modules/array.prototype.flatmap/node_modules/function.prototype.name": { "version": "1.1.6", @@ -9099,16 +9190,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flatmap/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/array.prototype.flatmap/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9319,6 +9407,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraybuffer.prototype.slice/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/arraybuffer.prototype.slice/node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -9335,22 +9437,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arraybuffer.prototype.slice/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/arraybuffer.prototype.slice/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/arraybuffer.prototype.slice/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9549,6 +9642,20 @@ "deep-equal": "^2.0.5" } }, + "node_modules/axobject-query/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axobject-query/node_modules/deep-equal": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", @@ -9614,22 +9721,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axobject-query/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/axobject-query/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/axobject-query/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10745,40 +10843,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/call-bind": { - "version": "2023.10.0-SNAPSHOT", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/call-bind/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -12576,6 +12640,32 @@ "node": ">= 10" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-data-property/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -13033,65 +13123,6 @@ "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", "dev": true }, - "node_modules/es-set-tostringtag": { - "version": "2023.10.0-SNAPSHOT", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/es-set-tostringtag/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-set-tostringtag/node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-shim-unscopables": { - "version": "2023.10.0-SNAPSHOT", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - } - }, "node_modules/es-to-primitive": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -13244,18 +13275,19 @@ } }, "node_modules/eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -14464,6 +14496,15 @@ "node": ">= 0.6" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/functions-have-names": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -14533,6 +14574,21 @@ "node": ">=8" } }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-stream": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -14561,22 +14617,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/get-symbol-description/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/get-symbol-description/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14694,6 +14743,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globalthis/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globalthis/node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -14797,27 +14858,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gopd/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/gopd/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -14851,39 +14891,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-property-descriptors": { - "version": "2023.10.0-SNAPSHOT", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/has-property-descriptors/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-proto": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", @@ -14914,11 +14921,17 @@ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true }, - "node_modules/has/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/hosted-git-info": { "version": "2023.10.0-SNAPSHOT", @@ -15588,6 +15601,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arguments/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arguments/node_modules/has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -15741,6 +15768,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-shared-array-buffer/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -15774,6 +15815,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-weakref/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakset": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", @@ -15787,22 +15842,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakset/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/is-weakset/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/is-weakset/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -17894,6 +17942,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-is/node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -17910,6 +17972,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-is/node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -17937,6 +18011,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.assign/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.assign/node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -17953,6 +18041,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.assign/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.assign/node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -17979,6 +18079,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.values/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.values/node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -18048,11 +18162,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.values/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "node_modules/object.values/node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/object.values/node_modules/function.prototype.name": { "version": "1.1.6", @@ -18072,16 +18194,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.values/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/object.values/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -19941,22 +20060,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/qs/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/qs/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/qs/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -20216,22 +20328,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-array-concat/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/safe-array-concat/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/safe-array-concat/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -20257,22 +20362,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-regex-test/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/safe-regex-test/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/safe-regex-test/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -20400,18 +20498,6 @@ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", "dev": true }, - "node_modules/selfsigned": { - "version": "2023.10.0-SNAPSHOT", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", - "dev": true, - "dependencies": { - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", @@ -20695,6 +20781,33 @@ "node": ">= 0.8" } }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-length/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/setimmediate": { "version": "2023.10.0-SNAPSHOT", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -21021,22 +21134,15 @@ "node": ">= 0.4" } }, - "node_modules/stop-iteration-iterator/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/stop-iteration-iterator/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/stop-iteration-iterator/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -21132,6 +21238,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.trim/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trim/node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -21201,11 +21321,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.trim/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "node_modules/string.prototype.trim/node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/string.prototype.trim/node_modules/function.prototype.name": { "version": "1.1.6", @@ -21225,16 +21353,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.trim/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/string.prototype.trim/node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -21730,22 +21855,15 @@ "node": ">= 0.4" } }, - "node_modules/typed-array-buffer/node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/typed-array-buffer/node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "node_modules/typed-array-buffer/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -21818,6 +21936,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-array-byte-length/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-byte-length/node_modules/has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -21886,6 +22018,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-array-byte-offset/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-byte-offset/node_modules/has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -21949,6 +22095,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-array-length/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-length/node_modules/has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -22026,6 +22186,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbox-primitive/node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/unbox-primitive/node_modules/has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -23177,6 +23351,19 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/webpack-dev-server/node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", @@ -27625,20 +27812,28 @@ } }, "@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" + }, + "dependencies": { + "@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + } } }, "@humanwhocodes/module-importer": { @@ -27647,11 +27842,6 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true }, - "@humanwhocodes/object-schema": { - "version": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, "@ionic/angular": { "version": "6.7.5", "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-6.7.5.tgz", @@ -28627,6 +28817,15 @@ "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==", "dev": true }, + "@types/node-forge": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.8.tgz", + "integrity": "sha512-vGXshY9vim9CJjrpcS5raqSjEfKlJcWy2HNdgUasR66fAnVEYarrf1ULV4nfvpC1nZq/moA9qyqBcu83x+Jlrg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/q": { "version": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", "integrity": "sha512-qYi3YV9inU/REEfxwVcGZzbS3KG/Xs90lv0Pr+lDtuVjBPGd1A+eciXzVSaRvLify132BfcvhvEjeVahrUl0Ug==", @@ -29049,6 +29248,12 @@ } } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "@webassemblyjs/ast": { "version": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", @@ -29233,9 +29438,9 @@ } }, "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true }, "acorn-jsx": { @@ -29498,6 +29703,17 @@ "deep-equal": "^2.0.5" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "deep-equal": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", @@ -29551,22 +29767,13 @@ "stop-iteration-iterator": "^1.0.0" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" } }, "has-tostringtag": { @@ -29741,22 +29948,15 @@ "is-array-buffer": "^3.0.1" }, "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "has-tostringtag": { @@ -29821,6 +30021,17 @@ "is-string": "^1.0.7" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -29878,11 +30089,16 @@ "which-typed-array": "^1.1.10" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } }, "function.prototype.name": { "version": "1.1.6", @@ -29896,16 +30112,13 @@ "functions-have-names": "^1.2.3" } }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" } }, "has-tostringtag": { @@ -30061,6 +30274,17 @@ "es-shim-unscopables": "^1.0.0" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -30118,11 +30342,25 @@ "which-typed-array": "^1.1.10" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } }, "function.prototype.name": { "version": "1.1.6", @@ -30136,16 +30374,13 @@ "functions-have-names": "^1.2.3" } }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" } }, "has-tostringtag": { @@ -30290,6 +30525,17 @@ "es-shim-unscopables": "^1.0.0" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -30347,11 +30593,25 @@ "which-typed-array": "^1.1.10" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } }, "function.prototype.name": { "version": "1.1.6", @@ -30365,16 +30625,13 @@ "functions-have-names": "^1.2.3" } }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" } }, "has-tostringtag": { @@ -30521,6 +30778,17 @@ "is-shared-array-buffer": "^1.0.2" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -30531,22 +30799,13 @@ "object-keys": "^1.1.1" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" } }, "has-tostringtag": { @@ -30678,6 +30937,17 @@ "deep-equal": "^2.0.5" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "deep-equal": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", @@ -30731,22 +31001,13 @@ "stop-iteration-iterator": "^1.0.0" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" } }, "has-tostringtag": { @@ -31551,35 +31812,6 @@ } } }, - "call-bind": { - "version": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } - } - } - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -32875,6 +33107,28 @@ "execa": "^5.0.0" } }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "dependencies": { + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + } + } + }, "define-lazy-prop": { "version": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", @@ -33226,53 +33480,6 @@ "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", "dev": true }, - "es-set-tostringtag": { - "version": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, - "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - } - } - }, - "es-shim-unscopables": { - "version": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, "es-to-primitive": { "version": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", @@ -33378,18 +33585,19 @@ "dev": true }, "eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -34268,6 +34476,12 @@ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, "functions-have-names": { "version": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", @@ -34322,6 +34536,18 @@ } } }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, "get-stream": { "version": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", @@ -34336,22 +34562,15 @@ "get-intrinsic": "^1.1.1" }, "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } } } @@ -34443,6 +34662,15 @@ "object-keys": "^1.1.1" } }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -34516,26 +34744,6 @@ "dev": true, "requires": { "get-intrinsic": "^1.1.3" - }, - "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } - } } }, "graphemer": { @@ -34556,14 +34764,6 @@ "dev": true, "requires": { "function-bind": "^1.1.1" - }, - "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - } } }, "has-bigints": { @@ -34571,34 +34771,6 @@ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true }, - "has-property-descriptors": { - "version": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.1" - }, - "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } - } - } - }, "has-proto": { "version": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", @@ -34614,6 +34786,15 @@ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, "hosted-git-info": { "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", @@ -35108,6 +35289,17 @@ "has-tostringtag": "^1.0.0" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -35200,6 +35392,19 @@ "dev": true, "requires": { "call-bind": "^1.0.2" + }, + "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + } } }, "is-stream": { @@ -35218,6 +35423,19 @@ "dev": true, "requires": { "call-bind": "^1.0.2" + }, + "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + } } }, "is-weakset": { @@ -35229,22 +35447,15 @@ "get-intrinsic": "^1.1.1" }, "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } } } @@ -36857,6 +37068,17 @@ "define-properties": "^1.1.3" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -36867,6 +37089,15 @@ "object-keys": "^1.1.1" } }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -36886,6 +37117,17 @@ "object-keys": "^1.1.1" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -36896,6 +37138,15 @@ "object-keys": "^1.1.1" } }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -36915,6 +37166,17 @@ "es-abstract": "^1.22.1" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -36972,11 +37234,16 @@ "which-typed-array": "^1.1.10" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } }, "function.prototype.name": { "version": "1.1.6", @@ -36990,16 +37257,13 @@ "functions-have-names": "^1.2.3" } }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" } }, "has-tostringtag": { @@ -38365,22 +38629,15 @@ "side-channel": "^1.0.4" }, "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "object-inspect": { @@ -38576,22 +38833,15 @@ "isarray": "^2.0.5" }, "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "isarray": { @@ -38612,22 +38862,15 @@ "is-regex": "^1.1.4" }, "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "has-tostringtag": { @@ -38705,14 +38948,6 @@ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", "dev": true }, - "selfsigned": { - "version": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", - "dev": true, - "requires": { - "node-forge": "^1" - } - }, "semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", @@ -38943,6 +39178,29 @@ } } }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "dependencies": { + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + } + } + }, "setimmediate": { "version": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", @@ -39201,22 +39459,15 @@ "internal-slot": "^1.0.4" }, "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "internal-slot": { @@ -39287,6 +39538,17 @@ "es-abstract": "^1.20.4" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -39344,11 +39606,16 @@ "which-typed-array": "^1.1.10" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } }, "function.prototype.name": { "version": "1.1.6", @@ -39362,16 +39629,13 @@ "functions-have-names": "^1.2.3" } }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "get-intrinsic": "^1.2.2" } }, "has-tostringtag": { @@ -39721,22 +39985,15 @@ "is-typed-array": "^1.1.10" }, "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "has-tostringtag": { @@ -39784,6 +40041,17 @@ "is-typed-array": "^1.1.10" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -39830,6 +40098,17 @@ "is-typed-array": "^1.1.10" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -39873,6 +40152,17 @@ "is-typed-array": "^1.1.9" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -39923,6 +40213,17 @@ "which-boxed-primitive": "^1.0.2" }, "dependencies": { + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", @@ -41036,6 +41337,16 @@ "ajv-keywords": "^5.1.0" } }, + "selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "requires": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + } + }, "webpack-dev-middleware": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", 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 fbb9e4f7ffc..438a406709c 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 @@ -36,6 +36,7 @@ describe('History EnergyMonitor', () => { } }); + } { diff --git a/ui/src/app/edge/settings/systemupdate/executeSystemUpdate.ts b/ui/src/app/edge/settings/systemupdate/executeSystemUpdate.ts index b3c332c50da..ab622a4a3a8 100644 --- a/ui/src/app/edge/settings/systemupdate/executeSystemUpdate.ts +++ b/ui/src/app/edge/settings/systemupdate/executeSystemUpdate.ts @@ -2,6 +2,7 @@ import { Subject, timer } from "rxjs"; import { takeUntil } from "rxjs/operators"; import { ComponentJsonApiRequest } from "src/app/shared/jsonrpc/request/componentJsonApiRequest"; import { Edge, Websocket } from "src/app/shared/shared"; +import { Role } from "src/app/shared/type/role"; import { environment } from "src/environments"; import { ExecuteSystemUpdateRequest } from "./executeSystemUpdateRequest"; import { GetSystemUpdateStateRequest } from "./getSystemUpdateStateRequest"; @@ -51,7 +52,7 @@ export class ExecuteSystemUpdate { return new Promise((resolve, reject) => { // if the version is a SNAPSHOT always set the udpate state // to updated with the current SNAPSHOT version - if (this.edge.isSnapshot()) { + if (this.edge.isSnapshot() && !this.edge.roleIsAtLeast(Role.ADMIN)) { let updateState = { updated: { version: this.edge.version } }; this.setSystemUpdateState(updateState); this.stopRefreshSystemUpdateState(); diff --git a/ui/src/app/index/overview/overview.component.html b/ui/src/app/index/overview/overview.component.html index e290bef0023..29892807edb 100644 --- a/ui/src/app/index/overview/overview.component.html +++ b/ui/src/app/index/overview/overview.component.html @@ -88,8 +88,7 @@

{{ edge.comment }}

- + @@ -101,7 +100,7 @@

{{ edge.comment }}

- diff --git a/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts b/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts index 6404bd865d6..bac206b86fd 100644 --- a/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts +++ b/ui/src/app/shared/genericComponents/chart/abstracthistorychart.ts @@ -8,6 +8,7 @@ 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 { JsonrpcResponseError } from '../../jsonrpc/base'; import { QueryHistoricTimeseriesDataRequest } from '../../jsonrpc/request/queryHistoricTimeseriesDataRequest'; @@ -45,7 +46,7 @@ export abstract class AbstractHistoryChart implements OnInit { protected isDataExisting: boolean = true; protected config: EdgeConfig = null; protected errorResponse: JsonrpcResponseError | null = null; - protected static phaseColors: string[] = ['rgb(255,127,80)', 'rgb(0,0,255)', 'rgb(128,128,0)']; + protected static readonly phaseColors: string[] = ['rgb(255,127,80)', 'rgb(0,0,255)', 'rgb(128,128,0)']; private legendOptions: { label: string, strokeThroughHidingStyle: boolean, hideLabelInLegend: boolean }[] = []; private channelData: { data: { [name: string]: number[] } } = { data: {} }; @@ -271,6 +272,13 @@ export abstract class AbstractHistoryChart implements OnInit { this.queryHistoricTimeseriesEnergyPerPeriod(this.service.historyPeriod.value.from, this.service.historyPeriod.value.to), this.queryHistoricTimeseriesEnergy(this.service.historyPeriod.value.from, this.service.historyPeriod.value.to) ]).then(([energyPeriodResponse, energyResponse]) => { + + + // 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]; + } + let displayValues = AbstractHistoryChart.fillChart(this.chartType, this.chartObject, energyPeriodResponse, energyResponse); this.datasets = displayValues.datasets; this.colors = displayValues.colors; diff --git a/ui/src/app/shared/service/utils.spec.ts b/ui/src/app/shared/service/utils.spec.ts new file mode 100644 index 00000000000..da55890a55c --- /dev/null +++ b/ui/src/app/shared/service/utils.spec.ts @@ -0,0 +1,13 @@ +import { Utils } from "./utils"; + +fdescribe('Utils', () => { + + it('#subtractSafely', () => { + expect(Utils.subtractSafely(null, null)).toEqual(null); + expect(Utils.subtractSafely(null, undefined)).toEqual(null); + expect(Utils.subtractSafely(0, null)).toEqual(0); + expect(Utils.subtractSafely(1, 1)).toEqual(0); + expect(Utils.subtractSafely(1, 2)).toEqual(-1); + expect(Utils.subtractSafely(1)).toEqual(1); + }); +}); \ No newline at end of file diff --git a/ui/src/app/shared/service/utils.ts b/ui/src/app/shared/service/utils.ts index 38226fac0fd..5dbb55618ac 100644 --- a/ui/src/app/shared/service/utils.ts +++ b/ui/src/app/shared/service/utils.ts @@ -111,19 +111,17 @@ export class Utils { * @returns a number, if at least one value is not null, else null */ public static subtractSafely(...values: (number | null)[]): number { - let result = null; - - for (const value of values) { - if (value !== null) { - if (result === null) { - result = value; + return values + .filter(value => value !== null && value !== undefined) + .reduce((sum, curr) => { + if (sum == null) { + sum = curr; } else { - result -= value; + sum -= curr; } - } - } - return result; + return sum; + }, null); } /** diff --git a/ui/src/assets/i18n/de.json b/ui/src/assets/i18n/de.json index 40b9a3c5147..884da5dde2f 100644 --- a/ui/src/assets/i18n/de.json +++ b/ui/src/assets/i18n/de.json @@ -560,7 +560,7 @@ "NO_EDGE_AVAILABLE": "Sie haben noch kein {{edgeShortName}} hinzugefügt.", "VISIBLE_HERE_AFTER_INSTALLATION": "Nachdem Ihr {{edgeShortName}} durch einen Installateur in Betrieb genommen wurde, sehen Sie es an dieser Stelle.", "NO_EDGE_FOR_USER": "Leider wurde noch kein {{edgeShortName}} mit Ihrem Account verknüpft.", - "FIRST_SETUP_PROTOCOL": "Inbetriebnahme" + "FIRST_SETUP_PROTOCOL": "Erstinbetriebnahme" }, "INSTALLATION": { "ATTENTION_MESSAGE": "Beachten Sie, dass die Auswahl nachträglich nicht mehr rückgängig gemacht werden kann.", diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index fd2475b53fd..27ad8590527 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -560,7 +560,7 @@ "NO_EDGE_AVAILABLE": "You have not added any {{edgeShortName}} yet.", "NO_EDGE_FOR_USER": "Sorry, no {{value}} has been associated with your account yet.", "VISIBLE_HERE_AFTER_INSTALLATION": "After your {{value}} has been commissioned by an installer, you will see it at this point.", - "FIRST_SETUP_PROTOCOL": "Commissioning" + "FIRST_SETUP_PROTOCOL": "Initial commissioning" }, "INSTALLATION": { "ATTENTION_MESSAGE": "Note that the selection cannot be undone afterwards.", diff --git a/ui/src/global.scss b/ui/src/global.scss index 82fc4e006ca..52913813d95 100644 --- a/ui/src/global.scss +++ b/ui/src/global.scss @@ -55,7 +55,14 @@ formly-field-ion-radio { } } +.custom-spinner { + ion-spinner { + left: 0 !important; + } +} + .line-break { + formly-wrapper-ion-form-field>ion-item>ion-label>span>small, formly-field-ion-radio>ion-list>ion-radio-group>ion-item>ion-label { white-space: pre-line !important; @@ -238,6 +245,7 @@ formly-input-section { } :root { + // Used in IBN, dynamicFeedInLimitation formly-field-ion-select { ion-select::part(text) { From 212bde7313ebf9e5c4ee494830ef6d14d6be851f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:17:19 +0100 Subject: [PATCH 26/27] Bump commons-io:commons-io from 2.14.0 to 2.15.0 in /cnf (#2417) * Bump commons-io:commons-io from 2.14.0 to 2.15.0 in /cnf Bumps commons-io:commons-io from 2.14.0 to 2.15.0. --- updated-dependencies: - dependency-name: commons-io:commons-io dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update bndrun * Fix ENTSO-E App * Add missing translateParams --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Feilmeier --- cnf/pom.xml | 2 +- io.openems.backend.application/BackendApp.bndrun | 2 +- io.openems.edge.application/EdgeApp.bndrun | 2 +- .../src/io/openems/edge/app/timeofusetariff/EntsoE.java | 1 + ui/src/app/index/overview/overview.component.html | 3 ++- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cnf/pom.xml b/cnf/pom.xml index 514e82f1ea6..c36d355c66a 100644 --- a/cnf/pom.xml +++ b/cnf/pom.xml @@ -109,7 +109,7 @@ commons-io commons-io - 2.14.0 + 2.15.0
diff --git a/io.openems.backend.application/BackendApp.bndrun b/io.openems.backend.application/BackendApp.bndrun index 923187ee9a5..6f832ba0462 100644 --- a/io.openems.backend.application/BackendApp.bndrun +++ b/io.openems.backend.application/BackendApp.bndrun @@ -101,7 +101,7 @@ io.reactivex.rxjava3.rxjava;version='[3.1.8,3.1.9)',\ org.apache.commons.commons-csv;version='[1.10.0,1.10.1)',\ org.apache.commons.commons-fileupload;version='[1.5.0,1.5.1)',\ - org.apache.commons.commons-io;version='[2.14.0,2.14.1)',\ + org.apache.commons.commons-io;version='[2.15.0,2.15.1)',\ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ org.apache.felix.fileinstall;version='[3.7.4,3.7.5)',\ diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun index d5983a7ac23..4c4d07740af 100644 --- a/io.openems.edge.application/EdgeApp.bndrun +++ b/io.openems.edge.application/EdgeApp.bndrun @@ -373,7 +373,7 @@ javax.xml.soap-api;version='[1.4.0,1.4.1)',\ org.apache.commons.commons-csv;version='[1.10.0,1.10.1)',\ org.apache.commons.commons-fileupload;version='[1.5.0,1.5.1)',\ - org.apache.commons.commons-io;version='[2.14.0,2.14.1)',\ + org.apache.commons.commons-io;version='[2.15.0,2.15.1)',\ org.apache.commons.math3;version='[3.6.1,3.6.2)',\ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ org.apache.felix.eventadmin;version='[1.6.4,1.6.5)',\ diff --git a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java index ffaca7888c9..4dd77b2f921 100644 --- a/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java +++ b/io.openems.edge.core/src/io/openems/edge/app/timeofusetariff/EntsoE.java @@ -120,6 +120,7 @@ protected ThrowingTriFunction, L new EdgeConfig.Component(ctrlEssTimeOfUseTariffId, alias, "Controller.Ess.Time-Of-Use-Tariff", JsonUtils.buildJsonObject() // .addProperty("ess.id", "ess0") // + .addPropertyIfNotNull("biddingZone", biddingZone) // .build()), // new EdgeConfig.Component(timeOfUseTariffProviderId, this.getName(l), "TimeOfUseTariff.ENTSO-E", JsonUtils.buildJsonObject() // diff --git a/ui/src/app/index/overview/overview.component.html b/ui/src/app/index/overview/overview.component.html index 29892807edb..ceb8d8c680a 100644 --- a/ui/src/app/index/overview/overview.component.html +++ b/ui/src/app/index/overview/overview.component.html @@ -17,7 +17,8 @@ [translateParams]="{edgeShortName: environment.edgeShortName}">Index.NO_EDGE_AVAILABLE -

INSTALLATION.CLICK_RECOMMENDATION

+

+ INSTALLATION.CLICK_RECOMMENDATION

From b522a065024f5d976f5a30b81397ebc472369a5a Mon Sep 17 00:00:00 2001 From: Stefan Feilmeier Date: Wed, 1 Nov 2023 17:24:30 +0100 Subject: [PATCH 27/27] Push version to 2023.11.0 --- io.openems.common/src/io/openems/common/OpenemsConstants.java | 2 +- ui/package-lock.json | 4 ++-- ui/package.json | 2 +- ui/src/app/changelog/view/component/changelog.constants.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/io.openems.common/src/io/openems/common/OpenemsConstants.java b/io.openems.common/src/io/openems/common/OpenemsConstants.java index 59d1325b7b4..0f8a1f6b29d 100644 --- a/io.openems.common/src/io/openems/common/OpenemsConstants.java +++ b/io.openems.common/src/io/openems/common/OpenemsConstants.java @@ -43,7 +43,7 @@ public class OpenemsConstants { /** * The additional version string. */ - public static final String VERSION_STRING = "SNAPSHOT"; + public static final String VERSION_STRING = ""; /** * The complete version as a SemanticVersion. diff --git a/ui/package-lock.json b/ui/package-lock.json index 7ac5df7d9ad..583b633ec60 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "openems-ui", - "version": "2023.11.0-SNAPSHOT", + "version": "2023.11.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openems-ui", - "version": "2023.11.0-SNAPSHOT", + "version": "2023.11.0", "license": "AGPL-3.0", "dependencies": { "@angular/animations": "~15.2.9", diff --git a/ui/package.json b/ui/package.json index be8b14c1dbd..74684f9a9f0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "openems-ui", - "version": "2023.11.0-SNAPSHOT", + "version": "2023.11.0", "license": "AGPL-3.0", "private": true, "dependencies": { diff --git a/ui/src/app/changelog/view/component/changelog.constants.ts b/ui/src/app/changelog/view/component/changelog.constants.ts index b41d1331209..132b632528c 100644 --- a/ui/src/app/changelog/view/component/changelog.constants.ts +++ b/ui/src/app/changelog/view/component/changelog.constants.ts @@ -2,7 +2,7 @@ import { Role } from "src/app/shared/type/role"; export class Changelog { - public static readonly UI_VERSION = "2023.11.0-SNAPSHOT"; + public static readonly UI_VERSION = "2023.11.0"; public static product(...products: Product[]) { return products.map(product => Changelog.link(product.name, product.url)).join(", ") + '. ';