smart-home-engine 0.26.3 → 0.27.1

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.
@@ -1,4 +1,4 @@
1
- import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-DtmmxR--.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-ZrhJ-PgI.js";/*!-----------------------------------------------------------------------------
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
4
4
  * Released under the MIT license
@@ -155,10 +155,10 @@
155
155
  }
156
156
  })();
157
157
  </script>
158
- <script type="module" crossorigin src="/assets/index-DtmmxR--.js"></script>
158
+ <script type="module" crossorigin src="/assets/index-ZrhJ-PgI.js"></script>
159
159
  <link rel="modulepreload" crossorigin href="/assets/monaco-langs-BW2J83t5.js">
160
160
  <link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
161
- <link rel="stylesheet" crossorigin href="/assets/index-CND95LL-.css">
161
+ <link rel="stylesheet" crossorigin href="/assets/index-6dbanXAb.css">
162
162
  </head>
163
163
  <body>
164
164
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "0.26.3",
3
+ "version": "0.27.1",
4
4
  "description": "Node.js based script runner for use in MQTT based Smart Home environments",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -241,6 +241,28 @@ function sunScheduleEvent(obj, shift) {
241
241
  let mqtt = null;
242
242
  let connected = false;
243
243
 
244
+ // Deferred start — wait for retained MQTT state before running scripts
245
+ let _started = false;
246
+ let _startupTimeout = null;
247
+ let _quietTimer = null;
248
+ const _RETAIN_QUIET_MS = 300; // ms of silence after last retained msg → start
249
+ const _STARTUP_TIMEOUT_MS = 10000; // ms to wait for broker before starting anyway
250
+
251
+ function startOnce(reason) {
252
+ if (_started) return;
253
+ _started = true;
254
+ if (_startupTimeout) {
255
+ clearTimeout(_startupTimeout);
256
+ _startupTimeout = null;
257
+ }
258
+ if (_quietTimer) {
259
+ clearTimeout(_quietTimer);
260
+ _quietTimer = null;
261
+ }
262
+ if (reason) log.info(reason);
263
+ start();
264
+ }
265
+
244
266
  // Wire up the MQTT API: pass the state store and a getter for the live MQTT client.
245
267
  // The getter always returns the current value of `mqtt` (null until connected).
246
268
  require('./web/mqtt-api').init(store, () => mqtt);
@@ -323,6 +345,12 @@ if (config.url) {
323
345
  log.debug('mqtt subscribe #');
324
346
  mqtt.subscribe('#');
325
347
  mqttEventCallbacks.filter((c) => c.event === 'connect').forEach((c) => c.callback());
348
+
349
+ // Arm the quiet-period timer (only needed on first connect, before scripts start)
350
+ if (!_started) {
351
+ if (_quietTimer) clearTimeout(_quietTimer);
352
+ _quietTimer = setTimeout(() => startOnce('mqtt: retained state ready, starting scripts'), _RETAIN_QUIET_MS);
353
+ }
326
354
  });
327
355
 
328
356
  mqtt.on('close', () => {
@@ -339,6 +367,13 @@ if (config.url) {
339
367
 
340
368
  mqtt.on('message', (topic, payload, msg) => {
341
369
  _mqttMsgCount++;
370
+
371
+ // Reset the quiet-period timer while the initial retained-message burst is in flight
372
+ if (!_started && msg.retain && _quietTimer) {
373
+ clearTimeout(_quietTimer);
374
+ _quietTimer = setTimeout(() => startOnce('mqtt: retained state ready, starting scripts'), _RETAIN_QUIET_MS);
375
+ }
376
+
342
377
  if (shedb.handleMqttMessage(topic, payload)) return;
343
378
 
344
379
  const state = require('./lib/parse-payload')(payload);
@@ -428,8 +463,18 @@ if (config.matterStorage) {
428
463
  log.warn('matter controller disabled — set matterStorage in config.json to enable');
429
464
  }
430
465
 
431
- // Start scripts immediately — MQTT retained state will populate the store asynchronously
432
- start();
466
+ // If no broker is configured, start scripts immediately.
467
+ // If a broker is configured, startOnce() fires from the quiet-period timer inside
468
+ // mqtt.on('connect') once the retained-message burst settles, or from the
469
+ // startup-timeout fallback if the broker is unreachable.
470
+ if (config.url) {
471
+ _startupTimeout = setTimeout(() => {
472
+ log.warn('mqtt startup timeout — starting scripts without retained state');
473
+ startOnce();
474
+ }, _STARTUP_TIMEOUT_MS);
475
+ } else {
476
+ start();
477
+ }
433
478
 
434
479
  function stateChange(topic, state, oldState, msg) {
435
480
  subscriptions.forEach((subs) => {
@@ -198,4 +198,136 @@ router.post('/push', async (req, res) => {
198
198
  }
199
199
  });
200
200
 
201
+ // GET /she/git/log?path=<relPath>&limit=<n>
202
+ // Returns commits touching the given file (relative to scriptDir), up to limit.
203
+ router.get('/log', async (req, res) => {
204
+ const scriptDir = getRoot(req);
205
+ if (!scriptDir) return res.status(500).json({ error: 'scriptDir not configured' });
206
+
207
+ const relPath = String(req.query.path ?? '').trim();
208
+ if (!relPath) return res.status(400).json({ error: 'Missing path parameter' });
209
+
210
+ const limit = Math.min(parseInt(String(req.query.limit ?? '30'), 10) || 30, 200);
211
+
212
+ const gitRoot = await getGitRoot(scriptDir);
213
+ if (!gitRoot) return res.status(404).json({ error: 'Not a git repository' });
214
+
215
+ const abs = safePath(scriptDir, relPath);
216
+ if (!abs) return res.status(400).json({ error: 'Invalid path' });
217
+
218
+ const relToRoot = path.relative(gitRoot, abs).replace(/\\/g, '/');
219
+
220
+ try {
221
+ const { stdout } = await git(
222
+ ['log', '--follow', '--format=%H%x1f%s%x1f%an%x1f%ai', `-n`, String(limit), '--', relToRoot],
223
+ gitRoot,
224
+ );
225
+ const commits = stdout
226
+ .split('\n')
227
+ .filter(Boolean)
228
+ .map((line) => {
229
+ const [hash, subject, author, date] = line.split('\x1f');
230
+ return { hash, subject, author, date };
231
+ });
232
+ res.json(commits);
233
+ } catch (e) {
234
+ res.status(500).json({ error: e.message });
235
+ }
236
+ });
237
+
238
+ // GET /she/git/show?hash=<hash>&path=<relPath>
239
+ // Returns { content: string, binary: false } or { content: null, binary: true }.
240
+ router.get('/show', async (req, res) => {
241
+ const scriptDir = getRoot(req);
242
+ if (!scriptDir) return res.status(500).json({ error: 'scriptDir not configured' });
243
+
244
+ const hash = String(req.query.hash ?? '').trim();
245
+ if (!/^[0-9a-f]{4,64}$/i.test(hash)) return res.status(400).json({ error: 'Invalid hash' });
246
+
247
+ const relPath = String(req.query.path ?? '').trim();
248
+ if (!relPath) return res.status(400).json({ error: 'Missing path parameter' });
249
+
250
+ const gitRoot = await getGitRoot(scriptDir);
251
+ if (!gitRoot) return res.status(404).json({ error: 'Not a git repository' });
252
+
253
+ const abs = safePath(scriptDir, relPath);
254
+ if (!abs) return res.status(400).json({ error: 'Invalid path' });
255
+
256
+ const relToRoot = path.relative(gitRoot, abs).replace(/\\/g, '/');
257
+
258
+ try {
259
+ const { stdout } = await git(['show', `${hash}:${relToRoot}`], gitRoot);
260
+ const binary = stdout.includes('\0');
261
+ res.json({ content: binary ? null : stdout, binary });
262
+ } catch (e) {
263
+ res.status(500).json({ error: e.message });
264
+ }
265
+ });
266
+
267
+ // GET /she/git/log?path=<relPath>&limit=<n>
268
+ // Returns commits touching the given file (relative to scriptDir), up to limit.
269
+ router.get('/log', async (req, res) => {
270
+ const scriptDir = getRoot(req);
271
+ if (!scriptDir) return res.status(500).json({ error: 'scriptDir not configured' });
272
+
273
+ const relPath = String(req.query.path ?? '').trim();
274
+ if (!relPath) return res.status(400).json({ error: 'Missing path parameter' });
275
+
276
+ const limit = Math.min(parseInt(String(req.query.limit ?? '30'), 10) || 30, 200);
277
+
278
+ const gitRoot = await getGitRoot(scriptDir);
279
+ if (!gitRoot) return res.status(404).json({ error: 'Not a git repository' });
280
+
281
+ const abs = safePath(scriptDir, relPath);
282
+ if (!abs) return res.status(400).json({ error: 'Invalid path' });
283
+
284
+ const relToRoot = path.relative(gitRoot, abs).replace(/\\/g, '/');
285
+
286
+ try {
287
+ const { stdout } = await git(
288
+ ['log', '--follow', '--format=%H%x1f%s%x1f%an%x1f%ai', `-n`, String(limit), '--', relToRoot],
289
+ gitRoot,
290
+ );
291
+ const commits = stdout
292
+ .split('\n')
293
+ .filter(Boolean)
294
+ .map((line) => {
295
+ const [hash, subject, author, date] = line.split('\x1f');
296
+ return { hash, subject, author, date };
297
+ });
298
+ res.json(commits);
299
+ } catch (e) {
300
+ res.status(500).json({ error: e.message });
301
+ }
302
+ });
303
+
304
+ // GET /she/git/show?hash=<hash>&path=<relPath>
305
+ // Returns { content: string, binary: false } or { content: null, binary: true }.
306
+ router.get('/show', async (req, res) => {
307
+ const scriptDir = getRoot(req);
308
+ if (!scriptDir) return res.status(500).json({ error: 'scriptDir not configured' });
309
+
310
+ const hash = String(req.query.hash ?? '').trim();
311
+ if (!/^[0-9a-f]{4,64}$/i.test(hash)) return res.status(400).json({ error: 'Invalid hash' });
312
+
313
+ const relPath = String(req.query.path ?? '').trim();
314
+ if (!relPath) return res.status(400).json({ error: 'Missing path parameter' });
315
+
316
+ const gitRoot = await getGitRoot(scriptDir);
317
+ if (!gitRoot) return res.status(404).json({ error: 'Not a git repository' });
318
+
319
+ const abs = safePath(scriptDir, relPath);
320
+ if (!abs) return res.status(400).json({ error: 'Invalid path' });
321
+
322
+ const relToRoot = path.relative(gitRoot, abs).replace(/\\/g, '/');
323
+
324
+ try {
325
+ const { stdout } = await git(['show', `${hash}:${relToRoot}`], gitRoot);
326
+ const binary = stdout.includes('\0');
327
+ res.json({ content: binary ? null : stdout, binary });
328
+ } catch (e) {
329
+ res.status(500).json({ error: e.message });
330
+ }
331
+ });
332
+
201
333
  module.exports = { router, git, getGitRoot };