@rfranzoi/scrypted-mqtt-securitysystem 1.0.36 → 1.0.38

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.
@@ -34062,6 +34062,24 @@ function falsy(v) {
34062
34062
  }
34063
34063
  function normalize(s) { return (s || '').trim().toLowerCase(); }
34064
34064
  function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
34065
+ function deepEqual(a, b) {
34066
+ try {
34067
+ return JSON.stringify(a) === JSON.stringify(b);
34068
+ }
34069
+ catch {
34070
+ return a === b;
34071
+ }
34072
+ }
34073
+ /** Imposta una property e, se cambia, emette anche l'evento Scrypted corrispondente */
34074
+ function setAndEmit(dev, key, value, iface) {
34075
+ if (dev[key] === value)
34076
+ return;
34077
+ dev[key] = value;
34078
+ try {
34079
+ dev.onDeviceEvent?.(iface, value);
34080
+ }
34081
+ catch { }
34082
+ }
34065
34083
  /** Outgoing predefiniti (PAI-like). Chiavi numeriche per compatibilità enum */
34066
34084
  const DEFAULT_OUTGOING = {
34067
34085
  [SecuritySystemMode.Disarmed]: 'disarm',
@@ -34094,46 +34112,145 @@ class BaseMqttSensor extends ScryptedDeviceBase {
34094
34112
  handleMqtt(topic, payload) {
34095
34113
  const p = payload?.toString() ?? '';
34096
34114
  const np = normalize(p);
34115
+ // Online
34097
34116
  if (topic === this.cfg.topics.online) {
34098
34117
  if (truthy(np) || np === 'online')
34099
- this.online = true;
34118
+ setAndEmit(this, 'online', true, ScryptedInterface.Online);
34100
34119
  if (falsy(np) || np === 'offline')
34101
- this.online = false;
34120
+ setAndEmit(this, 'online', false, ScryptedInterface.Online);
34102
34121
  }
34122
+ // Tamper
34103
34123
  if (topic === this.cfg.topics.tamper) {
34104
34124
  if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
34105
- this.tampered = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34125
+ const t = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34126
+ setAndEmit(this, 'tampered', t, ScryptedInterface.TamperSensor);
34106
34127
  }
34107
34128
  else if (falsy(np))
34108
- this.tampered = false;
34129
+ setAndEmit(this, 'tampered', false, ScryptedInterface.TamperSensor);
34109
34130
  }
34131
+ // Battery
34110
34132
  if (topic === this.cfg.topics.batteryLevel) {
34111
34133
  const n = clamp(parseFloat(p), 0, 100);
34112
34134
  if (isFinite(n))
34113
- this.batteryLevel = n;
34135
+ setAndEmit(this, 'batteryLevel', n, ScryptedInterface.Battery);
34114
34136
  }
34115
34137
  else if (topic === this.cfg.topics.lowBattery && !this.cfg.topics.batteryLevel) {
34116
- this.batteryLevel = truthy(np) ? 10 : 100;
34138
+ const n = truthy(np) ? 10 : 100;
34139
+ setAndEmit(this, 'batteryLevel', n, ScryptedInterface.Battery);
34117
34140
  }
34141
+ // Primary
34118
34142
  this.handlePrimary(topic, np, p);
34119
34143
  }
34120
34144
  }
34145
+ /** === SENSORI CON PARSING ROBUSTO + EMISSIONE EVENTI === */
34121
34146
  class ContactMqttSensor extends BaseMqttSensor {
34122
- handlePrimary(topic, np) {
34123
- if (topic === this.cfg.topics.contact)
34124
- this.entryOpen = truthy(np);
34147
+ handlePrimary(topic, np, raw) {
34148
+ if (topic !== this.cfg.topics.contact)
34149
+ return;
34150
+ let val;
34151
+ // stringhe comuni
34152
+ if (['open', 'opened', '1', 'true', 'on', 'yes'].includes(np))
34153
+ val = true;
34154
+ else if (['closed', 'close', '0', 'false', 'off', 'no', 'shut'].includes(np))
34155
+ val = false;
34156
+ // JSON comuni
34157
+ if (val === undefined) {
34158
+ try {
34159
+ const j = JSON.parse(raw);
34160
+ if (typeof j?.open === 'boolean')
34161
+ val = !!j.open;
34162
+ else if (typeof j?.opened === 'boolean')
34163
+ val = !!j.opened;
34164
+ else if (typeof j?.contact === 'boolean')
34165
+ val = !j.contact; // contact:false => OPEN
34166
+ else if (typeof j?.state === 'string') {
34167
+ const s = String(j.state).toLowerCase();
34168
+ if (s === 'open')
34169
+ val = true;
34170
+ if (s === 'closed')
34171
+ val = false;
34172
+ }
34173
+ }
34174
+ catch { }
34175
+ }
34176
+ if (val !== undefined) {
34177
+ setAndEmit(this, 'entryOpen', val, ScryptedInterface.EntrySensor);
34178
+ }
34179
+ else {
34180
+ this.console?.debug?.(`Contact payload non gestito (${this.cfg.id}): "${raw}"`);
34181
+ }
34125
34182
  }
34126
34183
  }
34127
34184
  class MotionMqttSensor extends BaseMqttSensor {
34128
- handlePrimary(topic, np) {
34129
- if (topic === this.cfg.topics.motion)
34130
- this.motionDetected = truthy(np);
34185
+ handlePrimary(topic, np, raw) {
34186
+ if (topic !== this.cfg.topics.motion)
34187
+ return;
34188
+ let val;
34189
+ if (['motion', 'detected', 'active', '1', 'true', 'on', 'yes'].includes(np))
34190
+ val = true;
34191
+ else if (['clear', 'inactive', 'no_motion', 'none', '0', 'false', 'off', 'no'].includes(np))
34192
+ val = false;
34193
+ if (val === undefined) {
34194
+ try {
34195
+ const j = JSON.parse(raw);
34196
+ if (typeof j?.motion === 'boolean')
34197
+ val = !!j.motion;
34198
+ else if (typeof j?.occupancy === 'boolean')
34199
+ val = !!j.occupancy;
34200
+ else if (typeof j?.presence === 'boolean')
34201
+ val = !!j.presence;
34202
+ else if (typeof j?.state === 'string') {
34203
+ const s = String(j.state).toLowerCase();
34204
+ if (['on', 'motion', 'detected', 'active'].includes(s))
34205
+ val = true;
34206
+ if (['off', 'clear', 'inactive'].includes(s))
34207
+ val = false;
34208
+ }
34209
+ }
34210
+ catch { }
34211
+ }
34212
+ if (val !== undefined) {
34213
+ setAndEmit(this, 'motionDetected', val, ScryptedInterface.MotionSensor);
34214
+ }
34215
+ else {
34216
+ this.console?.debug?.(`Motion payload non gestito (${this.cfg.id}): "${raw}"`);
34217
+ }
34131
34218
  }
34132
34219
  }
34133
34220
  class OccupancyMqttSensor extends BaseMqttSensor {
34134
- handlePrimary(topic, np) {
34135
- if (topic === this.cfg.topics.occupancy)
34136
- this.occupied = truthy(np);
34221
+ handlePrimary(topic, np, raw) {
34222
+ if (topic !== this.cfg.topics.occupancy)
34223
+ return;
34224
+ let val;
34225
+ if (['occupied', 'presence', 'present', '1', 'true', 'on', 'yes'].includes(np))
34226
+ val = true;
34227
+ else if (['unoccupied', 'vacant', 'absent', '0', 'false', 'off', 'no', 'clear'].includes(np))
34228
+ val = false;
34229
+ if (val === undefined) {
34230
+ try {
34231
+ const j = JSON.parse(raw);
34232
+ if (typeof j?.occupied === 'boolean')
34233
+ val = !!j.occupied;
34234
+ else if (typeof j?.presence === 'boolean')
34235
+ val = !!j.presence;
34236
+ else if (typeof j?.occupancy === 'boolean')
34237
+ val = !!j.occupancy;
34238
+ else if (typeof j?.state === 'string') {
34239
+ const s = String(j.state).toLowerCase();
34240
+ if (['occupied', 'presence', 'present', 'on'].includes(s))
34241
+ val = true;
34242
+ if (['vacant', 'absent', 'clear', 'off'].includes(s))
34243
+ val = false;
34244
+ }
34245
+ }
34246
+ catch { }
34247
+ }
34248
+ if (val !== undefined) {
34249
+ setAndEmit(this, 'occupied', val, ScryptedInterface.OccupancySensor);
34250
+ }
34251
+ else {
34252
+ this.console?.debug?.(`Occupancy payload non gestito (${this.cfg.id}): "${raw}"`);
34253
+ }
34137
34254
  }
34138
34255
  }
34139
34256
  /** ----------------- Main Plugin ----------------- */
@@ -34142,8 +34259,8 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34142
34259
  super();
34143
34260
  this.sensorsCfg = [];
34144
34261
  this.devices = new Map();
34145
- // evitiamo spam: ricordiamo se abbiamo già provato ad annunciare
34146
- this.triedDiscoveryOnce = false;
34262
+ // Evita loop di log: tenta una volta finché deviceManager non c’è, poi riprova su eventi utili.
34263
+ this.discoveryPostponed = false;
34147
34264
  // Tipo in UI (best-effort)
34148
34265
  setTimeout(() => {
34149
34266
  try {
@@ -34323,17 +34440,17 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34323
34440
  safeDiscoverSensors(triggeredByChange = false) {
34324
34441
  const dmAny = sdk?.deviceManager;
34325
34442
  if (!dmAny) {
34326
- if (!this.triedDiscoveryOnce) {
34443
+ // Posticipa una sola volta; poi riproviamo su connect MQTT e al primo messaggio
34444
+ if (!this.discoveryPostponed) {
34327
34445
  this.console.log('Device discovery postponed: deviceManager not ready yet.');
34328
- this.triedDiscoveryOnce = true;
34446
+ this.discoveryPostponed = true;
34329
34447
  }
34330
- // Riprovaremo in due casi: a) settaggi cambiati (già chiama safeDiscoverSensors)
34331
- // b) al primo messaggio MQTT (vedi handler sotto).
34332
34448
  return;
34333
34449
  }
34334
- // Se arriviamo qui, il manager c’è: esegui discover.
34335
- this.triedDiscoveryOnce = false;
34450
+ this.discoveryPostponed = false;
34336
34451
  this.discoverSensors(dmAny);
34452
+ if (triggeredByChange)
34453
+ this.console.log('Sensors discovered/updated.');
34337
34454
  }
34338
34455
  /** discoverSensors con deviceManager garantito */
34339
34456
  discoverSensors(dmAny) {
@@ -34380,7 +34497,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34380
34497
  }
34381
34498
  const hasBattery = !!(cfg.topics.batteryLevel || cfg.topics.lowBattery);
34382
34499
  if (hasBattery && dev.batteryLevel === undefined)
34383
- dev.batteryLevel = 100;
34500
+ setAndEmit(dev, 'batteryLevel', 100, ScryptedInterface.Battery);
34384
34501
  }
34385
34502
  // 4) Cleanup
34386
34503
  const announced = new Set(manifests.map(m => m.nativeId));
@@ -34445,7 +34562,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34445
34562
  const tOnline = this.storage.getItem('topicOnline') || '';
34446
34563
  client.on('connect', () => {
34447
34564
  this.console.log('MQTT connected');
34448
- this.online = true;
34565
+ setAndEmit(this, 'online', true, ScryptedInterface.Online);
34449
34566
  if (subs.length)
34450
34567
  client.subscribe(subs, { qos: 0 }, (err) => { if (err)
34451
34568
  this.console.error('subscribe error', err); });
@@ -34453,7 +34570,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34453
34570
  this.safeDiscoverSensors(true);
34454
34571
  });
34455
34572
  client.on('reconnect', () => this.console.log('MQTT reconnecting...'));
34456
- client.on('close', () => { this.console.log('MQTT closed'); this.online = false; });
34573
+ client.on('close', () => { this.console.log('MQTT closed'); setAndEmit(this, 'online', false, ScryptedInterface.Online); });
34457
34574
  client.on('error', (e) => { this.console.error('MQTT error', e); });
34458
34575
  client.on('message', (topic, payload) => {
34459
34576
  try {
@@ -34461,23 +34578,26 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34461
34578
  const np = normalize(p);
34462
34579
  if (topic === tOnline) {
34463
34580
  if (truthy(np) || np === 'online')
34464
- this.online = true;
34581
+ setAndEmit(this, 'online', true, ScryptedInterface.Online);
34465
34582
  if (falsy(np) || np === 'offline')
34466
- this.online = false;
34583
+ setAndEmit(this, 'online', false, ScryptedInterface.Online);
34467
34584
  return;
34468
34585
  }
34469
34586
  if (topic === tTamper) {
34470
- if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np))
34471
- this.tampered = ['cover', 'intrusion'].find(x => x === np) || true;
34472
- else if (falsy(np))
34473
- this.tampered = false;
34587
+ if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np)) {
34588
+ const t = ['cover', 'intrusion'].find(x => x === np) || true;
34589
+ setAndEmit(this, 'tampered', t, ScryptedInterface.TamperSensor);
34590
+ }
34591
+ else if (falsy(np)) {
34592
+ setAndEmit(this, 'tampered', false, ScryptedInterface.TamperSensor);
34593
+ }
34474
34594
  return;
34475
34595
  }
34476
34596
  if (topic === tCurrent) {
34477
34597
  const mode = payloadToMode(payload);
34478
34598
  const isAlarm = ['alarm', 'triggered'].includes(np);
34479
34599
  const current = this.securitySystemState || { mode: SecuritySystemMode.Disarmed };
34480
- this.securitySystemState = {
34600
+ const newState = {
34481
34601
  mode: mode ?? current.mode,
34482
34602
  supportedModes: current.supportedModes ?? [
34483
34603
  SecuritySystemMode.Disarmed,
@@ -34487,6 +34607,13 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34487
34607
  ],
34488
34608
  triggered: isAlarm || undefined,
34489
34609
  };
34610
+ if (!deepEqual(this.securitySystemState, newState)) {
34611
+ this.securitySystemState = newState;
34612
+ try {
34613
+ this.onDeviceEvent?.(ScryptedInterface.SecuritySystem, newState);
34614
+ }
34615
+ catch { }
34616
+ }
34490
34617
  return;
34491
34618
  }
34492
34619
  if (topic === tTarget) {
@@ -34495,8 +34622,8 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34495
34622
  return;
34496
34623
  }
34497
34624
  // Dispatch ai sensori
34498
- // (E prova ad annunciare se non l’abbiamo ancora fatto e ora il manager è pronto)
34499
- if (this.triedDiscoveryOnce)
34625
+ // (E prova ad annunciare se era stato posticipato e ora il manager è pronto)
34626
+ if (this.discoveryPostponed)
34500
34627
  this.safeDiscoverSensors(true);
34501
34628
  for (const dev of this.devices.values())
34502
34629
  dev.handleMqtt(topic, payload);
package/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rfranzoi/scrypted-mqtt-securitysystem",
3
- "version": "1.0.36",
3
+ "version": "1.0.38",
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",