@rfranzoi/scrypted-mqtt-securitysystem 1.0.40 → 1.0.41

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.
@@ -34028,23 +34028,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
34028
34028
  return (mod && mod.__esModule) ? mod : { "default": mod };
34029
34029
  };
34030
34030
  Object.defineProperty(exports, "__esModule", ({ value: true }));
34031
- // --- Preload: silenzia SOLO il warning facoltativo di sdk.json ---
34032
- (() => {
34033
- const swallow = (orig) => (...args) => {
34034
- const txt = args.map(a => typeof a === 'string' ? a : (a?.message || '')).join(' ');
34035
- if (txt.includes('failed to load custom interface descriptors'))
34036
- return;
34037
- return orig(...args);
34038
- };
34039
- console.error = swallow(console.error.bind(console));
34040
- console.warn = swallow(console.warn.bind(console));
34041
- })();
34042
- // Carica lo SDK in runtime (evita ESM per garantire l’ordine col preload)
34031
+ // --- Silence ONLY the optional sdk.json warning from @scrypted/sdk ---
34032
+ const __origConsoleError = console.error.bind(console);
34033
+ console.error = (...args) => {
34034
+ const first = args?.[0];
34035
+ const msg = typeof first === 'string' ? first : (first?.message || '');
34036
+ if (typeof msg === 'string' && msg.includes('failed to load custom interface descriptors')) {
34037
+ // swallow just this warning
34038
+ return;
34039
+ }
34040
+ __origConsoleError(...args);
34041
+ };
34042
+ // Carica lo SDK (non tipizziamo qui per non perdere le proprietà runtime)
34043
34043
  const sdk = __webpack_require__(/*! @scrypted/sdk */ "./node_modules/@scrypted/sdk/dist/src/index.js");
34044
- // Valori runtime (enum, classi, manager)
34045
- const { ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, // enum: valori
34046
- SecuritySystemMode, // enum: valori
34047
- systemManager, } = sdk;
34044
+ // Valori runtime (enum/classi/manager) dal modulo SDK
34045
+ const { ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, // valore (enum)
34046
+ SecuritySystemMode, // valore (enum)
34047
+ systemManager,
34048
+ // ⚠️ NON destrutturare deviceManager: va letto sempre "al volo" da sdk.deviceManager.
34049
+ } = sdk;
34048
34050
  const mqtt_1 = __importDefault(__webpack_require__(/*! mqtt */ "./node_modules/mqtt/build/index.js"));
34049
34051
  /** utils */
34050
34052
  function truthy(v) {
@@ -34059,16 +34061,21 @@ function falsy(v) {
34059
34061
  const s = v.toString().trim().toLowerCase();
34060
34062
  return s === '0' || s === 'false' || s === 'offline' || s === 'no' || s === 'off';
34061
34063
  }
34062
- function normalize(s) { return (s || '').trim().toLowerCase(); }
34063
- function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
34064
- /** Outgoing predefiniti (PAI-like). Chiavi numeriche per compatibilità enum */
34064
+ function normalize(s) {
34065
+ return (s || '').trim().toLowerCase();
34066
+ }
34067
+ function clamp(n, min, max) {
34068
+ return Math.max(min, Math.min(max, n));
34069
+ }
34070
+ /** SecuritySystem outgoing defaults (PAI-like)
34071
+ * Nota: usare number come chiave evita stranezze con gli enum in TS. */
34065
34072
  const DEFAULT_OUTGOING = {
34066
34073
  [SecuritySystemMode.Disarmed]: 'disarm',
34067
34074
  [SecuritySystemMode.HomeArmed]: 'arm_home',
34068
34075
  [SecuritySystemMode.AwayArmed]: 'arm_away',
34069
34076
  [SecuritySystemMode.NightArmed]: 'arm_night',
34070
34077
  };
34071
- /** Parse incoming payload -> final mode (ignora transitori) */
34078
+ /** Parse incoming payload -> final mode (ignore transition states) */
34072
34079
  function payloadToMode(payload) {
34073
34080
  if (payload == null)
34074
34081
  return;
@@ -34081,6 +34088,7 @@ function payloadToMode(payload) {
34081
34088
  return SecuritySystemMode.AwayArmed;
34082
34089
  if (['arm_night', 'night', 'armed_night', 'sleep', 'arm_sleep', 'armed_sleep'].includes(p))
34083
34090
  return SecuritySystemMode.NightArmed;
34091
+ // transitori: non cambiano il mode
34084
34092
  if (['entry_delay', 'exit_delay', 'pending', 'arming', 'disarming'].includes(p))
34085
34093
  return undefined;
34086
34094
  return undefined;
@@ -34090,66 +34098,76 @@ class BaseMqttSensor extends ScryptedDeviceBase {
34090
34098
  super(nativeId);
34091
34099
  this.cfg = cfg;
34092
34100
  }
34101
+ /** Called by parent on each MQTT message */
34093
34102
  handleMqtt(topic, payload) {
34094
34103
  const p = payload?.toString() ?? '';
34095
34104
  const np = normalize(p);
34105
+ // online
34096
34106
  if (topic === this.cfg.topics.online) {
34097
34107
  if (truthy(np) || np === 'online')
34098
34108
  this.online = true;
34099
34109
  if (falsy(np) || np === 'offline')
34100
34110
  this.online = false;
34101
34111
  }
34112
+ // tamper
34102
34113
  if (topic === this.cfg.topics.tamper) {
34103
34114
  if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
34104
34115
  this.tampered = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34105
34116
  }
34106
- else if (falsy(np))
34117
+ else if (falsy(np)) {
34107
34118
  this.tampered = false;
34119
+ }
34108
34120
  }
34121
+ // battery
34109
34122
  if (topic === this.cfg.topics.batteryLevel) {
34110
34123
  const n = clamp(parseFloat(p), 0, 100);
34111
34124
  if (isFinite(n))
34112
34125
  this.batteryLevel = n;
34113
34126
  }
34114
34127
  else if (topic === this.cfg.topics.lowBattery && !this.cfg.topics.batteryLevel) {
34128
+ // se abbiamo SOLO lowBattery (bool):
34115
34129
  this.batteryLevel = truthy(np) ? 10 : 100;
34116
34130
  }
34131
+ // primary handled by subclasses
34117
34132
  this.handlePrimary(topic, np, p);
34118
34133
  }
34119
34134
  }
34120
34135
  class ContactMqttSensor extends BaseMqttSensor {
34121
34136
  handlePrimary(topic, np) {
34122
- if (topic === this.cfg.topics.contact)
34137
+ if (topic === this.cfg.topics.contact) {
34123
34138
  this.entryOpen = truthy(np);
34139
+ }
34124
34140
  }
34125
34141
  }
34126
34142
  class MotionMqttSensor extends BaseMqttSensor {
34127
34143
  handlePrimary(topic, np) {
34128
- if (topic === this.cfg.topics.motion)
34144
+ if (topic === this.cfg.topics.motion) {
34129
34145
  this.motionDetected = truthy(np);
34146
+ }
34130
34147
  }
34131
34148
  }
34132
34149
  class OccupancyMqttSensor extends BaseMqttSensor {
34133
34150
  handlePrimary(topic, np) {
34134
- if (topic === this.cfg.topics.occupancy)
34151
+ if (topic === this.cfg.topics.occupancy) {
34135
34152
  this.occupied = truthy(np);
34153
+ }
34136
34154
  }
34137
34155
  }
34138
34156
  /** ----------------- Main Plugin ----------------- */
34139
34157
  class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34140
34158
  constructor() {
34141
34159
  super();
34160
+ // sensor management
34142
34161
  this.sensorsCfg = [];
34143
34162
  this.devices = new Map();
34144
- this.triedDiscoveryOnce = false;
34145
- // Tipo in UI (best-effort)
34163
+ // (facoltativo) Imposta il device type in UI
34146
34164
  setTimeout(() => {
34147
34165
  try {
34148
34166
  systemManager.getDeviceById(this.id)?.setType?.(ScryptedDeviceType.SecuritySystem);
34149
34167
  }
34150
34168
  catch { }
34151
34169
  });
34152
- // Stato di default
34170
+ // Default state
34153
34171
  this.securitySystemState = this.securitySystemState || {
34154
34172
  mode: SecuritySystemMode.Disarmed,
34155
34173
  supportedModes: [
@@ -34160,12 +34178,12 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34160
34178
  ],
34161
34179
  };
34162
34180
  this.online = this.online ?? false;
34163
- // Config sensori e announce
34181
+ // Load sensors config and announce devices
34164
34182
  this.loadSensorsFromStorage();
34165
- this.safeDiscoverSensors(); // non spamma se deviceManager non c'è
34166
- // Connect MQTT
34183
+ this.discoverSensors().catch((e) => this.console.error('discoverSensors error', e));
34184
+ // Connect on start
34167
34185
  this.connectMqtt().catch((e) => this.console.error('MQTT connect error:', e));
34168
- // Shutdown pulito
34186
+ // chiusura pulita del client MQTT ai reload/stop del plugin
34169
34187
  try {
34170
34188
  process.once('SIGTERM', () => { try {
34171
34189
  this.client?.end(true);
@@ -34182,7 +34200,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34182
34200
  }
34183
34201
  catch { }
34184
34202
  }
34185
- /** ---- Settings ---- */
34203
+ // helpers persistenza
34186
34204
  saveSensorsToStorage() {
34187
34205
  try {
34188
34206
  this.storage.setItem('sensorsJson', JSON.stringify(this.sensorsCfg));
@@ -34191,14 +34209,17 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34191
34209
  this.console.error('saveSensorsToStorage error', e);
34192
34210
  }
34193
34211
  }
34212
+ /** ---- Settings UI ---- */
34194
34213
  async getSettings() {
34195
34214
  const out = [
34215
+ // MQTT Core
34196
34216
  { group: 'MQTT', key: 'brokerUrl', title: 'Broker URL', placeholder: 'mqtt://127.0.0.1:1883', value: this.storage.getItem('brokerUrl') || 'mqtt://127.0.0.1:1883' },
34197
34217
  { group: 'MQTT', key: 'username', title: 'Username', type: 'string', value: this.storage.getItem('username') || '' },
34198
34218
  { group: 'MQTT', key: 'password', title: 'Password', type: 'password', value: this.storage.getItem('password') || '' },
34199
34219
  { group: 'MQTT', key: 'clientId', title: 'Client ID', placeholder: 'scrypted-paradox', value: this.storage.getItem('clientId') || 'scrypted-paradox' },
34200
34220
  { group: 'MQTT', key: 'tls', title: 'Use TLS', type: 'boolean', value: this.storage.getItem('tls') === 'true' },
34201
34221
  { group: 'MQTT', key: 'rejectUnauthorized', title: 'Reject Unauthorized (TLS)', type: 'boolean', value: this.storage.getItem('rejectUnauthorized') !== 'false', description: 'Disattiva solo con broker self-signed.' },
34222
+ // Alarm Topics
34202
34223
  { group: 'Alarm Topics', key: 'topicSetTarget', title: 'Set Target State (publish)', placeholder: 'paradox/control/partitions/Area_1', value: this.storage.getItem('topicSetTarget') || '' },
34203
34224
  { group: 'Alarm Topics', key: 'topicGetTarget', title: 'Get Target State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/target_state', value: this.storage.getItem('topicGetTarget') || '' },
34204
34225
  { group: 'Alarm Topics', key: 'topicGetCurrent', title: 'Get Current State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/current_state', value: this.storage.getItem('topicGetCurrent') || '' },
@@ -34206,69 +34227,32 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34206
34227
  { group: 'Alarm Topics', key: 'topicOnline', title: 'Get Online (subscribe)', placeholder: 'paradox/interface/availability', value: this.storage.getItem('topicOnline') || '' },
34207
34228
  { group: 'Publish Options', key: 'qos', title: 'QoS', type: 'integer', value: parseInt(this.storage.getItem('qos') || '0') },
34208
34229
  { 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.' },
34213
34230
  ];
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
34231
+ // ---- UI Add Sensor ----
34232
+ 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: 'Fill the fields above and toggle this on to create the sensor. After creation, restart this plugin to see the accessory listed below. To show it in HomeKit, restart the HomeKit plugin as well.' });
34233
+ // ---- UI per sensori esistenti ----
34217
34234
  for (const cfg of this.sensorsCfg) {
34218
34235
  const gid = `Sensor: ${cfg.name} [${cfg.id}]`;
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.' });
34220
- if (cfg.kind === 'contact')
34221
- out.push({ group: gid, key: `sensor.${cfg.id}.topic.contact`, title: 'Contact State Topic', value: cfg.topics.contact || '' });
34222
- else if (cfg.kind === 'motion')
34223
- out.push({ group: gid, key: `sensor.${cfg.id}.topic.motion`, title: 'Motion Detected Topic', value: cfg.topics.motion || '' });
34224
- else
34225
- out.push({ group: gid, key: `sensor.${cfg.id}.topic.occupancy`, title: 'Occupancy Detected Topic', value: cfg.topics.occupancy || '' });
34236
+ 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'] });
34237
+ // primary per tipo
34238
+ if (cfg.kind === 'contact') {
34239
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.contact`, title: 'Contact State Topic', value: cfg.topics.contact || '', placeholder: 'paradox/states/zones/XYZ/open' });
34240
+ }
34241
+ else if (cfg.kind === 'motion') {
34242
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.motion`, title: 'Motion Detected Topic', value: cfg.topics.motion || '', placeholder: 'paradox/states/zones/XYZ/open' });
34243
+ }
34244
+ else {
34245
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.occupancy`, title: 'Occupancy Detected Topic', value: cfg.topics.occupancy || '', placeholder: 'paradox/states/zones/XYZ/open' });
34246
+ }
34247
+ // extra opzionali
34226
34248
  out.push({ group: gid, key: `sensor.${cfg.id}.topic.batteryLevel`, title: 'Battery Level Topic (0..100)', value: cfg.topics.batteryLevel || '' }, { group: gid, key: `sensor.${cfg.id}.topic.lowBattery`, title: 'Low Battery Topic (bool)', value: cfg.topics.lowBattery || '' }, { group: gid, key: `sensor.${cfg.id}.topic.tamper`, title: 'Tamper Topic', value: cfg.topics.tamper || '' }, { group: gid, key: `sensor.${cfg.id}.topic.online`, title: 'Online Topic', value: cfg.topics.online || '' }, { group: gid, key: `sensor.${cfg.id}.remove`, title: 'Remove sensor', type: 'boolean' });
34227
34249
  }
34228
34250
  return out;
34229
34251
  }
34230
34252
  async putSetting(key, value) {
34253
+ // salva sempre nella storage la value del campo (così resta in UI)
34231
34254
  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 ----
34255
+ // --- Add Sensor workflow ---
34272
34256
  if (key === 'new.create' && String(value) === 'true') {
34273
34257
  const id = (this.storage.getItem('new.id') || '').trim();
34274
34258
  const name = (this.storage.getItem('new.name') || '').trim() || id;
@@ -34283,15 +34267,16 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34283
34267
  }
34284
34268
  this.sensorsCfg.push({ id, name, kind, topics: {} });
34285
34269
  this.saveSensorsToStorage();
34270
+ // pulisci i campi "new.*"
34286
34271
  this.storage.removeItem('new.id');
34287
34272
  this.storage.removeItem('new.name');
34288
34273
  this.storage.removeItem('new.kind');
34289
34274
  this.storage.removeItem('new.create');
34290
- this.safeDiscoverSensors(true);
34275
+ await this.discoverSensors();
34291
34276
  await this.connectMqtt(true);
34292
34277
  return;
34293
34278
  }
34294
- // ---- Edit/Remove/Rename sensore ----
34279
+ // --- Edit/Remove sensore esistente ---
34295
34280
  const m = key.match(/^sensor\.([^\.]+)\.(.+)$/);
34296
34281
  if (m) {
34297
34282
  const sid = m[1];
@@ -34301,60 +34286,41 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34301
34286
  this.console.warn('putSetting: sensor non trovato', sid);
34302
34287
  return;
34303
34288
  }
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
34328
34289
  if (prop === 'remove' && String(value) === 'true') {
34290
+ // elimina
34329
34291
  this.sensorsCfg = this.sensorsCfg.filter(s => s.id !== sid);
34330
34292
  this.saveSensorsToStorage();
34331
34293
  try {
34294
+ this.devices.delete(`sensor:${sid}`);
34332
34295
  sdk?.deviceManager?.onDeviceRemoved?.(`sensor:${sid}`);
34333
34296
  }
34334
34297
  catch { }
34298
+ // pulisci flag
34335
34299
  this.storage.removeItem(key);
34336
- this.safeDiscoverSensors(true);
34300
+ await this.discoverSensors();
34337
34301
  await this.connectMqtt(true);
34338
34302
  return;
34339
34303
  }
34340
- // edit
34341
- if (prop === 'name')
34304
+ if (prop === 'name') {
34342
34305
  cfg.name = String(value);
34343
- else if (prop === 'kind')
34306
+ }
34307
+ else if (prop === 'kind') {
34344
34308
  cfg.kind = String(value);
34309
+ }
34345
34310
  else if (prop.startsWith('topic.')) {
34346
34311
  const tk = prop.substring('topic.'.length);
34347
34312
  cfg.topics[tk] = String(value).trim();
34348
34313
  }
34349
34314
  this.saveSensorsToStorage();
34350
- this.safeDiscoverSensors(true);
34315
+ await this.discoverSensors();
34351
34316
  await this.connectMqtt(true);
34352
34317
  return;
34353
34318
  }
34354
- // Altre impostazioni: riconnetti
34319
+ // --- Altro (MQTT / Alarm settings) ---
34355
34320
  if (key === 'sensorsJson') {
34321
+ // non più mostrato, ma se presente da vecchie versioni
34356
34322
  this.loadSensorsFromStorage();
34357
- this.safeDiscoverSensors(true);
34323
+ await this.discoverSensors();
34358
34324
  await this.connectMqtt(true);
34359
34325
  }
34360
34326
  else {
@@ -34362,12 +34328,15 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34362
34328
  }
34363
34329
  }
34364
34330
  /** ---- DeviceProvider ---- */
34365
- async getDevice(nativeId) { return this.devices.get(nativeId); }
34331
+ async getDevice(nativeId) {
34332
+ return this.devices.get(nativeId);
34333
+ }
34366
34334
  async releaseDevice(_id, nativeId) {
34367
34335
  try {
34368
34336
  const dev = this.devices.get(nativeId);
34369
- if (dev)
34337
+ if (dev) {
34370
34338
  this.devices.delete(nativeId);
34339
+ }
34371
34340
  try {
34372
34341
  sdk?.deviceManager?.onDeviceRemoved?.(nativeId);
34373
34342
  }
@@ -34381,6 +34350,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34381
34350
  try {
34382
34351
  const raw = this.storage.getItem('sensorsJson') || '[]';
34383
34352
  const parsed = JSON.parse(raw);
34353
+ // sanitize
34384
34354
  this.sensorsCfg = (parsed || []).filter(x => x && x.id && x.name && x.kind && x.topics);
34385
34355
  }
34386
34356
  catch (e) {
@@ -34388,22 +34358,15 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34388
34358
  this.sensorsCfg = [];
34389
34359
  }
34390
34360
  }
34391
- /** Annuncia i sensori SOLO se deviceManager è pronto. */
34392
- safeDiscoverSensors(triggeredByChange = false) {
34361
+ /** ===== discoverSensors: annuncia PRIMA, istanzia DOPO (con retry se manager non pronto) ===== */
34362
+ async discoverSensors() {
34393
34363
  const dmAny = sdk?.deviceManager;
34394
34364
  if (!dmAny) {
34395
- if (!this.triedDiscoveryOnce) {
34396
- this.console.log('Device discovery postponed: deviceManager not ready yet.');
34397
- this.triedDiscoveryOnce = true;
34398
- }
34365
+ this.console.warn('deviceManager not ready yet, retrying in 1s…');
34366
+ setTimeout(() => this.discoverSensors().catch(e => this.console.error('discoverSensors retry error', e)), 1000);
34399
34367
  return;
34400
34368
  }
34401
- this.triedDiscoveryOnce = false;
34402
- this.discoverSensors(dmAny);
34403
- }
34404
- /** discoverSensors con deviceManager garantito */
34405
- discoverSensors(dmAny) {
34406
- // 1) Manifests
34369
+ // 1) Prepara i manifest
34407
34370
  const manifests = this.sensorsCfg.map(cfg => {
34408
34371
  const nativeId = `sensor:${cfg.id}`;
34409
34372
  const t = cfg.topics || {};
@@ -34423,10 +34386,18 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34423
34386
  // 2) Annuncio
34424
34387
  if (typeof dmAny.onDevicesChanged === 'function') {
34425
34388
  dmAny.onDevicesChanged({ devices: manifests });
34389
+ this.console.log('Annunciati (batch):', manifests.map(m => m.nativeId).join(', '));
34426
34390
  }
34427
34391
  else if (typeof dmAny.onDeviceDiscovered === 'function') {
34428
- for (const m of manifests)
34392
+ for (const m of manifests) {
34429
34393
  dmAny.onDeviceDiscovered(m);
34394
+ this.console.log('Annunciato:', m.nativeId);
34395
+ }
34396
+ }
34397
+ else {
34398
+ this.console.warn('deviceManager has no discovery methods yet, retrying in 1s…');
34399
+ setTimeout(() => this.discoverSensors().catch(e => this.console.error('discoverSensors retry error', e)), 1000);
34400
+ return;
34430
34401
  }
34431
34402
  // 3) Istanzia/aggiorna
34432
34403
  for (const cfg of this.sensorsCfg) {
@@ -34445,16 +34416,18 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34445
34416
  dev.cfg = cfg;
34446
34417
  }
34447
34418
  const hasBattery = !!(cfg.topics.batteryLevel || cfg.topics.lowBattery);
34448
- if (hasBattery && dev.batteryLevel === undefined)
34419
+ if (hasBattery && dev.batteryLevel === undefined) {
34449
34420
  dev.batteryLevel = 100;
34421
+ }
34450
34422
  }
34451
- // 4) Cleanup
34423
+ // 4) Rimuovi quelli spariti
34452
34424
  const announced = new Set(manifests.map(m => m.nativeId));
34453
34425
  for (const [nativeId] of this.devices) {
34454
34426
  if (!announced.has(nativeId)) {
34455
34427
  try {
34456
34428
  this.devices.delete(nativeId);
34457
- dmAny.onDeviceRemoved?.(nativeId);
34429
+ sdk?.deviceManager?.onDeviceRemoved?.(nativeId);
34430
+ this.console.log('Rimosso:', nativeId);
34458
34431
  }
34459
34432
  catch { }
34460
34433
  }
@@ -34468,7 +34441,13 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34468
34441
  const clientId = this.storage.getItem('clientId') || 'scrypted-paradox';
34469
34442
  const tls = this.storage.getItem('tls') === 'true';
34470
34443
  const rejectUnauthorized = this.storage.getItem('rejectUnauthorized') !== 'false';
34471
- const opts = { clientId, username, password, clean: true, reconnectPeriod: 3000 };
34444
+ const opts = {
34445
+ clientId,
34446
+ username,
34447
+ password,
34448
+ clean: true,
34449
+ reconnectPeriod: 3000,
34450
+ };
34472
34451
  if (tls) {
34473
34452
  opts.protocol = 'mqtts';
34474
34453
  opts.rejectUnauthorized = rejectUnauthorized;
@@ -34477,34 +34456,38 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34477
34456
  }
34478
34457
  collectAllSubscriptions() {
34479
34458
  const subs = new Set();
34459
+ // alarm
34480
34460
  for (const k of ['topicGetTarget', 'topicGetCurrent', 'topicTamper', 'topicOnline']) {
34481
34461
  const v = this.storage.getItem(k);
34482
34462
  if (v)
34483
34463
  subs.add(v);
34484
34464
  }
34465
+ // sensors
34485
34466
  for (const s of this.sensorsCfg) {
34486
34467
  const t = s.topics || {};
34487
34468
  [t.contact, t.motion, t.occupancy, t.batteryLevel, t.lowBattery, t.tamper, t.online]
34488
- .filter(Boolean).forEach(x => subs.add(String(x)));
34469
+ .filter(Boolean)
34470
+ .forEach(x => subs.add(String(x)));
34489
34471
  }
34490
34472
  return Array.from(subs);
34491
34473
  }
34492
34474
  async connectMqtt(_reconnect = false) {
34493
34475
  const subs = this.collectAllSubscriptions();
34494
- if (!subs.length && !this.storage.getItem('topicSetTarget'))
34476
+ if (!subs.length && !this.storage.getItem('topicSetTarget')) {
34495
34477
  this.console.warn('Configura almeno un topic nelle impostazioni.');
34478
+ }
34496
34479
  if (this.client) {
34497
34480
  try {
34498
34481
  this.client.end(true);
34499
34482
  }
34500
34483
  catch { }
34501
- ;
34502
34484
  this.client = undefined;
34503
34485
  }
34504
34486
  const { url, opts } = this.getMqttOptions();
34505
34487
  this.console.log(`Connecting MQTT ${url} ...`);
34506
34488
  const client = mqtt_1.default.connect(url, opts);
34507
34489
  this.client = client;
34490
+ // cache alarm topics for fast compare
34508
34491
  const tTarget = this.storage.getItem('topicGetTarget') || '';
34509
34492
  const tCurrent = this.storage.getItem('topicGetCurrent') || '';
34510
34493
  const tTamper = this.storage.getItem('topicTamper') || '';
@@ -34512,11 +34495,12 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34512
34495
  client.on('connect', () => {
34513
34496
  this.console.log('MQTT connected');
34514
34497
  this.online = true;
34515
- if (subs.length)
34516
- client.subscribe(subs, { qos: 0 }, (err) => { if (err)
34517
- this.console.error('subscribe error', err); });
34518
- // Al primo connect riprova ad annunciare i sensori
34519
- this.safeDiscoverSensors(true);
34498
+ if (subs.length) {
34499
+ client.subscribe(subs, { qos: 0 }, (err) => {
34500
+ if (err)
34501
+ this.console.error('subscribe error', err);
34502
+ });
34503
+ }
34520
34504
  });
34521
34505
  client.on('reconnect', () => this.console.log('MQTT reconnecting...'));
34522
34506
  client.on('close', () => { this.console.log('MQTT closed'); this.online = false; });
@@ -34525,6 +34509,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34525
34509
  try {
34526
34510
  const p = payload?.toString() ?? '';
34527
34511
  const np = normalize(p);
34512
+ // ---- Alarm handling ----
34528
34513
  if (topic === tOnline) {
34529
34514
  if (truthy(np) || np === 'online')
34530
34515
  this.online = true;
@@ -34533,17 +34518,19 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34533
34518
  return;
34534
34519
  }
34535
34520
  if (topic === tTamper) {
34536
- if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np))
34521
+ if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np)) {
34537
34522
  this.tampered = ['cover', 'intrusion'].find(x => x === np) || true;
34538
- else if (falsy(np))
34523
+ }
34524
+ else if (falsy(np)) {
34539
34525
  this.tampered = false;
34526
+ }
34540
34527
  return;
34541
34528
  }
34542
34529
  if (topic === tCurrent) {
34543
34530
  const mode = payloadToMode(payload);
34544
34531
  const isAlarm = ['alarm', 'triggered'].includes(np);
34545
34532
  const current = this.securitySystemState || { mode: SecuritySystemMode.Disarmed };
34546
- this.securitySystemState = {
34533
+ const newState = {
34547
34534
  mode: mode ?? current.mode,
34548
34535
  supportedModes: current.supportedModes ?? [
34549
34536
  SecuritySystemMode.Disarmed,
@@ -34553,6 +34540,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34553
34540
  ],
34554
34541
  triggered: isAlarm || undefined,
34555
34542
  };
34543
+ this.securitySystemState = newState;
34556
34544
  return;
34557
34545
  }
34558
34546
  if (topic === tTarget) {
@@ -34560,11 +34548,10 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34560
34548
  this.console.log('Target state reported:', p, '->', this.pendingTarget);
34561
34549
  return;
34562
34550
  }
34563
- // Dispatch ai sensori + eventuale discover ritardato
34564
- if (this.triedDiscoveryOnce)
34565
- this.safeDiscoverSensors(true);
34566
- for (const dev of this.devices.values())
34551
+ // ---- Sensor dispatch ----
34552
+ for (const dev of this.devices.values()) {
34567
34553
  dev.handleMqtt(topic, payload);
34554
+ }
34568
34555
  }
34569
34556
  catch (e) {
34570
34557
  this.console.error('MQTT message handler error', e);
@@ -34589,7 +34576,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34589
34576
  async armSecuritySystem(mode) {
34590
34577
  const payload = this.getOutgoing(mode);
34591
34578
  this.console.log('armSecuritySystem', mode, '->', payload);
34592
- this.pendingTarget = mode;
34579
+ this.pendingTarget = mode; // memorizza target, ma NON cambiare il current
34593
34580
  this.publishSetTarget(payload);
34594
34581
  }
34595
34582
  async disarmSecuritySystem() {
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.40",
3
+ "version": "1.0.41",
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",