@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration.js","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Client, EntityListener, LinkStatus } from './client.ts';
|
|
2
|
+
import { type EntityRef } from './entity.ts';
|
|
3
|
+
import type { ConnectionStatus, Integration, Unsubscribe } from './integration.ts';
|
|
4
|
+
import type { BrowseQuery, BrowseResult, Entity, HistoryQuery, HistoryResult } from './model/index.ts';
|
|
5
|
+
/** In-process `Client` over integrations, with no runtime server. Used by the gallery and tests. */
|
|
6
|
+
export declare class LocalClient implements Client {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(integrations: Integration[]);
|
|
9
|
+
get link(): LinkStatus;
|
|
10
|
+
connect(): void;
|
|
11
|
+
close(): void;
|
|
12
|
+
onLinkChange(listener: (status: LinkStatus) => void): Unsubscribe;
|
|
13
|
+
getIntegrationStatus(integration: string): ConnectionStatus | undefined;
|
|
14
|
+
getIntegrationStatuses(): Readonly<Record<string, ConnectionStatus>>;
|
|
15
|
+
onIntegrationStatusChange(listener: () => void): Unsubscribe;
|
|
16
|
+
getEntity(ref: EntityRef): Entity | null;
|
|
17
|
+
subscribe(ref: EntityRef, listener: EntityListener): Unsubscribe;
|
|
18
|
+
command(ref: EntityRef, name: string, args?: Record<string, unknown>): Promise<void>;
|
|
19
|
+
browse(ref: EntityRef, query: BrowseQuery): Promise<BrowseResult>;
|
|
20
|
+
history(ref: EntityRef, query: HistoryQuery): Promise<HistoryResult>;
|
|
21
|
+
callRaw(integration: string, request: Record<string, unknown>): Promise<unknown>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { parseEntityRef, UnknownEntityError } from './entity.js';
|
|
2
|
+
/** In-process `Client` over integrations, with no runtime server. Used by the gallery and tests. */
|
|
3
|
+
export class LocalClient {
|
|
4
|
+
#integrations = new Map();
|
|
5
|
+
#link = 'closed';
|
|
6
|
+
#linkListeners = new Set();
|
|
7
|
+
#statusSnapshot = {};
|
|
8
|
+
constructor(integrations) {
|
|
9
|
+
for (const integration of integrations) {
|
|
10
|
+
this.#integrations.set(integration.id, integration);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
get link() {
|
|
14
|
+
return this.#link;
|
|
15
|
+
}
|
|
16
|
+
connect() {
|
|
17
|
+
if (this.#link === 'open') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.#setLink('open');
|
|
21
|
+
for (const integration of this.#integrations.values()) {
|
|
22
|
+
void integration.connect();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
close() {
|
|
26
|
+
this.#setLink('closed');
|
|
27
|
+
}
|
|
28
|
+
onLinkChange(listener) {
|
|
29
|
+
this.#linkListeners.add(listener);
|
|
30
|
+
return () => {
|
|
31
|
+
this.#linkListeners.delete(listener);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
getIntegrationStatus(integration) {
|
|
35
|
+
return this.#integrations.get(integration)?.status;
|
|
36
|
+
}
|
|
37
|
+
getIntegrationStatuses() {
|
|
38
|
+
const next = Object.fromEntries([...this.#integrations.values()].map((i) => [i.id, i.status]));
|
|
39
|
+
const same = Object.keys(next).length === Object.keys(this.#statusSnapshot).length &&
|
|
40
|
+
Object.entries(next).every(([id, status]) => this.#statusSnapshot[id] === status);
|
|
41
|
+
if (!same) {
|
|
42
|
+
this.#statusSnapshot = next;
|
|
43
|
+
}
|
|
44
|
+
return this.#statusSnapshot;
|
|
45
|
+
}
|
|
46
|
+
onIntegrationStatusChange(listener) {
|
|
47
|
+
const offs = [...this.#integrations.values()].map((i) => i.onStatusChange(listener));
|
|
48
|
+
return () => offs.forEach((off) => off());
|
|
49
|
+
}
|
|
50
|
+
getEntity(ref) {
|
|
51
|
+
const { integration, id } = parseEntityRef(ref);
|
|
52
|
+
return this.#integrations.get(integration)?.getEntity(id) ?? null;
|
|
53
|
+
}
|
|
54
|
+
subscribe(ref, listener) {
|
|
55
|
+
const { integration, id } = parseEntityRef(ref);
|
|
56
|
+
const source = this.#integrations.get(integration);
|
|
57
|
+
if (!source) {
|
|
58
|
+
listener(null);
|
|
59
|
+
return () => { };
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return source.subscribe(id, (entity) => listener(entity ?? null));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof UnknownEntityError) {
|
|
66
|
+
listener(null);
|
|
67
|
+
return () => { };
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
command(ref, name, args) {
|
|
73
|
+
const { integration, id } = parseEntityRef(ref);
|
|
74
|
+
const source = this.#integrations.get(integration);
|
|
75
|
+
if (!source) {
|
|
76
|
+
return Promise.reject(new Error(`Unknown integration "${integration}"`));
|
|
77
|
+
}
|
|
78
|
+
return source.command(id, name, args);
|
|
79
|
+
}
|
|
80
|
+
browse(ref, query) {
|
|
81
|
+
const { integration, id } = parseEntityRef(ref);
|
|
82
|
+
const source = this.#integrations.get(integration);
|
|
83
|
+
if (!source) {
|
|
84
|
+
return Promise.reject(new Error(`Unknown integration "${integration}"`));
|
|
85
|
+
}
|
|
86
|
+
if (!source.browse) {
|
|
87
|
+
return Promise.reject(new Error(`"${integration}" has no media library`));
|
|
88
|
+
}
|
|
89
|
+
return source.browse(id, query);
|
|
90
|
+
}
|
|
91
|
+
history(ref, query) {
|
|
92
|
+
const { integration, id } = parseEntityRef(ref);
|
|
93
|
+
const source = this.#integrations.get(integration);
|
|
94
|
+
if (!source) {
|
|
95
|
+
return Promise.reject(new Error(`Unknown integration "${integration}"`));
|
|
96
|
+
}
|
|
97
|
+
if (!source.history) {
|
|
98
|
+
return Promise.reject(new Error(`"${integration}" keeps no history`));
|
|
99
|
+
}
|
|
100
|
+
return source.history(id, query);
|
|
101
|
+
}
|
|
102
|
+
callRaw(integration, request) {
|
|
103
|
+
const source = this.#integrations.get(integration);
|
|
104
|
+
if (!source) {
|
|
105
|
+
return Promise.reject(new Error(`Unknown integration "${integration}"`));
|
|
106
|
+
}
|
|
107
|
+
if (!source.callRaw) {
|
|
108
|
+
return Promise.reject(new Error(`Integration "${integration}" has no raw requests`));
|
|
109
|
+
}
|
|
110
|
+
return source.callRaw(request);
|
|
111
|
+
}
|
|
112
|
+
#setLink(status) {
|
|
113
|
+
if (status === this.#link) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#link = status;
|
|
117
|
+
for (const listener of Array.from(this.#linkListeners)) {
|
|
118
|
+
listener(status);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-client.js","sourceRoot":"","sources":["../src/local-client.ts"],"names":[],"mappings":"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"}
|
package/dist/mock.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BaseIntegration } from './base-integration.ts';
|
|
2
|
+
import { type ActionEntity, type BrowseItem, type BrowseQuery, type BrowseResult, type ClimateEntity, type EntityInput, type GenericEntity, type LightEntity, type MediaPlayerEntity, type PersonEntity, type SensorEntity, type SwitchEntity, type WeatherEntity } from './model/index.ts';
|
|
3
|
+
type Init<E extends {
|
|
4
|
+
kind: string;
|
|
5
|
+
ref: unknown;
|
|
6
|
+
}, K extends keyof E = never> = Partial<Omit<E, 'ref' | 'kind' | K>>;
|
|
7
|
+
export declare function mockMediaPlayer(init?: Init<MediaPlayerEntity, 'capabilities'> & {
|
|
8
|
+
capabilities?: Partial<MediaPlayerEntity['capabilities']>;
|
|
9
|
+
}): EntityInput;
|
|
10
|
+
export declare function mockLight(init?: Init<LightEntity, 'capabilities'> & {
|
|
11
|
+
capabilities?: Partial<LightEntity['capabilities']>;
|
|
12
|
+
}): EntityInput;
|
|
13
|
+
export declare function mockClimate(init?: Init<ClimateEntity, 'capabilities'> & {
|
|
14
|
+
capabilities?: Partial<ClimateEntity['capabilities']>;
|
|
15
|
+
}): EntityInput;
|
|
16
|
+
export declare function mockSensor(init: Init<SensorEntity> & {
|
|
17
|
+
value: string;
|
|
18
|
+
}): EntityInput;
|
|
19
|
+
export declare function mockSwitch(init?: Init<SwitchEntity>): EntityInput;
|
|
20
|
+
export declare function mockAction(init?: Init<ActionEntity>): EntityInput;
|
|
21
|
+
export declare function mockPerson(init?: Init<PersonEntity>): EntityInput;
|
|
22
|
+
export declare function mockWeather(init?: Init<WeatherEntity>): EntityInput;
|
|
23
|
+
export declare function mockGeneric(init: Init<GenericEntity> & {
|
|
24
|
+
value: string;
|
|
25
|
+
}): EntityInput;
|
|
26
|
+
export interface MockCall {
|
|
27
|
+
entityId: string;
|
|
28
|
+
command: string;
|
|
29
|
+
args?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
/** A fake media library: the items inside each folder, by folder id (`root` for the top level). */
|
|
32
|
+
export type MockLibrary = Record<string, {
|
|
33
|
+
title?: string;
|
|
34
|
+
items: BrowseItem[];
|
|
35
|
+
}>;
|
|
36
|
+
/** A small library with the shapes a real one has: shelves, each holding playlists, albums,
|
|
37
|
+
* artists or stations, which in turn hold tracks. */
|
|
38
|
+
export declare function mockLibrary(): MockLibrary;
|
|
39
|
+
export interface MockIntegrationOptions {
|
|
40
|
+
id?: string;
|
|
41
|
+
/** What `browse` lists and `playMedia` plays. Defaults to none: the players have no library. */
|
|
42
|
+
library?: MockLibrary;
|
|
43
|
+
/** Entities by local id, built with the `mock*` builders. */
|
|
44
|
+
entities?: Record<string, EntityInput>;
|
|
45
|
+
}
|
|
46
|
+
/** In-memory integration with fixture entities, for tests, the gallery and screenshots. */
|
|
47
|
+
export declare class MockIntegration extends BaseIntegration {
|
|
48
|
+
#private;
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly calls: MockCall[];
|
|
51
|
+
constructor(options?: MockIntegrationOptions);
|
|
52
|
+
connect(): Promise<void>;
|
|
53
|
+
disconnect(): void;
|
|
54
|
+
/** Adds an entity, or replaces one. */
|
|
55
|
+
set(entityId: string, input: EntityInput): void;
|
|
56
|
+
/** Patches an entity: top-level fields are replaced, `capabilities` and `media` are merged. */
|
|
57
|
+
update(entityId: string, patch: Partial<EntityInput>): void;
|
|
58
|
+
browse(entityId: string, query: BrowseQuery): Promise<BrowseResult>;
|
|
59
|
+
command(entityId: string, name: string, args?: Record<string, unknown>): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export {};
|
package/dist/mock.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { BaseIntegration } from './base-integration.js';
|
|
2
|
+
import { UnknownEntityError } from './entity.js';
|
|
3
|
+
import { isCommandOf, } from './model/index.js';
|
|
4
|
+
export function mockMediaPlayer(init = {}) {
|
|
5
|
+
const { capabilities, ...rest } = init;
|
|
6
|
+
return {
|
|
7
|
+
kind: 'mediaPlayer',
|
|
8
|
+
name: 'Player',
|
|
9
|
+
availability: 'ready',
|
|
10
|
+
playback: 'idle',
|
|
11
|
+
muted: false,
|
|
12
|
+
...rest,
|
|
13
|
+
capabilities: {
|
|
14
|
+
volume: rest.volume !== undefined,
|
|
15
|
+
mute: true,
|
|
16
|
+
next: true,
|
|
17
|
+
previous: true,
|
|
18
|
+
browse: false,
|
|
19
|
+
search: false,
|
|
20
|
+
seek: false,
|
|
21
|
+
shuffle: false,
|
|
22
|
+
transfer: false,
|
|
23
|
+
group: false,
|
|
24
|
+
...capabilities,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function mockLight(init = {}) {
|
|
29
|
+
const { capabilities, ...rest } = init;
|
|
30
|
+
return {
|
|
31
|
+
kind: 'light',
|
|
32
|
+
name: 'Light',
|
|
33
|
+
availability: 'ready',
|
|
34
|
+
on: false,
|
|
35
|
+
...rest,
|
|
36
|
+
capabilities: {
|
|
37
|
+
brightness: rest.brightness !== undefined,
|
|
38
|
+
colorTemperature: false,
|
|
39
|
+
color: false,
|
|
40
|
+
...capabilities,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function mockClimate(init = {}) {
|
|
45
|
+
const { capabilities, ...rest } = init;
|
|
46
|
+
return {
|
|
47
|
+
kind: 'climate',
|
|
48
|
+
name: 'Climate',
|
|
49
|
+
availability: 'ready',
|
|
50
|
+
mode: 'off',
|
|
51
|
+
unit: '°C',
|
|
52
|
+
...rest,
|
|
53
|
+
capabilities: {
|
|
54
|
+
modes: ['off', 'heat'],
|
|
55
|
+
presets: [],
|
|
56
|
+
targetTemperature: rest.targetTemperature !== undefined,
|
|
57
|
+
step: 0.5,
|
|
58
|
+
range: { min: 5, max: 35 },
|
|
59
|
+
...capabilities,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function mockSensor(init) {
|
|
64
|
+
const numeric = Number(init.value);
|
|
65
|
+
return {
|
|
66
|
+
kind: 'sensor',
|
|
67
|
+
name: 'Sensor',
|
|
68
|
+
availability: 'ready',
|
|
69
|
+
...(init.value.trim() !== '' && Number.isFinite(numeric) ? { numeric } : {}),
|
|
70
|
+
...init,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function mockSwitch(init = {}) {
|
|
74
|
+
return { kind: 'switch', name: 'Switch', availability: 'ready', on: false, ...init };
|
|
75
|
+
}
|
|
76
|
+
export function mockAction(init = {}) {
|
|
77
|
+
return { kind: 'action', name: 'Action', availability: 'ready', ...init };
|
|
78
|
+
}
|
|
79
|
+
export function mockPerson(init = {}) {
|
|
80
|
+
const location = init.location ?? 'home';
|
|
81
|
+
return {
|
|
82
|
+
kind: 'person',
|
|
83
|
+
name: 'Person',
|
|
84
|
+
availability: 'ready',
|
|
85
|
+
home: location === 'home',
|
|
86
|
+
location,
|
|
87
|
+
...init,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function mockWeather(init = {}) {
|
|
91
|
+
return {
|
|
92
|
+
kind: 'weather',
|
|
93
|
+
name: 'Weather',
|
|
94
|
+
availability: 'ready',
|
|
95
|
+
condition: 'sunny',
|
|
96
|
+
temperature: 20,
|
|
97
|
+
unit: '°C',
|
|
98
|
+
...init,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function mockGeneric(init) {
|
|
102
|
+
return { kind: 'generic', name: 'Entity', availability: 'ready', ...init };
|
|
103
|
+
}
|
|
104
|
+
/** Cover art for the mock library: a stable picture per item, from a placeholder service. */
|
|
105
|
+
const cover = (id) => `https://picsum.photos/seed/${id}/400`;
|
|
106
|
+
const libraryTrack = (id, title, artist) => ({
|
|
107
|
+
id,
|
|
108
|
+
title,
|
|
109
|
+
subtitle: artist,
|
|
110
|
+
artworkUrl: cover(id),
|
|
111
|
+
kind: 'track',
|
|
112
|
+
playable: true,
|
|
113
|
+
expandable: false,
|
|
114
|
+
});
|
|
115
|
+
const libraryFolder = (id, title, kind, subtitle) => ({
|
|
116
|
+
id,
|
|
117
|
+
title,
|
|
118
|
+
...(subtitle ? { subtitle } : {}),
|
|
119
|
+
...(kind === 'folder' ? {} : { artworkUrl: cover(id) }),
|
|
120
|
+
kind,
|
|
121
|
+
playable: kind !== 'folder',
|
|
122
|
+
expandable: true,
|
|
123
|
+
});
|
|
124
|
+
/** A small library with the shapes a real one has: shelves, each holding playlists, albums,
|
|
125
|
+
* artists or stations, which in turn hold tracks. */
|
|
126
|
+
export function mockLibrary() {
|
|
127
|
+
return {
|
|
128
|
+
root: {
|
|
129
|
+
items: [
|
|
130
|
+
libraryFolder('recent', 'Recently played', 'folder'),
|
|
131
|
+
libraryFolder('playlists', 'Playlists', 'folder'),
|
|
132
|
+
libraryFolder('albums', 'Albums', 'folder'),
|
|
133
|
+
libraryFolder('artists', 'Artists', 'folder'),
|
|
134
|
+
libraryFolder('radio', 'Radio', 'folder'),
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
recent: {
|
|
138
|
+
title: 'Recently played',
|
|
139
|
+
items: [
|
|
140
|
+
libraryTrack('t-blank-space', 'Blank Space', 'Taylor Swift'),
|
|
141
|
+
libraryTrack('t-dreams', 'Dreams', 'Fleetwood Mac'),
|
|
142
|
+
libraryTrack('t-kids', 'Kids', 'MGMT'),
|
|
143
|
+
libraryTrack('t-style', 'Style', 'Taylor Swift'),
|
|
144
|
+
libraryTrack('t-electric', 'Electric Feel', 'MGMT'),
|
|
145
|
+
libraryTrack('t-go-your-own-way', 'Go Your Own Way', 'Fleetwood Mac'),
|
|
146
|
+
libraryTrack('t-borderline', 'Borderline', 'Tame Impala'),
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
playlists: {
|
|
150
|
+
title: 'Playlists',
|
|
151
|
+
items: [
|
|
152
|
+
libraryFolder('pl-morning', 'Morning coffee', 'playlist', '24 tracks'),
|
|
153
|
+
libraryFolder('pl-dinner', 'Dinner party', 'playlist', '41 tracks'),
|
|
154
|
+
libraryFolder('pl-focus', 'Deep focus', 'playlist', '60 tracks'),
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
albums: {
|
|
158
|
+
title: 'Albums',
|
|
159
|
+
items: [
|
|
160
|
+
libraryFolder('al-1989', '1989', 'album', 'Taylor Swift'),
|
|
161
|
+
libraryFolder('al-rumours', 'Rumours', 'album', 'Fleetwood Mac'),
|
|
162
|
+
libraryFolder('al-oracular', 'Oracular Spectacular', 'album', 'MGMT'),
|
|
163
|
+
libraryFolder('al-currents', 'Currents', 'album', 'Tame Impala'),
|
|
164
|
+
libraryFolder('al-ram', 'Random Access Memories', 'album', 'Daft Punk'),
|
|
165
|
+
libraryFolder('al-folklore', 'Folklore', 'album', 'Taylor Swift'),
|
|
166
|
+
libraryFolder('al-hounds', 'Hounds of Love', 'album', 'Kate Bush'),
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
artists: {
|
|
170
|
+
title: 'Artists',
|
|
171
|
+
items: [
|
|
172
|
+
libraryFolder('ar-swift', 'Taylor Swift', 'artist'),
|
|
173
|
+
libraryFolder('ar-mac', 'Fleetwood Mac', 'artist'),
|
|
174
|
+
libraryFolder('ar-mgmt', 'MGMT', 'artist'),
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
radio: {
|
|
178
|
+
title: 'Radio',
|
|
179
|
+
items: [
|
|
180
|
+
{ ...libraryFolder('rd-jazz', 'Jazz FM', 'radio'), expandable: false },
|
|
181
|
+
{ ...libraryFolder('rd-lofi', 'Lofi Beats', 'radio'), expandable: false },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
'pl-morning': {
|
|
185
|
+
title: 'Morning coffee',
|
|
186
|
+
items: [
|
|
187
|
+
libraryTrack('t-dreams', 'Dreams', 'Fleetwood Mac'),
|
|
188
|
+
libraryTrack('t-kids', 'Kids', 'MGMT'),
|
|
189
|
+
libraryTrack('t-blank-space', 'Blank Space', 'Taylor Swift'),
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
'al-1989': {
|
|
193
|
+
title: '1989',
|
|
194
|
+
items: [
|
|
195
|
+
libraryTrack('t-blank-space', 'Blank Space', 'Taylor Swift'),
|
|
196
|
+
libraryTrack('t-style', 'Style', 'Taylor Swift'),
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
'al-rumours': {
|
|
200
|
+
title: 'Rumours',
|
|
201
|
+
items: [
|
|
202
|
+
libraryTrack('t-dreams', 'Dreams', 'Fleetwood Mac'),
|
|
203
|
+
libraryTrack('t-go-your-own-way', 'Go Your Own Way', 'Fleetwood Mac'),
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
'al-oracular': {
|
|
207
|
+
title: 'Oracular Spectacular',
|
|
208
|
+
items: [
|
|
209
|
+
libraryTrack('t-kids', 'Kids', 'MGMT'),
|
|
210
|
+
libraryTrack('t-electric', 'Electric Feel', 'MGMT'),
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
'al-currents': {
|
|
214
|
+
title: 'Currents',
|
|
215
|
+
items: [
|
|
216
|
+
libraryTrack('t-borderline', 'Borderline', 'Tame Impala'),
|
|
217
|
+
libraryTrack('t-let-it-happen', 'Let It Happen', 'Tame Impala'),
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
'al-ram': {
|
|
221
|
+
title: 'Random Access Memories',
|
|
222
|
+
items: [
|
|
223
|
+
libraryTrack('t-get-lucky', 'Get Lucky', 'Daft Punk'),
|
|
224
|
+
libraryTrack('t-instant-crush', 'Instant Crush', 'Daft Punk'),
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
'al-folklore': {
|
|
228
|
+
title: 'Folklore',
|
|
229
|
+
items: [
|
|
230
|
+
libraryTrack('t-cardigan', 'Cardigan', 'Taylor Swift'),
|
|
231
|
+
libraryTrack('t-august', 'August', 'Taylor Swift'),
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
'al-hounds': {
|
|
235
|
+
title: 'Hounds of Love',
|
|
236
|
+
items: [
|
|
237
|
+
libraryTrack('t-running', 'Running Up That Hill', 'Kate Bush'),
|
|
238
|
+
libraryTrack('t-cloudbusting', 'Cloudbusting', 'Kate Bush'),
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
'ar-swift': {
|
|
242
|
+
title: 'Taylor Swift',
|
|
243
|
+
items: [libraryFolder('al-1989', '1989', 'album')],
|
|
244
|
+
},
|
|
245
|
+
'ar-mac': { title: 'Fleetwood Mac', items: [libraryFolder('al-rumours', 'Rumours', 'album')] },
|
|
246
|
+
'ar-mgmt': {
|
|
247
|
+
title: 'MGMT',
|
|
248
|
+
items: [libraryFolder('al-oracular', 'Oracular Spectacular', 'album')],
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const clamp01 = (value) => Math.min(1, Math.max(0, value));
|
|
253
|
+
/** What a command does to an entity's state, for the kinds that have commands. */
|
|
254
|
+
function applyCommand(entity, name, args = {}) {
|
|
255
|
+
const num = (key) => {
|
|
256
|
+
const value = args[key];
|
|
257
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
258
|
+
throw new Error(`"${name}" needs a numeric "${key}"`);
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
};
|
|
262
|
+
switch (entity.kind) {
|
|
263
|
+
case 'mediaPlayer':
|
|
264
|
+
switch (name) {
|
|
265
|
+
case 'play':
|
|
266
|
+
return { playback: 'playing' };
|
|
267
|
+
case 'pause':
|
|
268
|
+
return { playback: 'paused' };
|
|
269
|
+
case 'togglePlay':
|
|
270
|
+
return { playback: entity.playback === 'playing' ? 'paused' : 'playing' };
|
|
271
|
+
case 'setVolume':
|
|
272
|
+
return { volume: clamp01(num('volume')) };
|
|
273
|
+
case 'setMuted':
|
|
274
|
+
return { muted: args.muted === true };
|
|
275
|
+
case 'setShuffle':
|
|
276
|
+
return { shuffle: args.shuffle === true };
|
|
277
|
+
case 'seek':
|
|
278
|
+
return {
|
|
279
|
+
position: Math.max(0, num('position')),
|
|
280
|
+
positionUpdatedAt: new Date().toISOString(),
|
|
281
|
+
};
|
|
282
|
+
default:
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
case 'light':
|
|
286
|
+
switch (name) {
|
|
287
|
+
case 'turnOn':
|
|
288
|
+
return { on: true };
|
|
289
|
+
case 'turnOff':
|
|
290
|
+
return { on: false };
|
|
291
|
+
case 'toggle':
|
|
292
|
+
return { on: !entity.on };
|
|
293
|
+
case 'setBrightness': {
|
|
294
|
+
const brightness = clamp01(num('brightness'));
|
|
295
|
+
return brightness === 0 ? { on: false } : { on: true, brightness };
|
|
296
|
+
}
|
|
297
|
+
case 'setColorTemperature':
|
|
298
|
+
return { on: true, color: { mode: 'temperature', kelvin: num('kelvin') } };
|
|
299
|
+
case 'setColor':
|
|
300
|
+
return {
|
|
301
|
+
on: true,
|
|
302
|
+
color: { mode: 'color', hue: num('hue'), saturation: num('saturation') },
|
|
303
|
+
};
|
|
304
|
+
default:
|
|
305
|
+
return {};
|
|
306
|
+
}
|
|
307
|
+
case 'climate':
|
|
308
|
+
switch (name) {
|
|
309
|
+
case 'setMode':
|
|
310
|
+
return { mode: args.mode };
|
|
311
|
+
case 'setTargetTemperature':
|
|
312
|
+
return { targetTemperature: num('temperature') };
|
|
313
|
+
case 'setPreset':
|
|
314
|
+
return { preset: String(args.preset) };
|
|
315
|
+
default:
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
case 'switch':
|
|
319
|
+
switch (name) {
|
|
320
|
+
case 'turnOn':
|
|
321
|
+
return { on: true };
|
|
322
|
+
case 'turnOff':
|
|
323
|
+
return { on: false };
|
|
324
|
+
case 'toggle':
|
|
325
|
+
return { on: !entity.on };
|
|
326
|
+
default:
|
|
327
|
+
return {};
|
|
328
|
+
}
|
|
329
|
+
case 'action':
|
|
330
|
+
return { lastTriggered: new Date().toISOString() };
|
|
331
|
+
default:
|
|
332
|
+
return {};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/** In-memory integration with fixture entities, for tests, the gallery and screenshots. */
|
|
336
|
+
export class MockIntegration extends BaseIntegration {
|
|
337
|
+
id;
|
|
338
|
+
calls = [];
|
|
339
|
+
/** The inputs currently stored, so `update` can patch them. */
|
|
340
|
+
#inputs = new Map();
|
|
341
|
+
#library;
|
|
342
|
+
constructor(options = {}) {
|
|
343
|
+
super();
|
|
344
|
+
this.id = options.id ?? 'ha';
|
|
345
|
+
this.#library = options.library ?? {};
|
|
346
|
+
for (const [entityId, input] of Object.entries(options.entities ?? {})) {
|
|
347
|
+
this.#inputs.set(entityId, input);
|
|
348
|
+
this.setEntity(entityId, input);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
connect() {
|
|
352
|
+
this.setStatus('connected');
|
|
353
|
+
return Promise.resolve();
|
|
354
|
+
}
|
|
355
|
+
disconnect() {
|
|
356
|
+
this.setStatus('disconnected');
|
|
357
|
+
}
|
|
358
|
+
/** Adds an entity, or replaces one. */
|
|
359
|
+
set(entityId, input) {
|
|
360
|
+
this.#inputs.set(entityId, input);
|
|
361
|
+
this.setEntity(entityId, input);
|
|
362
|
+
}
|
|
363
|
+
/** Patches an entity: top-level fields are replaced, `capabilities` and `media` are merged. */
|
|
364
|
+
update(entityId, patch) {
|
|
365
|
+
const current = this.#inputs.get(entityId);
|
|
366
|
+
if (!current) {
|
|
367
|
+
throw new UnknownEntityError(this.id, entityId);
|
|
368
|
+
}
|
|
369
|
+
const now = new Date().toISOString();
|
|
370
|
+
const merged = { ...current, ...patch, lastUpdated: now, lastChanged: now };
|
|
371
|
+
for (const key of ['capabilities', 'media']) {
|
|
372
|
+
const before = current[key];
|
|
373
|
+
const change = patch[key];
|
|
374
|
+
if (typeof before === 'object' && typeof change === 'object' && before && change) {
|
|
375
|
+
merged[key] = { ...before, ...change };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
this.set(entityId, merged);
|
|
379
|
+
}
|
|
380
|
+
browse(entityId, query) {
|
|
381
|
+
if (!this.getEntity(entityId)) {
|
|
382
|
+
return Promise.reject(new UnknownEntityError(this.id, entityId));
|
|
383
|
+
}
|
|
384
|
+
if (query.search !== undefined) {
|
|
385
|
+
const needle = query.search.trim().toLowerCase();
|
|
386
|
+
const seen = new Set();
|
|
387
|
+
const items = Object.values(this.#library)
|
|
388
|
+
.flatMap((folder) => folder.items)
|
|
389
|
+
.filter((item) => {
|
|
390
|
+
const hit = needle !== '' &&
|
|
391
|
+
`${item.title} ${item.subtitle ?? ''}`.toLowerCase().includes(needle) &&
|
|
392
|
+
!seen.has(item.id);
|
|
393
|
+
seen.add(item.id);
|
|
394
|
+
return hit;
|
|
395
|
+
});
|
|
396
|
+
return Promise.resolve({ title: `Results for “${query.search}”`, items });
|
|
397
|
+
}
|
|
398
|
+
const folder = this.#library[query.path ?? 'root'];
|
|
399
|
+
return folder
|
|
400
|
+
? Promise.resolve({ ...(folder.title ? { title: folder.title } : {}), items: folder.items })
|
|
401
|
+
: Promise.reject(new Error(`Nothing at "${query.path}"`));
|
|
402
|
+
}
|
|
403
|
+
#playMedia(entityId, item) {
|
|
404
|
+
const found = Object.values(this.#library)
|
|
405
|
+
.flatMap((folder) => folder.items)
|
|
406
|
+
.find((candidate) => candidate.id === item);
|
|
407
|
+
const current = this.#inputs.get(entityId);
|
|
408
|
+
if (!found?.playable || !current || current.kind !== 'mediaPlayer') {
|
|
409
|
+
return Promise.reject(new Error(`Cannot play "${String(item)}"`));
|
|
410
|
+
}
|
|
411
|
+
const now = new Date().toISOString();
|
|
412
|
+
this.set(entityId, {
|
|
413
|
+
...current,
|
|
414
|
+
playback: 'playing',
|
|
415
|
+
media: {
|
|
416
|
+
title: found.title,
|
|
417
|
+
...(found.subtitle ? { artist: found.subtitle } : {}),
|
|
418
|
+
...(found.artworkUrl ? { artworkUrl: found.artworkUrl } : {}),
|
|
419
|
+
},
|
|
420
|
+
position: 0,
|
|
421
|
+
duration: 215,
|
|
422
|
+
positionUpdatedAt: now,
|
|
423
|
+
lastUpdated: now,
|
|
424
|
+
lastChanged: now,
|
|
425
|
+
});
|
|
426
|
+
return Promise.resolve();
|
|
427
|
+
}
|
|
428
|
+
command(entityId, name, args) {
|
|
429
|
+
this.calls.push({ entityId, command: name, ...(args ? { args } : {}) });
|
|
430
|
+
const entity = this.getEntity(entityId);
|
|
431
|
+
if (!entity) {
|
|
432
|
+
return Promise.reject(new UnknownEntityError(this.id, entityId));
|
|
433
|
+
}
|
|
434
|
+
if (!isCommandOf(entity.kind, name)) {
|
|
435
|
+
return Promise.reject(new Error(`A ${entity.kind} has no "${name}" command`));
|
|
436
|
+
}
|
|
437
|
+
if (entity.kind === 'mediaPlayer' && name === 'playMedia') {
|
|
438
|
+
return this.#playMedia(entityId, args?.item);
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
this.update(entityId, applyCommand(entity, name, args));
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
445
|
+
}
|
|
446
|
+
return Promise.resolve();
|
|
447
|
+
}
|
|
448
|
+
}
|