@smarthome.exchange/api 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/changelog.md +22 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.smarthomeexchangeapiclient.d.ts +46 -0
- package/dist_ts/classes.smarthomeexchangeapiclient.js +133 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -0
- package/dist_ts/plugins.d.ts +8 -0
- package/dist_ts/plugins.js +12 -0
- package/license.md +21 -0
- package/package.json +43 -0
- package/readme.md +154 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.smarthomeexchangeapiclient.ts +180 -0
- package/ts/index.ts +1 -0
- package/ts/plugins.ts +16 -0
package/changelog.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2026-08-10 - 0.1.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add stoppable home-snapshot polling (client)
|
|
8
|
+
- Poll `getHomeSnapshot` with configurable cadence and first-run behavior.
|
|
9
|
+
- Report poll errors without terminating the watcher, wait for in-flight transport work during shutdown, and never wait on user handlers that may stop their own watcher.
|
|
10
|
+
- Reject fractional, sub-millisecond, and timer-overflow polling intervals.
|
|
11
|
+
|
|
12
|
+
### Changes
|
|
13
|
+
|
|
14
|
+
- Migrate HTTP requests to TypedRequest 7 and socket requests to TypedSocket 7.
|
|
15
|
+
- Type request helper method arguments from each request contract's literal `method`.
|
|
16
|
+
- Remove the unused `useSocket` client option.
|
|
17
|
+
- Expose required `live` or `stale` availability with every home snapshot response.
|
|
18
|
+
- Document that v0.1 approval submission records decisions and receipts without resuming or executing tool calls.
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
|
|
22
|
+
- Cover snapshot availability through the direct HTTP TypedRequest path with a loopback server while retaining polling coverage.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autocreated commitinfo by @push.rocks/commitinfo
|
|
3
|
+
*/
|
|
4
|
+
export const commitinfo = {
|
|
5
|
+
name: '@smarthome.exchange/api',
|
|
6
|
+
version: '0.1.0',
|
|
7
|
+
description: 'Typed API client for smarthome.exchange hubs.'
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx5QkFBeUI7SUFDL0IsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLCtDQUErQztDQUM3RCxDQUFBIn0=
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
export interface IShxApiClientOptions {
|
|
3
|
+
hubUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface IHomeSnapshotWatchOptions {
|
|
6
|
+
intervalMs?: number;
|
|
7
|
+
immediate?: boolean;
|
|
8
|
+
onError?: (errorArg: unknown) => void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export type THomeSnapshotWatchCallback = (snapshotResponseArg: plugins.shxInterfaces.request.IReq_GetHomeSnapshot['response']) => void | Promise<void>;
|
|
11
|
+
export type THomeSnapshotWatchStop = () => Promise<void>;
|
|
12
|
+
export declare class SmarthomeExchangeApiClient {
|
|
13
|
+
private hubUrl;
|
|
14
|
+
typedrouter: plugins.typedrequest.TypedRouter<plugins.typedRequestInterfaces.ITypedRequest>;
|
|
15
|
+
typedsocketClient?: plugins.typedsocket.TypedSocket;
|
|
16
|
+
constructor(optionsArg?: IShxApiClientOptions);
|
|
17
|
+
private get httpEndpoint();
|
|
18
|
+
private createWsRequest;
|
|
19
|
+
private createHttpRequest;
|
|
20
|
+
private fire;
|
|
21
|
+
start(): Promise<void>;
|
|
22
|
+
stop(): Promise<void>;
|
|
23
|
+
listDevices(requestArg?: plugins.shxInterfaces.request.IReq_ListDevices['request']): Promise<{
|
|
24
|
+
devices: plugins.shxInterfaces.data.IDeviceDefinition[];
|
|
25
|
+
}>;
|
|
26
|
+
listAgents(): Promise<{
|
|
27
|
+
agents: plugins.shxInterfaces.data.IAgentDefinition[];
|
|
28
|
+
statuses: plugins.shxInterfaces.data.IAgentStatus[];
|
|
29
|
+
}>;
|
|
30
|
+
listTools(requestArg?: plugins.shxInterfaces.request.IReq_ListTools['request']): Promise<{
|
|
31
|
+
tools: plugins.shxInterfaces.data.IToolDefinition[];
|
|
32
|
+
}>;
|
|
33
|
+
getHomeSnapshot(): Promise<{
|
|
34
|
+
availability: plugins.shxInterfaces.data.THomeSnapshotAvailability;
|
|
35
|
+
snapshot: plugins.shxInterfaces.data.IHomeSnapshot;
|
|
36
|
+
}>;
|
|
37
|
+
watchHomeSnapshot(callbackArg: THomeSnapshotWatchCallback, optionsArg?: IHomeSnapshotWatchOptions): THomeSnapshotWatchStop;
|
|
38
|
+
executeToolPlan(planArg: plugins.shxInterfaces.data.IToolPlan): Promise<plugins.shxInterfaces.data.IToolPlanResult>;
|
|
39
|
+
listApprovals(requestArg?: plugins.shxInterfaces.request.IReq_ListApprovals['request']): Promise<{
|
|
40
|
+
approvals: plugins.shxInterfaces.data.IApprovalRequest[];
|
|
41
|
+
}>;
|
|
42
|
+
submitApproval(requestArg: plugins.shxInterfaces.request.IReq_SubmitApproval['request']): Promise<{
|
|
43
|
+
approval: plugins.shxInterfaces.data.IApprovalRequest;
|
|
44
|
+
receipt: plugins.shxInterfaces.data.IAuditReceipt;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
export class SmarthomeExchangeApiClient {
|
|
3
|
+
hubUrl;
|
|
4
|
+
typedrouter = new plugins.typedrequest.TypedRouter();
|
|
5
|
+
typedsocketClient;
|
|
6
|
+
constructor(optionsArg = {}) {
|
|
7
|
+
this.hubUrl = (optionsArg.hubUrl || 'http://localhost:8080').replace(/\/$/, '');
|
|
8
|
+
}
|
|
9
|
+
get httpEndpoint() {
|
|
10
|
+
return `${this.hubUrl}/typedrequest`;
|
|
11
|
+
}
|
|
12
|
+
createWsRequest(operationArg) {
|
|
13
|
+
return this.typedsocketClient?.createTypedRequest(operationArg);
|
|
14
|
+
}
|
|
15
|
+
createHttpRequest(operationArg) {
|
|
16
|
+
return new plugins.typedrequest.TypedRequest(this.httpEndpoint, operationArg);
|
|
17
|
+
}
|
|
18
|
+
async fire(operationArg, payloadArg) {
|
|
19
|
+
const wsRequest = this.createWsRequest(operationArg);
|
|
20
|
+
if (wsRequest) {
|
|
21
|
+
return wsRequest.fire(payloadArg);
|
|
22
|
+
}
|
|
23
|
+
return this.createHttpRequest(operationArg).fire(payloadArg);
|
|
24
|
+
}
|
|
25
|
+
async start() {
|
|
26
|
+
this.typedsocketClient = await plugins.typedsocket.TypedSocket.createClient(this.typedrouter, this.hubUrl);
|
|
27
|
+
}
|
|
28
|
+
async stop() {
|
|
29
|
+
await this.typedsocketClient?.stop();
|
|
30
|
+
this.typedsocketClient = undefined;
|
|
31
|
+
}
|
|
32
|
+
async listDevices(requestArg = {}) {
|
|
33
|
+
return this.fire('listDevices', requestArg);
|
|
34
|
+
}
|
|
35
|
+
async listAgents() {
|
|
36
|
+
return this.fire('listAgents', {});
|
|
37
|
+
}
|
|
38
|
+
async listTools(requestArg = {}) {
|
|
39
|
+
return this.fire('listTools', requestArg);
|
|
40
|
+
}
|
|
41
|
+
async getHomeSnapshot() {
|
|
42
|
+
return this.fire('getHomeSnapshot', {});
|
|
43
|
+
}
|
|
44
|
+
watchHomeSnapshot(callbackArg, optionsArg = {}) {
|
|
45
|
+
const intervalMs = optionsArg.intervalMs ?? 5000;
|
|
46
|
+
if (!Number.isInteger(intervalMs) || intervalMs < 1 || intervalMs > 2_147_483_647) {
|
|
47
|
+
throw new Error('watchHomeSnapshot intervalMs must be an integer from 1 through 2147483647.');
|
|
48
|
+
}
|
|
49
|
+
let stopped = false;
|
|
50
|
+
let timeout;
|
|
51
|
+
let activePoll;
|
|
52
|
+
let userHandlerActive = false;
|
|
53
|
+
const clearScheduledPoll = () => {
|
|
54
|
+
if (timeout) {
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
timeout = undefined;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const scheduleNextPoll = () => {
|
|
60
|
+
if (stopped) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
timeout = setTimeout(() => {
|
|
64
|
+
timeout = undefined;
|
|
65
|
+
activePoll = runPoll();
|
|
66
|
+
}, intervalMs);
|
|
67
|
+
};
|
|
68
|
+
const handlePollError = async (errorArg) => {
|
|
69
|
+
if (stopped) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (optionsArg.onError) {
|
|
73
|
+
userHandlerActive = true;
|
|
74
|
+
try {
|
|
75
|
+
await optionsArg.onError(errorArg);
|
|
76
|
+
}
|
|
77
|
+
catch (errorHandlerError) {
|
|
78
|
+
console.error('watchHomeSnapshot error handler failed:', errorHandlerError);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
userHandlerActive = false;
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.error('watchHomeSnapshot poll failed:', errorArg);
|
|
86
|
+
};
|
|
87
|
+
const runPoll = async () => {
|
|
88
|
+
try {
|
|
89
|
+
const snapshotResponse = await this.getHomeSnapshot();
|
|
90
|
+
if (!stopped) {
|
|
91
|
+
userHandlerActive = true;
|
|
92
|
+
try {
|
|
93
|
+
await callbackArg(snapshotResponse);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
userHandlerActive = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
await handlePollError(error);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
activePoll = undefined;
|
|
105
|
+
scheduleNextPoll();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
if (optionsArg.immediate === false) {
|
|
109
|
+
scheduleNextPoll();
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
activePoll = runPoll();
|
|
113
|
+
}
|
|
114
|
+
return async () => {
|
|
115
|
+
stopped = true;
|
|
116
|
+
clearScheduledPoll();
|
|
117
|
+
const pollToWait = userHandlerActive ? undefined : activePoll;
|
|
118
|
+
await pollToWait;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async executeToolPlan(planArg) {
|
|
122
|
+
return this.fire('executeToolPlan', {
|
|
123
|
+
plan: planArg,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async listApprovals(requestArg = {}) {
|
|
127
|
+
return this.fire('listApprovals', requestArg);
|
|
128
|
+
}
|
|
129
|
+
async submitApproval(requestArg) {
|
|
130
|
+
return this.fire('submitApproval', requestArg);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zbWFydGhvbWVleGNoYW5nZWFwaWNsaWVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2NsYXNzZXMuc21hcnRob21lZXhjaGFuZ2VhcGljbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFrQnhDLE1BQU0sT0FBTywwQkFBMEI7SUFDN0IsTUFBTSxDQUFTO0lBQ2hCLFdBQVcsR0FBRyxJQUFJLE9BQU8sQ0FBQyxZQUFZLENBQUMsV0FBVyxFQUFFLENBQUM7SUFDckQsaUJBQWlCLENBQW1DO0lBRTNELFlBQVksYUFBbUMsRUFBRTtRQUMvQyxJQUFJLENBQUMsTUFBTSxHQUFHLENBQUMsVUFBVSxDQUFDLE1BQU0sSUFBSSx1QkFBdUIsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDbEYsQ0FBQztJQUVELElBQVksWUFBWTtRQUN0QixPQUFPLEdBQUcsSUFBSSxDQUFDLE1BQU0sZUFBZSxDQUFDO0lBQ3ZDLENBQUM7SUFFTyxlQUFlLENBQ3JCLFlBQXlCO1FBRXpCLE9BQU8sSUFBSSxDQUFDLGlCQUFpQixFQUFFLGtCQUFrQixDQUFJLFlBQVksQ0FBQyxDQUFDO0lBQ3JFLENBQUM7SUFFTyxpQkFBaUIsQ0FDdkIsWUFBeUI7UUFFekIsT0FBTyxJQUFJLE9BQU8sQ0FBQyxZQUFZLENBQUMsWUFBWSxDQUFJLElBQUksQ0FBQyxZQUFZLEVBQUUsWUFBWSxDQUFDLENBQUM7SUFDbkYsQ0FBQztJQUVPLEtBQUssQ0FBQyxJQUFJLENBQ2hCLFlBQXlCLEVBQ3pCLFVBQXdCO1FBRXhCLE1BQU0sU0FBUyxHQUFHLElBQUksQ0FBQyxlQUFlLENBQUksWUFBWSxDQUFDLENBQUM7UUFDeEQsSUFBSSxTQUFTLEVBQUUsQ0FBQztZQUNkLE9BQU8sU0FBUyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUNwQyxDQUFD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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './classes.smarthomeexchangeapiclient.js';
|
package/dist_ts/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as shxInterfaces from '@smarthome.exchange/interfaces';
|
|
2
|
+
export { shxInterfaces };
|
|
3
|
+
import * as typedrequest from '@api.global/typedrequest';
|
|
4
|
+
import * as typedRequestInterfaces from '@api.global/typedrequest-interfaces';
|
|
5
|
+
import * as typedsocket from '@api.global/typedsocket';
|
|
6
|
+
export { typedrequest, typedRequestInterfaces, typedsocket };
|
|
7
|
+
import * as smartpromise from '@push.rocks/smartpromise';
|
|
8
|
+
export { smartpromise };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Project scope
|
|
2
|
+
import * as shxInterfaces from '@smarthome.exchange/interfaces';
|
|
3
|
+
export { shxInterfaces };
|
|
4
|
+
// @api.global scope
|
|
5
|
+
import * as typedrequest from '@api.global/typedrequest';
|
|
6
|
+
import * as typedRequestInterfaces from '@api.global/typedrequest-interfaces';
|
|
7
|
+
import * as typedsocket from '@api.global/typedsocket';
|
|
8
|
+
export { typedrequest, typedRequestInterfaces, typedsocket };
|
|
9
|
+
// @push.rocks scope
|
|
10
|
+
import * as smartpromise from '@push.rocks/smartpromise';
|
|
11
|
+
export { smartpromise };
|
|
12
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsZ0JBQWdCO0FBQ2hCLE9BQU8sS0FBSyxhQUFhLE1BQU0sZ0NBQWdDLENBQUM7QUFFaEUsT0FBTyxFQUFFLGFBQWEsRUFBRSxDQUFDO0FBRXpCLG9CQUFvQjtBQUNwQixPQUFPLEtBQUssWUFBWSxNQUFNLDBCQUEwQixDQUFDO0FBQ3pELE9BQU8sS0FBSyxzQkFBc0IsTUFBTSxxQ0FBcUMsQ0FBQztBQUM5RSxPQUFPLEtBQUssV0FBVyxNQUFNLHlCQUF5QixDQUFDO0FBRXZELE9BQU8sRUFBRSxZQUFZLEVBQUUsc0JBQXNCLEVBQUUsV0FBVyxFQUFFLENBQUM7QUFFN0Qsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFFekQsT0FBTyxFQUFFLFlBQVksRUFBRSxDQUFDIn0=
|
package/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Task Venture Capital GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smarthome.exchange/api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Typed API client for smarthome.exchange hubs.",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist_ts/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"author": "Task Venture Capital GmbH",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@api.global/typedrequest": "^7.0.2",
|
|
14
|
+
"@api.global/typedrequest-interfaces": "^7.0.0",
|
|
15
|
+
"@api.global/typedsocket": "^7.1.0",
|
|
16
|
+
"@push.rocks/smartpromise": "^4.2.4",
|
|
17
|
+
"@smarthome.exchange/interfaces": "0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@git.zone/tsbuild": "^4.4.2",
|
|
21
|
+
"@git.zone/tsdoc": "^2.1.1",
|
|
22
|
+
"@git.zone/tsrun": "^2.0.6",
|
|
23
|
+
"@git.zone/tstest": "^4.0.0",
|
|
24
|
+
"@types/node": "^26.2.0"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"ts/**/*",
|
|
28
|
+
"dist/**/*",
|
|
29
|
+
"dist_*/**/*",
|
|
30
|
+
"dist_ts/**/*",
|
|
31
|
+
"readme.md",
|
|
32
|
+
"changelog.md",
|
|
33
|
+
"license.md"
|
|
34
|
+
],
|
|
35
|
+
"browserslist": [
|
|
36
|
+
"last 1 chrome versions"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "tstest test/ --verbose --logfile --timeout 60",
|
|
40
|
+
"build": "tsbuild tsfolders --allowimplicitany",
|
|
41
|
+
"buildDocs": "tsdoc"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @smarthome.exchange/api
|
|
2
|
+
|
|
3
|
+
🔌 Typed API client for connecting tools, CLIs, automations, and apps to a smarthome.exchange hub.
|
|
4
|
+
|
|
5
|
+
This package wraps the hub's TypedRequest boundary in a programmer-friendly client. It speaks HTTP by default, switches to TypedSocket after `start()`, and returns the exact response shapes from `@smarthome.exchange/interfaces`.
|
|
6
|
+
|
|
7
|
+
## Issue Reporting and Security
|
|
8
|
+
|
|
9
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @smarthome.exchange/api
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { SmarthomeExchangeApiClient } from '@smarthome.exchange/api';
|
|
21
|
+
|
|
22
|
+
const client = new SmarthomeExchangeApiClient({
|
|
23
|
+
hubUrl: 'http://localhost:8080',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const snapshotResponse = await client.getHomeSnapshot();
|
|
27
|
+
console.log(snapshotResponse.availability, snapshotResponse.snapshot.home.name);
|
|
28
|
+
|
|
29
|
+
const deviceResponse = await client.listDevices({
|
|
30
|
+
capability: 'light',
|
|
31
|
+
});
|
|
32
|
+
console.log(deviceResponse.devices.map((deviceArg) => deviceArg.name));
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Socket Mode
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const client = new SmarthomeExchangeApiClient({ hubUrl: 'http://localhost:8080' });
|
|
39
|
+
|
|
40
|
+
await client.start();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const agents = await client.listAgents();
|
|
44
|
+
console.log(agents.statuses);
|
|
45
|
+
} finally {
|
|
46
|
+
await client.stop();
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Calling `start()` creates a TypedSocket 7 client and resolves after its package-major compatibility handshake. That handshake establishes transport compatibility only; application authentication and authorization remain server responsibilities. Calls made before `start()` or after `stop()` use HTTP requests against `${hubUrl}/typedrequest`.
|
|
51
|
+
|
|
52
|
+
## Version 7 Transport
|
|
53
|
+
|
|
54
|
+
The client has migrated both transport paths to the version 7 stack: HTTP calls use TypedRequest 7 envelopes and socket calls use TypedSocket 7 with its exact package-major handshake. Request helper method arguments are derived from each shared contract's literal `method`, keeping every client method aligned with `@smarthome.exchange/interfaces`. The socket handshake authorizes transport compatibility only; application authentication and authorization remain server responsibilities.
|
|
55
|
+
|
|
56
|
+
## Client Methods
|
|
57
|
+
|
|
58
|
+
| Method | Typed operation | Returns |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `listDevices({ room?, capability? })` | `listDevices` | `{ devices }` |
|
|
61
|
+
| `listAgents()` | `listAgents` | `{ agents, statuses }` |
|
|
62
|
+
| `listTools(filter?)` | `listTools` | `{ tools }` |
|
|
63
|
+
| `getHomeSnapshot()` | `getHomeSnapshot` | `{ availability, snapshot }` |
|
|
64
|
+
| `watchHomeSnapshot(callback, options?)` | local polling helper | Async stop function |
|
|
65
|
+
| `executeToolPlan(plan)` | `executeToolPlan` | `IToolPlanResult` |
|
|
66
|
+
| `listApprovals(filter?)` | `listApprovals` | `{ approvals }` |
|
|
67
|
+
| `submitApproval(request)` | `submitApproval` | Updated approval result |
|
|
68
|
+
|
|
69
|
+
## Approval Decisions
|
|
70
|
+
|
|
71
|
+
In v0.1, `submitApproval()` is decision-record-only. It records `approved` or `rejected` on the approval and returns the updated record plus its audit receipt. It never resumes or executes the related tool call, and an approved decision must not be interpreted as an execution result.
|
|
72
|
+
|
|
73
|
+
## Snapshot Watching
|
|
74
|
+
|
|
75
|
+
`watchHomeSnapshot(callback, options?)` is a client-local polling helper around `getHomeSnapshot()`. It does not add a server push contract. The callback receives the same `{ availability, snapshot }` response returned by `getHomeSnapshot()`. `availability` is `live` for a successfully served live hub response and `stale` when the response carries stale snapshot data. A raw `IHomeSnapshot` without this response envelope is not automatically live. The async stop function clears the polling timer and waits for active transport work unless a user callback or `onError` handler is already running. It never waits on active user handlers, so they may await their own stop function without a circular wait and external shutdown cannot be held open by user code. A handler already in progress may finish after stopping resolves, but no later poll is scheduled. `intervalMs` defaults to `5000` and must be an integer from `1` through `2147483647`.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const stopWatching = client.watchHomeSnapshot(
|
|
79
|
+
async ({ availability, snapshot }) => {
|
|
80
|
+
console.log(availability, snapshot.home.name, snapshot.devices.length);
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
intervalMs: 5000,
|
|
84
|
+
immediate: true,
|
|
85
|
+
onError: async (errorArg) => {
|
|
86
|
+
console.error('Snapshot refresh failed:', errorArg);
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Later, when the UI or process is shutting down:
|
|
92
|
+
await stopWatching();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Tool Plan Example
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const result = await client.executeToolPlan({
|
|
99
|
+
id: 'plan:dim-living-room',
|
|
100
|
+
callerId: 'app:console',
|
|
101
|
+
title: 'Dim living room lights',
|
|
102
|
+
reason: 'The user requested a 35% living-room brightness level.',
|
|
103
|
+
confidence: 1,
|
|
104
|
+
calls: [{
|
|
105
|
+
id: 'call:living-room-dim',
|
|
106
|
+
toolId: 'device:homeassistant:light.living_room:brightness:write',
|
|
107
|
+
callerId: 'app:console',
|
|
108
|
+
input: { value: 35 },
|
|
109
|
+
}],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
for (const callResult of result.results) {
|
|
113
|
+
if (callResult.status === 'failed') {
|
|
114
|
+
throw new Error(callResult.errorMessage);
|
|
115
|
+
}
|
|
116
|
+
if (callResult.status === 'indeterminate') {
|
|
117
|
+
console.error('Device outcome requires reconciliation:', callResult.errorMessage);
|
|
118
|
+
}
|
|
119
|
+
if (callResult.status === 'queuedForApproval') {
|
|
120
|
+
console.log('Approval required:', callResult.approvalId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Use a tool identifier returned by `listTools()` for a currently normalized integration-owned entity. Do not construct identifiers from seeded demo device IDs: a device without a live integration owner cannot dispatch a physical write and fails closed. Inspect every per-call result because a successful `executeToolPlan()` request may still contain `failed`, `indeterminate`, `suggested`, or `queuedForApproval` outcomes.
|
|
126
|
+
|
|
127
|
+
## Scripts
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pnpm test
|
|
131
|
+
pnpm build
|
|
132
|
+
pnpm buildDocs
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License and Legal Information
|
|
136
|
+
|
|
137
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./license.md) file.
|
|
138
|
+
|
|
139
|
+
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
140
|
+
|
|
141
|
+
### Trademarks
|
|
142
|
+
|
|
143
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
144
|
+
|
|
145
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
146
|
+
|
|
147
|
+
### Company Information
|
|
148
|
+
|
|
149
|
+
Task Venture Capital GmbH\
|
|
150
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
151
|
+
|
|
152
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
153
|
+
|
|
154
|
+
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
export interface IShxApiClientOptions {
|
|
4
|
+
hubUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface IHomeSnapshotWatchOptions {
|
|
8
|
+
intervalMs?: number;
|
|
9
|
+
immediate?: boolean;
|
|
10
|
+
onError?: (errorArg: unknown) => void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type THomeSnapshotWatchCallback = (
|
|
14
|
+
snapshotResponseArg: plugins.shxInterfaces.request.IReq_GetHomeSnapshot['response']
|
|
15
|
+
) => void | Promise<void>;
|
|
16
|
+
|
|
17
|
+
export type THomeSnapshotWatchStop = () => Promise<void>;
|
|
18
|
+
|
|
19
|
+
export class SmarthomeExchangeApiClient {
|
|
20
|
+
private hubUrl: string;
|
|
21
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
22
|
+
public typedsocketClient?: plugins.typedsocket.TypedSocket;
|
|
23
|
+
|
|
24
|
+
constructor(optionsArg: IShxApiClientOptions = {}) {
|
|
25
|
+
this.hubUrl = (optionsArg.hubUrl || 'http://localhost:8080').replace(/\/$/, '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private get httpEndpoint() {
|
|
29
|
+
return `${this.hubUrl}/typedrequest`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private createWsRequest<T extends plugins.typedRequestInterfaces.ITypedRequest>(
|
|
33
|
+
operationArg: T['method']
|
|
34
|
+
) {
|
|
35
|
+
return this.typedsocketClient?.createTypedRequest<T>(operationArg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private createHttpRequest<T extends plugins.typedRequestInterfaces.ITypedRequest>(
|
|
39
|
+
operationArg: T['method']
|
|
40
|
+
) {
|
|
41
|
+
return new plugins.typedrequest.TypedRequest<T>(this.httpEndpoint, operationArg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async fire<T extends plugins.typedRequestInterfaces.ITypedRequest>(
|
|
45
|
+
operationArg: T['method'],
|
|
46
|
+
payloadArg: T['request']
|
|
47
|
+
): Promise<T['response']> {
|
|
48
|
+
const wsRequest = this.createWsRequest<T>(operationArg);
|
|
49
|
+
if (wsRequest) {
|
|
50
|
+
return wsRequest.fire(payloadArg);
|
|
51
|
+
}
|
|
52
|
+
return this.createHttpRequest<T>(operationArg).fire(payloadArg);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async start() {
|
|
56
|
+
this.typedsocketClient = await plugins.typedsocket.TypedSocket.createClient(
|
|
57
|
+
this.typedrouter,
|
|
58
|
+
this.hubUrl
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public async stop() {
|
|
63
|
+
await this.typedsocketClient?.stop();
|
|
64
|
+
this.typedsocketClient = undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public async listDevices(requestArg: plugins.shxInterfaces.request.IReq_ListDevices['request'] = {}) {
|
|
68
|
+
return this.fire<plugins.shxInterfaces.request.IReq_ListDevices>('listDevices', requestArg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async listAgents() {
|
|
72
|
+
return this.fire<plugins.shxInterfaces.request.IReq_ListAgents>('listAgents', {});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async listTools(requestArg: plugins.shxInterfaces.request.IReq_ListTools['request'] = {}) {
|
|
76
|
+
return this.fire<plugins.shxInterfaces.request.IReq_ListTools>('listTools', requestArg);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async getHomeSnapshot() {
|
|
80
|
+
return this.fire<plugins.shxInterfaces.request.IReq_GetHomeSnapshot>('getHomeSnapshot', {});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public watchHomeSnapshot(
|
|
84
|
+
callbackArg: THomeSnapshotWatchCallback,
|
|
85
|
+
optionsArg: IHomeSnapshotWatchOptions = {}
|
|
86
|
+
): THomeSnapshotWatchStop {
|
|
87
|
+
const intervalMs = optionsArg.intervalMs ?? 5000;
|
|
88
|
+
if (!Number.isInteger(intervalMs) || intervalMs < 1 || intervalMs > 2_147_483_647) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
'watchHomeSnapshot intervalMs must be an integer from 1 through 2147483647.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let stopped = false;
|
|
95
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
96
|
+
let activePoll: Promise<void> | undefined;
|
|
97
|
+
let userHandlerActive = false;
|
|
98
|
+
|
|
99
|
+
const clearScheduledPoll = () => {
|
|
100
|
+
if (timeout) {
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
timeout = undefined;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const scheduleNextPoll = () => {
|
|
107
|
+
if (stopped) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
timeout = setTimeout(() => {
|
|
111
|
+
timeout = undefined;
|
|
112
|
+
activePoll = runPoll();
|
|
113
|
+
}, intervalMs);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handlePollError = async (errorArg: unknown) => {
|
|
117
|
+
if (stopped) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (optionsArg.onError) {
|
|
121
|
+
userHandlerActive = true;
|
|
122
|
+
try {
|
|
123
|
+
await optionsArg.onError(errorArg);
|
|
124
|
+
} catch (errorHandlerError) {
|
|
125
|
+
console.error('watchHomeSnapshot error handler failed:', errorHandlerError);
|
|
126
|
+
} finally {
|
|
127
|
+
userHandlerActive = false;
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.error('watchHomeSnapshot poll failed:', errorArg);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const runPoll = async () => {
|
|
135
|
+
try {
|
|
136
|
+
const snapshotResponse = await this.getHomeSnapshot();
|
|
137
|
+
if (!stopped) {
|
|
138
|
+
userHandlerActive = true;
|
|
139
|
+
try {
|
|
140
|
+
await callbackArg(snapshotResponse);
|
|
141
|
+
} finally {
|
|
142
|
+
userHandlerActive = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
await handlePollError(error);
|
|
147
|
+
} finally {
|
|
148
|
+
activePoll = undefined;
|
|
149
|
+
scheduleNextPoll();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (optionsArg.immediate === false) {
|
|
154
|
+
scheduleNextPoll();
|
|
155
|
+
} else {
|
|
156
|
+
activePoll = runPoll();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return async () => {
|
|
160
|
+
stopped = true;
|
|
161
|
+
clearScheduledPoll();
|
|
162
|
+
const pollToWait = userHandlerActive ? undefined : activePoll;
|
|
163
|
+
await pollToWait;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public async executeToolPlan(planArg: plugins.shxInterfaces.data.IToolPlan) {
|
|
168
|
+
return this.fire<plugins.shxInterfaces.request.IReq_ExecuteToolPlan>('executeToolPlan', {
|
|
169
|
+
plan: planArg,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public async listApprovals(requestArg: plugins.shxInterfaces.request.IReq_ListApprovals['request'] = {}) {
|
|
174
|
+
return this.fire<plugins.shxInterfaces.request.IReq_ListApprovals>('listApprovals', requestArg);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public async submitApproval(requestArg: plugins.shxInterfaces.request.IReq_SubmitApproval['request']) {
|
|
178
|
+
return this.fire<plugins.shxInterfaces.request.IReq_SubmitApproval>('submitApproval', requestArg);
|
|
179
|
+
}
|
|
180
|
+
}
|
package/ts/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './classes.smarthomeexchangeapiclient.js';
|
package/ts/plugins.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Project scope
|
|
2
|
+
import * as shxInterfaces from '@smarthome.exchange/interfaces';
|
|
3
|
+
|
|
4
|
+
export { shxInterfaces };
|
|
5
|
+
|
|
6
|
+
// @api.global scope
|
|
7
|
+
import * as typedrequest from '@api.global/typedrequest';
|
|
8
|
+
import * as typedRequestInterfaces from '@api.global/typedrequest-interfaces';
|
|
9
|
+
import * as typedsocket from '@api.global/typedsocket';
|
|
10
|
+
|
|
11
|
+
export { typedrequest, typedRequestInterfaces, typedsocket };
|
|
12
|
+
|
|
13
|
+
// @push.rocks scope
|
|
14
|
+
import * as smartpromise from '@push.rocks/smartpromise';
|
|
15
|
+
|
|
16
|
+
export { smartpromise };
|