@hashsome/core 0.1.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/LICENSE +674 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/base-integration.d.ts +30 -0
- package/dist/base-integration.js +106 -0
- package/dist/base-integration.js.map +1 -0
- package/dist/client.d.ts +63 -0
- package/dist/client.js +199 -0
- package/dist/client.js.map +1 -0
- package/dist/codegen.d.ts +6 -0
- package/dist/codegen.js +19 -0
- package/dist/codegen.js.map +1 -0
- package/dist/conformance.d.ts +23 -0
- package/dist/conformance.js +196 -0
- package/dist/conformance.js.map +1 -0
- package/dist/emitter.d.ts +8 -0
- package/dist/emitter.js +18 -0
- package/dist/emitter.js.map +1 -0
- package/dist/entity.d.ts +24 -0
- package/dist/entity.js +21 -0
- package/dist/entity.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +61 -0
- package/dist/integration.js +1 -0
- package/dist/integration.js.map +1 -0
- package/dist/local-client.d.ts +22 -0
- package/dist/local-client.js +121 -0
- package/dist/local-client.js.map +1 -0
- package/dist/mock.d.ts +61 -0
- package/dist/mock.js +448 -0
- package/dist/mock.js.map +1 -0
- package/dist/model/action.d.ts +9 -0
- package/dist/model/action.js +1 -0
- package/dist/model/action.js.map +1 -0
- package/dist/model/base.d.ts +21 -0
- package/dist/model/base.js +1 -0
- package/dist/model/base.js.map +1 -0
- package/dist/model/browse.d.ts +28 -0
- package/dist/model/browse.js +1 -0
- package/dist/model/browse.js.map +1 -0
- package/dist/model/climate.d.ts +37 -0
- package/dist/model/climate.js +1 -0
- package/dist/model/climate.js.map +1 -0
- package/dist/model/generic.d.ts +7 -0
- package/dist/model/generic.js +1 -0
- package/dist/model/generic.js.map +1 -0
- package/dist/model/history.d.ts +23 -0
- package/dist/model/history.js +1 -0
- package/dist/model/history.js.map +1 -0
- package/dist/model/index.d.ts +51 -0
- package/dist/model/index.js +38 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/light.d.ts +46 -0
- package/dist/model/light.js +1 -0
- package/dist/model/light.js.map +1 -0
- package/dist/model/media-player.d.ts +68 -0
- package/dist/model/media-player.js +1 -0
- package/dist/model/media-player.js.map +1 -0
- package/dist/model/person.d.ts +10 -0
- package/dist/model/person.js +1 -0
- package/dist/model/person.js.map +1 -0
- package/dist/model/sensor.d.ts +12 -0
- package/dist/model/sensor.js +1 -0
- package/dist/model/sensor.js.map +1 -0
- package/dist/model/switch.d.ts +10 -0
- package/dist/model/switch.js +1 -0
- package/dist/model/switch.js.map +1 -0
- package/dist/model/weather.d.ts +16 -0
- package/dist/model/weather.js +19 -0
- package/dist/model/weather.js.map +1 -0
- package/dist/protocol.d.ts +61 -0
- package/dist/protocol.js +102 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +36 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { encodeMessage, parseServerMessage } from './protocol.js';
|
|
2
|
+
/**
|
|
3
|
+
* Browser-side (and React-free) client for the runtime proxy. Ref-counts
|
|
4
|
+
* subscriptions, resubscribes and reconnects with backoff. Listeners receive
|
|
5
|
+
* `undefined` until the server has answered, then an entity or `null` (unknown entity).
|
|
6
|
+
*/
|
|
7
|
+
export class RemoteClient {
|
|
8
|
+
#options;
|
|
9
|
+
#socket;
|
|
10
|
+
#link = 'closed';
|
|
11
|
+
#linkListeners = new Set();
|
|
12
|
+
#integrationStatus = new Map();
|
|
13
|
+
#statusSnapshot = {};
|
|
14
|
+
#statusListeners = new Set();
|
|
15
|
+
#entities = new Map();
|
|
16
|
+
#listeners = new Map();
|
|
17
|
+
#pending = new Map();
|
|
18
|
+
#nextId = 1;
|
|
19
|
+
#retry = 0;
|
|
20
|
+
#timer;
|
|
21
|
+
#stopped = true;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.#options = options;
|
|
24
|
+
}
|
|
25
|
+
get link() {
|
|
26
|
+
return this.#link;
|
|
27
|
+
}
|
|
28
|
+
connect() {
|
|
29
|
+
if (!this.#stopped) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.#stopped = false;
|
|
33
|
+
this.#open();
|
|
34
|
+
}
|
|
35
|
+
close() {
|
|
36
|
+
this.#stopped = true;
|
|
37
|
+
clearTimeout(this.#timer);
|
|
38
|
+
this.#socket?.close();
|
|
39
|
+
this.#socket = undefined;
|
|
40
|
+
this.#setLink('closed');
|
|
41
|
+
}
|
|
42
|
+
onLinkChange(listener) {
|
|
43
|
+
this.#linkListeners.add(listener);
|
|
44
|
+
return () => this.#linkListeners.delete(listener) && undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Status of a backend integration as reported by the runtime (`undefined` until known). */
|
|
47
|
+
getIntegrationStatus(integration) {
|
|
48
|
+
return this.#integrationStatus.get(integration);
|
|
49
|
+
}
|
|
50
|
+
getIntegrationStatuses() {
|
|
51
|
+
return this.#statusSnapshot;
|
|
52
|
+
}
|
|
53
|
+
onIntegrationStatusChange(listener) {
|
|
54
|
+
this.#statusListeners.add(listener);
|
|
55
|
+
return () => this.#statusListeners.delete(listener) && undefined;
|
|
56
|
+
}
|
|
57
|
+
getEntity(ref) {
|
|
58
|
+
return this.#entities.get(ref);
|
|
59
|
+
}
|
|
60
|
+
subscribe(ref, listener) {
|
|
61
|
+
let set = this.#listeners.get(ref);
|
|
62
|
+
if (!set) {
|
|
63
|
+
set = new Set();
|
|
64
|
+
this.#listeners.set(ref, set);
|
|
65
|
+
this.#send({ type: 'subscribe', ref });
|
|
66
|
+
}
|
|
67
|
+
set.add(listener);
|
|
68
|
+
listener(this.#entities.get(ref));
|
|
69
|
+
return () => {
|
|
70
|
+
set.delete(listener);
|
|
71
|
+
if (set.size === 0) {
|
|
72
|
+
this.#listeners.delete(ref);
|
|
73
|
+
this.#send({ type: 'unsubscribe', ref });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async command(ref, name, args) {
|
|
78
|
+
await this.#request((id) => ({
|
|
79
|
+
type: 'command',
|
|
80
|
+
id,
|
|
81
|
+
ref,
|
|
82
|
+
command: name,
|
|
83
|
+
...(args ? { args } : {}),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
async browse(ref, query) {
|
|
87
|
+
return (await this.#request((id) => ({
|
|
88
|
+
type: 'query',
|
|
89
|
+
id,
|
|
90
|
+
ref,
|
|
91
|
+
query: 'browse',
|
|
92
|
+
args: { ...query },
|
|
93
|
+
})));
|
|
94
|
+
}
|
|
95
|
+
async history(ref, query) {
|
|
96
|
+
return (await this.#request((id) => ({
|
|
97
|
+
type: 'query',
|
|
98
|
+
id,
|
|
99
|
+
ref,
|
|
100
|
+
query: 'history',
|
|
101
|
+
args: { ...query },
|
|
102
|
+
})));
|
|
103
|
+
}
|
|
104
|
+
callRaw(integration, request) {
|
|
105
|
+
return this.#request((id) => ({ type: 'raw', id, integration, request }));
|
|
106
|
+
}
|
|
107
|
+
#request(build) {
|
|
108
|
+
const id = this.#nextId++;
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
if (this.#socket?.readyState !== 1) {
|
|
111
|
+
reject(new Error('Not connected to runtime'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.#pending.set(id, { resolve, reject });
|
|
115
|
+
this.#send(build(id));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
#setLink(status) {
|
|
119
|
+
if (status === this.#link) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.#link = status;
|
|
123
|
+
for (const listener of Array.from(this.#linkListeners)) {
|
|
124
|
+
listener(status);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
#send(message) {
|
|
128
|
+
if (this.#socket?.readyState === 1) {
|
|
129
|
+
this.#socket.send(encodeMessage(message));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
#open() {
|
|
133
|
+
this.#setLink('connecting');
|
|
134
|
+
const socket = (this.#options.createSocket ?? ((url) => new WebSocket(url)))(this.#options.url);
|
|
135
|
+
this.#socket = socket;
|
|
136
|
+
socket.addEventListener('open', () => {
|
|
137
|
+
this.#retry = 0;
|
|
138
|
+
this.#setLink('open');
|
|
139
|
+
for (const ref of this.#listeners.keys()) {
|
|
140
|
+
this.#send({ type: 'subscribe', ref });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
socket.addEventListener('message', (event) => {
|
|
144
|
+
const message = parseServerMessage(String(event.data));
|
|
145
|
+
if (message) {
|
|
146
|
+
this.#handle(message);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
socket.addEventListener('close', () => {
|
|
150
|
+
if (this.#socket !== socket) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.#socket = undefined;
|
|
154
|
+
for (const { reject } of this.#pending.values()) {
|
|
155
|
+
reject(new Error('Connection lost'));
|
|
156
|
+
}
|
|
157
|
+
this.#pending.clear();
|
|
158
|
+
this.#setLink('closed');
|
|
159
|
+
if (this.#stopped) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const min = this.#options.reconnectMinMs ?? 500;
|
|
163
|
+
const max = this.#options.reconnectMaxMs ?? 10_000;
|
|
164
|
+
const delay = Math.min(max, min * 2 ** this.#retry++);
|
|
165
|
+
this.#timer = setTimeout(() => this.#open(), delay);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
#handle(message) {
|
|
169
|
+
switch (message.type) {
|
|
170
|
+
case 'entity':
|
|
171
|
+
this.#entities.set(message.ref, message.entity);
|
|
172
|
+
for (const listener of Array.from(this.#listeners.get(message.ref) ?? [])) {
|
|
173
|
+
listener(message.entity);
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case 'status':
|
|
177
|
+
this.#integrationStatus.set(message.integration, message.status);
|
|
178
|
+
this.#statusSnapshot = Object.fromEntries(this.#integrationStatus);
|
|
179
|
+
for (const listener of Array.from(this.#statusListeners)) {
|
|
180
|
+
listener();
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
case 'result': {
|
|
184
|
+
const pending = this.#pending.get(message.id);
|
|
185
|
+
if (!pending) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
this.#pending.delete(message.id);
|
|
189
|
+
if (message.ok) {
|
|
190
|
+
pending.resolve(message.data);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
pending.reject(new Error(message.error));
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EntityRef } from './entity.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Generate an `entities.d.ts` that augments `KnownEntities`, so entity refs
|
|
4
|
+
* autocomplete and typos fail typechecking.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateEntityTypes(refs: readonly (EntityRef | string)[]): string;
|
package/dist/codegen.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate an `entities.d.ts` that augments `KnownEntities`, so entity refs
|
|
3
|
+
* autocomplete and typos fail typechecking.
|
|
4
|
+
*/
|
|
5
|
+
export function generateEntityTypes(refs) {
|
|
6
|
+
const unique = [...new Set(refs)].toSorted();
|
|
7
|
+
const lines = unique.map((ref) => ` ${JSON.stringify(ref)}: true;`);
|
|
8
|
+
return [
|
|
9
|
+
'// Generated by @hashsome/core. Do not edit.',
|
|
10
|
+
"import '@hashsome/core';",
|
|
11
|
+
'',
|
|
12
|
+
"declare module '@hashsome/core' {",
|
|
13
|
+
' interface KnownEntities {',
|
|
14
|
+
...lines,
|
|
15
|
+
' }',
|
|
16
|
+
'}',
|
|
17
|
+
'',
|
|
18
|
+
].join('\n');
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegen.js","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAqC;IACvE,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvE,OAAO;QACL,8CAA8C;QAC9C,0BAA0B;QAC1B,EAAE;QACF,mCAAmC;QACnC,6BAA6B;QAC7B,GAAG,KAAK;QACR,KAAK;QACL,GAAG;QACH,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Integration } from './integration.ts';
|
|
2
|
+
import { type EntityKind } from './model/index.ts';
|
|
3
|
+
export interface ConformanceOptions {
|
|
4
|
+
/** Shown in test names, e.g. `Home Assistant`. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** The id the integration uses unless told otherwise (`ha`, `ma`). */
|
|
7
|
+
defaultId: string;
|
|
8
|
+
/** Builds the integration's mock, with the integration's own representative devices. */
|
|
9
|
+
createMock: (options?: {
|
|
10
|
+
id?: string;
|
|
11
|
+
}) => Integration;
|
|
12
|
+
/** Every kind the real integration can produce; the mock has to cover all of them. */
|
|
13
|
+
kinds: readonly EntityKind[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The checks every integration's mock must pass, so a project can try any device without a
|
|
17
|
+
* backend. Run it from the integration package's own tests:
|
|
18
|
+
*
|
|
19
|
+
* runIntegrationConformance({ name, defaultId, kinds, createMock });
|
|
20
|
+
*
|
|
21
|
+
* Imports `vitest`, which is why it lives behind its own `@hashsome/core/conformance` entry point.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runIntegrationConformance(options: ConformanceOptions): void;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { UnknownEntityError } from './entity.js';
|
|
3
|
+
import { COMMAND_NAMES } from './model/index.js';
|
|
4
|
+
const AVAILABILITY = ['ready', 'unavailable', 'unknown'];
|
|
5
|
+
/**
|
|
6
|
+
* The checks every integration's mock must pass, so a project can try any device without a
|
|
7
|
+
* backend. Run it from the integration package's own tests:
|
|
8
|
+
*
|
|
9
|
+
* runIntegrationConformance({ name, defaultId, kinds, createMock });
|
|
10
|
+
*
|
|
11
|
+
* Imports `vitest`, which is why it lives behind its own `@hashsome/core/conformance` entry point.
|
|
12
|
+
*/
|
|
13
|
+
export function runIntegrationConformance(options) {
|
|
14
|
+
const { name, defaultId, createMock, kinds } = options;
|
|
15
|
+
describe(`${name} mock conformance`, () => {
|
|
16
|
+
test('uses the integration’s default id, and accepts another one', () => {
|
|
17
|
+
expect(createMock().id).toBe(defaultId);
|
|
18
|
+
expect(createMock({ id: 'other' }).id).toBe('other');
|
|
19
|
+
});
|
|
20
|
+
test('connects, and then lists a representative set of entities', async () => {
|
|
21
|
+
const mock = createMock();
|
|
22
|
+
await mock.connect();
|
|
23
|
+
expect(mock.status).toBe('connected');
|
|
24
|
+
expect(mock.listEntities().length).toBeGreaterThan(0);
|
|
25
|
+
});
|
|
26
|
+
test('covers every kind the integration can produce', async () => {
|
|
27
|
+
const mock = createMock();
|
|
28
|
+
await mock.connect();
|
|
29
|
+
const present = new Set(mock.listEntities().map((entity) => entity.kind));
|
|
30
|
+
expect(kinds.filter((kind) => !present.has(kind))).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
test('every entity is valid for its kind, and addressed by this integration', async () => {
|
|
33
|
+
const mock = createMock();
|
|
34
|
+
await mock.connect();
|
|
35
|
+
const problems = [];
|
|
36
|
+
for (const entity of mock.listEntities()) {
|
|
37
|
+
if (!entity.ref.startsWith(`${mock.id}:`)) {
|
|
38
|
+
problems.push(`${entity.ref}: not addressed by "${mock.id}"`);
|
|
39
|
+
}
|
|
40
|
+
if (!Object.keys(COMMAND_NAMES).includes(entity.kind)) {
|
|
41
|
+
problems.push(`${entity.ref}: unknown kind "${entity.kind}"`);
|
|
42
|
+
}
|
|
43
|
+
if (entity.name.length === 0) {
|
|
44
|
+
problems.push(`${entity.ref}: empty name`);
|
|
45
|
+
}
|
|
46
|
+
if (!AVAILABILITY.includes(entity.availability)) {
|
|
47
|
+
problems.push(`${entity.ref}: bad availability "${entity.availability}"`);
|
|
48
|
+
}
|
|
49
|
+
if (mock.getEntity(localId(mock.id, entity)) !== entity) {
|
|
50
|
+
problems.push(`${entity.ref}: getEntity does not return the listed entity`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
expect(problems).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
test('includes an unavailable device, so a dashboard can show that state', async () => {
|
|
56
|
+
const mock = createMock();
|
|
57
|
+
await mock.connect();
|
|
58
|
+
expect(mock.listEntities().some((entity) => entity.availability === 'unavailable')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
test('subscribe delivers the current entity at once, and throws for an unknown id', async () => {
|
|
61
|
+
const mock = createMock();
|
|
62
|
+
await mock.connect();
|
|
63
|
+
const [first] = mock.listEntities();
|
|
64
|
+
expect(first).toBeDefined();
|
|
65
|
+
const seen = [];
|
|
66
|
+
mock.subscribe(localId(mock.id, first), (entity) => seen.push(entity))();
|
|
67
|
+
expect(seen).toEqual([first]);
|
|
68
|
+
let thrown;
|
|
69
|
+
try {
|
|
70
|
+
mock.subscribe('does-not-exist', () => { });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
thrown = error;
|
|
74
|
+
}
|
|
75
|
+
expect(thrown).toBeInstanceOf(UnknownEntityError);
|
|
76
|
+
});
|
|
77
|
+
test('commands reject for an unknown entity and for a command the kind does not define', async () => {
|
|
78
|
+
const mock = createMock();
|
|
79
|
+
await mock.connect();
|
|
80
|
+
await expect(mock.command('does-not-exist', 'toggle')).rejects.toThrow(/does-not-exist/);
|
|
81
|
+
const [first] = mock.listEntities();
|
|
82
|
+
await expect(mock.command(localId(mock.id, first), 'definitely-not-a-command')).rejects.toThrow(/definitely-not-a-command/);
|
|
83
|
+
});
|
|
84
|
+
test('every command of a kind with commands runs on an entity of that kind', async () => {
|
|
85
|
+
const mock = createMock();
|
|
86
|
+
await mock.connect();
|
|
87
|
+
const failures = [];
|
|
88
|
+
for (const entity of mock.listEntities()) {
|
|
89
|
+
if (entity.availability !== 'ready') {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const argsFor = SAMPLE_ARGS[entity.kind] ?? {};
|
|
93
|
+
for (const command of COMMAND_NAMES[entity.kind]) {
|
|
94
|
+
if (command === 'playMedia') {
|
|
95
|
+
continue; // needs a real item id; the library test above plays one
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await mock.command(localId(mock.id, entity), command, argsFor[command]);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
failures.push(`${entity.ref} ${command}: ${String(error)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
expect(failures).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
test('a player that reports a library can be browsed, searched and played from', async () => {
|
|
108
|
+
const mock = createMock();
|
|
109
|
+
await mock.connect();
|
|
110
|
+
const failures = [];
|
|
111
|
+
for (const entity of mock.listEntities()) {
|
|
112
|
+
if (entity.kind !== 'mediaPlayer' || !entity.capabilities.browse) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const id = localId(mock.id, entity);
|
|
116
|
+
if (!mock.browse) {
|
|
117
|
+
failures.push(`${entity.ref}: reports browse but the integration has no browse()`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const root = await mock.browse(id, {});
|
|
121
|
+
if (root.items.length === 0) {
|
|
122
|
+
failures.push(`${entity.ref}: the top level of the library is empty`);
|
|
123
|
+
}
|
|
124
|
+
for (const item of root.items) {
|
|
125
|
+
if (item.title === '' || item.id === '') {
|
|
126
|
+
failures.push(`${entity.ref}: an item has no id or title`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const folder = root.items.find((item) => item.expandable);
|
|
130
|
+
if (folder) {
|
|
131
|
+
const inside = await mock.browse(id, { path: folder.id });
|
|
132
|
+
if (inside.items.length === 0) {
|
|
133
|
+
failures.push(`${entity.ref}: "${folder.title}" lists nothing`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const playable = await firstPlayable(mock, id);
|
|
137
|
+
if (!playable) {
|
|
138
|
+
failures.push(`${entity.ref}: nothing in the library can be played`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
try {
|
|
142
|
+
await mock.command(id, 'playMedia', { item: playable });
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
failures.push(`${entity.ref} playMedia: ${String(error)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (entity.capabilities.search) {
|
|
149
|
+
const found = await mock.browse(id, { search: root.items[0]?.title.slice(0, 3) ?? 'a' });
|
|
150
|
+
if (!Array.isArray(found.items)) {
|
|
151
|
+
failures.push(`${entity.ref}: search did not return a list`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
expect(failures).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
test('the mock never carries credentials', async () => {
|
|
158
|
+
const mock = createMock();
|
|
159
|
+
await mock.connect();
|
|
160
|
+
expect(JSON.stringify(mock.listEntities())).not.toMatch(/token|password|secret/i);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const localId = (integrationId, entity) => entity.ref.slice(integrationId.length + 1);
|
|
165
|
+
/** Arguments that make each command valid, for the commands that take any. */
|
|
166
|
+
const SAMPLE_ARGS = {
|
|
167
|
+
mediaPlayer: { setVolume: { volume: 0.5 }, setMuted: { muted: true }, seek: { position: 30 } },
|
|
168
|
+
light: {
|
|
169
|
+
setBrightness: { brightness: 0.5 },
|
|
170
|
+
setColorTemperature: { kelvin: 3000 },
|
|
171
|
+
setColor: { hue: 120, saturation: 80 },
|
|
172
|
+
},
|
|
173
|
+
climate: {
|
|
174
|
+
setMode: { mode: 'off' },
|
|
175
|
+
setTargetTemperature: { temperature: 20 },
|
|
176
|
+
setPreset: { preset: 'home' },
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
/** The id of the first playable item found by walking the library, a few levels down. */
|
|
180
|
+
async function firstPlayable(integration, entityId, path, depth = 0) {
|
|
181
|
+
if (!integration.browse || depth > 3) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const level = await integration.browse(entityId, path === undefined ? {} : { path });
|
|
185
|
+
const direct = level.items.find((item) => item.playable && !item.expandable);
|
|
186
|
+
if (direct) {
|
|
187
|
+
return direct.id;
|
|
188
|
+
}
|
|
189
|
+
for (const item of level.items.filter((candidate) => candidate.expandable)) {
|
|
190
|
+
const found = await firstPlayable(integration, entityId, item.id, depth + 1);
|
|
191
|
+
if (found) {
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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"}
|
package/dist/emitter.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Tiny listener set shared by integrations. */
|
|
2
|
+
export class Emitter {
|
|
3
|
+
#listeners = new Set();
|
|
4
|
+
on(listener) {
|
|
5
|
+
this.#listeners.add(listener);
|
|
6
|
+
return () => {
|
|
7
|
+
this.#listeners.delete(listener);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
emit(value) {
|
|
11
|
+
for (const listener of Array.from(this.#listeners)) {
|
|
12
|
+
listener(value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
get size() {
|
|
16
|
+
return this.#listeners.size;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emitter.js","sourceRoot":"","sources":["../src/emitter.ts"],"names":[],"mappings":"AAEA,gDAAgD;AAChD,MAAM,OAAO,OAAO;IAClB,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IAE3C,EAAE,CAAC,QAA4B;QAC7B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC9B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,CAAC,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,KAAQ;QACX,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YACnD,QAAQ,CAAC,KAAK,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF"}
|
package/dist/entity.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity refs address an entity across integrations: `<integration>:<id>`,
|
|
3
|
+
* e.g. `ha:light.kitchen_lamp` or `ma:living_room`.
|
|
4
|
+
*
|
|
5
|
+
* `KnownEntities` is empty by default. Generated `entities.d.ts` files (see
|
|
6
|
+
* `generateEntityTypes`) augment it so refs autocomplete and are type-checked.
|
|
7
|
+
*
|
|
8
|
+
* The entity itself — the generic model every integration produces — lives in `./model/`.
|
|
9
|
+
*/
|
|
10
|
+
export interface KnownEntities {
|
|
11
|
+
}
|
|
12
|
+
export type EntityRef = keyof KnownEntities extends never ? `${string}:${string}` : Extract<keyof KnownEntities, string>;
|
|
13
|
+
export interface ParsedEntityRef {
|
|
14
|
+
integration: string;
|
|
15
|
+
id: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseEntityRef(ref: string): ParsedEntityRef;
|
|
18
|
+
export declare function formatEntityRef(integration: string, id: string): EntityRef;
|
|
19
|
+
/** Thrown by `Integration.subscribe` (and `command`) for an id the integration does not have. */
|
|
20
|
+
export declare class UnknownEntityError extends Error {
|
|
21
|
+
readonly integration: string;
|
|
22
|
+
readonly entityId: string;
|
|
23
|
+
constructor(integration: string, entityId: string);
|
|
24
|
+
}
|
package/dist/entity.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function parseEntityRef(ref) {
|
|
2
|
+
const index = ref.indexOf(':');
|
|
3
|
+
if (index <= 0 || index === ref.length - 1) {
|
|
4
|
+
throw new Error(`Invalid entity ref "${ref}", expected "<integration>:<id>"`);
|
|
5
|
+
}
|
|
6
|
+
return { integration: ref.slice(0, index), id: ref.slice(index + 1) };
|
|
7
|
+
}
|
|
8
|
+
export function formatEntityRef(integration, id) {
|
|
9
|
+
return `${integration}:${id}`;
|
|
10
|
+
}
|
|
11
|
+
/** Thrown by `Integration.subscribe` (and `command`) for an id the integration does not have. */
|
|
12
|
+
export class UnknownEntityError extends Error {
|
|
13
|
+
integration;
|
|
14
|
+
entityId;
|
|
15
|
+
constructor(integration, entityId) {
|
|
16
|
+
super(`Unknown entity "${integration}:${entityId}"`);
|
|
17
|
+
this.integration = integration;
|
|
18
|
+
this.entityId = entityId;
|
|
19
|
+
this.name = 'UnknownEntityError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity.js","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAqBA,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,uBAAuB,GAAG,kCAAkC,CAAC,CAAC;IAChF,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,EAAU;IAC7D,OAAO,GAAG,WAAW,IAAI,EAAE,EAAe,CAAC;AAC7C,CAAC;AAED,iGAAiG;AACjG,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,WAAW,CAAS;IACpB,QAAQ,CAAS;IAE1B,YAAY,WAAmB,EAAE,QAAgB;QAC/C,KAAK,CAAC,mBAAmB,WAAW,IAAI,QAAQ,GAAG,CAAC,CAAC;QACrD,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './entity.ts';
|
|
2
|
+
export * from './model/index.ts';
|
|
3
|
+
export * from './integration.ts';
|
|
4
|
+
export * from './base-integration.ts';
|
|
5
|
+
export * from './mock.ts';
|
|
6
|
+
export * from './protocol.ts';
|
|
7
|
+
export * from './codegen.ts';
|
|
8
|
+
export * from './client.ts';
|
|
9
|
+
export * from './local-client.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './entity.js';
|
|
2
|
+
export * from './model/index.js';
|
|
3
|
+
export * from './integration.js';
|
|
4
|
+
export * from './base-integration.js';
|
|
5
|
+
export * from './mock.js';
|
|
6
|
+
export * from './protocol.js';
|
|
7
|
+
export * from './codegen.js';
|
|
8
|
+
export * from './client.js';
|
|
9
|
+
export * from './local-client.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { EntityRef } from './entity.ts';
|
|
2
|
+
import type { BrowseQuery, BrowseResult, Entity, HistoryQuery, HistoryResult } from './model/index.ts';
|
|
3
|
+
export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
4
|
+
export type Unsubscribe = () => void;
|
|
5
|
+
/**
|
|
6
|
+
* A backend the runtime can talk to (Home Assistant, Music Assistant, ...). This is the whole
|
|
7
|
+
* surface a third-party integration package implements — see ARCHITECTURE.md, "The integration
|
|
8
|
+
* contract", for the rules each member must follow.
|
|
9
|
+
*
|
|
10
|
+
* Integrations work only in **local ids** (the part of a ref after `<integration>:`); the runtime
|
|
11
|
+
* does all the addressing, and `BaseIntegration` stamps `ref` onto the entities it stores.
|
|
12
|
+
*/
|
|
13
|
+
export interface Integration {
|
|
14
|
+
/** The prefix of every ref this integration owns. Fixed for the instance's lifetime. */
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly status: ConnectionStatus;
|
|
17
|
+
/** Resolves once connected **and** the first full set of entities is loaded; rejects if the
|
|
18
|
+
* first attempt fails; idempotent. After a first success the integration reconnects itself. */
|
|
19
|
+
connect(): Promise<void>;
|
|
20
|
+
/** Synchronous and idempotent: closes the link, cancels timers, rejects in-flight commands. */
|
|
21
|
+
disconnect(): void;
|
|
22
|
+
/** Called only when the status actually changes. */
|
|
23
|
+
onStatusChange(listener: (status: ConnectionStatus) => void): Unsubscribe;
|
|
24
|
+
listEntities(): Entity[];
|
|
25
|
+
getEntity(entityId: string): Entity | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Calls `listener` immediately with the current entity, then on every change (`undefined` if
|
|
28
|
+
* the entity is removed). Throws `UnknownEntityError` for an id this integration does not have.
|
|
29
|
+
*/
|
|
30
|
+
subscribe(entityId: string, listener: (entity: Entity | undefined) => void): Unsubscribe;
|
|
31
|
+
/**
|
|
32
|
+
* Runs a named command from the entity's kind (`setVolume`, `next`). Resolves once the backend
|
|
33
|
+
* has accepted it, not when the state changes. Rejects with a displayable `Error` for an
|
|
34
|
+
* unknown entity, a command the kind does not define, invalid arguments, or a refusing backend.
|
|
35
|
+
* Arguments come from a browser: validate them.
|
|
36
|
+
*/
|
|
37
|
+
command(entityId: string, name: string, args?: Record<string, unknown>): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Lists one level of the entity's media library (or searches it). Only for players that report
|
|
40
|
+
* `capabilities.browse`; the library is the integration's own, with no mixing across backends.
|
|
41
|
+
* Rejects with a displayable `Error` for an unknown entity or path. Arguments come from a browser.
|
|
42
|
+
*/
|
|
43
|
+
browse?(entityId: string, query: BrowseQuery): Promise<BrowseResult>;
|
|
44
|
+
/**
|
|
45
|
+
* The entity's past values, from the backend's own record (Home Assistant's long-term
|
|
46
|
+
* statistics), bucketed. Optional: only for backends that keep history. Resolves with no points
|
|
47
|
+
* for an entity with none, and rejects with a displayable `Error` for an unknown entity.
|
|
48
|
+
* Arguments come from a browser: validate them.
|
|
49
|
+
*/
|
|
50
|
+
history?(entityId: string, query: HistoryQuery): Promise<HistoryResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Fetches a file the backend serves for its entities, such as artwork, with the integration's own
|
|
53
|
+
* credentials. `path` is the backend's own path and starts with `/`; it comes from a browser, so
|
|
54
|
+
* refuse anything else. The runtime serves only image responses. Optional: omit it when entities
|
|
55
|
+
* only carry addresses the browser can open itself.
|
|
56
|
+
*/
|
|
57
|
+
fetchAsset?(path: string): Promise<Response>;
|
|
58
|
+
/** Escape hatch: a backend-specific request no command covers. `@hashsome/ui` never calls it. */
|
|
59
|
+
callRaw?(request: Record<string, unknown>): Promise<unknown>;
|
|
60
|
+
}
|
|
61
|
+
export type { EntityRef };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|