@enyo-energy/energy-app-sdk 1.38.0 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -18
- package/dist/cjs/implementations/appliances/appliance-manager.cjs +180 -1
- package/dist/cjs/implementations/appliances/appliance-manager.d.cts +126 -2
- package/dist/cjs/implementations/appliances/in-memory-appliance-manager.cjs +41 -0
- package/dist/cjs/implementations/appliances/in-memory-appliance-manager.d.cts +19 -1
- package/dist/cjs/packages/energy-app-appliance.d.cts +32 -4
- package/dist/cjs/packages/energy-app-notification.d.cts +164 -2
- package/dist/cjs/types/enyo-appliance.d.cts +65 -0
- package/dist/cjs/types/enyo-notification.d.cts +55 -2
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/implementations/appliances/appliance-manager.d.ts +126 -2
- package/dist/implementations/appliances/appliance-manager.js +178 -0
- package/dist/implementations/appliances/in-memory-appliance-manager.d.ts +19 -1
- package/dist/implementations/appliances/in-memory-appliance-manager.js +41 -0
- package/dist/packages/energy-app-appliance.d.ts +32 -4
- package/dist/packages/energy-app-notification.d.ts +164 -2
- package/dist/types/enyo-appliance.d.ts +65 -0
- package/dist/types/enyo-notification.d.ts +55 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1290,32 +1290,150 @@ prices.onPriceChange((newPrice) => {
|
|
|
1290
1290
|
|
|
1291
1291
|
#### `useNotification(): EnergyAppNotification`
|
|
1292
1292
|
|
|
1293
|
-
Send notifications to users
|
|
1293
|
+
Send notifications to users. Every category must be registered before it is used; the host uses the category for the
|
|
1294
|
+
user's on/off switch and for rate limits.
|
|
1294
1295
|
|
|
1295
1296
|
```typescript
|
|
1296
1297
|
const notifications = energyApp.useNotification();
|
|
1297
1298
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1299
|
+
notifications.registerNotificationCategory({
|
|
1300
|
+
category: 'connection-issue',
|
|
1301
|
+
name: [
|
|
1302
|
+
{ language: 'en', name: 'Connection issue' },
|
|
1303
|
+
{ language: 'de', name: 'Verbindungsproblem' }
|
|
1304
|
+
]
|
|
1304
1305
|
});
|
|
1305
1306
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
type: '
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
}
|
|
1307
|
+
const notificationId = notifications.sendNotification(
|
|
1308
|
+
'connection-issue',
|
|
1309
|
+
{ applianceId, target: { type: 'appliance-settings', applianceId } },
|
|
1310
|
+
[
|
|
1311
|
+
{ language: 'en', title: 'Inverter offline', body: 'Inverter communication lost - please check the connection.' },
|
|
1312
|
+
{ language: 'de', title: 'Wechselrichter offline', body: 'Keine Verbindung zum Wechselrichter - bitte Verbindung prüfen.' }
|
|
1313
|
+
]
|
|
1314
|
+
);
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
Categories:
|
|
1318
|
+
|
|
1319
|
+
| Category | Use when |
|
|
1320
|
+
|---|---|
|
|
1321
|
+
| `connection-issue` | An appliance, gateway or upstream service is unreachable. |
|
|
1322
|
+
| `onboarding-required` | The user has to complete (or revisit) an onboarding flow. |
|
|
1323
|
+
| `charging-issue` | The app asked a charger to charge, but it is not charging. See [Charging issues](#charging-issues). |
|
|
1324
|
+
| `battery-issue` | The app commanded a battery, but it does not follow the command. See [Battery issues](#battery-issues). |
|
|
1325
|
+
| `authentication-required` | A sign-in that used to work was lost. See [Lost sign-in](#lost-sign-in). |
|
|
1326
|
+
|
|
1327
|
+
Targets (`options.target`) decide where tapping the notification leads:
|
|
1328
|
+
|
|
1329
|
+
| Target | Opens |
|
|
1330
|
+
|---|---|
|
|
1331
|
+
| `{ type: 'onboarding', onboardingName }` | A registered onboarding guide. |
|
|
1332
|
+
| `{ type: 'energy-app-settings' }` | The package's own settings. |
|
|
1333
|
+
| `{ type: 'appliance-settings', applianceId }` | The settings screen of an appliance. |
|
|
1334
|
+
| `{ type: 'appliance', applianceId }` | The live view of an appliance (current state, power and session). |
|
|
1335
|
+
| `{ type: 'authentication', applianceId? }` | The package's pending sign-in, created with `requestAuthentication()`. |
|
|
1336
|
+
| `{ type: 'subscription-offer', planKey }` | The offer screen for a subscription plan. |
|
|
1312
1337
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1338
|
+
`sendNotification` throws an `EnyoNotificationSendError` with `type: 'not-registered'` if the category was not
|
|
1339
|
+
registered, or `type: 'rate-limit'` if the host's rate limit for the category is exceeded.
|
|
1340
|
+
|
|
1341
|
+
##### Charging issues
|
|
1342
|
+
|
|
1343
|
+
Only the app that decided a charger should charge knows that it should be charging, so the app sends
|
|
1344
|
+
`charging-issue` notifications. The host relies on these rules and does not re-check them. Send one only when
|
|
1345
|
+
**all** of the following hold:
|
|
1346
|
+
|
|
1347
|
+
- The app itself asked the charger to charge: it sent a start or resume, or it is giving the charger more than 0 W.
|
|
1348
|
+
- The charger has stayed non-charging for at least 5 minutes after that request (status is not `Charging`, or it is
|
|
1349
|
+
`Charging` but draws almost no power).
|
|
1350
|
+
- The charger is not `Suspended` with reason `EnyoChargerApplianceSuspendedReasonEnum.Ev`: the car is full or paused
|
|
1351
|
+
charging itself, which is not a fault.
|
|
1352
|
+
- No grid-operator limit (§14a / `GridOperatorPowerLimitationV1`) is currently pushing the charger to 0.
|
|
1353
|
+
|
|
1354
|
+
Send it **at most once per plug-in**: wait until the charger is `Available` again (car unplugged) before sending
|
|
1355
|
+
another one for that appliance. Always pass `applianceId` and the `appliance` target. The host drops more than one
|
|
1356
|
+
`charging-issue` per appliance per 30 minutes with a `rate-limit` error.
|
|
1357
|
+
|
|
1358
|
+
```typescript
|
|
1359
|
+
const notification = energyApp.useNotification();
|
|
1360
|
+
|
|
1361
|
+
notification.registerNotificationCategory({
|
|
1362
|
+
category: 'charging-issue',
|
|
1363
|
+
name: [
|
|
1364
|
+
{language: 'de', name: 'Ladeprobleme'},
|
|
1365
|
+
{language: 'en', name: 'Charging issues'},
|
|
1366
|
+
],
|
|
1367
|
+
description: [
|
|
1368
|
+
{language: 'de', name: 'Ladeprobleme', description: 'Wenn dein Auto nicht lädt, obwohl es laden sollte.'},
|
|
1369
|
+
{language: 'en', name: 'Charging issues', description: 'When your car is not charging although it should.'},
|
|
1370
|
+
],
|
|
1318
1371
|
});
|
|
1372
|
+
|
|
1373
|
+
notification.sendNotification(
|
|
1374
|
+
'charging-issue',
|
|
1375
|
+
{applianceId, target: {type: 'appliance', applianceId}},
|
|
1376
|
+
[
|
|
1377
|
+
{language: 'de', title: 'Dein Auto lädt nicht', body: 'Die Wallbox sollte gerade laden, tut es aber nicht. Prüfe bitte Auto und Ladekabel.'},
|
|
1378
|
+
{language: 'en', title: 'Your car is not charging', body: 'The wallbox should be charging right now but isn\'t. Please check the car and the charging cable.'},
|
|
1379
|
+
],
|
|
1380
|
+
);
|
|
1381
|
+
```
|
|
1382
|
+
|
|
1383
|
+
##### Battery issues
|
|
1384
|
+
|
|
1385
|
+
`battery-issue` follows the same idea for home batteries. Send one only when **all** of the following hold:
|
|
1386
|
+
|
|
1387
|
+
- The app itself commanded the battery (`SetStorageControlV2` with mode `Controlled`, or one of the other storage
|
|
1388
|
+
commands) and that command is still in effect. Never while the battery runs on `Auto`.
|
|
1389
|
+
- For at least 5 minutes the battery has not followed it, judged by `batteryPowerW` in `BatteryValuesUpdateV1`:
|
|
1390
|
+
not charging on `Charge`, not discharging on `Discharge`, or moving power noticeably on `Hold`.
|
|
1391
|
+
- The battery is not simply at its limit: no charge command at or above `maxSoC` (or 100 %), no discharge command
|
|
1392
|
+
at or below `minSoC`.
|
|
1393
|
+
- A discharge to the house is not just capped by low household consumption.
|
|
1394
|
+
- No grid-operator limit (§14a) is pushing the battery to 0, and `controlAllowed` is not `false`.
|
|
1395
|
+
|
|
1396
|
+
Send it **at most once per issue**: wait until the battery follows a command again, or the app releases it to
|
|
1397
|
+
`Auto`, before sending another one. Always pass `applianceId` and the `appliance` target. The host drops more than
|
|
1398
|
+
one `battery-issue` per appliance per 30 minutes with a `rate-limit` error.
|
|
1399
|
+
|
|
1400
|
+
```typescript
|
|
1401
|
+
notification.sendNotification(
|
|
1402
|
+
'battery-issue',
|
|
1403
|
+
{applianceId, target: {type: 'appliance', applianceId}},
|
|
1404
|
+
[
|
|
1405
|
+
{language: 'de', title: 'Dein Speicher reagiert nicht', body: 'Der Batteriespeicher folgt der Steuerung gerade nicht. Prüfe bitte, ob die externe Steuerung am Speicher aktiviert ist.'},
|
|
1406
|
+
{language: 'en', title: 'Your battery is not responding', body: 'The home battery is not following the control right now. Please check that external control is enabled on the battery.'},
|
|
1407
|
+
],
|
|
1408
|
+
);
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
##### Lost sign-in
|
|
1412
|
+
|
|
1413
|
+
`requestAuthentication()` already prompts the user, including for the first sign-in during onboarding.
|
|
1414
|
+
`authentication-required` is only for a sign-in that **used to work** and was lost. Send one only when:
|
|
1415
|
+
|
|
1416
|
+
- The package was `Authenticated` before, the vendor now rejects the credentials or token, and a refresh failed.
|
|
1417
|
+
- The failure is not transient. Timeouts or a vendor outage are a `connection-issue`.
|
|
1418
|
+
- The user did not sign out themselves.
|
|
1419
|
+
|
|
1420
|
+
Before sending, set the state with `setAuthenticationState()` and create a new request with
|
|
1421
|
+
`requestAuthentication()`, so the `authentication` target has a sign-in to open. Send it **at most once per lost
|
|
1422
|
+
session**, and not again until the package is `Authenticated` again. The host allows one per package (or per
|
|
1423
|
+
appliance, for an appliance sign-in) per 24 hours.
|
|
1424
|
+
|
|
1425
|
+
```typescript
|
|
1426
|
+
await auth.setAuthenticationState({state: EnyoAuthenticationStateEnum.AuthenticationFailed});
|
|
1427
|
+
await auth.requestAuthentication({authenticationType: 'oauth', oauth: {}, oneTimeAuthentication: false});
|
|
1428
|
+
|
|
1429
|
+
notification.sendNotification(
|
|
1430
|
+
'authentication-required',
|
|
1431
|
+
{target: {type: 'authentication'}},
|
|
1432
|
+
[
|
|
1433
|
+
{language: 'de', title: 'Bitte erneut anmelden', body: 'Deine Anmeldung ist abgelaufen. Melde dich erneut an, damit wir deine Geräte weiter steuern können.'},
|
|
1434
|
+
{language: 'en', title: 'Please sign in again', body: 'Your sign-in has expired. Sign in again so we can keep controlling your devices.'},
|
|
1435
|
+
],
|
|
1436
|
+
);
|
|
1319
1437
|
```
|
|
1320
1438
|
|
|
1321
1439
|
### App Intelligence
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ApplianceManager = exports.ApplianceManagerDisposedError = exports.DuplicateIdentifierError = exports.MissingIdentifierError = void 0;
|
|
3
|
+
exports.ApplianceManager = exports.ApplianceGroupNotFoundError = exports.ApplianceManagerDisposedError = exports.DuplicateIdentifierError = exports.MissingIdentifierError = void 0;
|
|
4
4
|
const index_js_1 = require("../../index.cjs");
|
|
5
5
|
const enyo_appliance_js_1 = require("../../types/enyo-appliance.cjs");
|
|
6
6
|
/**
|
|
@@ -49,6 +49,20 @@ class ApplianceManagerDisposedError extends Error {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
exports.ApplianceManagerDisposedError = ApplianceManagerDisposedError;
|
|
52
|
+
/**
|
|
53
|
+
* Thrown by the {@link ApplianceManager} group helpers when no group with the
|
|
54
|
+
* given key exists in the package.
|
|
55
|
+
*/
|
|
56
|
+
class ApplianceGroupNotFoundError extends Error {
|
|
57
|
+
/** The group key that was looked up */
|
|
58
|
+
key;
|
|
59
|
+
constructor(key) {
|
|
60
|
+
super(`No appliance group with key "${key}" exists.`);
|
|
61
|
+
this.name = 'ApplianceGroupNotFoundError';
|
|
62
|
+
this.key = key;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.ApplianceGroupNotFoundError = ApplianceGroupNotFoundError;
|
|
52
66
|
/** Sub-objects on EnyoAppliance that {@link ApplianceManager} shallow-merges during updates. */
|
|
53
67
|
const MERGEABLE_METADATA_KEYS = [
|
|
54
68
|
'metadata',
|
|
@@ -220,6 +234,7 @@ class ApplianceManager {
|
|
|
220
234
|
...(appliance.cloudPackageId !== undefined && { cloudPackageId: appliance.cloudPackageId }),
|
|
221
235
|
...(appliance.availableFeatures !== undefined && { availableFeatures: appliance.availableFeatures }),
|
|
222
236
|
...(appliance.compatibilityModes !== undefined && { compatibilityModes: appliance.compatibilityModes }),
|
|
237
|
+
...(appliance.hidden !== undefined && { hidden: appliance.hidden }),
|
|
223
238
|
};
|
|
224
239
|
let applianceData = newApplianceData;
|
|
225
240
|
if (existingApplianceId) {
|
|
@@ -741,6 +756,170 @@ class ApplianceManager {
|
|
|
741
756
|
}
|
|
742
757
|
}
|
|
743
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Creates the group with the given {@link ApplianceGroupConfig.key}, or
|
|
761
|
+
* updates it when it already exists. The group is shown in the UI as one
|
|
762
|
+
* collapsible entry with the summed values of its members.
|
|
763
|
+
*
|
|
764
|
+
* An appliance belongs to at most one group, so members currently listed
|
|
765
|
+
* in another group of the package are taken out of it first; a group left
|
|
766
|
+
* empty by that is removed.
|
|
767
|
+
*
|
|
768
|
+
* @param config The group's key, name and member appliance IDs
|
|
769
|
+
* @returns The ID of the saved group
|
|
770
|
+
* @throws {Error} when `config.applianceIds` is empty
|
|
771
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
772
|
+
*/
|
|
773
|
+
async createOrUpdateGroup(config) {
|
|
774
|
+
this.throwIfDisposed();
|
|
775
|
+
const applianceIds = [...new Set(config.applianceIds)];
|
|
776
|
+
if (applianceIds.length === 0) {
|
|
777
|
+
throw new Error(`Appliance group "${config.key}" needs at least one appliance; use removeGroup to drop it.`);
|
|
778
|
+
}
|
|
779
|
+
const groups = await this.loadGroups();
|
|
780
|
+
const existing = groups.find(g => g.key === config.key);
|
|
781
|
+
// Take the members out of every other group before saving, otherwise
|
|
782
|
+
// the host rejects the save for an appliance being in two groups.
|
|
783
|
+
const members = new Set(applianceIds);
|
|
784
|
+
for (const other of groups) {
|
|
785
|
+
if (other === existing || !other.applianceIds.some(id => members.has(id)))
|
|
786
|
+
continue;
|
|
787
|
+
const remaining = other.applianceIds.filter(id => !members.has(id));
|
|
788
|
+
if (remaining.length === 0) {
|
|
789
|
+
await this.deleteGroup(other.id);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
await this.persistGroup({ ...this.withoutId(other), applianceIds: remaining }, other.id);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return this.persistGroup({ key: config.key, name: config.name, applianceIds }, existing?.id);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Returns all appliance groups of the package.
|
|
799
|
+
*
|
|
800
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
801
|
+
*/
|
|
802
|
+
async getGroups() {
|
|
803
|
+
this.throwIfDisposed();
|
|
804
|
+
return this.loadGroups();
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Returns the group with the given key, or `null` when there is none.
|
|
808
|
+
*
|
|
809
|
+
* @param key The app-defined group key
|
|
810
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
811
|
+
*/
|
|
812
|
+
async findGroupByKey(key) {
|
|
813
|
+
this.throwIfDisposed();
|
|
814
|
+
return (await this.loadGroups()).find(g => g.key === key) ?? null;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Returns the group an appliance belongs to, or `null` when it is not
|
|
818
|
+
* grouped.
|
|
819
|
+
*
|
|
820
|
+
* @param applianceId The appliance to look up
|
|
821
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
822
|
+
*/
|
|
823
|
+
async findGroupOfAppliance(applianceId) {
|
|
824
|
+
this.throwIfDisposed();
|
|
825
|
+
return (await this.loadGroups()).find(g => g.applianceIds.includes(applianceId)) ?? null;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Adds an appliance to the end of an existing group, moving it out of any
|
|
829
|
+
* other group it was in. Does nothing when it is already a member.
|
|
830
|
+
*
|
|
831
|
+
* @param key The key of the group to add the appliance to
|
|
832
|
+
* @param applianceId The appliance to add
|
|
833
|
+
* @returns The ID of the group
|
|
834
|
+
* @throws {ApplianceGroupNotFoundError} when no group has the given key
|
|
835
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
836
|
+
*/
|
|
837
|
+
async addApplianceToGroup(key, applianceId) {
|
|
838
|
+
this.throwIfDisposed();
|
|
839
|
+
const group = await this.findGroupByKey(key);
|
|
840
|
+
if (!group) {
|
|
841
|
+
throw new ApplianceGroupNotFoundError(key);
|
|
842
|
+
}
|
|
843
|
+
if (group.applianceIds.includes(applianceId)) {
|
|
844
|
+
return group.id;
|
|
845
|
+
}
|
|
846
|
+
return this.createOrUpdateGroup({
|
|
847
|
+
key,
|
|
848
|
+
name: group.name,
|
|
849
|
+
applianceIds: [...group.applianceIds, applianceId],
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Takes an appliance out of its group, so the UI shows it individually
|
|
854
|
+
* again. A group left without members is removed.
|
|
855
|
+
*
|
|
856
|
+
* @param applianceId The appliance to take out of its group
|
|
857
|
+
* @returns `true` when the appliance was in a group, `false` otherwise
|
|
858
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
859
|
+
*/
|
|
860
|
+
async removeApplianceFromGroup(applianceId) {
|
|
861
|
+
this.throwIfDisposed();
|
|
862
|
+
const group = await this.findGroupOfAppliance(applianceId);
|
|
863
|
+
if (!group) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
const remaining = group.applianceIds.filter(id => id !== applianceId);
|
|
867
|
+
if (remaining.length === 0) {
|
|
868
|
+
await this.deleteGroup(group.id);
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
await this.persistGroup({ ...this.withoutId(group), applianceIds: remaining }, group.id);
|
|
872
|
+
}
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Removes the group with the given key. Its members stay and are shown
|
|
877
|
+
* individually again.
|
|
878
|
+
*
|
|
879
|
+
* @param key The key of the group to remove
|
|
880
|
+
* @returns `true` when a group was removed, `false` when none had the key
|
|
881
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
882
|
+
*/
|
|
883
|
+
async removeGroup(key) {
|
|
884
|
+
this.throwIfDisposed();
|
|
885
|
+
const group = await this.findGroupByKey(key);
|
|
886
|
+
if (!group) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
await this.deleteGroup(group.id);
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Loads the package's appliance groups from storage. `protected` so
|
|
894
|
+
* subclasses (e.g. {@link InMemoryApplianceManager}) can swap the storage
|
|
895
|
+
* the group helpers run on.
|
|
896
|
+
*/
|
|
897
|
+
async loadGroups() {
|
|
898
|
+
return this.energyApp.useAppliances().listGroups();
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Saves an appliance group to storage.
|
|
902
|
+
*
|
|
903
|
+
* @param group The group to save
|
|
904
|
+
* @param groupId ID of the group to update, or `undefined` to create one
|
|
905
|
+
* @returns The ID of the saved group
|
|
906
|
+
*/
|
|
907
|
+
async persistGroup(group, groupId) {
|
|
908
|
+
return this.energyApp.useAppliances().saveGroup(group, groupId);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Deletes an appliance group from storage.
|
|
912
|
+
*
|
|
913
|
+
* @param groupId ID of the group to delete
|
|
914
|
+
*/
|
|
915
|
+
async deleteGroup(groupId) {
|
|
916
|
+
await this.energyApp.useAppliances().removeGroup(groupId);
|
|
917
|
+
}
|
|
918
|
+
/** Returns a copy of the group without its `id`, ready to be saved again. */
|
|
919
|
+
withoutId(group) {
|
|
920
|
+
const { id: _id, ...rest } = group;
|
|
921
|
+
return rest;
|
|
922
|
+
}
|
|
744
923
|
/**
|
|
745
924
|
* Gets the current identifier strategy.
|
|
746
925
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EnergyApp } from "../../index.cjs";
|
|
2
2
|
import type { EnyoNetworkDevice } from "../../types/enyo-network-device.cjs";
|
|
3
|
-
import { EnyoAppliance, EnyoApplianceAvailableFeaturesEnum, EnyoApplianceConnectionType, EnyoApplianceMetadata, EnyoApplianceName, EnyoApplianceStateEnum, EnyoApplianceTopology, EnyoApplianceTypeEnum } from "../../types/enyo-appliance.cjs";
|
|
3
|
+
import { EnyoAppliance, EnyoApplianceAvailableFeaturesEnum, EnyoApplianceConnectionType, EnyoApplianceGroup, EnyoApplianceMetadata, EnyoApplianceName, EnyoApplianceStateEnum, EnyoApplianceTopology, EnyoApplianceTypeEnum } from "../../types/enyo-appliance.cjs";
|
|
4
4
|
import type { EnyoApplianceCreatedFilter } from "../../packages/energy-app-appliance.cjs";
|
|
5
5
|
import type { EnyoChargerApplianceMetadata } from "../../types/enyo-charger-appliance.cjs";
|
|
6
6
|
import type { EnyoHeatpumpApplianceDomesticHotWater, EnyoHeatpumpApplianceHeatingCircuit, EnyoHeatpumpApplianceMetadata } from "../../types/enyo-heatpump-appliance.cjs";
|
|
@@ -40,6 +40,36 @@ export declare class DuplicateIdentifierError extends Error {
|
|
|
40
40
|
export declare class ApplianceManagerDisposedError extends Error {
|
|
41
41
|
constructor();
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Thrown by the {@link ApplianceManager} group helpers when no group with the
|
|
45
|
+
* given key exists in the package.
|
|
46
|
+
*/
|
|
47
|
+
export declare class ApplianceGroupNotFoundError extends Error {
|
|
48
|
+
/** The group key that was looked up */
|
|
49
|
+
readonly key: string;
|
|
50
|
+
constructor(key: string);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Configuration for creating or updating an appliance group via
|
|
54
|
+
* {@link ApplianceManager.createOrUpdateGroup}. See {@link EnyoApplianceGroup}
|
|
55
|
+
* for what a group is and how the UI shows it.
|
|
56
|
+
*/
|
|
57
|
+
export interface ApplianceGroupConfig {
|
|
58
|
+
/**
|
|
59
|
+
* Stable, app-defined key identifying the group within the package — e.g.
|
|
60
|
+
* the serial number of the gateway the members sit behind. A group with
|
|
61
|
+
* this key is updated in place; otherwise a new one is created.
|
|
62
|
+
*/
|
|
63
|
+
key: string;
|
|
64
|
+
/** Name of the group shown in the UI, in the supported languages */
|
|
65
|
+
name: EnyoApplianceName[];
|
|
66
|
+
/**
|
|
67
|
+
* IDs of the member appliances (your package's own), in display order.
|
|
68
|
+
* Must not be empty — use {@link ApplianceManager.removeGroup} to drop a
|
|
69
|
+
* group. Appliances currently in another group are moved into this one.
|
|
70
|
+
*/
|
|
71
|
+
applianceIds: string[];
|
|
72
|
+
}
|
|
43
73
|
/**
|
|
44
74
|
* Configuration for creating or updating an appliance.
|
|
45
75
|
*/
|
|
@@ -78,6 +108,12 @@ export interface ApplianceConfig {
|
|
|
78
108
|
* appliance is created or updated.
|
|
79
109
|
*/
|
|
80
110
|
cloudPackageId?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Whether the appliance is hidden. Forwarded to {@link EnyoAppliance.hidden}
|
|
113
|
+
* when the appliance is created or updated. Omit to keep the stored value on
|
|
114
|
+
* an update; pass `false` to unhide it.
|
|
115
|
+
*/
|
|
116
|
+
hidden?: boolean;
|
|
81
117
|
}
|
|
82
118
|
/**
|
|
83
119
|
* Configuration options for the ApplianceManager.
|
|
@@ -434,6 +470,93 @@ export declare class ApplianceManager {
|
|
|
434
470
|
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
435
471
|
*/
|
|
436
472
|
setIdentifierStrategy(strategy: IdentifierStrategy, rebuildCache: boolean): Promise<void>;
|
|
473
|
+
/**
|
|
474
|
+
* Creates the group with the given {@link ApplianceGroupConfig.key}, or
|
|
475
|
+
* updates it when it already exists. The group is shown in the UI as one
|
|
476
|
+
* collapsible entry with the summed values of its members.
|
|
477
|
+
*
|
|
478
|
+
* An appliance belongs to at most one group, so members currently listed
|
|
479
|
+
* in another group of the package are taken out of it first; a group left
|
|
480
|
+
* empty by that is removed.
|
|
481
|
+
*
|
|
482
|
+
* @param config The group's key, name and member appliance IDs
|
|
483
|
+
* @returns The ID of the saved group
|
|
484
|
+
* @throws {Error} when `config.applianceIds` is empty
|
|
485
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
486
|
+
*/
|
|
487
|
+
createOrUpdateGroup(config: ApplianceGroupConfig): Promise<string>;
|
|
488
|
+
/**
|
|
489
|
+
* Returns all appliance groups of the package.
|
|
490
|
+
*
|
|
491
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
492
|
+
*/
|
|
493
|
+
getGroups(): Promise<EnyoApplianceGroup[]>;
|
|
494
|
+
/**
|
|
495
|
+
* Returns the group with the given key, or `null` when there is none.
|
|
496
|
+
*
|
|
497
|
+
* @param key The app-defined group key
|
|
498
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
499
|
+
*/
|
|
500
|
+
findGroupByKey(key: string): Promise<EnyoApplianceGroup | null>;
|
|
501
|
+
/**
|
|
502
|
+
* Returns the group an appliance belongs to, or `null` when it is not
|
|
503
|
+
* grouped.
|
|
504
|
+
*
|
|
505
|
+
* @param applianceId The appliance to look up
|
|
506
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
507
|
+
*/
|
|
508
|
+
findGroupOfAppliance(applianceId: string): Promise<EnyoApplianceGroup | null>;
|
|
509
|
+
/**
|
|
510
|
+
* Adds an appliance to the end of an existing group, moving it out of any
|
|
511
|
+
* other group it was in. Does nothing when it is already a member.
|
|
512
|
+
*
|
|
513
|
+
* @param key The key of the group to add the appliance to
|
|
514
|
+
* @param applianceId The appliance to add
|
|
515
|
+
* @returns The ID of the group
|
|
516
|
+
* @throws {ApplianceGroupNotFoundError} when no group has the given key
|
|
517
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
518
|
+
*/
|
|
519
|
+
addApplianceToGroup(key: string, applianceId: string): Promise<string>;
|
|
520
|
+
/**
|
|
521
|
+
* Takes an appliance out of its group, so the UI shows it individually
|
|
522
|
+
* again. A group left without members is removed.
|
|
523
|
+
*
|
|
524
|
+
* @param applianceId The appliance to take out of its group
|
|
525
|
+
* @returns `true` when the appliance was in a group, `false` otherwise
|
|
526
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
527
|
+
*/
|
|
528
|
+
removeApplianceFromGroup(applianceId: string): Promise<boolean>;
|
|
529
|
+
/**
|
|
530
|
+
* Removes the group with the given key. Its members stay and are shown
|
|
531
|
+
* individually again.
|
|
532
|
+
*
|
|
533
|
+
* @param key The key of the group to remove
|
|
534
|
+
* @returns `true` when a group was removed, `false` when none had the key
|
|
535
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
536
|
+
*/
|
|
537
|
+
removeGroup(key: string): Promise<boolean>;
|
|
538
|
+
/**
|
|
539
|
+
* Loads the package's appliance groups from storage. `protected` so
|
|
540
|
+
* subclasses (e.g. {@link InMemoryApplianceManager}) can swap the storage
|
|
541
|
+
* the group helpers run on.
|
|
542
|
+
*/
|
|
543
|
+
protected loadGroups(): Promise<EnyoApplianceGroup[]>;
|
|
544
|
+
/**
|
|
545
|
+
* Saves an appliance group to storage.
|
|
546
|
+
*
|
|
547
|
+
* @param group The group to save
|
|
548
|
+
* @param groupId ID of the group to update, or `undefined` to create one
|
|
549
|
+
* @returns The ID of the saved group
|
|
550
|
+
*/
|
|
551
|
+
protected persistGroup(group: Omit<EnyoApplianceGroup, 'id'>, groupId: string | undefined): Promise<string>;
|
|
552
|
+
/**
|
|
553
|
+
* Deletes an appliance group from storage.
|
|
554
|
+
*
|
|
555
|
+
* @param groupId ID of the group to delete
|
|
556
|
+
*/
|
|
557
|
+
protected deleteGroup(groupId: string): Promise<void>;
|
|
558
|
+
/** Returns a copy of the group without its `id`, ready to be saved again. */
|
|
559
|
+
private withoutId;
|
|
437
560
|
/**
|
|
438
561
|
* Gets the current identifier strategy.
|
|
439
562
|
*/
|
|
@@ -495,5 +618,6 @@ export interface PartialEnyoAppliance {
|
|
|
495
618
|
* Optional identifier of the cloud-deployed energy app package that manages
|
|
496
619
|
* this appliance. Mirrors {@link EnyoAppliance.cloudPackageId}.
|
|
497
620
|
*/
|
|
498
|
-
cloudPackageId?: string;
|
|
621
|
+
cloudPackageId?: string; /** Whether the appliance is hidden. Mirrors {@link EnyoAppliance.hidden}. */
|
|
622
|
+
hidden?: boolean;
|
|
499
623
|
}
|
|
@@ -13,6 +13,7 @@ class InMemoryApplianceManager extends appliance_manager_js_1.ApplianceManager {
|
|
|
13
13
|
memoryStore = new Map();
|
|
14
14
|
nextId = 1;
|
|
15
15
|
networkDevicesStore = new Map();
|
|
16
|
+
groupStore = new Map();
|
|
16
17
|
/**
|
|
17
18
|
* Creates a new DemoApplianceManager instance.
|
|
18
19
|
* @param energyApp The EnergyApp instance (not used for persistence in demo)
|
|
@@ -75,6 +76,7 @@ class InMemoryApplianceManager extends appliance_manager_js_1.ApplianceManager {
|
|
|
75
76
|
...(appliance.cloudPackageId !== undefined && { cloudPackageId: appliance.cloudPackageId }),
|
|
76
77
|
...(appliance.availableFeatures !== undefined && { availableFeatures: appliance.availableFeatures }),
|
|
77
78
|
...(appliance.compatibilityModes !== undefined && { compatibilityModes: appliance.compatibilityModes }),
|
|
79
|
+
...(appliance.hidden !== undefined && { hidden: appliance.hidden }),
|
|
78
80
|
};
|
|
79
81
|
// On update, merge onto the stored appliance so type-specific metadata and
|
|
80
82
|
// top-level fields that were not supplied in this call are preserved.
|
|
@@ -212,6 +214,19 @@ class InMemoryApplianceManager extends appliance_manager_js_1.ApplianceManager {
|
|
|
212
214
|
if (this.memoryStore.has(applianceId)) {
|
|
213
215
|
this.memoryStore.delete(applianceId);
|
|
214
216
|
this.networkDevicesStore.delete(applianceId);
|
|
217
|
+
// Mirror the host: a removed appliance drops out of its group,
|
|
218
|
+
// and a group left without members is removed.
|
|
219
|
+
for (const group of [...this.groupStore.values()]) {
|
|
220
|
+
if (!group.applianceIds.includes(applianceId))
|
|
221
|
+
continue;
|
|
222
|
+
const remaining = group.applianceIds.filter(id => id !== applianceId);
|
|
223
|
+
if (remaining.length === 0) {
|
|
224
|
+
this.groupStore.delete(group.id);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
this.groupStore.set(group.id, { ...group, applianceIds: remaining });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
215
230
|
// Clean up cache (from parent class)
|
|
216
231
|
this.clearCache();
|
|
217
232
|
console.log(`[DEMO] Removed appliance ${applianceId}`);
|
|
@@ -287,12 +302,38 @@ class InMemoryApplianceManager extends appliance_manager_js_1.ApplianceManager {
|
|
|
287
302
|
this.updateCache(appliance);
|
|
288
303
|
}
|
|
289
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Loads the appliance groups from memory.
|
|
307
|
+
* @returns Copies of all stored groups
|
|
308
|
+
*/
|
|
309
|
+
async loadGroups() {
|
|
310
|
+
return Array.from(this.groupStore.values(), g => structuredClone(g));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Saves an appliance group in memory.
|
|
314
|
+
* @param group The group to save
|
|
315
|
+
* @param groupId ID of the group to update, or `undefined` to create one
|
|
316
|
+
* @returns The ID of the saved group
|
|
317
|
+
*/
|
|
318
|
+
async persistGroup(group, groupId) {
|
|
319
|
+
const id = groupId ?? (0, node_crypto_1.randomUUID)();
|
|
320
|
+
this.groupStore.set(id, { ...structuredClone(group), id });
|
|
321
|
+
return id;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Deletes an appliance group from memory.
|
|
325
|
+
* @param groupId ID of the group to delete
|
|
326
|
+
*/
|
|
327
|
+
async deleteGroup(groupId) {
|
|
328
|
+
this.groupStore.delete(groupId);
|
|
329
|
+
}
|
|
290
330
|
/**
|
|
291
331
|
* Clears all data from memory (for testing purposes).
|
|
292
332
|
*/
|
|
293
333
|
clearAllData() {
|
|
294
334
|
this.memoryStore.clear();
|
|
295
335
|
this.networkDevicesStore.clear();
|
|
336
|
+
this.groupStore.clear();
|
|
296
337
|
this.clearCache();
|
|
297
338
|
this.nextId = 1;
|
|
298
339
|
console.log('[DEMO] All data cleared from memory');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EnergyApp } from "../../index.cjs";
|
|
2
|
-
import { EnyoAppliance, EnyoApplianceConnectionType, EnyoApplianceMetadata, EnyoApplianceStateEnum, EnyoApplianceTypeEnum } from "../../types/enyo-appliance.cjs";
|
|
2
|
+
import { EnyoAppliance, EnyoApplianceConnectionType, EnyoApplianceGroup, EnyoApplianceMetadata, EnyoApplianceStateEnum, EnyoApplianceTypeEnum } from "../../types/enyo-appliance.cjs";
|
|
3
3
|
import { ApplianceConfig, ApplianceManager, ApplianceManagerConfig, FindResult } from "./appliance-manager.cjs";
|
|
4
4
|
import { IdentifierStrategy } from "./identifier-strategies.cjs";
|
|
5
5
|
/**
|
|
@@ -11,6 +11,7 @@ export declare class InMemoryApplianceManager extends ApplianceManager {
|
|
|
11
11
|
private memoryStore;
|
|
12
12
|
private nextId;
|
|
13
13
|
private networkDevicesStore;
|
|
14
|
+
private groupStore;
|
|
14
15
|
/**
|
|
15
16
|
* Creates a new DemoApplianceManager instance.
|
|
16
17
|
* @param energyApp The EnergyApp instance (not used for persistence in demo)
|
|
@@ -99,6 +100,23 @@ export declare class InMemoryApplianceManager extends ApplianceManager {
|
|
|
99
100
|
* Refreshes the cache with all appliances from memory.
|
|
100
101
|
*/
|
|
101
102
|
refreshCache(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Loads the appliance groups from memory.
|
|
105
|
+
* @returns Copies of all stored groups
|
|
106
|
+
*/
|
|
107
|
+
protected loadGroups(): Promise<EnyoApplianceGroup[]>;
|
|
108
|
+
/**
|
|
109
|
+
* Saves an appliance group in memory.
|
|
110
|
+
* @param group The group to save
|
|
111
|
+
* @param groupId ID of the group to update, or `undefined` to create one
|
|
112
|
+
* @returns The ID of the saved group
|
|
113
|
+
*/
|
|
114
|
+
protected persistGroup(group: Omit<EnyoApplianceGroup, 'id'>, groupId: string | undefined): Promise<string>;
|
|
115
|
+
/**
|
|
116
|
+
* Deletes an appliance group from memory.
|
|
117
|
+
* @param groupId ID of the group to delete
|
|
118
|
+
*/
|
|
119
|
+
protected deleteGroup(groupId: string): Promise<void>;
|
|
102
120
|
/**
|
|
103
121
|
* Clears all data from memory (for testing purposes).
|
|
104
122
|
*/
|