@nebulahome/package-sdk 1.0.1 → 1.2.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/dist/testing.js CHANGED
@@ -38,6 +38,52 @@ async function check(schema, value) {
38
38
  }
39
39
  //#endregion
40
40
  //#region src/testing/index.ts
41
+ /** The most entities and points one read of the past takes, as the platform has it. */
42
+ const HISTORY_MAX_ENTITIES = 64;
43
+ const HISTORY_MAX_POINTS = 1e4;
44
+ /** An hour and a day, in milliseconds. */
45
+ const HOUR_MS = 36e5;
46
+ const DAY_MS = 24 * HOUR_MS;
47
+ /**
48
+ * The resolution the platform reads a window at when a read leaves it to it.
49
+ *
50
+ * @param spanMs - How long the window is.
51
+ * @returns Raw up to an hour, minutes up to 30 hours, hours up to 80 days, days beyond.
52
+ */
53
+ function resolutionFor(spanMs) {
54
+ if (spanMs <= HOUR_MS) return "raw";
55
+ if (spanMs <= 30 * HOUR_MS) return "1m";
56
+ return spanMs <= 80 * DAY_MS ? "1h" : "1d";
57
+ }
58
+ /**
59
+ * Reads a window of points as the platform does: checked, oldest first, the newest kept.
60
+ *
61
+ * @param read - Which entities, which window, how many points each.
62
+ * @param pointsOf - An entity's points, in any order.
63
+ * @returns Each entity's points in the window, and which lost their oldest.
64
+ * @throws {Error} When the read names no entity or too many, or its window is backwards.
65
+ */
66
+ function readWindow(read, pointsOf) {
67
+ const from = new Date(read.from).getTime();
68
+ const to = new Date(read.to).getTime();
69
+ const limit = read.limit ?? HISTORY_MAX_POINTS;
70
+ if (read.entityIds.length === 0 || read.entityIds.length > HISTORY_MAX_ENTITIES) throw new Error(`A read of the past names 1 to ${String(HISTORY_MAX_ENTITIES)} entities`);
71
+ if (!(from < to)) throw new Error("The window starts at or after it ends");
72
+ const series = {};
73
+ const truncated = [];
74
+ for (const entityId of read.entityIds) {
75
+ const inWindow = pointsOf(entityId).filter((point) => {
76
+ const time = new Date(point.time).getTime();
77
+ return time >= from && time < to;
78
+ }).toSorted((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
79
+ series[entityId] = inWindow.slice(Math.max(0, inWindow.length - limit));
80
+ if (inWindow.length > limit) truncated.push(entityId);
81
+ }
82
+ return {
83
+ series,
84
+ truncated
85
+ };
86
+ }
41
87
  /** Service names the platform keeps for itself. */
42
88
  const RESERVED_SERVICES = /* @__PURE__ */ new Set([
43
89
  "command",
@@ -99,6 +145,7 @@ function createTestContext(options = {}) {
99
145
  const reports = [];
100
146
  const logs = [];
101
147
  const entities = new Map((options.entities ?? []).map((each) => [each.entityId, each]));
148
+ const histories = new Map(Object.entries(options.history ?? {}));
102
149
  const methods = /* @__PURE__ */ new Map();
103
150
  const services = /* @__PURE__ */ new Map();
104
151
  const answers = /* @__PURE__ */ new Map();
@@ -462,6 +509,29 @@ function createTestContext(options = {}) {
462
509
  };
463
510
  }
464
511
  },
512
+ history: {
513
+ /**
514
+ * The changes of some entities in a window.
515
+ *
516
+ * @param read - Which, when, how many.
517
+ * @returns Them, oldest first.
518
+ */
519
+ states: async (read) => Promise.resolve(readWindow(read, (entityId) => histories.get(entityId)?.states ?? [])),
520
+ /**
521
+ * The readings of some entities in a window, as given: a test gives the buckets it wants.
522
+ *
523
+ * @param read - Which, when, how many, at which resolution.
524
+ * @returns Them, oldest first, and the resolution.
525
+ */
526
+ telemetry: async (read) => {
527
+ const span = new Date(read.to).getTime() - new Date(read.from).getTime();
528
+ const resolution = read.resolution === void 0 || read.resolution === "auto" ? resolutionFor(span) : read.resolution;
529
+ return Promise.resolve({
530
+ ...readWindow(read, (entityId) => histories.get(entityId)?.telemetry ?? []),
531
+ resolution
532
+ });
533
+ }
534
+ },
465
535
  rpc: { method: rpcMethod },
466
536
  services: {
467
537
  provide,
@@ -578,6 +648,15 @@ function createTestContext(options = {}) {
578
648
  entities.set(snapshot.entityId, snapshot);
579
649
  },
580
650
  /**
651
+ * Gives an entity a past.
652
+ *
653
+ * @param entityId - The entity.
654
+ * @param history - Its changes and readings.
655
+ */
656
+ setHistory: (entityId, history) => {
657
+ histories.set(entityId, history);
658
+ },
659
+ /**
581
660
  * Changes an entity of the house.
582
661
  *
583
662
  * @param change - The change.
package/dist/ui.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { G as CapabilityState, H as CapabilityCommands, S as FormValues, ct as EntityDomain, d as EntityTypeDefinition, f as ValuesOf, i as SettingsDefinition, lt as EntityId, q as CommandableCapability, r as JsonValue, s as ArgsOf, t as JsonObject, u as CommandsOf, z as ValueOfField } from "./chunks/json.js";
1
+ import { G as CapabilityState, H as CapabilityCommands, S as FormValues, Y as DeclaredCapabilities, ct as EntityDomain, d as EntityTypeDefinition, f as ValuesOf, i as SettingsDefinition, lt as EntityId, q as CommandableCapability, r as JsonValue, s as ArgsOf, t as JsonObject, u as CommandsOf, z as ValueOfField } from "./chunks/json.js";
2
2
  import { a as PageProps, n as EntityViewProps, t as CardProps } from "./chunks/props.js";
3
+ import { a as TelemetryResolution, i as TelemetryPoint, n as HistorySeries, r as StateHistoryPoint, t as ChangeCause } from "./chunks/history.js";
3
4
  import { AnchorHTMLAttributes, ComponentType, ReactElement, ReactNode } from "react";
4
5
  //#region src/ui/types.d.ts
5
6
  /** The package drawn. */
@@ -15,7 +16,13 @@ type EntityView = {
15
16
  readonly name: string;
16
17
  readonly icon: string | null;
17
18
  readonly deviceId: string | null;
19
+ /** Its area's key - the entity's own, else its device's - as {@link AreaView} `key` says it. */
18
20
  readonly areaId: string | null;
21
+ /**
22
+ * What it declares it can do, with the device's limits: a cover's tilt, a thermostat's range
23
+ * and modes, a select's options. Since 1.2.
24
+ */
25
+ readonly capabilities: DeclaredCapabilities;
19
26
  readonly state: CapabilityState;
20
27
  readonly attributes: JsonObject;
21
28
  readonly isAvailable: boolean;
@@ -24,6 +31,79 @@ type EntityView = {
24
31
  readonly deviceClass: string | null;
25
32
  readonly displayPrecision: number | null;
26
33
  };
34
+ /** A window of the past: between two moments (ISO 8601), or the last so many milliseconds. */
35
+ type HistoryWindow = {
36
+ readonly from: string;
37
+ readonly to: string;
38
+ } | {
39
+ readonly lastMs: number;
40
+ };
41
+ /** Which entities' past `useStateHistory` reads, over which window. Since 1.2. */
42
+ type HistoryQuery = {
43
+ /** At most 64. */
44
+ readonly entityIds: readonly EntityId[];
45
+ readonly window: HistoryWindow;
46
+ /** False holds the read back. */
47
+ readonly isEnabled?: boolean;
48
+ /** Reads it again every so often; a window of the last so many milliseconds moves with it. */
49
+ readonly refetchIntervalMs?: number;
50
+ };
51
+ /** Which entities' readings `useTelemetry` reads: `auto` picks the resolution by the window. */
52
+ type TelemetryQuery = HistoryQuery & {
53
+ readonly resolution?: TelemetryResolution | 'auto';
54
+ };
55
+ /** A read of the past, as it stands. */
56
+ type History<TPoint> = {
57
+ /** Each entity's points, oldest first; empty while it loads. */
58
+ readonly series: HistorySeries<TPoint>;
59
+ /** The window read, in milliseconds since the epoch; undefined before the first answer. */
60
+ readonly window: {
61
+ readonly from: number;
62
+ readonly to: number;
63
+ } | undefined;
64
+ /** The entities whose window held more than one read returns: their oldest points are gone. */
65
+ readonly truncated: readonly EntityId[];
66
+ readonly isLoading: boolean;
67
+ readonly error: Error | null;
68
+ /** Reads it again. */
69
+ readonly refetch: () => void;
70
+ };
71
+ /** A read of readings, and the resolution it was read at. */
72
+ type Telemetry = History<TelemetryPoint> & {
73
+ readonly resolution: TelemetryResolution;
74
+ };
75
+ /** What kind of place an area is. */
76
+ type AreaKind = 'house' | 'floor' | 'room';
77
+ /** One area of the house: the house itself, a floor, a room. Since 1.2. */
78
+ type AreaView = {
79
+ /** What `EntityView.areaId` names it by. */
80
+ readonly key: string;
81
+ readonly name: string;
82
+ readonly kind: AreaKind;
83
+ /** The floor a room is on, the house a floor is in; null for the house. */
84
+ readonly parentKey: string | null;
85
+ readonly icon: string | null;
86
+ readonly sortOrder: number;
87
+ /** A floor's storey: 0 the ground floor, -1 the one below; null for the rest. */
88
+ readonly level: number | null;
89
+ /** The house's position, when the owner has set it; null for the rest. */
90
+ readonly latitude: number | null;
91
+ readonly longitude: number | null;
92
+ };
93
+ /** How a picture is fetched. */
94
+ type PictureOptions = {
95
+ /** A camera's still is fetched again this often; 15 seconds when not said, at least 2. */
96
+ readonly refreshMs?: number;
97
+ /** False holds it back. */
98
+ readonly isEnabled?: boolean;
99
+ };
100
+ /** A camera's latest still, or an image entity's picture, as an address an `<img>` shows. */
101
+ type EntityPicture = {
102
+ /** Null before there is one, or when the entity has none. */
103
+ readonly url: string | null;
104
+ readonly isLoading: boolean;
105
+ readonly error: Error | null;
106
+ };
27
107
  /** Which entities `useEntities` reads. */
28
108
  type EntitiesFilter = {
29
109
  /** Whose: the package's own (the default), anyone's, or another package's by id. */
@@ -128,6 +208,27 @@ type Locale = {
128
208
  readonly locale: string;
129
209
  readonly timeZone: string;
130
210
  };
211
+ /** A plural's English: the form for one, and the form for any other number. */
212
+ type PluralEnglish = {
213
+ readonly one: string;
214
+ readonly other: string;
215
+ };
216
+ /** What fills a string's `{{placeholders}}`; `count` also picks a plural's form. */
217
+ type TranslationValues = Readonly<Record<string, string | number>>;
218
+ /**
219
+ * Says one of the package's strings in the viewer's language.
220
+ *
221
+ * The key is looked up in the package's catalogue for that language (`locales/it.json`); when it
222
+ * is not there, the English given here is said instead. A plural takes its English as
223
+ * `{ one, other }` and the number as `count`: each language's catalogue holds the forms it needs.
224
+ */
225
+ type Translate = (key: string, english: string | PluralEnglish, values?: TranslationValues) => string;
226
+ /** The package's strings, in the viewer's language. */
227
+ type Translation = {
228
+ readonly t: Translate;
229
+ /** The language they are said in, `it`: the viewer's, whether or not the package has it. */
230
+ readonly locale: string;
231
+ };
131
232
  /** Which theme the interface draws in. */
132
233
  type Theme = {
133
234
  readonly scheme: 'light' | 'dark';
@@ -185,6 +286,16 @@ type UiHost = {
185
286
  readonly usePackageSettings: <TSettings extends SettingsDefinition | undefined = undefined>(settings?: TSettings) => PackageSettings<TSettings>;
186
287
  readonly useViewer: () => Viewer;
187
288
  readonly useLocale: () => Locale;
289
+ /** Since 1.1. */
290
+ readonly useTranslation: () => Translation;
291
+ /** Since 1.2. */
292
+ readonly useStateHistory: (query: HistoryQuery) => History<StateHistoryPoint>;
293
+ /** Since 1.2. */
294
+ readonly useTelemetry: (query: TelemetryQuery) => Telemetry;
295
+ /** Since 1.2. */
296
+ readonly useAreas: () => readonly AreaView[];
297
+ /** Since 1.2. */
298
+ readonly useEntityPicture: (entityId: EntityId, options?: PictureOptions) => EntityPicture;
188
299
  readonly useTheme: () => Theme;
189
300
  readonly usePortalContainer: () => HTMLElement | null;
190
301
  readonly usePackageRoute: () => PackageRoute;
@@ -272,6 +383,50 @@ export declare function useViewer(): Viewer;
272
383
  * @returns Them, for `Intl`.
273
384
  */
274
385
  export declare function useLocale(): Locale;
386
+ /**
387
+ * The package's strings in the viewer's language, from the catalogues in its `locales` folder
388
+ * (PACKAGES_DOCS.md 6.5).
389
+ *
390
+ * `t('clients.title', 'Clients')` says the key from the viewer's catalogue, and its English when
391
+ * the catalogue lacks it; outside an interface that lends this - a component's unit test - every
392
+ * string is its English.
393
+ *
394
+ * @returns `t`, and the language it speaks.
395
+ */
396
+ export declare function useTranslation(): Translation;
397
+ /**
398
+ * The recorded changes of some entities over a window, live as the window moves: what a chart of
399
+ * an on/off thing, an activity list or a replay of a day is drawn from. Read with the viewer's
400
+ * permissions: an entity they may not read has no points.
401
+ *
402
+ * @param query - Which entities (at most 64), which window, how often to read it again.
403
+ * @returns Each entity's changes, oldest first, and whether the read is still on its way.
404
+ */
405
+ export declare function useStateHistory(query: HistoryQuery): History<StateHistoryPoint>;
406
+ /**
407
+ * The numeric readings of some entities over a window: what a sensor's chart is drawn from. A
408
+ * long window is read in buckets of a minute, an hour or a day, each with its lowest and highest.
409
+ *
410
+ * @param query - Which entities (at most 64), which window, which resolution (`auto` by default).
411
+ * @returns Each entity's readings, oldest first, and the resolution they were read at.
412
+ */
413
+ export declare function useTelemetry(query: TelemetryQuery): Telemetry;
414
+ /**
415
+ * The areas of the house: the house itself (with its position, when set), its floors and its
416
+ * rooms, as the owner arranged them.
417
+ *
418
+ * @returns Them, in the owner's order.
419
+ */
420
+ export declare function useAreas(): readonly AreaView[];
421
+ /**
422
+ * A camera's latest still, fetched again every so often, or an image entity's picture, fetched
423
+ * again when it changes: an address an `<img>` shows, let go of when a newer one replaces it.
424
+ *
425
+ * @param entityId - A `camera.*` or an `image.*` entity.
426
+ * @param options - How often a still is fetched again, and whether at all.
427
+ * @returns The address, or null before there is one.
428
+ */
429
+ export declare function useEntityPicture(entityId: EntityId, options?: PictureOptions): EntityPicture;
275
430
  /**
276
431
  * The theme the interface draws in, for what draws its own colours: a canvas, a chart.
277
432
  *
@@ -317,4 +472,4 @@ export declare function PackageLink(props: PackageLinkProps): ReactElement;
317
472
  */
318
473
  export declare function PlatformLink(props: PlatformLinkProps): ReactElement;
319
474
  //#endregion
320
- export type { CardProps, CustomEntityState, EntitiesFilter, EntityView, EntityViewProps, Locale, Mutation, MutationOptions, PackageCall, PackageInfo, PackageLinkProps, PackageNavigate, PackageRoute, PackageSettings, PageProps, PlatformLinkProps, PlatformNavigate, PlatformTarget, RpcOptions, RpcState, SendCommand, SettingsPatch, Theme, UiHost, Viewer, VisibleSettings };
475
+ export type { AreaKind, AreaView, CardProps, ChangeCause, CustomEntityState, EntitiesFilter, EntityPicture, EntityView, EntityViewProps, History, HistoryQuery, HistorySeries, HistoryWindow, Locale, Mutation, MutationOptions, PackageCall, PackageInfo, PackageLinkProps, PackageNavigate, PackageRoute, PackageSettings, PageProps, PictureOptions, PlatformLinkProps, PlatformNavigate, PlatformTarget, PluralEnglish, RpcOptions, RpcState, SendCommand, SettingsPatch, StateHistoryPoint, Telemetry, TelemetryPoint, TelemetryQuery, TelemetryResolution, Theme, Translate, Translation, TranslationValues, UiHost, Viewer, VisibleSettings };
package/dist/ui.js CHANGED
@@ -1,5 +1,45 @@
1
- import { n as UI_MODULE, r as hostMember } from "./chunks/host.js";
1
+ import { i as lentMember, n as UI_MODULE, r as hostMember } from "./chunks/host.js";
2
2
  import { createElement } from "react";
3
+ //#region src/ui/english.ts
4
+ /** A `{{placeholder}}`, as i18next writes it. */
5
+ const PLACEHOLDER = /\{\{\s*(\w+)\s*\}\}/g;
6
+ /**
7
+ * Fills a string's placeholders.
8
+ *
9
+ * @param text - The string.
10
+ * @param values - What fills them; a placeholder with no value is left as written.
11
+ * @returns The string, filled.
12
+ */
13
+ function interpolate(text, values) {
14
+ return text.replaceAll(PLACEHOLDER, (whole, name) => {
15
+ const value = values?.[name];
16
+ return value === void 0 ? whole : String(value);
17
+ });
18
+ }
19
+ /**
20
+ * Says a string's English, its plural form chosen by `count`.
21
+ *
22
+ * @param english - The English, or a plural's two forms.
23
+ * @param values - What fills it.
24
+ * @returns The English, filled.
25
+ */
26
+ function englishOf(english, values) {
27
+ return interpolate(typeof english === "string" ? english : values?.["count"] === 1 ? english.one : english.other, values);
28
+ }
29
+ /** The package's strings in English. */
30
+ const ENGLISH = {
31
+ /**
32
+ * Says a string's English.
33
+ *
34
+ * @param key - Its key, unused: there is no catalogue here.
35
+ * @param english - Its English.
36
+ * @param values - What fills it.
37
+ * @returns The English, filled.
38
+ */
39
+ t: (key, english, values) => englishOf(english, values),
40
+ locale: "en"
41
+ };
42
+ //#endregion
3
43
  //#region src/ui/index.ts
4
44
  /**
5
45
  * `@nebulahome/package-sdk/ui`: what a package's page, cards and entity views are written with
@@ -119,6 +159,61 @@ function useLocale() {
119
159
  return ui("useLocale", "1.0")();
120
160
  }
121
161
  /**
162
+ * The package's strings in the viewer's language, from the catalogues in its `locales` folder
163
+ * (PACKAGES_DOCS.md 6.5).
164
+ *
165
+ * `t('clients.title', 'Clients')` says the key from the viewer's catalogue, and its English when
166
+ * the catalogue lacks it; outside an interface that lends this - a component's unit test - every
167
+ * string is its English.
168
+ *
169
+ * @returns `t`, and the language it speaks.
170
+ */
171
+ function useTranslation() {
172
+ const lent = lentMember(UI_MODULE, "useTranslation");
173
+ return lent === void 0 ? ENGLISH : lent();
174
+ }
175
+ /**
176
+ * The recorded changes of some entities over a window, live as the window moves: what a chart of
177
+ * an on/off thing, an activity list or a replay of a day is drawn from. Read with the viewer's
178
+ * permissions: an entity they may not read has no points.
179
+ *
180
+ * @param query - Which entities (at most 64), which window, how often to read it again.
181
+ * @returns Each entity's changes, oldest first, and whether the read is still on its way.
182
+ */
183
+ function useStateHistory(query) {
184
+ return ui("useStateHistory", "1.2")(query);
185
+ }
186
+ /**
187
+ * The numeric readings of some entities over a window: what a sensor's chart is drawn from. A
188
+ * long window is read in buckets of a minute, an hour or a day, each with its lowest and highest.
189
+ *
190
+ * @param query - Which entities (at most 64), which window, which resolution (`auto` by default).
191
+ * @returns Each entity's readings, oldest first, and the resolution they were read at.
192
+ */
193
+ function useTelemetry(query) {
194
+ return ui("useTelemetry", "1.2")(query);
195
+ }
196
+ /**
197
+ * The areas of the house: the house itself (with its position, when set), its floors and its
198
+ * rooms, as the owner arranged them.
199
+ *
200
+ * @returns Them, in the owner's order.
201
+ */
202
+ function useAreas() {
203
+ return ui("useAreas", "1.2")();
204
+ }
205
+ /**
206
+ * A camera's latest still, fetched again every so often, or an image entity's picture, fetched
207
+ * again when it changes: an address an `<img>` shows, let go of when a newer one replaces it.
208
+ *
209
+ * @param entityId - A `camera.*` or an `image.*` entity.
210
+ * @param options - How often a still is fetched again, and whether at all.
211
+ * @returns The address, or null before there is one.
212
+ */
213
+ function useEntityPicture(entityId, options) {
214
+ return ui("useEntityPicture", "1.2")(entityId, options);
215
+ }
216
+ /**
122
217
  * The theme the interface draws in, for what draws its own colours: a canvas, a chart.
123
218
  *
124
219
  * @returns Light or dark.
@@ -177,4 +272,4 @@ function PlatformLink(props) {
177
272
  return createElement(ui("PlatformLink", "1.0"), props);
178
273
  }
179
274
  //#endregion
180
- export { PackageLink, PlatformLink, useCommand, useCustomEntity, useEntities, useEntity, useLocale, usePackage, usePackageCall, usePackageMutation, usePackageNavigate, usePackageRoute, usePackageRpc, usePackageSettings, usePlatformNavigate, usePortalContainer, useTheme, useViewer };
275
+ export { PackageLink, PlatformLink, useAreas, useCommand, useCustomEntity, useEntities, useEntity, useEntityPicture, useLocale, usePackage, usePackageCall, usePackageMutation, usePackageNavigate, usePackageRoute, usePackageRpc, usePackageSettings, usePlatformNavigate, usePortalContainer, useStateHistory, useTelemetry, useTheme, useTranslation, useViewer };
package/dist/vite.d.ts CHANGED
@@ -52,8 +52,8 @@ type CheckedPackage = {
52
52
  readonly generated: GeneratedPackage;
53
53
  };
54
54
  /**
55
- * Checks a package without building it: its declaration against the platform's schemas, and the
56
- * files it names.
55
+ * Checks a package without building it: its declaration against the platform's schemas, the
56
+ * files it names, and its translation catalogues.
57
57
  *
58
58
  * @param options - Where the package is.
59
59
  * @returns The package, checked.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulahome/package-sdk",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Write a NebulaHome package: declare it, build it, and run it on the platform.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,6 +32,10 @@
32
32
  "types": "./dist/ui.d.ts",
33
33
  "default": "./dist/ui.js"
34
34
  },
35
+ "./sun": {
36
+ "types": "./dist/sun.d.ts",
37
+ "default": "./dist/sun.js"
38
+ },
35
39
  "./components": {
36
40
  "types": "./dist/components.d.ts",
37
41
  "default": "./dist/components.js"
@@ -97,9 +101,9 @@
97
101
  "yaml": "^2.9.0",
98
102
  "zod": "^4.5.4",
99
103
  "@nebulahome/eslint-config": "0.0.0",
104
+ "@nebulahome/ui": "0.0.0",
100
105
  "@nebulahome/tsconfig": "0.0.0",
101
- "@nebulahome/contracts": "0.0.0",
102
- "@nebulahome/ui": "0.0.0"
106
+ "@nebulahome/contracts": "0.0.0"
103
107
  },
104
108
  "scripts": {
105
109
  "build": "tsdown && node scripts/build-styles.js",