@nubisco/openbridge 0.29.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 +21 -0
- package/README.md +306 -0
- package/dist/__tests__/homekit-service-type.test.d.ts +2 -0
- package/dist/__tests__/homekit-service-type.test.d.ts.map +1 -0
- package/dist/__tests__/homekit-service-type.test.js +239 -0
- package/dist/__tests__/homekit-service-type.test.js.map +1 -0
- package/dist/__tests__/homekit-visibility.test.d.ts +2 -0
- package/dist/__tests__/homekit-visibility.test.d.ts.map +1 -0
- package/dist/__tests__/homekit-visibility.test.js +157 -0
- package/dist/__tests__/homekit-visibility.test.js.map +1 -0
- package/dist/__tests__/server.test.d.ts +2 -0
- package/dist/__tests__/server.test.d.ts.map +1 -0
- package/dist/__tests__/server.test.js +837 -0
- package/dist/__tests__/server.test.js.map +1 -0
- package/dist/__tests__/timeseries.test.d.ts +2 -0
- package/dist/__tests__/timeseries.test.d.ts.map +1 -0
- package/dist/__tests__/timeseries.test.js +216 -0
- package/dist/__tests__/timeseries.test.js.map +1 -0
- package/dist/auth.d.ts +23 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +171 -0
- package/dist/auth.js.map +1 -0
- package/dist/daemon.d.ts +57 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +636 -0
- package/dist/daemon.js.map +1 -0
- package/dist/homekit-service-type.d.ts +82 -0
- package/dist/homekit-service-type.d.ts.map +1 -0
- package/dist/homekit-service-type.js +184 -0
- package/dist/homekit-service-type.js.map +1 -0
- package/dist/homekit-visibility.d.ts +79 -0
- package/dist/homekit-visibility.d.ts.map +1 -0
- package/dist/homekit-visibility.js +164 -0
- package/dist/homekit-visibility.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +17 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +52 -0
- package/dist/metrics.js.map +1 -0
- package/dist/platform-client.d.ts +42 -0
- package/dist/platform-client.d.ts.map +1 -0
- package/dist/platform-client.js +92 -0
- package/dist/platform-client.js.map +1 -0
- package/dist/server.d.ts +30 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1744 -0
- package/dist/server.js.map +1 -0
- package/dist/timeseries.d.ts +134 -0
- package/dist/timeseries.d.ts.map +1 -0
- package/dist/timeseries.js +378 -0
- package/dist/timeseries.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +28 -0
- package/dist/version.js.map +1 -0
- package/package.json +79 -0
- package/scripts/entrypoint.sh +82 -0
- package/scripts/fix-node-pty.mjs +22 -0
- package/ui-dist/android-chrome-192x192.png +0 -0
- package/ui-dist/android-chrome-512x512.png +0 -0
- package/ui-dist/apple-touch-icon.png +0 -0
- package/ui-dist/assets/index-Q0sxZHIR.css +32 -0
- package/ui-dist/assets/index-ZZYyAzBo.js +106 -0
- package/ui-dist/favicon-16x16.png +0 -0
- package/ui-dist/favicon-32x32.png +0 -0
- package/ui-dist/favicon.ico +0 -0
- package/ui-dist/fonts/MesloLGLDZNerdFontMono-Bold.woff2 +0 -0
- package/ui-dist/fonts/MesloLGLDZNerdFontMono-BoldItalic.woff2 +0 -0
- package/ui-dist/fonts/MesloLGLDZNerdFontMono-Italic.woff2 +0 -0
- package/ui-dist/fonts/MesloLGLDZNerdFontMono-Regular.woff2 +0 -0
- package/ui-dist/index.html +18 -0
- package/ui-dist/logo.svg +30 -0
- package/ui-dist/nubisco-logo.svg +120 -0
- package/ui-dist/site.webmanifest +19 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, openSync, readSync, closeSync, statSync, appendFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { readFileSync, renameSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Default tiers: fine detail recently, coarse detail for a long time.
|
|
6
|
+
*
|
|
7
|
+
* Roughly 6.4 MB per device for six metrics over five years, against 9.8 MB for
|
|
8
|
+
* one metric over two years in the format this replaces.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_TIERS = [
|
|
11
|
+
{ name: 'raw', interval: 10, retention: 48 * 3600 },
|
|
12
|
+
{ name: 'minute', interval: 60, retention: 30 * 86400 },
|
|
13
|
+
{ name: 'fiveminute', interval: 300, retention: 365 * 86400 },
|
|
14
|
+
{ name: 'hour', interval: 3600, retention: 5 * 365 * 86400 },
|
|
15
|
+
];
|
|
16
|
+
const TS_BYTES = 4;
|
|
17
|
+
const VALUE_BYTES = 4;
|
|
18
|
+
/** True when this metric keeps min/max alongside the mean in rolled-up tiers. */
|
|
19
|
+
export function tracksExtremes(metric) {
|
|
20
|
+
if (metric.trackExtremes !== undefined)
|
|
21
|
+
return metric.trackExtremes;
|
|
22
|
+
return metric.kind === 'instant';
|
|
23
|
+
}
|
|
24
|
+
/** Byte width of one record for the given metric set. */
|
|
25
|
+
export function recordSize(metrics) {
|
|
26
|
+
return TS_BYTES + metrics.reduce((sum, m) => sum + VALUE_BYTES * (tracksExtremes(m) ? 3 : 1), 0);
|
|
27
|
+
}
|
|
28
|
+
/** Byte offset of a metric's value within a record. */
|
|
29
|
+
function metricOffset(metrics, index) {
|
|
30
|
+
let offset = TS_BYTES;
|
|
31
|
+
for (let i = 0; i < index; i++)
|
|
32
|
+
offset += VALUE_BYTES * (tracksExtremes(metrics[i]) ? 3 : 1);
|
|
33
|
+
return offset;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A device's history: one file per tier, all sharing the same metric layout.
|
|
37
|
+
*
|
|
38
|
+
* The layout is derived from the device's declared metrics. If a plugin changes
|
|
39
|
+
* its metric set between releases the record width changes with it, so the
|
|
40
|
+
* layout is written alongside the data and mismatched files are rotated out
|
|
41
|
+
* rather than silently misread.
|
|
42
|
+
*/
|
|
43
|
+
export class DeviceSeries {
|
|
44
|
+
dir;
|
|
45
|
+
deviceId;
|
|
46
|
+
metrics;
|
|
47
|
+
tiers;
|
|
48
|
+
recSize;
|
|
49
|
+
constructor(dir, deviceId, metrics, tiers = DEFAULT_TIERS) {
|
|
50
|
+
this.dir = dir;
|
|
51
|
+
this.deviceId = deviceId;
|
|
52
|
+
this.metrics = metrics;
|
|
53
|
+
this.tiers = tiers;
|
|
54
|
+
this.recSize = recordSize(metrics);
|
|
55
|
+
}
|
|
56
|
+
tierPath(tier) {
|
|
57
|
+
return join(this.dir, `${safeName(this.deviceId)}.${tier.name}.bin`);
|
|
58
|
+
}
|
|
59
|
+
layoutPath() {
|
|
60
|
+
return join(this.dir, `${safeName(this.deviceId)}.layout.json`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Ensure the on-disk layout matches the current metric set.
|
|
64
|
+
*
|
|
65
|
+
* A changed metric set makes every existing record unreadable, since the
|
|
66
|
+
* record width and field order both shift. Rotating the files aside loses
|
|
67
|
+
* history but never returns wrong numbers, which is the right trade for
|
|
68
|
+
* measurements people make decisions from.
|
|
69
|
+
*/
|
|
70
|
+
ensureLayout() {
|
|
71
|
+
mkdirSync(this.dir, { recursive: true });
|
|
72
|
+
const layout = {
|
|
73
|
+
recordSize: this.recSize,
|
|
74
|
+
metrics: this.metrics.map((m) => ({ key: m.key, extremes: tracksExtremes(m) })),
|
|
75
|
+
};
|
|
76
|
+
const path = this.layoutPath();
|
|
77
|
+
if (existsSync(path)) {
|
|
78
|
+
try {
|
|
79
|
+
const existing = JSON.parse(readFileSync(path, 'utf8'));
|
|
80
|
+
if (JSON.stringify(existing) === JSON.stringify(layout))
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* unreadable layout: treat as a mismatch */
|
|
85
|
+
}
|
|
86
|
+
const stamp = Math.floor(nowSeconds());
|
|
87
|
+
for (const tier of this.tiers) {
|
|
88
|
+
const file = this.tierPath(tier);
|
|
89
|
+
if (existsSync(file))
|
|
90
|
+
renameSync(file, `${file}.${stamp}.old`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
writeFileSync(path, JSON.stringify(layout));
|
|
94
|
+
}
|
|
95
|
+
/** Encode one sample into a record buffer. */
|
|
96
|
+
encode(sample, extremes) {
|
|
97
|
+
const buf = Buffer.alloc(this.recSize);
|
|
98
|
+
buf.writeUInt32LE(sample.t >>> 0, 0);
|
|
99
|
+
this.metrics.forEach((metric, index) => {
|
|
100
|
+
const offset = metricOffset(this.metrics, index);
|
|
101
|
+
const value = numeric(sample.values[metric.key]);
|
|
102
|
+
buf.writeFloatLE(value, offset);
|
|
103
|
+
if (tracksExtremes(metric)) {
|
|
104
|
+
const ext = extremes?.[metric.key];
|
|
105
|
+
buf.writeFloatLE(ext ? numeric(ext.min) : value, offset + 4);
|
|
106
|
+
buf.writeFloatLE(ext ? numeric(ext.max) : value, offset + 8);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return buf;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Append a sample to the finest tier.
|
|
113
|
+
*
|
|
114
|
+
* Records must stay time-ordered for range reads to work, so a sample that is
|
|
115
|
+
* not newer than the last one is dropped rather than appended out of order.
|
|
116
|
+
*/
|
|
117
|
+
append(sample) {
|
|
118
|
+
this.ensureLayout();
|
|
119
|
+
const tier = this.tiers[0];
|
|
120
|
+
const path = this.tierPath(tier);
|
|
121
|
+
const last = this.lastTimestamp(path);
|
|
122
|
+
if (last !== null && sample.t <= last)
|
|
123
|
+
return false;
|
|
124
|
+
appendFileSync(path, this.encode(sample));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
lastTimestamp(path) {
|
|
128
|
+
if (!existsSync(path))
|
|
129
|
+
return null;
|
|
130
|
+
const size = statSync(path).size;
|
|
131
|
+
if (size < this.recSize)
|
|
132
|
+
return null;
|
|
133
|
+
const fd = openSync(path, 'r');
|
|
134
|
+
try {
|
|
135
|
+
const buf = Buffer.alloc(TS_BYTES);
|
|
136
|
+
readSync(fd, buf, 0, TS_BYTES, size - this.recSize);
|
|
137
|
+
return buf.readUInt32LE(0);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
closeSync(fd);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Read every record in a tier whose timestamp falls within [from, to]. */
|
|
144
|
+
readTier(tier, from, to) {
|
|
145
|
+
const path = this.tierPath(tier);
|
|
146
|
+
const result = this.metrics.map(() => []);
|
|
147
|
+
if (!existsSync(path))
|
|
148
|
+
return result;
|
|
149
|
+
const size = statSync(path).size;
|
|
150
|
+
const count = Math.floor(size / this.recSize);
|
|
151
|
+
if (count === 0)
|
|
152
|
+
return result;
|
|
153
|
+
const fd = openSync(path, 'r');
|
|
154
|
+
try {
|
|
155
|
+
// Records are time-ordered and fixed-width, so the range is found by
|
|
156
|
+
// binary search rather than by scanning the file.
|
|
157
|
+
const startIndex = this.seek(fd, count, from);
|
|
158
|
+
if (startIndex >= count)
|
|
159
|
+
return result;
|
|
160
|
+
const span = count - startIndex;
|
|
161
|
+
const buf = Buffer.alloc(span * this.recSize);
|
|
162
|
+
readSync(fd, buf, 0, buf.length, startIndex * this.recSize);
|
|
163
|
+
for (let i = 0; i < span; i++) {
|
|
164
|
+
const base = i * this.recSize;
|
|
165
|
+
const t = buf.readUInt32LE(base);
|
|
166
|
+
if (t < from)
|
|
167
|
+
continue;
|
|
168
|
+
if (t > to)
|
|
169
|
+
break;
|
|
170
|
+
this.metrics.forEach((metric, index) => {
|
|
171
|
+
const offset = base + metricOffset(this.metrics, index);
|
|
172
|
+
const point = { t, value: buf.readFloatLE(offset) };
|
|
173
|
+
if (tracksExtremes(metric)) {
|
|
174
|
+
point.min = buf.readFloatLE(offset + 4);
|
|
175
|
+
point.max = buf.readFloatLE(offset + 8);
|
|
176
|
+
}
|
|
177
|
+
result[index].push(point);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
closeSync(fd);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Index of the first record at or after `from`. */
|
|
187
|
+
seek(fd, count, from) {
|
|
188
|
+
const buf = Buffer.alloc(TS_BYTES);
|
|
189
|
+
let lo = 0;
|
|
190
|
+
let hi = count;
|
|
191
|
+
while (lo < hi) {
|
|
192
|
+
const mid = (lo + hi) >> 1;
|
|
193
|
+
readSync(fd, buf, 0, TS_BYTES, mid * this.recSize);
|
|
194
|
+
if (buf.readUInt32LE(0) < from)
|
|
195
|
+
lo = mid + 1;
|
|
196
|
+
else
|
|
197
|
+
hi = mid;
|
|
198
|
+
}
|
|
199
|
+
return lo;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Pick the finest tier that covers the requested range without returning an
|
|
203
|
+
* unreasonable number of points to draw.
|
|
204
|
+
*/
|
|
205
|
+
chooseTier(from, to, maxPoints = 720) {
|
|
206
|
+
const span = Math.max(1, to - from);
|
|
207
|
+
for (const tier of this.tiers) {
|
|
208
|
+
const covers = span <= tier.retention;
|
|
209
|
+
const points = span / tier.interval;
|
|
210
|
+
if (covers && points <= maxPoints)
|
|
211
|
+
return tier;
|
|
212
|
+
}
|
|
213
|
+
return this.tiers[this.tiers.length - 1];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Read one metric over a range, choosing the tier automatically.
|
|
217
|
+
*
|
|
218
|
+
* The chosen tier can legitimately be empty: rollup only populates coarser
|
|
219
|
+
* tiers once windows complete, so a device installed ten minutes ago has raw
|
|
220
|
+
* data and nothing else. Falling back to finer tiers means the chart shows
|
|
221
|
+
* what exists instead of nothing. If a fallback tier holds more points than
|
|
222
|
+
* the caller can draw, it is decimated by striding rather than truncated, so
|
|
223
|
+
* the shape of the range is preserved.
|
|
224
|
+
*/
|
|
225
|
+
query(metricKey, from, to, maxPoints = 720) {
|
|
226
|
+
const index = this.metrics.findIndex((m) => m.key === metricKey);
|
|
227
|
+
const chosen = this.chooseTier(from, to, maxPoints);
|
|
228
|
+
if (index === -1)
|
|
229
|
+
return { tier: chosen, points: [] };
|
|
230
|
+
const start = this.tiers.indexOf(chosen);
|
|
231
|
+
for (let i = start; i >= 0; i--) {
|
|
232
|
+
const tier = this.tiers[i];
|
|
233
|
+
const points = this.readTier(tier, from, to)[index];
|
|
234
|
+
if (points.length > 0)
|
|
235
|
+
return { tier, points: decimate(points, maxPoints) };
|
|
236
|
+
}
|
|
237
|
+
return { tier: chosen, points: [] };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Roll finished windows from each tier into the next coarser one, then drop
|
|
241
|
+
* records past the tier's retention.
|
|
242
|
+
*
|
|
243
|
+
* Only windows that have fully elapsed are rolled up, so a partially filled
|
|
244
|
+
* bucket is never written as if it were complete.
|
|
245
|
+
*/
|
|
246
|
+
rollup(now = nowSeconds()) {
|
|
247
|
+
for (let i = 0; i < this.tiers.length - 1; i++) {
|
|
248
|
+
const source = this.tiers[i];
|
|
249
|
+
const target = this.tiers[i + 1];
|
|
250
|
+
const targetPath = this.tierPath(target);
|
|
251
|
+
const lastTarget = this.lastTimestamp(targetPath);
|
|
252
|
+
const windowStart = lastTarget === null ? 0 : lastTarget + target.interval;
|
|
253
|
+
const lastCompleteWindow = Math.floor(now / target.interval) * target.interval;
|
|
254
|
+
if (windowStart >= lastCompleteWindow)
|
|
255
|
+
continue;
|
|
256
|
+
const series = this.readTier(source, windowStart, lastCompleteWindow - 1);
|
|
257
|
+
if (series.every((points) => points.length === 0))
|
|
258
|
+
continue;
|
|
259
|
+
// Group source points by target window.
|
|
260
|
+
const windows = new Map();
|
|
261
|
+
series.forEach((points, metricIndex) => {
|
|
262
|
+
for (const point of points) {
|
|
263
|
+
const bucket = Math.floor(point.t / target.interval) * target.interval;
|
|
264
|
+
if (bucket >= lastCompleteWindow)
|
|
265
|
+
continue;
|
|
266
|
+
if (!windows.has(bucket))
|
|
267
|
+
windows.set(bucket, this.metrics.map(() => []));
|
|
268
|
+
windows.get(bucket)[metricIndex].push(point);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
const buckets = [...windows.keys()].sort((a, b) => a - b);
|
|
272
|
+
for (const bucket of buckets) {
|
|
273
|
+
const grouped = windows.get(bucket);
|
|
274
|
+
const values = {};
|
|
275
|
+
const extremes = {};
|
|
276
|
+
this.metrics.forEach((metric, index) => {
|
|
277
|
+
const points = grouped[index].filter((p) => p.value !== null && Number.isFinite(p.value));
|
|
278
|
+
if (points.length === 0)
|
|
279
|
+
return;
|
|
280
|
+
if (metric.kind === 'cumulative') {
|
|
281
|
+
// A counter's value for a window is where it ended up.
|
|
282
|
+
values[metric.key] = points[points.length - 1].value;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const sum = points.reduce((acc, p) => acc + p.value, 0);
|
|
286
|
+
values[metric.key] = sum / points.length;
|
|
287
|
+
}
|
|
288
|
+
if (tracksExtremes(metric)) {
|
|
289
|
+
const mins = points.map((p) => (p.min ?? p.value));
|
|
290
|
+
const maxes = points.map((p) => (p.max ?? p.value));
|
|
291
|
+
extremes[metric.key] = { min: Math.min(...mins), max: Math.max(...maxes) };
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
appendFileSync(targetPath, this.encode({ t: bucket, values }, extremes));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
this.prune(now);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Drop records older than each tier's retention.
|
|
301
|
+
*
|
|
302
|
+
* Records are time-ordered, so trimming is a copy of the surviving tail
|
|
303
|
+
* rather than a filter of the whole file.
|
|
304
|
+
*/
|
|
305
|
+
prune(now = nowSeconds()) {
|
|
306
|
+
for (const tier of this.tiers) {
|
|
307
|
+
const path = this.tierPath(tier);
|
|
308
|
+
if (!existsSync(path))
|
|
309
|
+
continue;
|
|
310
|
+
const cutoff = now - tier.retention;
|
|
311
|
+
const size = statSync(path).size;
|
|
312
|
+
const count = Math.floor(size / this.recSize);
|
|
313
|
+
if (count === 0)
|
|
314
|
+
continue;
|
|
315
|
+
const fd = openSync(path, 'r');
|
|
316
|
+
let keepFrom;
|
|
317
|
+
try {
|
|
318
|
+
keepFrom = this.seek(fd, count, cutoff);
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
closeSync(fd);
|
|
322
|
+
}
|
|
323
|
+
if (keepFrom === 0)
|
|
324
|
+
continue;
|
|
325
|
+
if (keepFrom >= count) {
|
|
326
|
+
writeFileSync(path, Buffer.alloc(0));
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const fdRead = openSync(path, 'r');
|
|
330
|
+
try {
|
|
331
|
+
const span = count - keepFrom;
|
|
332
|
+
const buf = Buffer.alloc(span * this.recSize);
|
|
333
|
+
readSync(fdRead, buf, 0, buf.length, keepFrom * this.recSize);
|
|
334
|
+
writeFileSync(path, buf);
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
closeSync(fdRead);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/** Total bytes on disk across all tiers. */
|
|
342
|
+
diskUsage() {
|
|
343
|
+
return this.tiers.reduce((sum, tier) => {
|
|
344
|
+
const path = this.tierPath(tier);
|
|
345
|
+
return sum + (existsSync(path) ? statSync(path).size : 0);
|
|
346
|
+
}, 0);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Reduce a series to at most `maxPoints` by striding.
|
|
351
|
+
*
|
|
352
|
+
* The last point is always kept: on a live chart the newest reading is the one
|
|
353
|
+
* people look at, and dropping it makes the line appear to lag.
|
|
354
|
+
*/
|
|
355
|
+
function decimate(points, maxPoints) {
|
|
356
|
+
if (points.length <= maxPoints || maxPoints <= 0)
|
|
357
|
+
return points;
|
|
358
|
+
const stride = Math.ceil(points.length / maxPoints);
|
|
359
|
+
const out = [];
|
|
360
|
+
for (let i = 0; i < points.length; i += stride)
|
|
361
|
+
out.push(points[i]);
|
|
362
|
+
const last = points[points.length - 1];
|
|
363
|
+
if (out[out.length - 1] !== last)
|
|
364
|
+
out.push(last);
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
function numeric(value) {
|
|
368
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
369
|
+
return Number.isFinite(n) ? n : 0;
|
|
370
|
+
}
|
|
371
|
+
/** Device ids come from plugins, so they must not be trusted as path segments. */
|
|
372
|
+
export function safeName(deviceId) {
|
|
373
|
+
return deviceId.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
374
|
+
}
|
|
375
|
+
function nowSeconds() {
|
|
376
|
+
return Math.floor(Date.now() / 1000);
|
|
377
|
+
}
|
|
378
|
+
//# sourceMappingURL=timeseries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeseries.js","sourceRoot":"","sources":["../src/timeseries.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,UAAU,oBAAoB,CAAA;AAC3C,eAAO,MAAM,YAAY,QAAmC,CAAA;AAY5D,eAAO,MAAM,kBAAkB,EAAE,MAAyB,CAAA"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version resolution, kept in its own module so that `openbridge --help` and
|
|
3
|
+
* `--version` can answer without importing the server (and with it Fastify,
|
|
4
|
+
* hap-nodejs and the whole plugin surface).
|
|
5
|
+
*
|
|
6
|
+
* Order of preference: the volume's version.json (written by the container
|
|
7
|
+
* entrypoint or by self-update), then OPENBRIDGE_VERSION, then our package.json.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
const _req = createRequire(import.meta.url);
|
|
13
|
+
const _ownPkg = _req('../package.json');
|
|
14
|
+
export const APP_VOLUME = '/opt/openbridge';
|
|
15
|
+
export const VERSION_FILE = join(APP_VOLUME, 'version.json');
|
|
16
|
+
function resolveVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const vf = JSON.parse(readFileSync(VERSION_FILE, 'utf8'));
|
|
19
|
+
if (vf.version)
|
|
20
|
+
return vf.version;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* not on volume */
|
|
24
|
+
}
|
|
25
|
+
return process.env.OPENBRIDGE_VERSION ?? _ownPkg.version;
|
|
26
|
+
}
|
|
27
|
+
export const OPENBRIDGE_VERSION = resolveVersion();
|
|
28
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAA;AAEtC,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAwB,CAAA;AAE9D,MAAM,CAAC,MAAM,UAAU,GAAG,iBAAiB,CAAA;AAC3C,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAA;AAE5D,SAAS,cAAc;IACrB,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAA;QACzD,IAAI,EAAE,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC,OAAO,CAAA;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,mBAAmB;IACrB,CAAC;IACD,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAA;AAC1D,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAW,cAAc,EAAE,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nubisco/openbridge",
|
|
3
|
+
"version": "0.29.0",
|
|
4
|
+
"description": "Local-first home automation bridge for developers. Plugin-based, TypeScript-first, Homebridge-compatible.",
|
|
5
|
+
"author": "José Silva <jose@nubisco.io>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/nubisco/openbridge#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/nubisco/openbridge.git",
|
|
11
|
+
"directory": "apps/daemon"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/nubisco/openbridge/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"homebridge",
|
|
18
|
+
"homekit",
|
|
19
|
+
"home-automation",
|
|
20
|
+
"hap",
|
|
21
|
+
"smart-home",
|
|
22
|
+
"plugin",
|
|
23
|
+
"bridge",
|
|
24
|
+
"typescript",
|
|
25
|
+
"docker",
|
|
26
|
+
"nas",
|
|
27
|
+
"synology"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"bin": {
|
|
32
|
+
"openbridge": "./dist/index.js",
|
|
33
|
+
"openbridge-daemon": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/",
|
|
37
|
+
"ui-dist/",
|
|
38
|
+
"scripts/",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@fastify/cors": "^9.0.0",
|
|
50
|
+
"@fastify/static": "^7.0.1",
|
|
51
|
+
"@fastify/websocket": "^8.3.0",
|
|
52
|
+
"fastify": "^4.26.0",
|
|
53
|
+
"hap-nodejs": "^0.14.3",
|
|
54
|
+
"qrcode-terminal": "^0.12.0",
|
|
55
|
+
"systeminformation": "^5.31.5",
|
|
56
|
+
"@nubisco/openbridge-compatibility-homebridge": "0.29.0",
|
|
57
|
+
"@nubisco/openbridge-logger": "0.29.0",
|
|
58
|
+
"@nubisco/openbridge-config": "0.29.0",
|
|
59
|
+
"@nubisco/openbridge-core": "0.29.0"
|
|
60
|
+
},
|
|
61
|
+
"optionalDependencies": {
|
|
62
|
+
"node-pty": "^1.1.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/node": "^20.0.0",
|
|
66
|
+
"tsx": "^4.7.0",
|
|
67
|
+
"typescript": "^5.4.0",
|
|
68
|
+
"vitest": "^3.2.0"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "tsc",
|
|
72
|
+
"dev": "OPENBRIDGE_DEV=true tsx watch --include $HOME/.openbridge/plugins/openbridge src/index.ts",
|
|
73
|
+
"start": "node dist/index.js",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"test:watch": "vitest",
|
|
76
|
+
"typecheck": "tsc --noEmit",
|
|
77
|
+
"postinstall": "node scripts/fix-node-pty.mjs"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# OpenBridge entrypoint — manages the volume overlay for self-updates.
|
|
3
|
+
#
|
|
4
|
+
# On first boot, copies the baked-in /app to the persistent volume.
|
|
5
|
+
# On subsequent boots, runs from the volume (which may have been self-updated).
|
|
6
|
+
# If the Docker image has a different build hash, re-copies from /app
|
|
7
|
+
# (unless the volume was self-updated to a newer version).
|
|
8
|
+
|
|
9
|
+
set -e
|
|
10
|
+
|
|
11
|
+
APP_VOLUME="/opt/openbridge"
|
|
12
|
+
APP_DIR="$APP_VOLUME/current"
|
|
13
|
+
BAKED_DIR="/app"
|
|
14
|
+
VERSION_FILE="$APP_VOLUME/version.json"
|
|
15
|
+
BAKED_VERSION="${OPENBRIDGE_VERSION:-0.0.0}"
|
|
16
|
+
BAKED_HASH="${OPENBRIDGE_BUILD_HASH:-unknown}"
|
|
17
|
+
|
|
18
|
+
should_bootstrap() {
|
|
19
|
+
# No version file = first boot
|
|
20
|
+
if [ ! -f "$VERSION_FILE" ]; then
|
|
21
|
+
echo "First boot — initialising from Docker image v$BAKED_VERSION ($BAKED_HASH)"
|
|
22
|
+
return 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
VOLUME_VERSION=$(node -e "try{const j=JSON.parse(require('fs').readFileSync('$VERSION_FILE','utf8'));process.stdout.write(j.version||'')}catch{}")
|
|
26
|
+
VOLUME_HASH=$(node -e "try{const j=JSON.parse(require('fs').readFileSync('$VERSION_FILE','utf8'));process.stdout.write(j.buildHash||'')}catch{}")
|
|
27
|
+
VOLUME_SOURCE=$(node -e "try{const j=JSON.parse(require('fs').readFileSync('$VERSION_FILE','utf8'));process.stdout.write(j.source||'')}catch{}")
|
|
28
|
+
|
|
29
|
+
if [ -z "$VOLUME_VERSION" ]; then
|
|
30
|
+
echo "Corrupt version file — re-initialising from Docker image"
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# If the volume was self-updated, only overwrite if the baked-in version is strictly newer
|
|
35
|
+
if [ "$VOLUME_SOURCE" = "self-update" ]; then
|
|
36
|
+
IS_NEWER=$(node -e "
|
|
37
|
+
const baked = '$BAKED_VERSION'.split('.').map(Number);
|
|
38
|
+
const vol = '$VOLUME_VERSION'.split('.').map(Number);
|
|
39
|
+
const newer = baked[0] > vol[0] || (baked[0] === vol[0] && baked[1] > vol[1]) || (baked[0] === vol[0] && baked[1] === vol[1] && baked[2] > vol[2]);
|
|
40
|
+
process.stdout.write(newer ? 'yes' : 'no');
|
|
41
|
+
")
|
|
42
|
+
if [ "$IS_NEWER" = "yes" ]; then
|
|
43
|
+
echo "Docker image v$BAKED_VERSION is newer than self-updated v$VOLUME_VERSION — updating"
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
echo "Running from self-updated volume v$VOLUME_VERSION"
|
|
47
|
+
return 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# For docker-image sourced volumes: re-copy if build hash differs
|
|
51
|
+
if [ "$BAKED_HASH" != "unknown" ] && [ "$BAKED_HASH" != "$VOLUME_HASH" ]; then
|
|
52
|
+
echo "Docker image build changed ($BAKED_HASH vs $VOLUME_HASH) — updating volume"
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if should_bootstrap; then
|
|
60
|
+
rm -rf "$APP_DIR"
|
|
61
|
+
mkdir -p "$APP_DIR"
|
|
62
|
+
cp -a "$BAKED_DIR/." "$APP_DIR/"
|
|
63
|
+
|
|
64
|
+
cat > "$VERSION_FILE" <<EOF
|
|
65
|
+
{
|
|
66
|
+
"version": "$BAKED_VERSION",
|
|
67
|
+
"buildHash": "$BAKED_HASH",
|
|
68
|
+
"arch": "$(uname -m)",
|
|
69
|
+
"source": "docker-image",
|
|
70
|
+
"installedAt": "$(date -Iseconds)"
|
|
71
|
+
}
|
|
72
|
+
EOF
|
|
73
|
+
echo "Initialised v$BAKED_VERSION ($BAKED_HASH) on volume"
|
|
74
|
+
else
|
|
75
|
+
VOLUME_VERSION=$(cat "$VERSION_FILE" | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
76
|
+
echo "Running from volume v$VOLUME_VERSION"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Fix node-pty spawn-helper permissions
|
|
80
|
+
find "$APP_DIR/apps/daemon/node_modules" -name "spawn-helper" -exec chmod +x {} \; 2>/dev/null || true
|
|
81
|
+
|
|
82
|
+
exec node "$APP_DIR/apps/daemon/dist/index.js"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Fixes execute permission on node-pty's spawn-helper binary.
|
|
4
|
+
* pnpm does not preserve +x on prebuild binaries, causing posix_spawnp to fail.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, chmodSync } from 'fs'
|
|
7
|
+
import { createRequire } from 'module'
|
|
8
|
+
import { dirname, resolve, join } from 'path'
|
|
9
|
+
|
|
10
|
+
const req = createRequire(import.meta.url)
|
|
11
|
+
try {
|
|
12
|
+
const nodePtyMain = req.resolve('node-pty')
|
|
13
|
+
const nodePtyDir = resolve(dirname(nodePtyMain), '..')
|
|
14
|
+
const arch = `${process.platform}-${process.arch}`
|
|
15
|
+
const spawnHelper = join(nodePtyDir, 'prebuilds', arch, 'spawn-helper')
|
|
16
|
+
if (existsSync(spawnHelper)) {
|
|
17
|
+
chmodSync(spawnHelper, 0o755)
|
|
18
|
+
console.log(`[postinstall] Fixed execute permission on ${spawnHelper}`)
|
|
19
|
+
}
|
|
20
|
+
} catch (_e) {
|
|
21
|
+
// node-pty not installed yet or not needed on this platform
|
|
22
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|