smart-home-engine 1.20.5 → 1.20.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "1.20.5",
3
+ "version": "1.20.7",
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
@@ -547,8 +547,13 @@ if (config.url) {
547
547
  state.ts = new Date().getTime();
548
548
  }
549
549
  oldState = store.getObject('mqtt::' + topic) || {};
550
- if (oldState.val !== state.val) {
551
- state.lc = state.ts;
550
+ // lc (last change): prefer a payload-provided value; otherwise set it
551
+ // on value change and carry the previous lc over when the value is
552
+ // unchanged. Previously the old lc was simply lost on repeated
553
+ // identical values, so she.mqtt.age() returned NaN for topics whose
554
+ // payloads don't carry their own lc (B5).
555
+ if (state.lc === undefined) {
556
+ state.lc = oldState.val !== state.val ? state.ts : (oldState.lc ?? state.ts);
552
557
  }
553
558
  store.setObject('mqtt::' + topic, state);
554
559
  stateChange(topic, state, oldState, msg);
@@ -18,6 +18,13 @@ const COMMISSION_TIMEOUT_MS = 120000;
18
18
  /** @type {import('@matter/main').ServerNode | null} */
19
19
  let _server = null;
20
20
  let _log = null;
21
+ // Rate-limit state for repeating matter.js error lines (B6)
22
+ let _lastMatterErr = { line: '', ts: 0, suppressed: 0 };
23
+
24
+ /** Depth-limited, cycle-safe rendering of arbitrary throwables/objects. */
25
+ function _inspect(v) {
26
+ return require('util').inspect(v, { depth: 6, breakLength: 160 });
27
+ }
21
28
  /** @type {((msg: object) => void) | null} */
22
29
  let _broadcast = null;
23
30
 
@@ -115,9 +122,49 @@ async function init(storagePath, log, broadcastFn) {
115
122
  Logger.format = LogFormat.PLAIN;
116
123
  Logger.level = LogLevel.INFO;
117
124
  Logger.destinations.default.write = (text, message) => {
118
- if (message.level >= LogLevel.ERROR) _log.error('matter.js:', text);
119
- else if (message.level === LogLevel.WARN) _log.warn('matter.js:', text);
120
- else _log.debug('matter.js:', text);
125
+ // she's log adds its own timestamp — drop matter.js's embedded one
126
+ let line = text.replace(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\s*/, '');
127
+ if (message.level >= LogLevel.ERROR) {
128
+ // PLAIN formatting renders some throwables as "{}" — recover the
129
+ // detail from the structured message values (B6).
130
+ if (/(\{\}|\[object \w+\])\s*$/.test(line)) {
131
+ const details = (message.values ?? [])
132
+ .filter((v) => v !== null && typeof v === 'object')
133
+ .map((v) => _inspect(v))
134
+ .filter((d) => d && d !== '{}');
135
+ if (details.length) line += ' — ' + details.join(' — ');
136
+ }
137
+ // Rate-limit identical repeating error lines (e.g. a failing
138
+ // reconnect loop): suppress repeats within 30 s, keep a heartbeat.
139
+ const now = Date.now();
140
+ if (line === _lastMatterErr.line && now - _lastMatterErr.ts < 30000) {
141
+ _lastMatterErr.ts = now;
142
+ _lastMatterErr.suppressed++;
143
+ if (_lastMatterErr.suppressed % 20 !== 0) return;
144
+ _log.error(`matter.js: (error repeated ${_lastMatterErr.suppressed} times) ${line}`);
145
+ return;
146
+ }
147
+ if (_lastMatterErr.suppressed % 20 !== 0) {
148
+ _log.error(`matter.js: (previous error repeated ${_lastMatterErr.suppressed} more times)`);
149
+ }
150
+ _lastMatterErr = { line, ts: now, suppressed: 0 };
151
+ _log.error('matter.js:', line);
152
+ } else if (message.level === LogLevel.WARN) {
153
+ _log.warn('matter.js:', line);
154
+ } else {
155
+ _log.debug('matter.js:', line);
156
+ }
157
+ };
158
+
159
+ // matter.js's default unhandled-error reporter logs the bare value, which
160
+ // renders as "{}" for non-Error throwables and drops cause / AggregateError
161
+ // details. The hook is explicitly replaceable — report full detail (B6).
162
+ Logger.reportUnhandledError = (error) => {
163
+ try {
164
+ _log.error('matter.js: unhandled error:', _inspect(error));
165
+ } catch {
166
+ /* never throw from the error reporter */
167
+ }
121
168
  };
122
169
 
123
170
  // Configure storage before anything else touches StorageService
@@ -52,9 +52,15 @@ module.exports = function (she, ctx = {}) {
52
52
  she.setValue(target, val);
53
53
  });
54
54
  },
55
- /** Seconds since the topic's value last changed. */
56
- age: function Sandbox_mqtt_age(topic) {
57
- return Math.round((new Date().getTime() - she.getProp(topic, 'lc')) / 1000);
55
+ /**
56
+ * Seconds since the topic's value last changed.
57
+ * Pass 'message' (or 'ts') as second argument to get the seconds since
58
+ * the last received message instead — repeated identical values reset
59
+ * that clock, useful for sensor heartbeat checks.
60
+ */
61
+ age: function Sandbox_mqtt_age(topic, mode) {
62
+ const prop = mode === 'message' || mode === 'ts' ? 'ts' : 'lc';
63
+ return Math.round((new Date().getTime() - she.getProp(topic, prop)) / 1000);
58
64
  },
59
65
  /**
60
66
  * Publish 1 to target when any source is truthy, 0 otherwise.
@@ -148,7 +154,8 @@ module.exports = function (she, ctx = {}) {
148
154
  she.http = {
149
155
  /**
150
156
  * Fetch a URL and return a Promise that resolves to the response body.
151
- * Resolves to parsed JSON when the Content-Type is application/json, plain text otherwise.
157
+ * Resolves to parsed JSON when the Content-Type is application/json and
158
+ * the body parses; falls back to the raw text body when parsing fails.
152
159
  * Rejects on non-2xx responses.
153
160
  * @param {string} url
154
161
  * @param {RequestInit} [options]
@@ -171,7 +178,15 @@ module.exports = function (she, ctx = {}) {
171
178
  const promise = fetch(url, { ...options, signal })
172
179
  .then(async (r) => {
173
180
  const ct = r.headers.get('content-type') || '';
174
- const body = ct.includes('json') ? await r.json() : await r.text();
181
+ const text = await r.text();
182
+ let body = text;
183
+ if (ct.includes('json')) {
184
+ try {
185
+ body = JSON.parse(text);
186
+ } catch {
187
+ // Content-Type lied — deliver the raw body instead of throwing
188
+ }
189
+ }
175
190
  const headers = {};
176
191
  r.headers.forEach((v, k) => {
177
192
  headers[k] = v;