@rfranzoi/scrypted-mqtt-securitysystem 1.0.47 → 1.0.49

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.
@@ -34077,8 +34077,12 @@ function falsy(v) {
34077
34077
  const s = v.toString().trim().toLowerCase();
34078
34078
  return s === '0' || s === 'false' || s === 'offline' || s === 'no' || s === 'off';
34079
34079
  }
34080
- function normalize(s) { return (s || '').trim().toLowerCase(); }
34081
- function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
34080
+ function normalize(s) {
34081
+ return (s || '').trim().toLowerCase();
34082
+ }
34083
+ function clamp(n, min, max) {
34084
+ return Math.max(min, Math.min(max, n));
34085
+ }
34082
34086
  /** SecuritySystem outgoing defaults (PAI-like) */
34083
34087
  const DEFAULT_OUTGOING = {
34084
34088
  [sdk_1.SecuritySystemMode.Disarmed]: 'disarm',
@@ -34086,8 +34090,8 @@ const DEFAULT_OUTGOING = {
34086
34090
  [sdk_1.SecuritySystemMode.AwayArmed]: 'arm_away',
34087
34091
  [sdk_1.SecuritySystemMode.NightArmed]: 'arm_night',
34088
34092
  };
34089
- /** Parse incoming payload -> final mode (ignore transition states) */
34090
- function payloadToMode(payload) {
34093
+ /** Fallback (non-strict) parser con sinonimi */
34094
+ function payloadToModeLoose(payload) {
34091
34095
  if (payload == null)
34092
34096
  return;
34093
34097
  const p = normalize(payload.toString());
@@ -34108,7 +34112,7 @@ class BaseMqttSensor extends sdk_1.ScryptedDeviceBase {
34108
34112
  super(nativeId);
34109
34113
  this.cfg = cfg;
34110
34114
  }
34111
- /** setter centralizzato: cambia lo stato SOLO se diverso e poi emette l'evento relativo */
34115
+ /** setter centralizzato + evento */
34112
34116
  setAndEmit(prop, val, iface) {
34113
34117
  const prev = this[prop];
34114
34118
  if (prev === val)
@@ -34144,19 +34148,16 @@ class BaseMqttSensor extends sdk_1.ScryptedDeviceBase {
34144
34148
  }
34145
34149
  // battery
34146
34150
  if (topic === this.cfg.topics.batteryLevel) {
34147
- // aggiorna solo con numeri validi 0..100
34148
34151
  const n = clamp(parseFloat(raw), 0, 100);
34149
34152
  if (Number.isFinite(n))
34150
34153
  this.setAndEmit('batteryLevel', n, sdk_1.ScryptedInterface.Battery);
34151
34154
  }
34152
34155
  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
34156
+ // Se abbiamo solo lowBattery: true => 10%, false => 100% (ma solo se non c'è già un valore)
34154
34157
  if (truthy(np))
34155
34158
  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)
34159
+ else if (falsy(np) && this.batteryLevel === undefined)
34158
34160
  this.setAndEmit('batteryLevel', 100, sdk_1.ScryptedInterface.Battery);
34159
- }
34160
34161
  }
34161
34162
  // primary handled by subclasses
34162
34163
  this.handlePrimary(topic, np, raw);
@@ -34251,6 +34252,61 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34251
34252
  }
34252
34253
  catch { }
34253
34254
  }
34255
+ // ====== Strict parsing helpers ======
34256
+ parseJsonArray(key, fallback) {
34257
+ try {
34258
+ const raw = (this.storage.getItem(key) || '').trim();
34259
+ if (!raw)
34260
+ return fallback;
34261
+ const arr = JSON.parse(raw);
34262
+ if (!Array.isArray(arr))
34263
+ return fallback;
34264
+ return arr.map((x) => normalize(String(x))).filter(Boolean);
34265
+ }
34266
+ catch {
34267
+ return fallback;
34268
+ }
34269
+ }
34270
+ useStrict() {
34271
+ return this.storage.getItem('strictParsing') === 'true';
34272
+ }
34273
+ parseIncomingMode(payload) {
34274
+ const np = normalize(payload?.toString?.() ?? String(payload ?? ''));
34275
+ if (!this.useStrict())
34276
+ return payloadToModeLoose(np);
34277
+ const currentVals = new Set(this.parseJsonArray('currentStateValues', ['armed_home', 'armed_away', 'armed_night', 'disarmed', 'triggered']));
34278
+ if (currentVals.has('disarmed') && np === 'disarmed')
34279
+ return sdk_1.SecuritySystemMode.Disarmed;
34280
+ if (currentVals.has('armed_home') && np === 'armed_home')
34281
+ return sdk_1.SecuritySystemMode.HomeArmed;
34282
+ if (currentVals.has('armed_away') && np === 'armed_away')
34283
+ return sdk_1.SecuritySystemMode.AwayArmed;
34284
+ if (currentVals.has('armed_night') && np === 'armed_night')
34285
+ return sdk_1.SecuritySystemMode.NightArmed;
34286
+ return undefined;
34287
+ }
34288
+ isTriggeredToken(np) {
34289
+ if (this.useStrict()) {
34290
+ const triggered = new Set(this.parseJsonArray('triggeredValues', ['triggered', 'alarm']));
34291
+ return triggered.has(np);
34292
+ }
34293
+ return np === 'triggered' || np === 'alarm';
34294
+ }
34295
+ /** Sceglie il token di publish preferito per ciascuna modalità usando targetStateValues quando strict=ON */
34296
+ preferredTokenForMode(mode) {
34297
+ const t = this.parseJsonArray('targetStateValues', ['armed_home', 'armed_away', 'armed_night', 'disarmed']);
34298
+ const pick = (...cands) => cands.find(c => t.includes(c));
34299
+ switch (mode) {
34300
+ case sdk_1.SecuritySystemMode.Disarmed:
34301
+ return pick('disarmed', 'disarm') || 'disarmed';
34302
+ case sdk_1.SecuritySystemMode.HomeArmed:
34303
+ return pick('armed_home', 'arm_home', 'home', 'stay') || 'armed_home';
34304
+ case sdk_1.SecuritySystemMode.AwayArmed:
34305
+ return pick('armed_away', 'arm_away', 'away') || 'armed_away';
34306
+ case sdk_1.SecuritySystemMode.NightArmed:
34307
+ return pick('armed_night', 'arm_night', 'night', 'sleep') || 'armed_night';
34308
+ }
34309
+ }
34254
34310
  // helpers persistenza
34255
34311
  saveSensorsToStorage() {
34256
34312
  try {
@@ -34278,6 +34334,16 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34278
34334
  { group: 'Alarm Topics', key: 'topicOnline', title: 'Get Online (subscribe)', placeholder: 'paradox/interface/availability', value: this.storage.getItem('topicOnline') || '' },
34279
34335
  { group: 'Publish Options', key: 'qos', title: 'QoS', type: 'integer', value: parseInt(this.storage.getItem('qos') || '0') },
34280
34336
  { group: 'Publish Options', key: 'retain', title: 'Retain', type: 'boolean', value: this.storage.getItem('retain') === 'true' },
34337
+ // --- Parsing / State tokens ---
34338
+ { group: 'Parsing / State tokens', key: 'strictParsing', title: 'Use strict tokens (disable synonyms)', type: 'boolean', value: this.storage.getItem('strictParsing') === 'true' },
34339
+ { 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"]' },
34340
+ { 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"]' },
34341
+ { group: 'Parsing / State tokens', key: 'triggeredValues', title: 'Triggered tokens (JSON array)', placeholder: '["triggered","alarm"]', value: this.storage.getItem('triggeredValues') || '["triggered","alarm"]' },
34342
+ // --- Publish Payloads (override) ---
34343
+ { group: 'Publish Payloads (override)', key: 'payloadDisarm', title: 'Payload for Disarm', placeholder: 'disarmed', value: this.storage.getItem('payloadDisarm') || '', description: 'Se vuoto: usa targetStateValues (strict ON) o i default arm_*/disarm (strict OFF).' },
34344
+ { group: 'Publish Payloads (override)', key: 'payloadHome', title: 'Payload for Home Armed', placeholder: 'armed_home', value: this.storage.getItem('payloadHome') || '' },
34345
+ { group: 'Publish Payloads (override)', key: 'payloadAway', title: 'Payload for Away Armed', placeholder: 'armed_away', value: this.storage.getItem('payloadAway') || '' },
34346
+ { group: 'Publish Payloads (override)', key: 'payloadNight', title: 'Payload for Night Armed', placeholder: 'armed_night', value: this.storage.getItem('payloadNight') || '' },
34281
34347
  ];
34282
34348
  // ---- UI Add Sensor ----
34283
34349
  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.' });
@@ -34301,6 +34367,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34301
34367
  return out;
34302
34368
  }
34303
34369
  async putSetting(key, value) {
34370
+ // salva sempre nella storage la value del campo (così resta in UI)
34304
34371
  this.storage.setItem(key, String(value));
34305
34372
  // --- Add Sensor workflow ---
34306
34373
  if (key === 'new.create' && String(value) === 'true') {
@@ -34317,6 +34384,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34317
34384
  }
34318
34385
  this.sensorsCfg.push({ id, name, kind, topics: {} });
34319
34386
  this.saveSensorsToStorage();
34387
+ // pulisci i campi "new.*"
34320
34388
  this.storage.removeItem('new.id');
34321
34389
  this.storage.removeItem('new.name');
34322
34390
  this.storage.removeItem('new.kind');
@@ -34336,6 +34404,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34336
34404
  return;
34337
34405
  }
34338
34406
  if (prop === 'remove' && String(value) === 'true') {
34407
+ // elimina
34339
34408
  this.sensorsCfg = this.sensorsCfg.filter(s => s.id !== sid);
34340
34409
  this.saveSensorsToStorage();
34341
34410
  try {
@@ -34343,15 +34412,18 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34343
34412
  deviceManager.onDeviceRemoved?.(`sensor:${sid}`);
34344
34413
  }
34345
34414
  catch { }
34415
+ // pulisci flag
34346
34416
  this.storage.removeItem(key);
34347
34417
  await this.discoverSensors();
34348
34418
  await this.connectMqtt(true);
34349
34419
  return;
34350
34420
  }
34351
- if (prop === 'name')
34421
+ if (prop === 'name') {
34352
34422
  cfg.name = String(value);
34353
- else if (prop === 'kind')
34423
+ }
34424
+ else if (prop === 'kind') {
34354
34425
  cfg.kind = String(value);
34426
+ }
34355
34427
  else if (prop.startsWith('topic.')) {
34356
34428
  const tk = prop.substring('topic.'.length);
34357
34429
  cfg.topics[tk] = String(value).trim();
@@ -34361,8 +34433,9 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34361
34433
  await this.connectMqtt(true);
34362
34434
  return;
34363
34435
  }
34364
- // --- Altro (MQTT / Alarm settings) ---
34436
+ // --- Altro (MQTT / Alarm settings / parsing / payloads) ---
34365
34437
  if (key === 'sensorsJson') {
34438
+ // non più mostrato, ma se presente da vecchie versioni
34366
34439
  this.loadSensorsFromStorage();
34367
34440
  await this.discoverSensors();
34368
34441
  await this.connectMqtt(true);
@@ -34378,8 +34451,9 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34378
34451
  async releaseDevice(_id, nativeId) {
34379
34452
  try {
34380
34453
  const dev = this.devices.get(nativeId);
34381
- if (dev)
34454
+ if (dev) {
34382
34455
  this.devices.delete(nativeId);
34456
+ }
34383
34457
  try {
34384
34458
  deviceManager.onDeviceRemoved?.(nativeId);
34385
34459
  }
@@ -34393,6 +34467,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34393
34467
  try {
34394
34468
  const raw = this.storage.getItem('sensorsJson') || '[]';
34395
34469
  const parsed = JSON.parse(raw);
34470
+ // sanitize
34396
34471
  this.sensorsCfg = (parsed || []).filter(x => x && x.id && x.name && x.kind && x.topics);
34397
34472
  }
34398
34473
  catch (e) {
@@ -34402,20 +34477,22 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34402
34477
  }
34403
34478
  /** ===== discoverSensors: annuncia PRIMA, istanzia DOPO ===== */
34404
34479
  async discoverSensors() {
34405
- // 1) Manifests
34480
+ // 1) Prepara i manifest (niente istanze qui)
34406
34481
  const manifests = this.sensorsCfg.map(cfg => {
34407
34482
  const nativeId = `sensor:${cfg.id}`;
34408
34483
  const t = cfg.topics || {};
34409
34484
  const interfaces = [sdk_1.ScryptedInterface.Online];
34485
+ // Tamper solo se c'è un topic tamper
34410
34486
  if (t.tamper)
34411
34487
  interfaces.push(sdk_1.ScryptedInterface.TamperSensor);
34488
+ // Interfaccia primaria
34412
34489
  if (cfg.kind === 'contact')
34413
34490
  interfaces.unshift(sdk_1.ScryptedInterface.EntrySensor);
34414
34491
  else if (cfg.kind === 'motion')
34415
34492
  interfaces.unshift(sdk_1.ScryptedInterface.MotionSensor);
34416
34493
  else
34417
34494
  interfaces.unshift(sdk_1.ScryptedInterface.OccupancySensor);
34418
- // Espone Battery SOLTANTO se è configurato almeno un topic batteria (stringa non vuota)
34495
+ // Battery solo se previsto
34419
34496
  if ((t.batteryLevel && t.batteryLevel.trim()) || (t.lowBattery && t.lowBattery.trim())) {
34420
34497
  interfaces.push(sdk_1.ScryptedInterface.Battery);
34421
34498
  }
@@ -34449,7 +34526,15 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34449
34526
  else {
34450
34527
  dev.cfg = cfg;
34451
34528
  }
34452
- // <<< RIMOSSO il default batteryLevel=100: niente valore finché non arriva un messaggio >>>
34529
+ // Default “OK” se abbiamo Battery ma nessun valore ancora ricevuto
34530
+ const hasBattery = !!(cfg.topics.batteryLevel && cfg.topics.batteryLevel.trim()) || !!(cfg.topics.lowBattery && cfg.topics.lowBattery.trim());
34531
+ if (hasBattery && dev.batteryLevel === undefined) {
34532
+ dev.batteryLevel = 100;
34533
+ try {
34534
+ dev.onDeviceEvent(sdk_1.ScryptedInterface.Battery, 100);
34535
+ }
34536
+ catch { }
34537
+ }
34453
34538
  }
34454
34539
  // 4) Rimuovi quelli spariti
34455
34540
  const announced = new Set(manifests.map(m => m.nativeId));
@@ -34588,8 +34673,8 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34588
34673
  return;
34589
34674
  }
34590
34675
  if (topic === tCurrent) {
34591
- const mode = payloadToMode(payload);
34592
- const isAlarm = ['alarm', 'triggered'].includes(np);
34676
+ const mode = this.parseIncomingMode(payload);
34677
+ const isAlarm = this.isTriggeredToken(np);
34593
34678
  const current = this.securitySystemState || { mode: sdk_1.SecuritySystemMode.Disarmed };
34594
34679
  const newState = {
34595
34680
  mode: mode ?? current.mode,
@@ -34609,7 +34694,7 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34609
34694
  return;
34610
34695
  }
34611
34696
  if (topic === tTarget) {
34612
- this.pendingTarget = payloadToMode(payload);
34697
+ this.pendingTarget = this.parseIncomingMode(payload);
34613
34698
  this.console.log('Target state reported:', p, '->', this.pendingTarget);
34614
34699
  return;
34615
34700
  }
@@ -34638,10 +34723,25 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34638
34723
  this.console.error('publish error', err);
34639
34724
  });
34640
34725
  }
34726
+ /** Sceglie il payload di publish rispettando override → strict tokens → default arm_* */
34727
+ getOutgoing(mode) {
34728
+ const overrides = {
34729
+ [sdk_1.SecuritySystemMode.Disarmed]: this.storage.getItem('payloadDisarm') || null,
34730
+ [sdk_1.SecuritySystemMode.HomeArmed]: this.storage.getItem('payloadHome') || null,
34731
+ [sdk_1.SecuritySystemMode.AwayArmed]: this.storage.getItem('payloadAway') || null,
34732
+ [sdk_1.SecuritySystemMode.NightArmed]: this.storage.getItem('payloadNight') || null,
34733
+ };
34734
+ const override = overrides[mode];
34735
+ if (override && override.trim().length)
34736
+ return override.trim();
34737
+ if (this.useStrict())
34738
+ return this.preferredTokenForMode(mode);
34739
+ return DEFAULT_OUTGOING[mode];
34740
+ }
34641
34741
  async armSecuritySystem(mode) {
34642
34742
  const payload = this.getOutgoing(mode);
34643
34743
  this.console.log('armSecuritySystem', mode, '->', payload);
34644
- this.pendingTarget = mode;
34744
+ this.pendingTarget = mode; // memorizza target, ma NON cambiare il current
34645
34745
  this.publishSetTarget(payload);
34646
34746
  }
34647
34747
  async disarmSecuritySystem() {
@@ -34650,15 +34750,6 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34650
34750
  this.pendingTarget = sdk_1.SecuritySystemMode.Disarmed;
34651
34751
  this.publishSetTarget(payload);
34652
34752
  }
34653
- getOutgoing(mode) {
34654
- const map = {
34655
- [sdk_1.SecuritySystemMode.Disarmed]: this.storage.getItem('payloadDisarm') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.Disarmed],
34656
- [sdk_1.SecuritySystemMode.HomeArmed]: this.storage.getItem('payloadHome') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.HomeArmed],
34657
- [sdk_1.SecuritySystemMode.AwayArmed]: this.storage.getItem('payloadAway') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.AwayArmed],
34658
- [sdk_1.SecuritySystemMode.NightArmed]: this.storage.getItem('payloadNight') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.NightArmed],
34659
- };
34660
- return map[mode];
34661
- }
34662
34753
  }
34663
34754
  exports["default"] = ParadoxMqttSecuritySystem;
34664
34755
 
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.49",
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",