@rfranzoi/scrypted-mqtt-securitysystem 1.0.37 → 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,157 +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
  }
34121
- /** === SENSORI CON PARSING ROBUSTO PER HOMEKIT === */
34145
+ /** === SENSORI CON PARSING ROBUSTO + EMISSIONE EVENTI === */
34122
34146
  class ContactMqttSensor extends BaseMqttSensor {
34123
34147
  handlePrimary(topic, np, raw) {
34124
34148
  if (topic !== this.cfg.topics.contact)
34125
34149
  return;
34150
+ let val;
34126
34151
  // 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
- }
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;
34135
34156
  // 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;
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;
34159
34172
  }
34160
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}"`);
34161
34181
  }
34162
- catch { }
34163
- this.console?.debug?.(`Contact payload non gestito (${this.cfg.id}): "${raw}"`);
34164
34182
  }
34165
34183
  }
34166
34184
  class MotionMqttSensor extends BaseMqttSensor {
34167
34185
  handlePrimary(topic, np, raw) {
34168
34186
  if (topic !== this.cfg.topics.motion)
34169
34187
  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;
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;
34201
34208
  }
34202
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}"`);
34203
34217
  }
34204
- catch { }
34205
- this.console?.debug?.(`Motion payload non gestito (${this.cfg.id}): "${raw}"`);
34206
34218
  }
34207
34219
  }
34208
34220
  class OccupancyMqttSensor extends BaseMqttSensor {
34209
34221
  handlePrimary(topic, np, raw) {
34210
34222
  if (topic !== this.cfg.topics.occupancy)
34211
34223
  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;
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;
34243
34244
  }
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}"`);
34245
34253
  }
34246
- catch { }
34247
- this.console?.debug?.(`Occupancy payload non gestito (${this.cfg.id}): "${raw}"`);
34248
34254
  }
34249
34255
  }
34250
34256
  /** ----------------- Main Plugin ----------------- */
@@ -34491,7 +34497,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34491
34497
  }
34492
34498
  const hasBattery = !!(cfg.topics.batteryLevel || cfg.topics.lowBattery);
34493
34499
  if (hasBattery && dev.batteryLevel === undefined)
34494
- dev.batteryLevel = 100;
34500
+ setAndEmit(dev, 'batteryLevel', 100, ScryptedInterface.Battery);
34495
34501
  }
34496
34502
  // 4) Cleanup
34497
34503
  const announced = new Set(manifests.map(m => m.nativeId));
@@ -34556,7 +34562,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34556
34562
  const tOnline = this.storage.getItem('topicOnline') || '';
34557
34563
  client.on('connect', () => {
34558
34564
  this.console.log('MQTT connected');
34559
- this.online = true;
34565
+ setAndEmit(this, 'online', true, ScryptedInterface.Online);
34560
34566
  if (subs.length)
34561
34567
  client.subscribe(subs, { qos: 0 }, (err) => { if (err)
34562
34568
  this.console.error('subscribe error', err); });
@@ -34564,7 +34570,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34564
34570
  this.safeDiscoverSensors(true);
34565
34571
  });
34566
34572
  client.on('reconnect', () => this.console.log('MQTT reconnecting...'));
34567
- 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); });
34568
34574
  client.on('error', (e) => { this.console.error('MQTT error', e); });
34569
34575
  client.on('message', (topic, payload) => {
34570
34576
  try {
@@ -34572,23 +34578,26 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34572
34578
  const np = normalize(p);
34573
34579
  if (topic === tOnline) {
34574
34580
  if (truthy(np) || np === 'online')
34575
- this.online = true;
34581
+ setAndEmit(this, 'online', true, ScryptedInterface.Online);
34576
34582
  if (falsy(np) || np === 'offline')
34577
- this.online = false;
34583
+ setAndEmit(this, 'online', false, ScryptedInterface.Online);
34578
34584
  return;
34579
34585
  }
34580
34586
  if (topic === tTamper) {
34581
- if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np))
34582
- this.tampered = ['cover', 'intrusion'].find(x => x === np) || true;
34583
- else if (falsy(np))
34584
- 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
+ }
34585
34594
  return;
34586
34595
  }
34587
34596
  if (topic === tCurrent) {
34588
34597
  const mode = payloadToMode(payload);
34589
34598
  const isAlarm = ['alarm', 'triggered'].includes(np);
34590
34599
  const current = this.securitySystemState || { mode: SecuritySystemMode.Disarmed };
34591
- this.securitySystemState = {
34600
+ const newState = {
34592
34601
  mode: mode ?? current.mode,
34593
34602
  supportedModes: current.supportedModes ?? [
34594
34603
  SecuritySystemMode.Disarmed,
@@ -34598,6 +34607,13 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34598
34607
  ],
34599
34608
  triggered: isAlarm || undefined,
34600
34609
  };
34610
+ if (!deepEqual(this.securitySystemState, newState)) {
34611
+ this.securitySystemState = newState;
34612
+ try {
34613
+ this.onDeviceEvent?.(ScryptedInterface.SecuritySystem, newState);
34614
+ }
34615
+ catch { }
34616
+ }
34601
34617
  return;
34602
34618
  }
34603
34619
  if (topic === tTarget) {
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.37",
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",