@switchbot/openapi-cli 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +34 -42
  2. package/dist/index.js +56945 -169
  3. package/dist/policy/schema/v0.2.json +1 -1
  4. package/package.json +3 -2
  5. package/dist/api/client.js +0 -235
  6. package/dist/auth.js +0 -20
  7. package/dist/commands/agent-bootstrap.js +0 -182
  8. package/dist/commands/auth.js +0 -354
  9. package/dist/commands/batch.js +0 -413
  10. package/dist/commands/cache.js +0 -126
  11. package/dist/commands/capabilities.js +0 -385
  12. package/dist/commands/catalog.js +0 -359
  13. package/dist/commands/completion.js +0 -385
  14. package/dist/commands/config.js +0 -376
  15. package/dist/commands/daemon.js +0 -367
  16. package/dist/commands/device-meta.js +0 -159
  17. package/dist/commands/devices.js +0 -948
  18. package/dist/commands/doctor.js +0 -1015
  19. package/dist/commands/events.js +0 -563
  20. package/dist/commands/expand.js +0 -130
  21. package/dist/commands/explain.js +0 -139
  22. package/dist/commands/health.js +0 -113
  23. package/dist/commands/history.js +0 -320
  24. package/dist/commands/identity.js +0 -59
  25. package/dist/commands/install.js +0 -246
  26. package/dist/commands/mcp.js +0 -2017
  27. package/dist/commands/plan.js +0 -653
  28. package/dist/commands/policy.js +0 -586
  29. package/dist/commands/quota.js +0 -78
  30. package/dist/commands/rules.js +0 -875
  31. package/dist/commands/scenes.js +0 -264
  32. package/dist/commands/schema.js +0 -177
  33. package/dist/commands/status-sync.js +0 -131
  34. package/dist/commands/uninstall.js +0 -237
  35. package/dist/commands/upgrade-check.js +0 -88
  36. package/dist/commands/watch.js +0 -194
  37. package/dist/commands/webhook.js +0 -182
  38. package/dist/config.js +0 -258
  39. package/dist/credentials/backends/file.js +0 -101
  40. package/dist/credentials/backends/linux.js +0 -129
  41. package/dist/credentials/backends/macos.js +0 -129
  42. package/dist/credentials/backends/windows.js +0 -215
  43. package/dist/credentials/keychain.js +0 -88
  44. package/dist/credentials/prime.js +0 -52
  45. package/dist/devices/cache.js +0 -293
  46. package/dist/devices/catalog.js +0 -767
  47. package/dist/devices/device-meta.js +0 -56
  48. package/dist/devices/history-agg.js +0 -138
  49. package/dist/devices/history-query.js +0 -181
  50. package/dist/devices/param-validator.js +0 -433
  51. package/dist/devices/resources.js +0 -270
  52. package/dist/install/default-steps.js +0 -257
  53. package/dist/install/preflight.js +0 -212
  54. package/dist/install/steps.js +0 -67
  55. package/dist/lib/command-keywords.js +0 -17
  56. package/dist/lib/daemon-state.js +0 -46
  57. package/dist/lib/destructive-mode.js +0 -12
  58. package/dist/lib/devices.js +0 -382
  59. package/dist/lib/idempotency.js +0 -106
  60. package/dist/lib/plan-store.js +0 -68
  61. package/dist/lib/request-context.js +0 -12
  62. package/dist/lib/scenes.js +0 -10
  63. package/dist/logger.js +0 -16
  64. package/dist/mcp/device-history.js +0 -145
  65. package/dist/mcp/events-subscription.js +0 -213
  66. package/dist/mqtt/client.js +0 -180
  67. package/dist/mqtt/credential.js +0 -30
  68. package/dist/policy/add-rule.js +0 -124
  69. package/dist/policy/diff.js +0 -91
  70. package/dist/policy/format.js +0 -57
  71. package/dist/policy/load.js +0 -61
  72. package/dist/policy/migrate.js +0 -67
  73. package/dist/policy/schema.js +0 -18
  74. package/dist/policy/validate.js +0 -262
  75. package/dist/rules/action.js +0 -205
  76. package/dist/rules/audit-query.js +0 -89
  77. package/dist/rules/conflict-analyzer.js +0 -203
  78. package/dist/rules/cron-scheduler.js +0 -186
  79. package/dist/rules/destructive.js +0 -52
  80. package/dist/rules/engine.js +0 -757
  81. package/dist/rules/matcher.js +0 -230
  82. package/dist/rules/pid-file.js +0 -95
  83. package/dist/rules/quiet-hours.js +0 -45
  84. package/dist/rules/suggest.js +0 -95
  85. package/dist/rules/throttle.js +0 -116
  86. package/dist/rules/types.js +0 -34
  87. package/dist/rules/webhook-listener.js +0 -223
  88. package/dist/rules/webhook-token.js +0 -90
  89. package/dist/schema/field-aliases.js +0 -131
  90. package/dist/sinks/dispatcher.js +0 -12
  91. package/dist/sinks/file.js +0 -19
  92. package/dist/sinks/format.js +0 -56
  93. package/dist/sinks/homeassistant.js +0 -44
  94. package/dist/sinks/openclaw.js +0 -33
  95. package/dist/sinks/stdout.js +0 -5
  96. package/dist/sinks/telegram.js +0 -28
  97. package/dist/sinks/types.js +0 -1
  98. package/dist/sinks/webhook.js +0 -22
  99. package/dist/status-sync/manager.js +0 -268
  100. package/dist/utils/arg-parsers.js +0 -66
  101. package/dist/utils/audit.js +0 -117
  102. package/dist/utils/filter.js +0 -189
  103. package/dist/utils/flags.js +0 -186
  104. package/dist/utils/format.js +0 -117
  105. package/dist/utils/health.js +0 -101
  106. package/dist/utils/help-json.js +0 -54
  107. package/dist/utils/name-resolver.js +0 -137
  108. package/dist/utils/output.js +0 -404
  109. package/dist/utils/quota.js +0 -227
  110. package/dist/utils/redact.js +0 -68
  111. package/dist/utils/retry.js +0 -140
  112. package/dist/utils/string.js +0 -22
  113. package/dist/version.js +0 -4
@@ -1,223 +0,0 @@
1
- /**
2
- * Local HTTP listener that delivers webhook events to the rules engine.
3
- *
4
- * Scope (E2):
5
- * - Binds to `127.0.0.1` only — the loopback interface keeps the
6
- * listener off the network by default. The plan's integration story
7
- * is that an agent or local script POSTs to this endpoint.
8
- * - Default port is 18790 (phase-3 design doc choice); override with
9
- * `--webhook-port <n>` in `switchbot rules run`. `--webhook-port 0`
10
- * asks the OS for an ephemeral port — useful in tests.
11
- * - Bearer-token auth on every request: `Authorization: Bearer <t>`.
12
- * The expected token comes from `WebhookTokenStore`; unauthorized
13
- * requests get a 401 with no body, no hint about which header
14
- * failed, and an audit entry (`rule-webhook-rejected`).
15
- * - Matches request path against registered webhook rules: only
16
- * `POST /path/exactly/as/declared`. Unknown paths return 404.
17
- *
18
- * Non-goals:
19
- * - No TLS; operators who expose this outside loopback are expected
20
- * to sit behind a reverse proxy that terminates TLS.
21
- * - No payload parsing beyond reading the body as a string — the
22
- * engine passes the raw body through in the event payload.
23
- */
24
- import http from 'node:http';
25
- import { timingSafeEqual } from 'node:crypto';
26
- import { writeAudit } from '../utils/audit.js';
27
- import { isWebhookTrigger } from './types.js';
28
- export const DEFAULT_WEBHOOK_PORT = 18790;
29
- const MAX_BODY_BYTES = 16 * 1024; // guard against huge POSTs from misbehaving callers
30
- export class WebhookListener {
31
- opts;
32
- server = null;
33
- pathIndex = new Map();
34
- actualPort = null;
35
- constructor(opts) {
36
- this.opts = opts;
37
- for (const rule of opts.rules) {
38
- if (!isWebhookTrigger(rule.when))
39
- continue;
40
- const normalised = normalisePath(rule.when.path);
41
- if (this.pathIndex.has(normalised)) {
42
- throw new Error(`WebhookListener: duplicate webhook path "${normalised}" — every webhook rule needs a unique path`);
43
- }
44
- this.pathIndex.set(normalised, rule);
45
- }
46
- }
47
- /** Start listening. Resolves once the server has bound a port. */
48
- async start() {
49
- if (this.server)
50
- return;
51
- const server = http.createServer((req, res) => {
52
- this.handle(req, res).catch((err) => {
53
- // The dispatch chain should never reject — but if it does,
54
- // make sure we close the socket so the caller doesn't hang.
55
- if (!res.headersSent) {
56
- res.writeHead(500);
57
- res.end();
58
- }
59
- // eslint-disable-next-line no-console
60
- console.error(`webhook-listener: unhandled dispatch error: ${err instanceof Error ? err.message : String(err)}`);
61
- });
62
- });
63
- const host = this.opts.host ?? '127.0.0.1';
64
- const port = this.opts.port ?? DEFAULT_WEBHOOK_PORT;
65
- await new Promise((resolve, reject) => {
66
- const onError = (err) => {
67
- server.off('listening', onListening);
68
- reject(err);
69
- };
70
- const onListening = () => {
71
- server.off('error', onError);
72
- resolve();
73
- };
74
- server.once('error', onError);
75
- server.once('listening', onListening);
76
- server.listen(port, host);
77
- });
78
- const address = server.address();
79
- this.actualPort = typeof address === 'object' && address ? address.port : port;
80
- this.server = server;
81
- }
82
- async stop() {
83
- if (!this.server)
84
- return;
85
- const server = this.server;
86
- this.server = null;
87
- this.actualPort = null;
88
- await new Promise((resolve) => server.close(() => resolve()));
89
- }
90
- getPort() {
91
- return this.actualPort;
92
- }
93
- listPaths() {
94
- return [...this.pathIndex.keys()].sort();
95
- }
96
- /**
97
- * Replace the current rule → path index. Used by `engine.reload`: the
98
- * listener keeps its open port and accepted connections, but routes
99
- * subsequent requests against the fresh policy.
100
- */
101
- updateRules(rules) {
102
- const next = new Map();
103
- for (const rule of rules) {
104
- if (!isWebhookTrigger(rule.when))
105
- continue;
106
- const normalised = normalisePath(rule.when.path);
107
- if (next.has(normalised)) {
108
- throw new Error(`WebhookListener.updateRules: duplicate webhook path "${normalised}"`);
109
- }
110
- next.set(normalised, rule);
111
- }
112
- this.pathIndex.clear();
113
- for (const [k, v] of next)
114
- this.pathIndex.set(k, v);
115
- }
116
- async handle(req, res) {
117
- // Auth gate first — reject everything else so a wrong token never
118
- // reveals which paths exist.
119
- if (!this.isAuthorized(req)) {
120
- writeAudit({
121
- t: this.now().toISOString(),
122
- kind: 'rule-webhook-rejected',
123
- deviceId: 'unknown',
124
- command: req.url ?? '',
125
- parameter: null,
126
- commandType: 'command',
127
- dryRun: true,
128
- result: 'error',
129
- error: 'unauthorized',
130
- });
131
- res.writeHead(401);
132
- res.end();
133
- return;
134
- }
135
- if (req.method !== 'POST') {
136
- res.writeHead(405, { Allow: 'POST' });
137
- res.end();
138
- return;
139
- }
140
- const reqUrl = req.url ?? '/';
141
- const questionMarkIdx = reqUrl.indexOf('?');
142
- const rawPath = questionMarkIdx === -1 ? reqUrl : reqUrl.slice(0, questionMarkIdx);
143
- const normalised = normalisePath(rawPath);
144
- const rule = this.pathIndex.get(normalised);
145
- if (!rule) {
146
- writeAudit({
147
- t: this.now().toISOString(),
148
- kind: 'rule-webhook-rejected',
149
- deviceId: 'unknown',
150
- command: rawPath,
151
- parameter: null,
152
- commandType: 'command',
153
- dryRun: true,
154
- result: 'error',
155
- error: 'unknown-path',
156
- });
157
- res.writeHead(404);
158
- res.end();
159
- return;
160
- }
161
- const body = await readLimitedBody(req, MAX_BODY_BYTES);
162
- if (body === null) {
163
- res.writeHead(413);
164
- res.end();
165
- return;
166
- }
167
- const event = {
168
- source: 'webhook',
169
- event: normalised,
170
- t: this.now(),
171
- payload: { path: normalised, body },
172
- };
173
- // Accept the request before dispatch so callers aren't held waiting
174
- // on rule actions (which can include SwitchBot API calls).
175
- res.writeHead(202, { 'Content-Type': 'application/json' });
176
- res.end(JSON.stringify({ status: 'accepted', path: normalised }));
177
- this.opts.dispatch(rule, event).catch(() => undefined);
178
- }
179
- isAuthorized(req) {
180
- const h = req.headers['authorization'];
181
- if (typeof h !== 'string')
182
- return false;
183
- const match = /^Bearer\s+(.+)$/i.exec(h.trim());
184
- if (!match)
185
- return false;
186
- const provided = Buffer.from(match[1].trim(), 'utf-8');
187
- const expected = Buffer.from(this.opts.bearerToken, 'utf-8');
188
- if (provided.length !== expected.length)
189
- return false;
190
- return timingSafeEqual(provided, expected);
191
- }
192
- now() {
193
- return this.opts.now ? this.opts.now() : new Date();
194
- }
195
- }
196
- function normalisePath(p) {
197
- if (!p)
198
- return '/';
199
- let out = p.trim();
200
- if (!out.startsWith('/'))
201
- out = `/${out}`;
202
- // Collapse a trailing slash (but leave the root '/').
203
- if (out.length > 1 && out.endsWith('/'))
204
- out = out.slice(0, -1);
205
- return out;
206
- }
207
- function readLimitedBody(req, max) {
208
- return new Promise((resolve, reject) => {
209
- const chunks = [];
210
- let total = 0;
211
- req.on('data', (chunk) => {
212
- total += chunk.length;
213
- if (total > max) {
214
- req.destroy();
215
- resolve(null);
216
- return;
217
- }
218
- chunks.push(chunk);
219
- });
220
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
221
- req.on('error', reject);
222
- });
223
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * Webhook bearer-token management for the rules engine.
3
- *
4
- * Responsibilities:
5
- * - Resolve the bearer token the listener will accept. The order is
6
- * env var (SWITCHBOT_WEBHOOK_TOKEN) → on-disk cache
7
- * (~/.switchbot/webhook-token, chmod 0600) → generate a fresh
8
- * 32-byte hex token and persist it.
9
- * - Rotate the token on demand (`rules webhook-rotate-token` cli).
10
- *
11
- * Why not the OS keychain (F1 abstraction)? The webhook bearer is a
12
- * single opaque string, whereas `CredentialStore` is shaped around the
13
- * SwitchBot {token,secret} bundle. Fitting a one-field artifact into
14
- * that contract bloats every profile; keeping it in a 0600 file gives
15
- * the same protection the CLI has used for `~/.switchbot/config.json`.
16
- * Promotion into the keychain is a future follow-up once the
17
- * abstraction grows a generic single-value slot.
18
- */
19
- import fs from 'node:fs';
20
- import os from 'node:os';
21
- import path from 'node:path';
22
- import { randomBytes } from 'node:crypto';
23
- const ENV_TOKEN = 'SWITCHBOT_WEBHOOK_TOKEN';
24
- const DEFAULT_FILE = '.switchbot/webhook-token';
25
- export class WebhookTokenStore {
26
- filePath;
27
- envLookup;
28
- constructor(opts = {}) {
29
- this.filePath = opts.filePath ?? path.join(os.homedir(), DEFAULT_FILE);
30
- this.envLookup = opts.envLookup ?? (() => process.env[ENV_TOKEN]);
31
- }
32
- /**
33
- * Return a bearer token, creating + persisting one if none exists yet.
34
- * Env var wins when set; otherwise the on-disk token is read (and
35
- * generated on first call).
36
- */
37
- getOrCreate() {
38
- const fromEnv = this.envLookup();
39
- if (fromEnv && fromEnv.trim().length > 0)
40
- return fromEnv.trim();
41
- const existing = this.readFromDisk();
42
- if (existing)
43
- return existing;
44
- const fresh = generateToken();
45
- this.writeToDisk(fresh);
46
- return fresh;
47
- }
48
- /**
49
- * Read the persisted token, returning null when the file is absent
50
- * or empty. Does NOT consult the env var — callers that want the
51
- * env-aware path should use `getOrCreate()`.
52
- */
53
- readFromDisk() {
54
- try {
55
- const raw = fs.readFileSync(this.filePath, 'utf-8').trim();
56
- return raw.length > 0 ? raw : null;
57
- }
58
- catch (err) {
59
- if (err.code === 'ENOENT')
60
- return null;
61
- throw err;
62
- }
63
- }
64
- /** Write a new token, persisting with 0600 perms. */
65
- rotate() {
66
- const fresh = generateToken();
67
- this.writeToDisk(fresh);
68
- return fresh;
69
- }
70
- getFilePath() {
71
- return this.filePath;
72
- }
73
- writeToDisk(token) {
74
- const dir = path.dirname(this.filePath);
75
- fs.mkdirSync(dir, { recursive: true });
76
- fs.writeFileSync(this.filePath, `${token}\n`, { mode: 0o600 });
77
- // mkdirSync + writeFileSync race can leave broader perms on Windows
78
- // (perm bits are mostly advisory there anyway), but on POSIX we
79
- // re-chmod to be explicit about intent.
80
- try {
81
- fs.chmodSync(this.filePath, 0o600);
82
- }
83
- catch {
84
- // non-POSIX filesystems may reject chmod — intentional best effort.
85
- }
86
- }
87
- }
88
- export function generateToken() {
89
- return randomBytes(32).toString('hex');
90
- }
@@ -1,131 +0,0 @@
1
- import { UsageError } from '../utils/output.js';
2
- /**
3
- * User-facing aliases for canonical field names.
4
- *
5
- * Keys are canonical names (matching API response keys and CLI/schema output);
6
- * values are lowercase alternatives a user may type for `--fields` or `--filter`.
7
- *
8
- * Conflict rules (do not add an alias that violates these — tests will fail):
9
- * - `temp` is exclusive to `temperature` (NOT `colorTemperature`, `targetTemperature`).
10
- * - `motion` is exclusive to `moveDetected`; `moving` uses `active` instead.
11
- * - `mode` is exclusive to top-level `mode` (preset); device-specific modes go through `deviceMode`.
12
- * - Reserved / too-generic words never appear as aliases: `auto`, `status`, `state`,
13
- * `switch`, `type`, `on`, `off`.
14
- * - Device-type words are never aliases: `lock`, `fan`.
15
- */
16
- export const FIELD_ALIASES = {
17
- // Identification (shared with list/filter)
18
- deviceId: ['id'],
19
- deviceName: ['name'],
20
- deviceType: ['type'],
21
- controlType: ['control'],
22
- roomName: ['room'],
23
- roomID: ['roomid'],
24
- familyName: ['family'],
25
- hubDeviceId: ['hub'],
26
- enableCloudService: ['cloud'],
27
- alias: ['alias'],
28
- // Phase 1 — common status fields
29
- battery: ['batt', 'bat'],
30
- temperature: ['temp', 'ambient'],
31
- colorTemperature: ['kelvin', 'colortemp'],
32
- humidity: ['humid', 'rh'],
33
- brightness: ['bright', 'bri'],
34
- fanSpeed: ['speed'],
35
- position: ['pos'],
36
- moveDetected: ['motion'],
37
- openState: ['open'],
38
- doorState: ['door'],
39
- CO2: ['co2'],
40
- power: ['enabled'],
41
- mode: ['preset'],
42
- // Phase 2 — niche device fields
43
- childLock: ['safe', 'childlock'],
44
- targetTemperature: ['setpoint', 'target'],
45
- electricCurrent: ['current', 'amps'],
46
- voltage: ['volts'],
47
- usedElectricity: ['energy', 'kwh'],
48
- electricityOfDay: ['daily', 'today'],
49
- weight: ['load'],
50
- version: ['firmware', 'fw'],
51
- lightLevel: ['light', 'lux'],
52
- oscillation: ['swing', 'osc'],
53
- verticalOscillation: ['vswing'],
54
- nightStatus: ['night'],
55
- chargingStatus: ['charging', 'charge'],
56
- switch1Status: ['ch1', 'channel1'],
57
- switch2Status: ['ch2', 'channel2'],
58
- taskType: ['task'],
59
- moving: ['active'],
60
- onlineStatus: ['online_status'],
61
- workingStatus: ['working'],
62
- // Phase 3 — catalog statusFields coverage
63
- group: ['cluster'],
64
- calibrate: ['calibration', 'calib'],
65
- direction: ['tilt'],
66
- deviceMode: ['devmode'],
67
- nebulizationEfficiency: ['mist', 'spray'],
68
- sound: ['audio'],
69
- lackWater: ['tank', 'water-low'],
70
- filterElement: ['filter'],
71
- color: ['rgb', 'hex'],
72
- useTime: ['runtime', 'uptime'],
73
- switchStatus: ['relay'],
74
- lockState: ['locked'],
75
- slidePosition: ['slide'],
76
- // Phase 4 — ultra-niche sensor + webhook fields (~98% coverage target)
77
- waterLeakDetect: ['leak', 'water'],
78
- pressure: ['press', 'pa'],
79
- moveCount: ['movecnt'],
80
- errorCode: ['err'],
81
- buttonName: ['btn', 'button'],
82
- pressedAt: ['pressed'],
83
- deviceMac: ['mac'],
84
- detectionState: ['detected', 'detect'],
85
- };
86
- /**
87
- * Resolve a user-typed field name to its canonical form against an allowed list.
88
- *
89
- * Matching is case-insensitive and trims surrounding whitespace. Direct matches
90
- * win over alias matches. Throws UsageError if the input is empty or does not
91
- * match any canonical / alias in the allowed list.
92
- */
93
- export function resolveField(input, allowedCanonical) {
94
- const normalized = input.trim().toLowerCase();
95
- if (!normalized) {
96
- throw new UsageError('Field name cannot be empty.');
97
- }
98
- for (const canonical of allowedCanonical) {
99
- if (canonical.toLowerCase() === normalized)
100
- return canonical;
101
- }
102
- for (const canonical of allowedCanonical) {
103
- const aliases = FIELD_ALIASES[canonical] ?? [];
104
- if (aliases.some((a) => a.toLowerCase() === normalized))
105
- return canonical;
106
- }
107
- throw new UsageError(`Unknown field "${input}". Supported: ${listSupportedFieldInputs(allowedCanonical).join(', ')}`);
108
- }
109
- /**
110
- * Resolve every field in a list. Preserves order and the original UsageError
111
- * from resolveField() on the first unknown input.
112
- */
113
- export function resolveFieldList(inputs, allowedCanonical) {
114
- return inputs.map((f) => resolveField(f, allowedCanonical));
115
- }
116
- export function listSupportedFieldInputs(allowedCanonical) {
117
- const out = new Set();
118
- for (const canonical of allowedCanonical) {
119
- out.add(canonical);
120
- for (const alias of FIELD_ALIASES[canonical] ?? [])
121
- out.add(alias);
122
- }
123
- return [...out];
124
- }
125
- /**
126
- * All canonical keys known to the alias registry. Use when no dynamic
127
- * canonical list is available (e.g. `watch` before the first poll response).
128
- */
129
- export function listAllCanonical() {
130
- return Object.keys(FIELD_ALIASES);
131
- }
@@ -1,12 +0,0 @@
1
- export class SinkDispatcher {
2
- sinks;
3
- constructor(sinks) {
4
- this.sinks = sinks;
5
- }
6
- async dispatch(event) {
7
- await Promise.allSettled(this.sinks.map((s) => s.write(event)));
8
- }
9
- async close() {
10
- await Promise.allSettled(this.sinks.map((s) => s.close?.()));
11
- }
12
- }
@@ -1,19 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- export class FileSink {
4
- filePath;
5
- constructor(filePath) {
6
- this.filePath = path.resolve(filePath);
7
- const dir = path.dirname(this.filePath);
8
- if (!fs.existsSync(dir))
9
- fs.mkdirSync(dir, { recursive: true });
10
- }
11
- async write(event) {
12
- try {
13
- fs.appendFileSync(this.filePath, JSON.stringify(event) + '\n', { encoding: 'utf-8' });
14
- }
15
- catch {
16
- // best-effort
17
- }
18
- }
19
- }
@@ -1,56 +0,0 @@
1
- const ICONS = {
2
- 'Bot': '🤖',
3
- 'Curtain': '🪟',
4
- 'Hub': '📡',
5
- 'Hub 2': '📡',
6
- 'Hub 3': '📡',
7
- 'Hub Mini': '📡',
8
- 'Smart Lock': '🔒',
9
- 'Smart Lock Pro': '🔒',
10
- 'Plug': '🔌',
11
- 'Plug Mini (US)': '🔌',
12
- 'Plug Mini (JP)': '🔌',
13
- 'Color Bulb': '💡',
14
- 'Strip Light': '💡',
15
- 'Contact Sensor': '🚪',
16
- 'Motion Sensor': '👁',
17
- 'Meter': '🌡',
18
- 'MeterPro': '🌡',
19
- 'Climate Panel': '🌡',
20
- 'WoMeter': '🌡',
21
- 'WoIOSensor': '🌡',
22
- };
23
- function icon(deviceType) {
24
- return ICONS[deviceType] ?? '📱';
25
- }
26
- export function formatEventText(context) {
27
- const type = context.deviceType ?? 'Unknown';
28
- const pfx = `${icon(type)} ${type}`;
29
- const parts = [];
30
- if (context.temperature !== undefined)
31
- parts.push(`${context.temperature}°C`);
32
- if (context.humidity !== undefined)
33
- parts.push(`${context.humidity}%`);
34
- if (parts.length)
35
- return `${pfx}: ${parts.join(' / ')}`;
36
- if (context.power !== undefined)
37
- return `${pfx}: ${context.power}`;
38
- if (context.lockState !== undefined)
39
- return `${pfx}: ${context.lockState}`;
40
- if (context.openState !== undefined)
41
- return `${pfx}: ${context.openState}`;
42
- if (context.detectionState !== undefined)
43
- return `${pfx}: ${context.detectionState}`;
44
- if (context.brightness !== undefined)
45
- return `${pfx}: ${context.brightness}`;
46
- return `${pfx}: state change`;
47
- }
48
- export function parseSinkEvent(payload) {
49
- const p = payload;
50
- const context = (p?.context ?? {});
51
- return {
52
- deviceId: String(context.deviceMac ?? 'unknown'),
53
- deviceType: String(context.deviceType ?? 'Unknown'),
54
- text: formatEventText(context),
55
- };
56
- }
@@ -1,44 +0,0 @@
1
- export class HomeAssistantSink {
2
- url;
3
- token;
4
- webhookId;
5
- eventType;
6
- constructor(opts) {
7
- this.url = opts.url.replace(/\/$/, '');
8
- this.token = opts.token;
9
- this.webhookId = opts.webhookId;
10
- this.eventType = opts.eventType ?? 'switchbot_event';
11
- }
12
- async write(event) {
13
- try {
14
- let endpoint;
15
- const headers = { 'content-type': 'application/json' };
16
- if (this.webhookId) {
17
- // Webhook mode: no auth needed, HA triggers automations directly
18
- endpoint = `${this.url}/api/webhook/${this.webhookId}`;
19
- }
20
- else if (this.token) {
21
- // REST event API: fires a custom event on the HA event bus
22
- endpoint = `${this.url}/api/events/${this.eventType}`;
23
- headers['authorization'] = `Bearer ${this.token}`;
24
- }
25
- else {
26
- console.error('[homeassistant] requires --ha-webhook-id or --ha-token');
27
- return;
28
- }
29
- const res = await fetch(endpoint, {
30
- method: 'POST',
31
- headers,
32
- body: JSON.stringify(event),
33
- signal: AbortSignal.timeout(10000),
34
- });
35
- if (!res.ok) {
36
- const body = await res.text().catch(() => '');
37
- console.error(`[homeassistant] POST failed: HTTP ${res.status} ${body.slice(0, 200)}`);
38
- }
39
- }
40
- catch (err) {
41
- console.error(`[homeassistant] error: ${err instanceof Error ? err.message : String(err)}`);
42
- }
43
- }
44
- }
@@ -1,33 +0,0 @@
1
- export class OpenClawSink {
2
- url;
3
- token;
4
- model;
5
- constructor(opts) {
6
- this.url = (opts.url ?? 'http://localhost:18789').replace(/\/$/, '');
7
- this.token = opts.token;
8
- this.model = opts.model;
9
- }
10
- async write(event) {
11
- try {
12
- const res = await fetch(`${this.url}/v1/chat/completions`, {
13
- method: 'POST',
14
- headers: {
15
- 'content-type': 'application/json',
16
- 'authorization': `Bearer ${this.token}`,
17
- },
18
- body: JSON.stringify({
19
- model: this.model,
20
- messages: [{ role: 'user', content: event.text }],
21
- }),
22
- signal: AbortSignal.timeout(10000),
23
- });
24
- if (!res.ok) {
25
- const body = await res.text().catch(() => '');
26
- console.error(`[openclaw] POST failed: HTTP ${res.status} ${body.slice(0, 200)}`);
27
- }
28
- }
29
- catch (err) {
30
- console.error(`[openclaw] error: ${err instanceof Error ? err.message : String(err)}`);
31
- }
32
- }
33
- }
@@ -1,5 +0,0 @@
1
- export class StdoutSink {
2
- async write(event) {
3
- console.log(JSON.stringify(event));
4
- }
5
- }
@@ -1,28 +0,0 @@
1
- export class TelegramSink {
2
- token;
3
- chatId;
4
- constructor(opts) {
5
- this.token = opts.token;
6
- this.chatId = opts.chatId;
7
- }
8
- async write(event) {
9
- try {
10
- const res = await fetch(`https://api.telegram.org/bot${this.token}/sendMessage`, {
11
- method: 'POST',
12
- headers: { 'content-type': 'application/json' },
13
- body: JSON.stringify({
14
- chat_id: this.chatId,
15
- text: event.text,
16
- }),
17
- signal: AbortSignal.timeout(10000),
18
- });
19
- if (!res.ok) {
20
- const body = await res.text().catch(() => '');
21
- console.error(`[telegram] POST failed: HTTP ${res.status} ${body.slice(0, 200)}`);
22
- }
23
- }
24
- catch (err) {
25
- console.error(`[telegram] error: ${err instanceof Error ? err.message : String(err)}`);
26
- }
27
- }
28
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,22 +0,0 @@
1
- export class WebhookSink {
2
- url;
3
- constructor(url) {
4
- this.url = url;
5
- }
6
- async write(event) {
7
- try {
8
- const res = await fetch(this.url, {
9
- method: 'POST',
10
- headers: { 'content-type': 'application/json' },
11
- body: JSON.stringify(event),
12
- signal: AbortSignal.timeout(10000),
13
- });
14
- if (!res.ok) {
15
- console.error(`[webhook] POST failed: HTTP ${res.status}`);
16
- }
17
- }
18
- catch (err) {
19
- console.error(`[webhook] error: ${err instanceof Error ? err.message : String(err)}`);
20
- }
21
- }
22
- }