@enyo-energy/energy-app-sdk 1.39.0 → 1.41.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 CHANGED
@@ -860,6 +860,8 @@ energyApp.useDataBus().sendMessage([{
860
860
  data: {
861
861
  vehicleId,
862
862
  socPercent: 62,
863
+ // optional: remaining range in km, when the source reports it (e.g. Smartcar)
864
+ rangeKm: 280,
863
865
  // omit only when the reading came straight off the wire
864
866
  measuredAtIso: new Date().toISOString(),
865
867
  },
@@ -870,6 +872,19 @@ That message is the only write path for a state of charge; it needs the
870
872
  `SendDataBusValues` permission. `getSoc()` resolves to `undefined` when nothing
871
873
  is known — an ordinary answer for a car with no SoC source, not an error.
872
874
 
875
+ The vehicle's battery size is written the same way: publish
876
+ `VehicleBatterySizeUpdateV1` with the usable capacity in watt-hours, and the
877
+ host stores it on the vehicle (replacing the previous value, including one the
878
+ user entered). It is read back as `batterySizeKwh`.
879
+
880
+ ```typescript
881
+ energyApp.useDataBus().sendMessage([{
882
+ type: 'message',
883
+ message: 'VehicleBatterySizeUpdateV1',
884
+ data: {vehicleId, batterySizeWh: 75_000},
885
+ }]);
886
+ ```
887
+
873
888
  ##### Car integrations: pairing a vehicle
874
889
 
875
890
  A package in the `vehicle` category talks to a manufacturer's cloud and links
@@ -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
  */
@@ -1,4 +1,4 @@
1
- import { EnyoAppliance, EnyoApplianceTypeEnum } from "../types/enyo-appliance.cjs";
1
+ import { EnyoAppliance, EnyoApplianceGroup, EnyoApplianceTypeEnum } from "../types/enyo-appliance.cjs";
2
2
  /**
3
3
  * Ownership scope for an appliance-created subscription.
4
4
  * - `'own'`: appliances created by your own package.
@@ -32,14 +32,41 @@ export interface EnyoApplianceCreatedFilter {
32
32
  export interface EnergyAppAppliance {
33
33
  /** Save or update an appliance in the system */
34
34
  save: (appliance: Omit<EnyoAppliance, 'id'>, applianceId: string | undefined) => Promise<string>;
35
- /** Get a list of all registered appliances of your package */
35
+ /**
36
+ * Get a list of all registered appliances of your package, including the
37
+ * ones marked {@link EnyoAppliance.hidden}.
38
+ */
36
39
  list: () => Promise<EnyoAppliance[]>;
37
- /** Get a list of all appliances. Needs a specific permission */
40
+ /**
41
+ * Get a list of all appliances. Needs a specific permission. Appliances of
42
+ * other packages marked {@link EnyoAppliance.hidden} are left out; your own
43
+ * package's hidden appliances are included.
44
+ */
38
45
  listAll: () => Promise<EnyoAppliance[]>;
39
46
  /** Get a specific appliance by its ID */
40
47
  getById: (id: string) => Promise<EnyoAppliance | null>;
41
48
  /** Remove an appliance by its ID */
42
49
  removeById: (id: string) => Promise<void>;
50
+ /**
51
+ * Save or update a group of your package's appliances, shown in the UI as
52
+ * one collapsible entry — see {@link EnyoApplianceGroup}.
53
+ *
54
+ * Rejected when a member is not an appliance of your package or already
55
+ * belongs to another group; move it by first saving that other group
56
+ * without it.
57
+ * @param group - The group to save
58
+ * @param groupId - ID of the group to update, or `undefined` to create one
59
+ * @returns The ID of the saved group
60
+ */
61
+ saveGroup: (group: Omit<EnyoApplianceGroup, 'id'>, groupId: string | undefined) => Promise<string>;
62
+ /** Get all appliance groups of your package */
63
+ listGroups: () => Promise<EnyoApplianceGroup[]>;
64
+ /**
65
+ * Remove an appliance group. Only the group goes away — its members stay
66
+ * and are shown individually again.
67
+ * @param groupId - ID of the group to remove
68
+ */
69
+ removeGroup: (groupId: string) => Promise<void>;
43
70
  /**
44
71
  * Listen for appliance updates (when an appliance is saved or modified).
45
72
  * @param listener - Callback invoked with the updated appliance
@@ -64,7 +91,8 @@ export interface EnergyAppAppliance {
64
91
  * The optional {@link EnyoApplianceCreatedFilter filter} controls scope and
65
92
  * category: by default only your own package's creations are delivered;
66
93
  * `scope: 'all'` and `scope: 'external'` observe appliances created by other
67
- * packages and require the `AllAppliances` permission.
94
+ * packages and require the `AllAppliances` permission. Appliances of other
95
+ * packages marked {@link EnyoAppliance.hidden} are never delivered.
68
96
  *
69
97
  * @param listener - Callback invoked with each newly-created appliance
70
98
  * @param filter - Optional scope/category filter; defaults to `{ scope: 'own' }`
@@ -29,6 +29,13 @@ import { EnyoLinkedVehicle, EnyoVehicle, EnyoVehiclePackageUnpairReasonEnum, Eny
29
29
  * That message is the only write path for a state of charge; publishing it
30
30
  * requires the `SendDataBusValues` permission.
31
31
  *
32
+ * ## Writing a battery size
33
+ *
34
+ * The same holds for the vehicle's battery size: an app that knows it publishes
35
+ * `EnyoDataBusVehicleBatterySizeUpdateV1` with `batterySizeWh`, and the host
36
+ * stores it on the vehicle, replacing the previous value. It is read back as
37
+ * {@link EnyoVehicle.batterySizeKwh}.
38
+ *
32
39
  * ## Pairing a car integration
33
40
  *
34
41
  * A package in the `vehicle` category — a manufacturer cloud integration —
@@ -392,4 +392,69 @@ export interface EnyoAppliance {
392
392
  * downstream consumers can correlate the appliance with its owning package.
393
393
  */
394
394
  cloudPackageId?: string;
395
+ /**
396
+ * Marks the appliance as hidden: it is kept in the system but treated as
397
+ * if it did not exist everywhere else.
398
+ *
399
+ * A hidden appliance:
400
+ * - is not shown in any UI (appliance lists, live views, reports, onboarding
401
+ * summaries, notifications);
402
+ * - is not used for energy management — it is neither controlled nor
403
+ * counted in energy flows, forecasts, savings or health evaluation;
404
+ * - has its data bus values, timeseries and events ignored by the host
405
+ * and by other packages;
406
+ * - is excluded from `useAppliances().listAll()` and from appliance
407
+ * events delivered to other packages.
408
+ *
409
+ * The owning package still sees it through
410
+ * `useAppliances().list()` and `getById()`,
411
+ * so it can keep recognising the device on rediscovery instead of creating
412
+ * it again, and unhide it later by saving it with `hidden: false`.
413
+ *
414
+ * Use it for appliances the package has to keep track of but that must not
415
+ * surface — e.g. a duplicate entity a device exposes alongside the real one,
416
+ * or a sub-device the user chose to ignore. Differs from
417
+ * {@link EnyoApplianceStatusEnum.Deactivated}, which keeps the appliance
418
+ * visible and only takes it out of energy management. Omitted means visible.
419
+ */
420
+ hidden?: boolean;
421
+ }
422
+ /**
423
+ * A named group of appliances that the app UI shows as one collapsible entry.
424
+ *
425
+ * Grouping is purely visual. The UI shows the group under its {@link name}
426
+ * with the summed values its members share (power, energy, …) and a
427
+ * collapse/expand toggle that reveals the individual members. The members stay
428
+ * ordinary appliances: their data is counted exactly as without the group,
429
+ * commands still target each member, and the group itself is never an
430
+ * appliance — so nothing is counted twice.
431
+ *
432
+ * Groups are defined by the energy app that owns the member appliances, and
433
+ * every user of the site sees the same groups. Use it when an app brings many
434
+ * appliances of one installation — e.g. dozens of micro-inverters — that would
435
+ * otherwise flood the appliance list.
436
+ *
437
+ * Rules enforced by the host:
438
+ * - every member must be an appliance of the package saving the group;
439
+ * - an appliance belongs to at most one group;
440
+ * - an appliance that is removed or marked {@link EnyoAppliance.hidden} drops
441
+ * out of its group, and a group left without members is removed.
442
+ *
443
+ * Members may be of different {@link EnyoApplianceTypeEnum types}; the UI then
444
+ * sums only the values all members share and shows the rest per member.
445
+ */
446
+ export interface EnyoApplianceGroup {
447
+ /** Unique identifier of the group, assigned by the host on first save */
448
+ id: string;
449
+ /**
450
+ * Stable, app-defined key of the group, unique within the package — e.g.
451
+ * the serial number of the gateway the members sit behind. Lets the app
452
+ * find its group again after a restart instead of creating a second one.
453
+ * Recommended; the `ApplianceManager` group helpers require it.
454
+ */
455
+ key?: string;
456
+ /** Name of the group shown in the UI, in the supported languages */
457
+ name: EnyoApplianceName[];
458
+ /** IDs of the member appliances, in the order the UI lists them when expanded */
459
+ applianceIds: string[];
395
460
  }
@@ -847,6 +847,8 @@ var EnyoDataBusMessageEnum;
847
847
  EnyoDataBusMessageEnum["StopAirConditioningV1"] = "StopAirConditioningV1";
848
848
  EnyoDataBusMessageEnum["ChangeAirConditioningOptimizationModeV1"] = "ChangeAirConditioningOptimizationModeV1";
849
849
  EnyoDataBusMessageEnum["VehicleSocUpdateV1"] = "VehicleSocUpdateV1";
850
+ /** Provide or update the usable traction battery capacity of a vehicle, in Wh. */
851
+ EnyoDataBusMessageEnum["VehicleBatterySizeUpdateV1"] = "VehicleBatterySizeUpdateV1";
850
852
  /** Request the current or estimated state of charge of a vehicle, by vehicle id. */
851
853
  EnyoDataBusMessageEnum["RequestVehicleSocEstimateV1"] = "RequestVehicleSocEstimateV1";
852
854
  /** Answer to {@link RequestVehicleSocEstimateV1} — the estimate with its age and source, or why none could be given. */