@rfranzoi/scrypted-mqtt-securitysystem 1.0.47 → 1.0.48

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.
@@ -34086,8 +34086,8 @@ const DEFAULT_OUTGOING = {
34086
34086
  [sdk_1.SecuritySystemMode.AwayArmed]: 'arm_away',
34087
34087
  [sdk_1.SecuritySystemMode.NightArmed]: 'arm_night',
34088
34088
  };
34089
- /** Parse incoming payload -> final mode (ignore transition states) */
34090
- function payloadToMode(payload) {
34089
+ /** Fallback (non-strict) parser con sinonimi */
34090
+ function payloadToModeLoose(payload) {
34091
34091
  if (payload == null)
34092
34092
  return;
34093
34093
  const p = normalize(payload.toString());
@@ -34108,7 +34108,7 @@ class BaseMqttSensor extends sdk_1.ScryptedDeviceBase {
34108
34108
  super(nativeId);
34109
34109
  this.cfg = cfg;
34110
34110
  }
34111
- /** setter centralizzato: cambia lo stato SOLO se diverso e poi emette l'evento relativo */
34111
+ /** setter centralizzato + evento */
34112
34112
  setAndEmit(prop, val, iface) {
34113
34113
  const prev = this[prop];
34114
34114
  if (prev === val)
@@ -34144,19 +34144,15 @@ class BaseMqttSensor extends sdk_1.ScryptedDeviceBase {
34144
34144
  }
34145
34145
  // battery
34146
34146
  if (topic === this.cfg.topics.batteryLevel) {
34147
- // aggiorna solo con numeri validi 0..100
34148
34147
  const n = clamp(parseFloat(raw), 0, 100);
34149
34148
  if (Number.isFinite(n))
34150
34149
  this.setAndEmit('batteryLevel', n, sdk_1.ScryptedInterface.Battery);
34151
34150
  }
34152
34151
  else if (topic === this.cfg.topics.lowBattery && !this.cfg.topics.batteryLevel) {
34153
- // LowBattery booleano: quando TRUE -> 10%; quando FALSE non forzare nulla, lascia il valore precedente
34154
34152
  if (truthy(np))
34155
34153
  this.setAndEmit('batteryLevel', 10, sdk_1.ScryptedInterface.Battery);
34156
- else if (falsy(np) && this.batteryLevel === undefined) {
34157
- // se è il primo messaggio ed è "ok", metti 100 (altrimenti resta undefined)
34154
+ else if (falsy(np) && this.batteryLevel === undefined)
34158
34155
  this.setAndEmit('batteryLevel', 100, sdk_1.ScryptedInterface.Battery);
34159
- }
34160
34156
  }
34161
34157
  // primary handled by subclasses
34162
34158
  this.handlePrimary(topic, np, raw);
@@ -34208,17 +34204,14 @@ class OccupancyMqttSensor extends BaseMqttSensor {
34208
34204
  class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34209
34205
  constructor() {
34210
34206
  super();
34211
- // sensor management
34212
34207
  this.sensorsCfg = [];
34213
34208
  this.devices = new Map();
34214
- // (facoltativo) Imposta il device type in UI
34215
34209
  setTimeout(() => {
34216
34210
  try {
34217
34211
  systemManager.getDeviceById(this.id)?.setType?.(sdk_1.ScryptedDeviceType.SecuritySystem);
34218
34212
  }
34219
34213
  catch { }
34220
34214
  });
34221
- // Default state
34222
34215
  this.securitySystemState = this.securitySystemState || {
34223
34216
  mode: sdk_1.SecuritySystemMode.Disarmed,
34224
34217
  supportedModes: [
@@ -34229,12 +34222,9 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34229
34222
  ],
34230
34223
  };
34231
34224
  this.online = this.online ?? false;
34232
- // Load sensors config and announce devices
34233
34225
  this.loadSensorsFromStorage();
34234
34226
  this.discoverSensors().catch(e => this.console.error('discoverSensors error', e));
34235
- // Connect on start
34236
34227
  this.connectMqtt().catch(e => this.console.error('MQTT connect error:', e));
34237
- // chiusura pulita del client MQTT ai reload/stop del plugin
34238
34228
  try {
34239
34229
  process.once('SIGTERM', () => { try {
34240
34230
  this.client?.end(true);
@@ -34251,6 +34241,60 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34251
34241
  }
34252
34242
  catch { }
34253
34243
  }
34244
+ /** ---- Helpers parsing ---- */
34245
+ parseJsonArray(key, fallback) {
34246
+ try {
34247
+ const raw = (this.storage.getItem(key) || '').trim();
34248
+ if (!raw)
34249
+ return fallback;
34250
+ const arr = JSON.parse(raw);
34251
+ if (!Array.isArray(arr))
34252
+ return fallback;
34253
+ return arr.map((x) => normalize(String(x))).filter(Boolean);
34254
+ }
34255
+ catch {
34256
+ return fallback;
34257
+ }
34258
+ }
34259
+ getStrictTokens() {
34260
+ const target = this.parseJsonArray('targetStateValues', ['armed_home', 'armed_away', 'armed_night', 'disarmed']);
34261
+ const current = this.parseJsonArray('currentStateValues', ['armed_home', 'armed_away', 'armed_night', 'disarmed', 'triggered']);
34262
+ const triggered = this.parseJsonArray('triggeredValues', ['triggered', 'alarm']);
34263
+ const union = new Set([...target, ...current]);
34264
+ return {
34265
+ union,
34266
+ triggered: new Set(triggered),
34267
+ };
34268
+ }
34269
+ useStrict() {
34270
+ return this.storage.getItem('strictParsing') === 'true';
34271
+ }
34272
+ parseIncomingMode(payload) {
34273
+ const p = payload?.toString?.() ?? String(payload ?? '');
34274
+ const np = normalize(p);
34275
+ if (!this.useStrict())
34276
+ return payloadToModeLoose(np);
34277
+ // strict: usa SOLO i token configurati
34278
+ const { union } = this.getStrictTokens();
34279
+ if (union.has('disarmed') && np === 'disarmed')
34280
+ return sdk_1.SecuritySystemMode.Disarmed;
34281
+ if (union.has('armed_home') && np === 'armed_home')
34282
+ return sdk_1.SecuritySystemMode.HomeArmed;
34283
+ if (union.has('armed_away') && np === 'armed_away')
34284
+ return sdk_1.SecuritySystemMode.AwayArmed;
34285
+ if (union.has('armed_night') && np === 'armed_night')
34286
+ return sdk_1.SecuritySystemMode.NightArmed;
34287
+ // transitori/altro: ignora
34288
+ return undefined;
34289
+ }
34290
+ isTriggeredToken(np) {
34291
+ if (this.useStrict()) {
34292
+ const { triggered } = this.getStrictTokens();
34293
+ return triggered.has(np);
34294
+ }
34295
+ // loose
34296
+ return np === 'triggered' || np === 'alarm';
34297
+ }
34254
34298
  // helpers persistenza
34255
34299
  saveSensorsToStorage() {
34256
34300
  try {
@@ -34278,6 +34322,11 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34278
34322
  { group: 'Alarm Topics', key: 'topicOnline', title: 'Get Online (subscribe)', placeholder: 'paradox/interface/availability', value: this.storage.getItem('topicOnline') || '' },
34279
34323
  { group: 'Publish Options', key: 'qos', title: 'QoS', type: 'integer', value: parseInt(this.storage.getItem('qos') || '0') },
34280
34324
  { group: 'Publish Options', key: 'retain', title: 'Retain', type: 'boolean', value: this.storage.getItem('retain') === 'true' },
34325
+ // --- Parsing / State tokens ---
34326
+ { group: 'Parsing / State tokens', key: 'strictParsing', title: 'Use strict tokens (disable synonyms)', type: 'boolean', value: this.storage.getItem('strictParsing') === 'true' },
34327
+ { group: 'Parsing / State tokens', key: 'targetStateValues', title: 'Accepted Target State Values (JSON array)', placeholder: '["armed_home","armed_away","armed_night","disarmed"]', value: this.storage.getItem('targetStateValues') || '["armed_home","armed_away","armed_night","disarmed"]' },
34328
+ { group: 'Parsing / State tokens', key: 'currentStateValues', title: 'Accepted Current State Values (JSON array)', placeholder: '["armed_home","armed_away","armed_night","disarmed","triggered"]', value: this.storage.getItem('currentStateValues') || '["armed_home","armed_away","armed_night","disarmed","triggered"]' },
34329
+ { group: 'Parsing / State tokens', key: 'triggeredValues', title: 'Triggered tokens (JSON array)', placeholder: '["triggered","alarm"]', value: this.storage.getItem('triggeredValues') || '["triggered","alarm"]' },
34281
34330
  ];
34282
34331
  // ---- UI Add Sensor ----
34283
34332
  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.' });
@@ -34285,7 +34334,6 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34285
34334
  for (const cfg of this.sensorsCfg) {
34286
34335
  const gid = `Sensor: ${cfg.name} [${cfg.id}]`;
34287
34336
  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'] });
34288
- // primary per tipo
34289
34337
  if (cfg.kind === 'contact') {
34290
34338
  out.push({ group: gid, key: `sensor.${cfg.id}.topic.contact`, title: 'Contact State Topic', value: cfg.topics.contact || '', placeholder: 'paradox/states/zones/XYZ/open' });
34291
34339
  }
@@ -34295,14 +34343,13 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34295
34343
  else {
34296
34344
  out.push({ group: gid, key: `sensor.${cfg.id}.topic.occupancy`, title: 'Occupancy Detected Topic', value: cfg.topics.occupancy || '', placeholder: 'paradox/states/zones/XYZ/open' });
34297
34345
  }
34298
- // extra opzionali
34299
34346
  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' });
34300
34347
  }
34301
34348
  return out;
34302
34349
  }
34303
34350
  async putSetting(key, value) {
34304
34351
  this.storage.setItem(key, String(value));
34305
- // --- Add Sensor workflow ---
34352
+ // Add Sensor
34306
34353
  if (key === 'new.create' && String(value) === 'true') {
34307
34354
  const id = (this.storage.getItem('new.id') || '').trim();
34308
34355
  const name = (this.storage.getItem('new.name') || '').trim() || id;
@@ -34325,7 +34372,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34325
34372
  await this.connectMqtt(true);
34326
34373
  return;
34327
34374
  }
34328
- // --- Edit/Remove sensore esistente ---
34375
+ // Edit/Remove sensore
34329
34376
  const m = key.match(/^sensor\.([^\.]+)\.(.+)$/);
34330
34377
  if (m) {
34331
34378
  const sid = m[1];
@@ -34361,7 +34408,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34361
34408
  await this.connectMqtt(true);
34362
34409
  return;
34363
34410
  }
34364
- // --- Altro (MQTT / Alarm settings) ---
34411
+ // altro
34365
34412
  if (key === 'sensorsJson') {
34366
34413
  this.loadSensorsFromStorage();
34367
34414
  await this.discoverSensors();
@@ -34372,9 +34419,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34372
34419
  }
34373
34420
  }
34374
34421
  /** ---- DeviceProvider ---- */
34375
- async getDevice(nativeId) {
34376
- return this.devices.get(nativeId);
34377
- }
34422
+ async getDevice(nativeId) { return this.devices.get(nativeId); }
34378
34423
  async releaseDevice(_id, nativeId) {
34379
34424
  try {
34380
34425
  const dev = this.devices.get(nativeId);
@@ -34415,10 +34460,8 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34415
34460
  interfaces.unshift(sdk_1.ScryptedInterface.MotionSensor);
34416
34461
  else
34417
34462
  interfaces.unshift(sdk_1.ScryptedInterface.OccupancySensor);
34418
- // Espone Battery SOLTANTO se è configurato almeno un topic batteria (stringa non vuota)
34419
- if ((t.batteryLevel && t.batteryLevel.trim()) || (t.lowBattery && t.lowBattery.trim())) {
34463
+ if ((t.batteryLevel && t.batteryLevel.trim()) || (t.lowBattery && t.lowBattery.trim()))
34420
34464
  interfaces.push(sdk_1.ScryptedInterface.Battery);
34421
- }
34422
34465
  return { nativeId, name: cfg.name, type: sdk_1.ScryptedDeviceType.Sensor, interfaces };
34423
34466
  });
34424
34467
  // 2) Annuncio
@@ -34433,7 +34476,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34433
34476
  this.console.log('Annunciato:', m.nativeId);
34434
34477
  }
34435
34478
  }
34436
- // 3) Istanzia/aggiorna DOPO l’annuncio
34479
+ // 3) Istanzia/aggiorna
34437
34480
  for (const cfg of this.sensorsCfg) {
34438
34481
  const nativeId = `sensor:${cfg.id}`;
34439
34482
  let dev = this.devices.get(nativeId);
@@ -34449,9 +34492,9 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34449
34492
  else {
34450
34493
  dev.cfg = cfg;
34451
34494
  }
34452
- // <<< RIMOSSO il default batteryLevel=100: niente valore finché non arriva un messaggio >>>
34495
+ // niente default batteria qui
34453
34496
  }
34454
- // 4) Rimuovi quelli spariti
34497
+ // 4) cleanup
34455
34498
  const announced = new Set(manifests.map(m => m.nativeId));
34456
34499
  for (const [nativeId] of this.devices) {
34457
34500
  if (!announced.has(nativeId)) {
@@ -34472,13 +34515,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34472
34515
  const clientId = this.storage.getItem('clientId') || 'scrypted-paradox';
34473
34516
  const tls = this.storage.getItem('tls') === 'true';
34474
34517
  const rejectUnauthorized = this.storage.getItem('rejectUnauthorized') !== 'false';
34475
- const opts = {
34476
- clientId,
34477
- username,
34478
- password,
34479
- clean: true,
34480
- reconnectPeriod: 3000,
34481
- };
34518
+ const opts = { clientId, username, password, clean: true, reconnectPeriod: 3000 };
34482
34519
  if (tls) {
34483
34520
  opts.protocol = 'mqtts';
34484
34521
  opts.rejectUnauthorized = rejectUnauthorized;
@@ -34487,13 +34524,11 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34487
34524
  }
34488
34525
  collectAllSubscriptions() {
34489
34526
  const subs = new Set();
34490
- // alarm
34491
34527
  for (const k of ['topicGetTarget', 'topicGetCurrent', 'topicTamper', 'topicOnline']) {
34492
34528
  const v = this.storage.getItem(k);
34493
34529
  if (v)
34494
34530
  subs.add(v);
34495
34531
  }
34496
- // sensors
34497
34532
  for (const s of this.sensorsCfg) {
34498
34533
  const t = s.topics || {};
34499
34534
  [t.contact, t.motion, t.occupancy, t.batteryLevel, t.lowBattery, t.tamper, t.online]
@@ -34512,13 +34547,13 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34512
34547
  this.client.end(true);
34513
34548
  }
34514
34549
  catch { }
34550
+ ;
34515
34551
  this.client = undefined;
34516
34552
  }
34517
34553
  const { url, opts } = this.getMqttOptions();
34518
34554
  this.console.log(`Connecting MQTT ${url} ...`);
34519
34555
  const client = mqtt_1.default.connect(url, opts);
34520
34556
  this.client = client;
34521
- // cache alarm topics for fast compare
34522
34557
  const tTarget = this.storage.getItem('topicGetTarget') || '';
34523
34558
  const tCurrent = this.storage.getItem('topicGetCurrent') || '';
34524
34559
  const tTamper = this.storage.getItem('topicTamper') || '';
@@ -34530,12 +34565,9 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34530
34565
  this.onDeviceEvent(sdk_1.ScryptedInterface.Online, true);
34531
34566
  }
34532
34567
  catch { }
34533
- if (subs.length) {
34534
- client.subscribe(subs, { qos: 0 }, (err) => {
34535
- if (err)
34536
- this.console.error('subscribe error', err);
34537
- });
34538
- }
34568
+ if (subs.length)
34569
+ client.subscribe(subs, { qos: 0 }, (err) => { if (err)
34570
+ this.console.error('subscribe error', err); });
34539
34571
  });
34540
34572
  client.on('reconnect', () => this.console.log('MQTT reconnecting...'));
34541
34573
  client.on('close', () => {
@@ -34551,7 +34583,6 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34551
34583
  try {
34552
34584
  const p = payload?.toString() ?? '';
34553
34585
  const np = normalize(p);
34554
- // ---- Alarm handling ----
34555
34586
  if (topic === tOnline) {
34556
34587
  if (truthy(np) || np === 'online') {
34557
34588
  this.online = true;
@@ -34588,8 +34619,8 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34588
34619
  return;
34589
34620
  }
34590
34621
  if (topic === tCurrent) {
34591
- const mode = payloadToMode(payload);
34592
- const isAlarm = ['alarm', 'triggered'].includes(np);
34622
+ const mode = this.parseIncomingMode(payload);
34623
+ const isAlarm = this.isTriggeredToken(np);
34593
34624
  const current = this.securitySystemState || { mode: sdk_1.SecuritySystemMode.Disarmed };
34594
34625
  const newState = {
34595
34626
  mode: mode ?? current.mode,
@@ -34609,14 +34640,13 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34609
34640
  return;
34610
34641
  }
34611
34642
  if (topic === tTarget) {
34612
- this.pendingTarget = payloadToMode(payload);
34643
+ this.pendingTarget = this.parseIncomingMode(payload);
34613
34644
  this.console.log('Target state reported:', p, '->', this.pendingTarget);
34614
34645
  return;
34615
34646
  }
34616
34647
  // ---- Sensor dispatch ----
34617
- for (const dev of this.devices.values()) {
34648
+ for (const dev of this.devices.values())
34618
34649
  dev.handleMqtt(topic, payload);
34619
- }
34620
34650
  }
34621
34651
  catch (e) {
34622
34652
  this.console.error('MQTT message handler error', e);
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.47",
3
+ "version": "1.0.48",
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",