@rfranzoi/scrypted-mqtt-securitysystem 1.0.39 → 1.0.40

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.
@@ -34039,73 +34039,36 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
34039
34039
  console.error = swallow(console.error.bind(console));
34040
34040
  console.warn = swallow(console.warn.bind(console));
34041
34041
  })();
34042
- // Runtime SDK via require (evita esecuzione anticipata del bundler)
34042
+ // Carica lo SDK in runtime (evita ESM per garantire l’ordine col preload)
34043
34043
  const sdk = __webpack_require__(/*! @scrypted/sdk */ "./node_modules/@scrypted/sdk/dist/src/index.js");
34044
- const { ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, SecuritySystemMode, systemManager, } = sdk;
34044
+ // Valori runtime (enum, classi, manager)
34045
+ const { ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, // enum: valori
34046
+ SecuritySystemMode, // enum: valori
34047
+ systemManager, } = sdk;
34045
34048
  const mqtt_1 = __importDefault(__webpack_require__(/*! mqtt */ "./node_modules/mqtt/build/index.js"));
34046
34049
  /** utils */
34047
- function normalize(s) { return (s || '').trim().toLowerCase(); }
34048
34050
  function truthy(v) {
34049
34051
  if (!v)
34050
34052
  return false;
34051
- const s = normalize(v);
34052
- return s === '1' || s === 'true' || s === 'online' || s === 'yes' || s === 'on' || s === 'ok' || s === 'open';
34053
+ const s = v.toString().trim().toLowerCase();
34054
+ return s === '1' || s === 'true' || s === 'online' || s === 'yes' || s === 'on' || s === 'ok';
34053
34055
  }
34054
34056
  function falsy(v) {
34055
34057
  if (!v)
34056
34058
  return false;
34057
- const s = normalize(v);
34058
- return s === '0' || s === 'false' || s === 'offline' || s === 'no' || s === 'off' || s === 'closed' || s === 'close';
34059
+ const s = v.toString().trim().toLowerCase();
34060
+ return s === '0' || s === 'false' || s === 'offline' || s === 'no' || s === 'off';
34059
34061
  }
34062
+ function normalize(s) { return (s || '').trim().toLowerCase(); }
34060
34063
  function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
34061
- function deepEqual(a, b) { try {
34062
- return JSON.stringify(a) === JSON.stringify(b);
34063
- }
34064
- catch {
34065
- return a === b;
34066
- } }
34067
- /** Match topic con wildcard MQTT (+, #) oppure confronto esatto */
34068
- function topicMatches(topic, pattern) {
34069
- if (!pattern)
34070
- return false;
34071
- if (pattern === topic)
34072
- return true;
34073
- if (!pattern.includes('+') && !pattern.includes('#'))
34074
- return false;
34075
- // Escapa tutto tranne '/'
34076
- const esc = pattern.replace(/[-/\\^$*?.()|[\]{}]/g, '\\$&');
34077
- const rx = '^' + esc
34078
- .replace(/\\\+/g, '[^/]+') // '+' => un segmento
34079
- .replace(/\\\#/g, '.+'); // '#" => qualsiasi suffisso
34080
- try {
34081
- return new RegExp(rx + '$').test(topic);
34082
- }
34083
- catch {
34084
- return false;
34085
- }
34086
- }
34087
- /** set + emit evento scrypted */
34088
- function setAndEmit(dev, key, value, iface, log) {
34089
- if (dev[key] === value)
34090
- return;
34091
- dev[key] = value;
34092
- try {
34093
- dev.onDeviceEvent?.(iface, value);
34094
- }
34095
- catch { }
34096
- try {
34097
- if (log)
34098
- dev.console?.log?.(log);
34099
- }
34100
- catch { }
34101
- }
34102
- /** Outgoing predefiniti (PAI-like). Chiavi numeriche per compat enum */
34064
+ /** Outgoing predefiniti (PAI-like). Chiavi numeriche per compatibilità enum */
34103
34065
  const DEFAULT_OUTGOING = {
34104
34066
  [SecuritySystemMode.Disarmed]: 'disarm',
34105
34067
  [SecuritySystemMode.HomeArmed]: 'arm_home',
34106
34068
  [SecuritySystemMode.AwayArmed]: 'arm_away',
34107
34069
  [SecuritySystemMode.NightArmed]: 'arm_night',
34108
34070
  };
34071
+ /** Parse incoming payload -> final mode (ignora transitori) */
34109
34072
  function payloadToMode(payload) {
34110
34073
  if (payload == null)
34111
34074
  return;
@@ -34128,148 +34091,48 @@ class BaseMqttSensor extends ScryptedDeviceBase {
34128
34091
  this.cfg = cfg;
34129
34092
  }
34130
34093
  handleMqtt(topic, payload) {
34131
- const raw = payload?.toString() ?? '';
34132
- const np = normalize(raw);
34133
- // Online
34134
- if (topicMatches(topic, this.cfg.topics.online)) {
34094
+ const p = payload?.toString() ?? '';
34095
+ const np = normalize(p);
34096
+ if (topic === this.cfg.topics.online) {
34135
34097
  if (truthy(np) || np === 'online')
34136
- setAndEmit(this, 'online', true, ScryptedInterface.Online, `[${this.name}] online=true`);
34137
- else if (falsy(np) || np === 'offline')
34138
- setAndEmit(this, 'online', false, ScryptedInterface.Online, `[${this.name}] online=false`);
34098
+ this.online = true;
34099
+ if (falsy(np) || np === 'offline')
34100
+ this.online = false;
34139
34101
  }
34140
- // Tamper
34141
- if (topicMatches(topic, this.cfg.topics.tamper)) {
34102
+ if (topic === this.cfg.topics.tamper) {
34142
34103
  if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
34143
- const t = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34144
- setAndEmit(this, 'tampered', t, ScryptedInterface.TamperSensor, `[${this.name}] tampered=${t}`);
34145
- }
34146
- else if (falsy(np)) {
34147
- setAndEmit(this, 'tampered', false, ScryptedInterface.TamperSensor, `[${this.name}] tampered=false`);
34104
+ this.tampered = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34148
34105
  }
34106
+ else if (falsy(np))
34107
+ this.tampered = false;
34149
34108
  }
34150
- // Battery
34151
- if (topicMatches(topic, this.cfg.topics.batteryLevel)) {
34152
- const n = clamp(parseFloat(raw), 0, 100);
34109
+ if (topic === this.cfg.topics.batteryLevel) {
34110
+ const n = clamp(parseFloat(p), 0, 100);
34153
34111
  if (isFinite(n))
34154
- setAndEmit(this, 'batteryLevel', n, ScryptedInterface.Battery, `[${this.name}] batteryLevel=${n}`);
34112
+ this.batteryLevel = n;
34155
34113
  }
34156
- else if (topicMatches(topic, this.cfg.topics.lowBattery) && !this.cfg.topics.batteryLevel) {
34157
- const n = truthy(np) ? 10 : 100;
34158
- setAndEmit(this, 'batteryLevel', n, ScryptedInterface.Battery, `[${this.name}] batteryLevel=${n} (lowBattery)`);
34114
+ else if (topic === this.cfg.topics.lowBattery && !this.cfg.topics.batteryLevel) {
34115
+ this.batteryLevel = truthy(np) ? 10 : 100;
34159
34116
  }
34160
- // Primario
34161
- this.handlePrimary(topic, np, raw);
34117
+ this.handlePrimary(topic, np, p);
34162
34118
  }
34163
34119
  }
34164
- /** === SENSORI: parsing robusto + eventi === */
34165
34120
  class ContactMqttSensor extends BaseMqttSensor {
34166
- handlePrimary(topic, np, raw) {
34167
- if (!topicMatches(topic, this.cfg.topics.contact))
34168
- return;
34169
- let val;
34170
- // stringhe comuni (True/False compresi via normalize)
34171
- if (['open', 'opened', '1', 'true', 'on', 'yes'].includes(np))
34172
- val = true;
34173
- else if (['closed', 'close', '0', 'false', 'off', 'no', 'shut'].includes(np))
34174
- val = false;
34175
- // JSON comuni
34176
- if (val === undefined) {
34177
- try {
34178
- const j = JSON.parse(raw);
34179
- if (typeof j?.open === 'boolean')
34180
- val = !!j.open;
34181
- else if (typeof j?.opened === 'boolean')
34182
- val = !!j.opened;
34183
- else if (typeof j?.contact === 'boolean')
34184
- val = !j.contact; // contact:false => aperto
34185
- else if (typeof j?.state === 'string') {
34186
- const s = normalize(j.state);
34187
- if (s === 'open')
34188
- val = true;
34189
- if (s === 'closed')
34190
- val = false;
34191
- }
34192
- }
34193
- catch { }
34194
- }
34195
- if (val !== undefined) {
34196
- setAndEmit(this, 'entryOpen', val, ScryptedInterface.EntrySensor, `[${this.name}] entryOpen=${val} (${topic})`);
34197
- }
34198
- else {
34199
- this.console?.debug?.(`Contact payload non gestito (${this.cfg.id}) topic=${topic} raw="${raw}"`);
34200
- }
34121
+ handlePrimary(topic, np) {
34122
+ if (topic === this.cfg.topics.contact)
34123
+ this.entryOpen = truthy(np);
34201
34124
  }
34202
34125
  }
34203
34126
  class MotionMqttSensor extends BaseMqttSensor {
34204
- handlePrimary(topic, np, raw) {
34205
- if (!topicMatches(topic, this.cfg.topics.motion))
34206
- return;
34207
- let val;
34208
- if (['motion', 'detected', 'active', '1', 'true', 'on', 'yes'].includes(np))
34209
- val = true;
34210
- else if (['clear', 'inactive', 'no_motion', 'none', '0', 'false', 'off', 'no'].includes(np))
34211
- val = false;
34212
- if (val === undefined) {
34213
- try {
34214
- const j = JSON.parse(raw);
34215
- if (typeof j?.motion === 'boolean')
34216
- val = !!j.motion;
34217
- else if (typeof j?.occupancy === 'boolean')
34218
- val = !!j.occupancy;
34219
- else if (typeof j?.presence === 'boolean')
34220
- val = !!j.presence;
34221
- else if (typeof j?.state === 'string') {
34222
- const s = normalize(j.state);
34223
- if (['on', 'motion', 'detected', 'active'].includes(s))
34224
- val = true;
34225
- if (['off', 'clear', 'inactive'].includes(s))
34226
- val = false;
34227
- }
34228
- }
34229
- catch { }
34230
- }
34231
- if (val !== undefined) {
34232
- setAndEmit(this, 'motionDetected', val, ScryptedInterface.MotionSensor, `[${this.name}] motionDetected=${val} (${topic})`);
34233
- }
34234
- else {
34235
- this.console?.debug?.(`Motion payload non gestito (${this.cfg.id}) topic=${topic} raw="${raw}"`);
34236
- }
34127
+ handlePrimary(topic, np) {
34128
+ if (topic === this.cfg.topics.motion)
34129
+ this.motionDetected = truthy(np);
34237
34130
  }
34238
34131
  }
34239
34132
  class OccupancyMqttSensor extends BaseMqttSensor {
34240
- handlePrimary(topic, np, raw) {
34241
- if (!topicMatches(topic, this.cfg.topics.occupancy))
34242
- return;
34243
- let val;
34244
- if (['occupied', 'presence', 'present', '1', 'true', 'on', 'yes'].includes(np))
34245
- val = true;
34246
- else if (['unoccupied', 'vacant', 'absent', '0', 'false', 'off', 'no', 'clear'].includes(np))
34247
- val = false;
34248
- if (val === undefined) {
34249
- try {
34250
- const j = JSON.parse(raw);
34251
- if (typeof j?.occupied === 'boolean')
34252
- val = !!j.occupied;
34253
- else if (typeof j?.presence === 'boolean')
34254
- val = !!j.presence;
34255
- else if (typeof j?.occupancy === 'boolean')
34256
- val = !!j.occupancy;
34257
- else if (typeof j?.state === 'string') {
34258
- const s = normalize(j.state);
34259
- if (['occupied', 'presence', 'present', 'on'].includes(s))
34260
- val = true;
34261
- if (['vacant', 'absent', 'clear', 'off'].includes(s))
34262
- val = false;
34263
- }
34264
- }
34265
- catch { }
34266
- }
34267
- if (val !== undefined) {
34268
- setAndEmit(this, 'occupied', val, ScryptedInterface.OccupancySensor, `[${this.name}] occupied=${val} (${topic})`);
34269
- }
34270
- else {
34271
- this.console?.debug?.(`Occupancy payload non gestito (${this.cfg.id}) topic=${topic} raw="${raw}"`);
34272
- }
34133
+ handlePrimary(topic, np) {
34134
+ if (topic === this.cfg.topics.occupancy)
34135
+ this.occupied = truthy(np);
34273
34136
  }
34274
34137
  }
34275
34138
  /** ----------------- Main Plugin ----------------- */
@@ -34278,7 +34141,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34278
34141
  super();
34279
34142
  this.sensorsCfg = [];
34280
34143
  this.devices = new Map();
34281
- this.discoveryPostponed = false;
34144
+ this.triedDiscoveryOnce = false;
34282
34145
  // Tipo in UI (best-effort)
34283
34146
  setTimeout(() => {
34284
34147
  try {
@@ -34297,9 +34160,12 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34297
34160
  ],
34298
34161
  };
34299
34162
  this.online = this.online ?? false;
34163
+ // Config sensori e announce
34300
34164
  this.loadSensorsFromStorage();
34301
- this.safeDiscoverSensors();
34165
+ this.safeDiscoverSensors(); // non spamma se deviceManager non c'è
34166
+ // Connect MQTT
34302
34167
  this.connectMqtt().catch((e) => this.console.error('MQTT connect error:', e));
34168
+ // Shutdown pulito
34303
34169
  try {
34304
34170
  process.once('SIGTERM', () => { try {
34305
34171
  this.client?.end(true);
@@ -34340,14 +34206,19 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34340
34206
  { group: 'Alarm Topics', key: 'topicOnline', title: 'Get Online (subscribe)', placeholder: 'paradox/interface/availability', value: this.storage.getItem('topicOnline') || '' },
34341
34207
  { group: 'Publish Options', key: 'qos', title: 'QoS', type: 'integer', value: parseInt(this.storage.getItem('qos') || '0') },
34342
34208
  { group: 'Publish Options', key: 'retain', title: 'Retain', type: 'boolean', value: this.storage.getItem('retain') === 'true' },
34209
+ // ---- Danger Zone ----
34210
+ { group: 'Danger Zone', key: 'danger.resetSensors', title: 'Reset all sensors (wipe)', type: 'boolean', description: 'Cancella tutti i sensori salvati e rimuove i device annunciati.' },
34211
+ { group: 'Danger Zone', key: 'danger.export', title: 'Export sensors JSON (read-only)', value: JSON.stringify(this.sensorsCfg, null, 2), readonly: true },
34212
+ { group: 'Danger Zone', key: 'danger.import', title: 'Import sensors JSON (paste & Save)', type: 'string', placeholder: '[]', description: 'Incolla JSON valido per sovrascrivere l’elenco sensori.' },
34343
34213
  ];
34344
- // Add Sensor
34345
- out.push({ group: 'Add Sensor', key: 'new.id', title: 'New Sensor ID', placeholder: 'porta-ingresso', value: this.storage.getItem('new.id') || '' }, { group: 'Add Sensor', key: 'new.name', title: 'Name', placeholder: 'Porta Ingresso', value: this.storage.getItem('new.name') || '' }, { group: 'Add Sensor', key: 'new.kind', title: 'Type', value: this.storage.getItem('new.kind') || 'contact', choices: ['contact', 'motion', 'occupancy'] }, { group: 'Add Sensor', key: 'new.create', title: 'Create sensor', type: 'boolean', description: 'Toggle ON to create the sensor.' });
34214
+ // ---- Add Sensor
34215
+ out.push({ group: 'Add Sensor', key: 'new.id', title: 'New Sensor ID', placeholder: 'porta-sogg', value: this.storage.getItem('new.id') || '' }, { group: 'Add Sensor', key: 'new.name', title: 'Name', placeholder: 'Porta Soggiorno', value: this.storage.getItem('new.name') || '' }, { group: 'Add Sensor', key: 'new.kind', title: 'Type', value: this.storage.getItem('new.kind') || 'contact', choices: ['contact', 'motion', 'occupancy'] }, { group: 'Add Sensor', key: 'new.create', title: 'Create sensor', type: 'boolean', description: 'Toggle ON e Save per creare.' });
34216
+ // ---- Sensors esistenti
34346
34217
  for (const cfg of this.sensorsCfg) {
34347
34218
  const gid = `Sensor: ${cfg.name} [${cfg.id}]`;
34348
- out.push({ group: gid, key: `sensor.${cfg.id}.name`, title: 'Name', value: cfg.name }, { group: gid, key: `sensor.${cfg.id}.kind`, title: 'Type', value: cfg.kind, choices: ['contact', 'motion', 'occupancy'] });
34219
+ out.push({ group: gid, key: `sensor.${cfg.id}.id`, title: 'ID (read-only)', value: cfg.id, readonly: true, description: 'L’ID identifica il device. Evita spazi/puntini.' }, { group: gid, key: `sensor.${cfg.id}.name`, title: 'Name', value: cfg.name }, { group: gid, key: `sensor.${cfg.id}.kind`, title: 'Type', value: cfg.kind, choices: ['contact', 'motion', 'occupancy'] }, { group: gid, key: `sensor.${cfg.id}.renameTo`, title: 'Clone/Rename → New ID', placeholder: 'es. porta-sogg', description: 'Compila e Save per clonare con nuovo ID e rimuovere il vecchio.' });
34349
34220
  if (cfg.kind === 'contact')
34350
- out.push({ group: gid, key: `sensor.${cfg.id}.topic.contact`, title: 'Contact State Topic', value: cfg.topics.contact || '', placeholder: 'paradox/states/zones/XYZ/open (supporta +/#)' });
34221
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.contact`, title: 'Contact State Topic', value: cfg.topics.contact || '' });
34351
34222
  else if (cfg.kind === 'motion')
34352
34223
  out.push({ group: gid, key: `sensor.${cfg.id}.topic.motion`, title: 'Motion Detected Topic', value: cfg.topics.motion || '' });
34353
34224
  else
@@ -34358,6 +34229,46 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34358
34229
  }
34359
34230
  async putSetting(key, value) {
34360
34231
  this.storage.setItem(key, String(value));
34232
+ // ---- Danger Zone: reset all ----
34233
+ if (key === 'danger.resetSensors' && String(value) === 'true') {
34234
+ this.storage.removeItem('danger.resetSensors');
34235
+ // rimuovi device annunciati
34236
+ try {
34237
+ const dm = sdk?.deviceManager;
34238
+ for (const nativeId of Array.from(this.devices.keys())) {
34239
+ try {
34240
+ dm?.onDeviceRemoved?.(nativeId);
34241
+ }
34242
+ catch { }
34243
+ }
34244
+ }
34245
+ catch { }
34246
+ this.devices.clear();
34247
+ this.sensorsCfg = [];
34248
+ this.saveSensorsToStorage();
34249
+ this.safeDiscoverSensors(true);
34250
+ await this.connectMqtt(true);
34251
+ return;
34252
+ }
34253
+ // ---- Danger Zone: import JSON ----
34254
+ if (key === 'danger.import') {
34255
+ try {
34256
+ const parsed = JSON.parse(String(value) || '[]');
34257
+ if (!Array.isArray(parsed))
34258
+ throw new Error('JSON non è un array.');
34259
+ // sanifica
34260
+ this.sensorsCfg = (parsed || []).filter(x => x && x.id && x.name && x.kind && x.topics);
34261
+ this.saveSensorsToStorage();
34262
+ this.storage.removeItem('danger.import');
34263
+ this.safeDiscoverSensors(true);
34264
+ await this.connectMqtt(true);
34265
+ }
34266
+ catch (e) {
34267
+ this.console.error('Import JSON error:', e);
34268
+ }
34269
+ return;
34270
+ }
34271
+ // ---- Add Sensor ----
34361
34272
  if (key === 'new.create' && String(value) === 'true') {
34362
34273
  const id = (this.storage.getItem('new.id') || '').trim();
34363
34274
  const name = (this.storage.getItem('new.name') || '').trim() || id;
@@ -34380,6 +34291,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34380
34291
  await this.connectMqtt(true);
34381
34292
  return;
34382
34293
  }
34294
+ // ---- Edit/Remove/Rename sensore ----
34383
34295
  const m = key.match(/^sensor\.([^\.]+)\.(.+)$/);
34384
34296
  if (m) {
34385
34297
  const sid = m[1];
@@ -34389,6 +34301,30 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34389
34301
  this.console.warn('putSetting: sensor non trovato', sid);
34390
34302
  return;
34391
34303
  }
34304
+ // rename/clone
34305
+ if (prop === 'renameTo') {
34306
+ const newId = String(value).trim();
34307
+ this.storage.removeItem(key);
34308
+ if (!newId)
34309
+ return;
34310
+ if (this.sensorsCfg.find(s => s.id === newId)) {
34311
+ this.console.warn('renameTo: ID già esistente:', newId);
34312
+ return;
34313
+ }
34314
+ const cloned = { ...cfg, id: newId, name: cfg.name };
34315
+ this.sensorsCfg.push(cloned);
34316
+ // rimuovi vecchio
34317
+ this.sensorsCfg = this.sensorsCfg.filter(s => s !== cfg);
34318
+ try {
34319
+ sdk?.deviceManager?.onDeviceRemoved?.(`sensor:${sid}`);
34320
+ }
34321
+ catch { }
34322
+ this.saveSensorsToStorage();
34323
+ this.safeDiscoverSensors(true);
34324
+ await this.connectMqtt(true);
34325
+ return;
34326
+ }
34327
+ // remove
34392
34328
  if (prop === 'remove' && String(value) === 'true') {
34393
34329
  this.sensorsCfg = this.sensorsCfg.filter(s => s.id !== sid);
34394
34330
  this.saveSensorsToStorage();
@@ -34401,6 +34337,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34401
34337
  await this.connectMqtt(true);
34402
34338
  return;
34403
34339
  }
34340
+ // edit
34404
34341
  if (prop === 'name')
34405
34342
  cfg.name = String(value);
34406
34343
  else if (prop === 'kind')
@@ -34414,6 +34351,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34414
34351
  await this.connectMqtt(true);
34415
34352
  return;
34416
34353
  }
34354
+ // Altre impostazioni: riconnetti
34417
34355
  if (key === 'sensorsJson') {
34418
34356
  this.loadSensorsFromStorage();
34419
34357
  this.safeDiscoverSensors(true);
@@ -34450,21 +34388,22 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34450
34388
  this.sensorsCfg = [];
34451
34389
  }
34452
34390
  }
34391
+ /** Annuncia i sensori SOLO se deviceManager è pronto. */
34453
34392
  safeDiscoverSensors(triggeredByChange = false) {
34454
34393
  const dmAny = sdk?.deviceManager;
34455
34394
  if (!dmAny) {
34456
- if (!this.discoveryPostponed) {
34395
+ if (!this.triedDiscoveryOnce) {
34457
34396
  this.console.log('Device discovery postponed: deviceManager not ready yet.');
34458
- this.discoveryPostponed = true;
34397
+ this.triedDiscoveryOnce = true;
34459
34398
  }
34460
34399
  return;
34461
34400
  }
34462
- this.discoveryPostponed = false;
34401
+ this.triedDiscoveryOnce = false;
34463
34402
  this.discoverSensors(dmAny);
34464
- if (triggeredByChange)
34465
- this.console.log('Sensors discovered/updated.');
34466
34403
  }
34404
+ /** discoverSensors con deviceManager garantito */
34467
34405
  discoverSensors(dmAny) {
34406
+ // 1) Manifests
34468
34407
  const manifests = this.sensorsCfg.map(cfg => {
34469
34408
  const nativeId = `sensor:${cfg.id}`;
34470
34409
  const t = cfg.topics || {};
@@ -34481,11 +34420,15 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34481
34420
  interfaces.push(ScryptedInterface.Battery);
34482
34421
  return { nativeId, name: cfg.name, type: ScryptedDeviceType.Sensor, interfaces };
34483
34422
  });
34484
- if (typeof dmAny.onDevicesChanged === 'function')
34423
+ // 2) Annuncio
34424
+ if (typeof dmAny.onDevicesChanged === 'function') {
34485
34425
  dmAny.onDevicesChanged({ devices: manifests });
34486
- else if (typeof dmAny.onDeviceDiscovered === 'function')
34426
+ }
34427
+ else if (typeof dmAny.onDeviceDiscovered === 'function') {
34487
34428
  for (const m of manifests)
34488
34429
  dmAny.onDeviceDiscovered(m);
34430
+ }
34431
+ // 3) Istanzia/aggiorna
34489
34432
  for (const cfg of this.sensorsCfg) {
34490
34433
  const nativeId = `sensor:${cfg.id}`;
34491
34434
  let dev = this.devices.get(nativeId);
@@ -34503,8 +34446,9 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34503
34446
  }
34504
34447
  const hasBattery = !!(cfg.topics.batteryLevel || cfg.topics.lowBattery);
34505
34448
  if (hasBattery && dev.batteryLevel === undefined)
34506
- setAndEmit(dev, 'batteryLevel', 100, ScryptedInterface.Battery, `[${cfg.name}] batteryLevel=100 (default)`);
34449
+ dev.batteryLevel = 100;
34507
34450
  }
34451
+ // 4) Cleanup
34508
34452
  const announced = new Set(manifests.map(m => m.nativeId));
34509
34453
  for (const [nativeId] of this.devices) {
34510
34454
  if (!announced.has(nativeId)) {
@@ -34567,41 +34511,39 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34567
34511
  const tOnline = this.storage.getItem('topicOnline') || '';
34568
34512
  client.on('connect', () => {
34569
34513
  this.console.log('MQTT connected');
34570
- setAndEmit(this, 'online', true, ScryptedInterface.Online, `[Alarm] online=true`);
34514
+ this.online = true;
34571
34515
  if (subs.length)
34572
34516
  client.subscribe(subs, { qos: 0 }, (err) => { if (err)
34573
34517
  this.console.error('subscribe error', err); });
34518
+ // Al primo connect riprova ad annunciare i sensori
34574
34519
  this.safeDiscoverSensors(true);
34575
34520
  });
34576
34521
  client.on('reconnect', () => this.console.log('MQTT reconnecting...'));
34577
- client.on('close', () => { this.console.log('MQTT closed'); setAndEmit(this, 'online', false, ScryptedInterface.Online, `[Alarm] online=false`); });
34522
+ client.on('close', () => { this.console.log('MQTT closed'); this.online = false; });
34578
34523
  client.on('error', (e) => { this.console.error('MQTT error', e); });
34579
34524
  client.on('message', (topic, payload) => {
34580
34525
  try {
34581
- const raw = payload?.toString() ?? '';
34582
- const np = normalize(raw);
34583
- if (topicMatches(topic, tOnline)) {
34526
+ const p = payload?.toString() ?? '';
34527
+ const np = normalize(p);
34528
+ if (topic === tOnline) {
34584
34529
  if (truthy(np) || np === 'online')
34585
- setAndEmit(this, 'online', true, ScryptedInterface.Online, `[Alarm] online=true (${topic})`);
34586
- else if (falsy(np) || np === 'offline')
34587
- setAndEmit(this, 'online', false, ScryptedInterface.Online, `[Alarm] online=false (${topic})`);
34530
+ this.online = true;
34531
+ if (falsy(np) || np === 'offline')
34532
+ this.online = false;
34588
34533
  return;
34589
34534
  }
34590
- if (topicMatches(topic, tTamper)) {
34591
- if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np)) {
34592
- const t = ['cover', 'intrusion'].find(x => x === np) || true;
34593
- setAndEmit(this, 'tampered', t, ScryptedInterface.TamperSensor, `[Alarm] tampered=${t} (${topic})`);
34594
- }
34595
- else if (falsy(np)) {
34596
- setAndEmit(this, 'tampered', false, ScryptedInterface.TamperSensor, `[Alarm] tampered=false (${topic})`);
34597
- }
34535
+ if (topic === tTamper) {
34536
+ if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np))
34537
+ this.tampered = ['cover', 'intrusion'].find(x => x === np) || true;
34538
+ else if (falsy(np))
34539
+ this.tampered = false;
34598
34540
  return;
34599
34541
  }
34600
- if (topicMatches(topic, tCurrent)) {
34542
+ if (topic === tCurrent) {
34601
34543
  const mode = payloadToMode(payload);
34602
34544
  const isAlarm = ['alarm', 'triggered'].includes(np);
34603
34545
  const current = this.securitySystemState || { mode: SecuritySystemMode.Disarmed };
34604
- const newState = {
34546
+ this.securitySystemState = {
34605
34547
  mode: mode ?? current.mode,
34606
34548
  supportedModes: current.supportedModes ?? [
34607
34549
  SecuritySystemMode.Disarmed,
@@ -34611,23 +34553,15 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34611
34553
  ],
34612
34554
  triggered: isAlarm || undefined,
34613
34555
  };
34614
- if (!deepEqual(this.securitySystemState, newState)) {
34615
- this.securitySystemState = newState;
34616
- try {
34617
- this.onDeviceEvent?.(ScryptedInterface.SecuritySystem, newState);
34618
- }
34619
- catch { }
34620
- this.console.log(`[Alarm] currentState=${JSON.stringify(newState)} (${topic})`);
34621
- }
34622
34556
  return;
34623
34557
  }
34624
- if (topicMatches(topic, tTarget)) {
34558
+ if (topic === tTarget) {
34625
34559
  this.pendingTarget = payloadToMode(payload);
34626
- this.console.log(`[Alarm] target reported: "${raw}" -> ${this.pendingTarget} (${topic})`);
34560
+ this.console.log('Target state reported:', p, '->', this.pendingTarget);
34627
34561
  return;
34628
34562
  }
34629
- // Sensors: se discovery rimandata, riprova
34630
- if (this.discoveryPostponed)
34563
+ // Dispatch ai sensori + eventuale discover ritardato
34564
+ if (this.triedDiscoveryOnce)
34631
34565
  this.safeDiscoverSensors(true);
34632
34566
  for (const dev of this.devices.values())
34633
34567
  dev.handleMqtt(topic, payload);
package/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rfranzoi/scrypted-mqtt-securitysystem",
3
- "version": "1.0.39",
3
+ "version": "1.0.40",
4
4
  "description": "Scrypted plugin: Paradox Security System via MQTT (PAI/PAI-MQTT style).",
5
5
  "license": "MIT",
6
6
  "main": "dist/main.nodejs.js",