@rfranzoi/scrypted-mqtt-securitysystem 1.0.35 → 1.0.37

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,25 +34028,24 @@ 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
- // --- 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)
34031
+ // --- Preload: silenzia SOLO il warning facoltativo di sdk.json ---
34032
+ // Copre console.error e console.warn (alcune versioni usano warn).
34033
+ (() => {
34034
+ const swallow = (orig) => (...args) => {
34035
+ const txt = args.map(a => typeof a === 'string' ? a : (a?.message || '')).join(' ');
34036
+ if (txt.includes('failed to load custom interface descriptors'))
34037
+ return;
34038
+ return orig(...args);
34039
+ };
34040
+ console.error = swallow(console.error.bind(console));
34041
+ console.warn = swallow(console.warn.bind(console));
34042
+ })();
34043
+ // Carica lo SDK (runtime only: niente import ESM per evitare che il bundler lo esegua prima del preload)
34043
34044
  const sdk = __webpack_require__(/*! @scrypted/sdk */ "./node_modules/@scrypted/sdk/dist/src/index.js");
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;
34045
+ // Valori runtime dal modulo SDK
34046
+ const { ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, // enum (valori)
34047
+ SecuritySystemMode, // enum (valori)
34048
+ systemManager, } = sdk;
34050
34049
  const mqtt_1 = __importDefault(__webpack_require__(/*! mqtt */ "./node_modules/mqtt/build/index.js"));
34051
34050
  /** utils */
34052
34051
  function truthy(v) {
@@ -34061,21 +34060,16 @@ function falsy(v) {
34061
34060
  const s = v.toString().trim().toLowerCase();
34062
34061
  return s === '0' || s === 'false' || s === 'offline' || s === 'no' || s === 'off';
34063
34062
  }
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. */
34063
+ function normalize(s) { return (s || '').trim().toLowerCase(); }
34064
+ function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
34065
+ /** Outgoing predefiniti (PAI-like). Chiavi numeriche per compatibilità enum */
34072
34066
  const DEFAULT_OUTGOING = {
34073
34067
  [SecuritySystemMode.Disarmed]: 'disarm',
34074
34068
  [SecuritySystemMode.HomeArmed]: 'arm_home',
34075
34069
  [SecuritySystemMode.AwayArmed]: 'arm_away',
34076
34070
  [SecuritySystemMode.NightArmed]: 'arm_night',
34077
34071
  };
34078
- /** Parse incoming payload -> final mode (ignore transition states) */
34072
+ /** Parse incoming payload -> final mode (ignora transitori) */
34079
34073
  function payloadToMode(payload) {
34080
34074
  if (payload == null)
34081
34075
  return;
@@ -34088,7 +34082,6 @@ function payloadToMode(payload) {
34088
34082
  return SecuritySystemMode.AwayArmed;
34089
34083
  if (['arm_night', 'night', 'armed_night', 'sleep', 'arm_sleep', 'armed_sleep'].includes(p))
34090
34084
  return SecuritySystemMode.NightArmed;
34091
- // transitori: non cambiano il mode
34092
34085
  if (['entry_delay', 'exit_delay', 'pending', 'arming', 'disarming'].includes(p))
34093
34086
  return undefined;
34094
34087
  return undefined;
@@ -34098,76 +34091,178 @@ class BaseMqttSensor extends ScryptedDeviceBase {
34098
34091
  super(nativeId);
34099
34092
  this.cfg = cfg;
34100
34093
  }
34101
- /** Called by parent on each MQTT message */
34102
34094
  handleMqtt(topic, payload) {
34103
34095
  const p = payload?.toString() ?? '';
34104
34096
  const np = normalize(p);
34105
- // online
34106
34097
  if (topic === this.cfg.topics.online) {
34107
34098
  if (truthy(np) || np === 'online')
34108
34099
  this.online = true;
34109
34100
  if (falsy(np) || np === 'offline')
34110
34101
  this.online = false;
34111
34102
  }
34112
- // tamper
34113
34103
  if (topic === this.cfg.topics.tamper) {
34114
34104
  if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
34115
34105
  this.tampered = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34116
34106
  }
34117
- else if (falsy(np)) {
34107
+ else if (falsy(np))
34118
34108
  this.tampered = false;
34119
- }
34120
34109
  }
34121
- // battery
34122
34110
  if (topic === this.cfg.topics.batteryLevel) {
34123
34111
  const n = clamp(parseFloat(p), 0, 100);
34124
34112
  if (isFinite(n))
34125
34113
  this.batteryLevel = n;
34126
34114
  }
34127
34115
  else if (topic === this.cfg.topics.lowBattery && !this.cfg.topics.batteryLevel) {
34128
- // se abbiamo SOLO lowBattery (bool):
34129
34116
  this.batteryLevel = truthy(np) ? 10 : 100;
34130
34117
  }
34131
- // primary handled by subclasses
34132
34118
  this.handlePrimary(topic, np, p);
34133
34119
  }
34134
34120
  }
34121
+ /** === SENSORI CON PARSING ROBUSTO PER HOMEKIT === */
34135
34122
  class ContactMqttSensor extends BaseMqttSensor {
34136
- handlePrimary(topic, np) {
34137
- if (topic === this.cfg.topics.contact) {
34138
- this.entryOpen = truthy(np);
34123
+ handlePrimary(topic, np, raw) {
34124
+ if (topic !== this.cfg.topics.contact)
34125
+ return;
34126
+ // stringhe comuni
34127
+ if (['open', 'opened', '1', 'true', 'on', 'yes'].includes(np)) {
34128
+ this.entryOpen = true;
34129
+ return;
34130
+ }
34131
+ if (['closed', 'close', '0', 'false', 'off', 'no', 'shut'].includes(np)) {
34132
+ this.entryOpen = false;
34133
+ return;
34134
+ }
34135
+ // JSON comuni
34136
+ try {
34137
+ const j = JSON.parse(raw);
34138
+ if (typeof j?.open === 'boolean') {
34139
+ this.entryOpen = !!j.open;
34140
+ return;
34141
+ }
34142
+ if (typeof j?.opened === 'boolean') {
34143
+ this.entryOpen = !!j.opened;
34144
+ return;
34145
+ }
34146
+ if (typeof j?.contact === 'boolean') {
34147
+ this.entryOpen = !j.contact;
34148
+ return;
34149
+ } // contact:false => OPEN
34150
+ if (typeof j?.state === 'string') {
34151
+ const s = String(j.state).toLowerCase();
34152
+ if (s === 'open') {
34153
+ this.entryOpen = true;
34154
+ return;
34155
+ }
34156
+ if (s === 'closed') {
34157
+ this.entryOpen = false;
34158
+ return;
34159
+ }
34160
+ }
34139
34161
  }
34162
+ catch { }
34163
+ this.console?.debug?.(`Contact payload non gestito (${this.cfg.id}): "${raw}"`);
34140
34164
  }
34141
34165
  }
34142
34166
  class MotionMqttSensor extends BaseMqttSensor {
34143
- handlePrimary(topic, np) {
34144
- if (topic === this.cfg.topics.motion) {
34145
- this.motionDetected = truthy(np);
34167
+ handlePrimary(topic, np, raw) {
34168
+ if (topic !== this.cfg.topics.motion)
34169
+ return;
34170
+ if (['motion', 'detected', 'active', '1', 'true', 'on', 'yes'].includes(np)) {
34171
+ this.motionDetected = true;
34172
+ return;
34173
+ }
34174
+ if (['clear', 'inactive', 'no_motion', 'none', '0', 'false', 'off', 'no'].includes(np)) {
34175
+ this.motionDetected = false;
34176
+ return;
34177
+ }
34178
+ try {
34179
+ const j = JSON.parse(raw);
34180
+ if (typeof j?.motion === 'boolean') {
34181
+ this.motionDetected = !!j.motion;
34182
+ return;
34183
+ }
34184
+ if (typeof j?.occupancy === 'boolean') {
34185
+ this.motionDetected = !!j.occupancy;
34186
+ return;
34187
+ }
34188
+ if (typeof j?.presence === 'boolean') {
34189
+ this.motionDetected = !!j.presence;
34190
+ return;
34191
+ }
34192
+ if (typeof j?.state === 'string') {
34193
+ const s = String(j.state).toLowerCase();
34194
+ if (['on', 'motion', 'detected', 'active'].includes(s)) {
34195
+ this.motionDetected = true;
34196
+ return;
34197
+ }
34198
+ if (['off', 'clear', 'inactive'].includes(s)) {
34199
+ this.motionDetected = false;
34200
+ return;
34201
+ }
34202
+ }
34146
34203
  }
34204
+ catch { }
34205
+ this.console?.debug?.(`Motion payload non gestito (${this.cfg.id}): "${raw}"`);
34147
34206
  }
34148
34207
  }
34149
34208
  class OccupancyMqttSensor extends BaseMqttSensor {
34150
- handlePrimary(topic, np) {
34151
- if (topic === this.cfg.topics.occupancy) {
34152
- this.occupied = truthy(np);
34209
+ handlePrimary(topic, np, raw) {
34210
+ if (topic !== this.cfg.topics.occupancy)
34211
+ return;
34212
+ if (['occupied', 'presence', 'present', '1', 'true', 'on', 'yes'].includes(np)) {
34213
+ this.occupied = true;
34214
+ return;
34215
+ }
34216
+ if (['unoccupied', 'vacant', 'absent', '0', 'false', 'off', 'no', 'clear'].includes(np)) {
34217
+ this.occupied = false;
34218
+ return;
34219
+ }
34220
+ try {
34221
+ const j = JSON.parse(raw);
34222
+ if (typeof j?.occupied === 'boolean') {
34223
+ this.occupied = !!j.occupied;
34224
+ return;
34225
+ }
34226
+ if (typeof j?.presence === 'boolean') {
34227
+ this.occupied = !!j.presence;
34228
+ return;
34229
+ }
34230
+ if (typeof j?.occupancy === 'boolean') {
34231
+ this.occupied = !!j.occupancy;
34232
+ return;
34233
+ }
34234
+ if (typeof j?.state === 'string') {
34235
+ const s = String(j.state).toLowerCase();
34236
+ if (['occupied', 'presence', 'present', 'on'].includes(s)) {
34237
+ this.occupied = true;
34238
+ return;
34239
+ }
34240
+ if (['vacant', 'absent', 'clear', 'off'].includes(s)) {
34241
+ this.occupied = false;
34242
+ return;
34243
+ }
34244
+ }
34153
34245
  }
34246
+ catch { }
34247
+ this.console?.debug?.(`Occupancy payload non gestito (${this.cfg.id}): "${raw}"`);
34154
34248
  }
34155
34249
  }
34156
34250
  /** ----------------- Main Plugin ----------------- */
34157
34251
  class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34158
34252
  constructor() {
34159
34253
  super();
34160
- // sensor management
34161
34254
  this.sensorsCfg = [];
34162
34255
  this.devices = new Map();
34163
- // (facoltativo) Imposta il device type in UI
34256
+ // Evita loop di log: tenta una volta finché deviceManager non c’è, poi riprova su eventi utili.
34257
+ this.discoveryPostponed = false;
34258
+ // Tipo in UI (best-effort)
34164
34259
  setTimeout(() => {
34165
34260
  try {
34166
34261
  systemManager.getDeviceById(this.id)?.setType?.(ScryptedDeviceType.SecuritySystem);
34167
34262
  }
34168
34263
  catch { }
34169
34264
  });
34170
- // Default state
34265
+ // Stato di default
34171
34266
  this.securitySystemState = this.securitySystemState || {
34172
34267
  mode: SecuritySystemMode.Disarmed,
34173
34268
  supportedModes: [
@@ -34178,12 +34273,12 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34178
34273
  ],
34179
34274
  };
34180
34275
  this.online = this.online ?? false;
34181
- // Load sensors config and announce devices
34276
+ // Config sensori e (tentativo) announce
34182
34277
  this.loadSensorsFromStorage();
34183
- this.discoverSensors().catch((e) => this.console.error('discoverSensors error', e));
34184
- // Connect on start
34278
+ this.safeDiscoverSensors(); // non spamma se deviceManager non c'è
34279
+ // Connect MQTT
34185
34280
  this.connectMqtt().catch((e) => this.console.error('MQTT connect error:', e));
34186
- // chiusura pulita del client MQTT ai reload/stop del plugin
34281
+ // Shutdown pulito
34187
34282
  try {
34188
34283
  process.once('SIGTERM', () => { try {
34189
34284
  this.client?.end(true);
@@ -34200,7 +34295,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34200
34295
  }
34201
34296
  catch { }
34202
34297
  }
34203
- // helpers persistenza
34298
+ /** ---- Settings ---- */
34204
34299
  saveSensorsToStorage() {
34205
34300
  try {
34206
34301
  this.storage.setItem('sensorsJson', JSON.stringify(this.sensorsCfg));
@@ -34209,17 +34304,14 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34209
34304
  this.console.error('saveSensorsToStorage error', e);
34210
34305
  }
34211
34306
  }
34212
- /** ---- Settings UI ---- */
34213
34307
  async getSettings() {
34214
34308
  const out = [
34215
- // MQTT Core
34216
34309
  { 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' },
34217
34310
  { group: 'MQTT', key: 'username', title: 'Username', type: 'string', value: this.storage.getItem('username') || '' },
34218
34311
  { group: 'MQTT', key: 'password', title: 'Password', type: 'password', value: this.storage.getItem('password') || '' },
34219
34312
  { group: 'MQTT', key: 'clientId', title: 'Client ID', placeholder: 'scrypted-paradox', value: this.storage.getItem('clientId') || 'scrypted-paradox' },
34220
34313
  { group: 'MQTT', key: 'tls', title: 'Use TLS', type: 'boolean', value: this.storage.getItem('tls') === 'true' },
34221
34314
  { 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
34223
34315
  { group: 'Alarm Topics', key: 'topicSetTarget', title: 'Set Target State (publish)', placeholder: 'paradox/control/partitions/Area_1', value: this.storage.getItem('topicSetTarget') || '' },
34224
34316
  { group: 'Alarm Topics', key: 'topicGetTarget', title: 'Get Target State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/target_state', value: this.storage.getItem('topicGetTarget') || '' },
34225
34317
  { group: 'Alarm Topics', key: 'topicGetCurrent', title: 'Get Current State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/current_state', value: this.storage.getItem('topicGetCurrent') || '' },
@@ -34228,31 +34320,24 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34228
34320
  { group: 'Publish Options', key: 'qos', title: 'QoS', type: 'integer', value: parseInt(this.storage.getItem('qos') || '0') },
34229
34321
  { group: 'Publish Options', key: 'retain', title: 'Retain', type: 'boolean', value: this.storage.getItem('retain') === 'true' },
34230
34322
  ];
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 ----
34323
+ // Add Sensor
34324
+ 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.' });
34325
+ // Sensors esistenti
34234
34326
  for (const cfg of this.sensorsCfg) {
34235
34327
  const gid = `Sensor: ${cfg.name} [${cfg.id}]`;
34236
34328
  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
34329
+ if (cfg.kind === 'contact')
34330
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.contact`, title: 'Contact State Topic', value: cfg.topics.contact || '' });
34331
+ else if (cfg.kind === 'motion')
34332
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.motion`, title: 'Motion Detected Topic', value: cfg.topics.motion || '' });
34333
+ else
34334
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.occupancy`, title: 'Occupancy Detected Topic', value: cfg.topics.occupancy || '' });
34248
34335
  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' });
34249
34336
  }
34250
34337
  return out;
34251
34338
  }
34252
34339
  async putSetting(key, value) {
34253
- // salva sempre nella storage la value del campo (così resta in UI)
34254
34340
  this.storage.setItem(key, String(value));
34255
- // --- Add Sensor workflow ---
34256
34341
  if (key === 'new.create' && String(value) === 'true') {
34257
34342
  const id = (this.storage.getItem('new.id') || '').trim();
34258
34343
  const name = (this.storage.getItem('new.name') || '').trim() || id;
@@ -34267,16 +34352,14 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34267
34352
  }
34268
34353
  this.sensorsCfg.push({ id, name, kind, topics: {} });
34269
34354
  this.saveSensorsToStorage();
34270
- // pulisci i campi "new.*"
34271
34355
  this.storage.removeItem('new.id');
34272
34356
  this.storage.removeItem('new.name');
34273
34357
  this.storage.removeItem('new.kind');
34274
34358
  this.storage.removeItem('new.create');
34275
- await this.discoverSensors();
34359
+ this.safeDiscoverSensors(true);
34276
34360
  await this.connectMqtt(true);
34277
34361
  return;
34278
34362
  }
34279
- // --- Edit/Remove sensore esistente ---
34280
34363
  const m = key.match(/^sensor\.([^\.]+)\.(.+)$/);
34281
34364
  if (m) {
34282
34365
  const sid = m[1];
@@ -34287,40 +34370,33 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34287
34370
  return;
34288
34371
  }
34289
34372
  if (prop === 'remove' && String(value) === 'true') {
34290
- // elimina
34291
34373
  this.sensorsCfg = this.sensorsCfg.filter(s => s.id !== sid);
34292
34374
  this.saveSensorsToStorage();
34293
34375
  try {
34294
- this.devices.delete(`sensor:${sid}`);
34295
34376
  sdk?.deviceManager?.onDeviceRemoved?.(`sensor:${sid}`);
34296
34377
  }
34297
34378
  catch { }
34298
- // pulisci flag
34299
34379
  this.storage.removeItem(key);
34300
- await this.discoverSensors();
34380
+ this.safeDiscoverSensors(true);
34301
34381
  await this.connectMqtt(true);
34302
34382
  return;
34303
34383
  }
34304
- if (prop === 'name') {
34384
+ if (prop === 'name')
34305
34385
  cfg.name = String(value);
34306
- }
34307
- else if (prop === 'kind') {
34386
+ else if (prop === 'kind')
34308
34387
  cfg.kind = String(value);
34309
- }
34310
34388
  else if (prop.startsWith('topic.')) {
34311
34389
  const tk = prop.substring('topic.'.length);
34312
34390
  cfg.topics[tk] = String(value).trim();
34313
34391
  }
34314
34392
  this.saveSensorsToStorage();
34315
- await this.discoverSensors();
34393
+ this.safeDiscoverSensors(true);
34316
34394
  await this.connectMqtt(true);
34317
34395
  return;
34318
34396
  }
34319
- // --- Altro (MQTT / Alarm settings) ---
34320
34397
  if (key === 'sensorsJson') {
34321
- // non più mostrato, ma se presente da vecchie versioni
34322
34398
  this.loadSensorsFromStorage();
34323
- await this.discoverSensors();
34399
+ this.safeDiscoverSensors(true);
34324
34400
  await this.connectMqtt(true);
34325
34401
  }
34326
34402
  else {
@@ -34328,15 +34404,12 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34328
34404
  }
34329
34405
  }
34330
34406
  /** ---- DeviceProvider ---- */
34331
- async getDevice(nativeId) {
34332
- return this.devices.get(nativeId);
34333
- }
34407
+ async getDevice(nativeId) { return this.devices.get(nativeId); }
34334
34408
  async releaseDevice(_id, nativeId) {
34335
34409
  try {
34336
34410
  const dev = this.devices.get(nativeId);
34337
- if (dev) {
34411
+ if (dev)
34338
34412
  this.devices.delete(nativeId);
34339
- }
34340
34413
  try {
34341
34414
  sdk?.deviceManager?.onDeviceRemoved?.(nativeId);
34342
34415
  }
@@ -34350,7 +34423,6 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34350
34423
  try {
34351
34424
  const raw = this.storage.getItem('sensorsJson') || '[]';
34352
34425
  const parsed = JSON.parse(raw);
34353
- // sanitize
34354
34426
  this.sensorsCfg = (parsed || []).filter(x => x && x.id && x.name && x.kind && x.topics);
34355
34427
  }
34356
34428
  catch (e) {
@@ -34358,15 +34430,25 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34358
34430
  this.sensorsCfg = [];
34359
34431
  }
34360
34432
  }
34361
- /** ===== discoverSensors: annuncia PRIMA, istanzia DOPO (con retry se manager non pronto) ===== */
34362
- async discoverSensors() {
34433
+ /** Annuncia i sensori SOLO se deviceManager è pronto. Niente loop infinito. */
34434
+ safeDiscoverSensors(triggeredByChange = false) {
34363
34435
  const dmAny = sdk?.deviceManager;
34364
34436
  if (!dmAny) {
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);
34437
+ // Posticipa una sola volta; poi riproviamo su connect MQTT e al primo messaggio
34438
+ if (!this.discoveryPostponed) {
34439
+ this.console.log('Device discovery postponed: deviceManager not ready yet.');
34440
+ this.discoveryPostponed = true;
34441
+ }
34367
34442
  return;
34368
34443
  }
34369
- // 1) Prepara i manifest
34444
+ this.discoveryPostponed = false;
34445
+ this.discoverSensors(dmAny);
34446
+ if (triggeredByChange)
34447
+ this.console.log('Sensors discovered/updated.');
34448
+ }
34449
+ /** discoverSensors con deviceManager garantito */
34450
+ discoverSensors(dmAny) {
34451
+ // 1) Manifests
34370
34452
  const manifests = this.sensorsCfg.map(cfg => {
34371
34453
  const nativeId = `sensor:${cfg.id}`;
34372
34454
  const t = cfg.topics || {};
@@ -34386,18 +34468,10 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34386
34468
  // 2) Annuncio
34387
34469
  if (typeof dmAny.onDevicesChanged === 'function') {
34388
34470
  dmAny.onDevicesChanged({ devices: manifests });
34389
- this.console.log('Annunciati (batch):', manifests.map(m => m.nativeId).join(', '));
34390
34471
  }
34391
34472
  else if (typeof dmAny.onDeviceDiscovered === 'function') {
34392
- for (const m of manifests) {
34473
+ for (const m of manifests)
34393
34474
  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;
34401
34475
  }
34402
34476
  // 3) Istanzia/aggiorna
34403
34477
  for (const cfg of this.sensorsCfg) {
@@ -34416,18 +34490,16 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34416
34490
  dev.cfg = cfg;
34417
34491
  }
34418
34492
  const hasBattery = !!(cfg.topics.batteryLevel || cfg.topics.lowBattery);
34419
- if (hasBattery && dev.batteryLevel === undefined) {
34493
+ if (hasBattery && dev.batteryLevel === undefined)
34420
34494
  dev.batteryLevel = 100;
34421
- }
34422
34495
  }
34423
- // 4) Rimuovi quelli spariti
34496
+ // 4) Cleanup
34424
34497
  const announced = new Set(manifests.map(m => m.nativeId));
34425
34498
  for (const [nativeId] of this.devices) {
34426
34499
  if (!announced.has(nativeId)) {
34427
34500
  try {
34428
34501
  this.devices.delete(nativeId);
34429
- sdk?.deviceManager?.onDeviceRemoved?.(nativeId);
34430
- this.console.log('Rimosso:', nativeId);
34502
+ dmAny.onDeviceRemoved?.(nativeId);
34431
34503
  }
34432
34504
  catch { }
34433
34505
  }
@@ -34441,13 +34513,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34441
34513
  const clientId = this.storage.getItem('clientId') || 'scrypted-paradox';
34442
34514
  const tls = this.storage.getItem('tls') === 'true';
34443
34515
  const rejectUnauthorized = this.storage.getItem('rejectUnauthorized') !== 'false';
34444
- const opts = {
34445
- clientId,
34446
- username,
34447
- password,
34448
- clean: true,
34449
- reconnectPeriod: 3000,
34450
- };
34516
+ const opts = { clientId, username, password, clean: true, reconnectPeriod: 3000 };
34451
34517
  if (tls) {
34452
34518
  opts.protocol = 'mqtts';
34453
34519
  opts.rejectUnauthorized = rejectUnauthorized;
@@ -34456,38 +34522,34 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34456
34522
  }
34457
34523
  collectAllSubscriptions() {
34458
34524
  const subs = new Set();
34459
- // alarm
34460
34525
  for (const k of ['topicGetTarget', 'topicGetCurrent', 'topicTamper', 'topicOnline']) {
34461
34526
  const v = this.storage.getItem(k);
34462
34527
  if (v)
34463
34528
  subs.add(v);
34464
34529
  }
34465
- // sensors
34466
34530
  for (const s of this.sensorsCfg) {
34467
34531
  const t = s.topics || {};
34468
34532
  [t.contact, t.motion, t.occupancy, t.batteryLevel, t.lowBattery, t.tamper, t.online]
34469
- .filter(Boolean)
34470
- .forEach(x => subs.add(String(x)));
34533
+ .filter(Boolean).forEach(x => subs.add(String(x)));
34471
34534
  }
34472
34535
  return Array.from(subs);
34473
34536
  }
34474
34537
  async connectMqtt(_reconnect = false) {
34475
34538
  const subs = this.collectAllSubscriptions();
34476
- if (!subs.length && !this.storage.getItem('topicSetTarget')) {
34539
+ if (!subs.length && !this.storage.getItem('topicSetTarget'))
34477
34540
  this.console.warn('Configura almeno un topic nelle impostazioni.');
34478
- }
34479
34541
  if (this.client) {
34480
34542
  try {
34481
34543
  this.client.end(true);
34482
34544
  }
34483
34545
  catch { }
34546
+ ;
34484
34547
  this.client = undefined;
34485
34548
  }
34486
34549
  const { url, opts } = this.getMqttOptions();
34487
34550
  this.console.log(`Connecting MQTT ${url} ...`);
34488
34551
  const client = mqtt_1.default.connect(url, opts);
34489
34552
  this.client = client;
34490
- // cache alarm topics for fast compare
34491
34553
  const tTarget = this.storage.getItem('topicGetTarget') || '';
34492
34554
  const tCurrent = this.storage.getItem('topicGetCurrent') || '';
34493
34555
  const tTamper = this.storage.getItem('topicTamper') || '';
@@ -34495,12 +34557,11 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34495
34557
  client.on('connect', () => {
34496
34558
  this.console.log('MQTT connected');
34497
34559
  this.online = true;
34498
- if (subs.length) {
34499
- client.subscribe(subs, { qos: 0 }, (err) => {
34500
- if (err)
34501
- this.console.error('subscribe error', err);
34502
- });
34503
- }
34560
+ if (subs.length)
34561
+ client.subscribe(subs, { qos: 0 }, (err) => { if (err)
34562
+ this.console.error('subscribe error', err); });
34563
+ // Al primo connect riprova (silenziosamente) ad annunciare i sensori
34564
+ this.safeDiscoverSensors(true);
34504
34565
  });
34505
34566
  client.on('reconnect', () => this.console.log('MQTT reconnecting...'));
34506
34567
  client.on('close', () => { this.console.log('MQTT closed'); this.online = false; });
@@ -34509,7 +34570,6 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34509
34570
  try {
34510
34571
  const p = payload?.toString() ?? '';
34511
34572
  const np = normalize(p);
34512
- // ---- Alarm handling ----
34513
34573
  if (topic === tOnline) {
34514
34574
  if (truthy(np) || np === 'online')
34515
34575
  this.online = true;
@@ -34518,19 +34578,17 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34518
34578
  return;
34519
34579
  }
34520
34580
  if (topic === tTamper) {
34521
- if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np)) {
34581
+ if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np))
34522
34582
  this.tampered = ['cover', 'intrusion'].find(x => x === np) || true;
34523
- }
34524
- else if (falsy(np)) {
34583
+ else if (falsy(np))
34525
34584
  this.tampered = false;
34526
- }
34527
34585
  return;
34528
34586
  }
34529
34587
  if (topic === tCurrent) {
34530
34588
  const mode = payloadToMode(payload);
34531
34589
  const isAlarm = ['alarm', 'triggered'].includes(np);
34532
34590
  const current = this.securitySystemState || { mode: SecuritySystemMode.Disarmed };
34533
- const newState = {
34591
+ this.securitySystemState = {
34534
34592
  mode: mode ?? current.mode,
34535
34593
  supportedModes: current.supportedModes ?? [
34536
34594
  SecuritySystemMode.Disarmed,
@@ -34540,7 +34598,6 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34540
34598
  ],
34541
34599
  triggered: isAlarm || undefined,
34542
34600
  };
34543
- this.securitySystemState = newState;
34544
34601
  return;
34545
34602
  }
34546
34603
  if (topic === tTarget) {
@@ -34548,10 +34605,12 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34548
34605
  this.console.log('Target state reported:', p, '->', this.pendingTarget);
34549
34606
  return;
34550
34607
  }
34551
- // ---- Sensor dispatch ----
34552
- for (const dev of this.devices.values()) {
34608
+ // Dispatch ai sensori
34609
+ // (E prova ad annunciare se era stato posticipato e ora il manager è pronto)
34610
+ if (this.discoveryPostponed)
34611
+ this.safeDiscoverSensors(true);
34612
+ for (const dev of this.devices.values())
34553
34613
  dev.handleMqtt(topic, payload);
34554
- }
34555
34614
  }
34556
34615
  catch (e) {
34557
34616
  this.console.error('MQTT message handler error', e);
@@ -34576,7 +34635,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34576
34635
  async armSecuritySystem(mode) {
34577
34636
  const payload = this.getOutgoing(mode);
34578
34637
  this.console.log('armSecuritySystem', mode, '->', payload);
34579
- this.pendingTarget = mode; // memorizza target, ma NON cambiare il current
34638
+ this.pendingTarget = mode;
34580
34639
  this.publishSetTarget(payload);
34581
34640
  }
34582
34641
  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.35",
3
+ "version": "1.0.37",
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",