@rfranzoi/scrypted-mqtt-securitysystem 1.0.15 → 1.0.18

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.
@@ -34063,7 +34063,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
34063
34063
  Object.defineProperty(exports, "__esModule", ({ value: true }));
34064
34064
  const sdk_1 = __importStar(__webpack_require__(/*! @scrypted/sdk */ "./node_modules/@scrypted/sdk/dist/src/index.js"));
34065
34065
  const mqtt_1 = __importDefault(__webpack_require__(/*! mqtt */ "./node_modules/mqtt/build/index.js"));
34066
- const { systemManager } = sdk_1.default;
34066
+ const { systemManager, deviceManager } = sdk_1.default;
34067
+ /** utils */
34067
34068
  function truthy(v) {
34068
34069
  if (!v)
34069
34070
  return false;
@@ -34079,20 +34080,24 @@ function falsy(v) {
34079
34080
  function normalize(s) {
34080
34081
  return (s || '').trim().toLowerCase();
34081
34082
  }
34082
- /** Default payloads for PAI/PAI-MQTT-like setups */
34083
+ function clamp(n, min, max) {
34084
+ return Math.max(min, Math.min(max, n));
34085
+ }
34086
+ function delay(ms) {
34087
+ return new Promise(res => setTimeout(res, ms));
34088
+ }
34089
+ /** SecuritySystem outgoing defaults (PAI-like) */
34083
34090
  const DEFAULT_OUTGOING = {
34084
34091
  [sdk_1.SecuritySystemMode.Disarmed]: 'disarm',
34085
34092
  [sdk_1.SecuritySystemMode.HomeArmed]: 'arm_home',
34086
34093
  [sdk_1.SecuritySystemMode.AwayArmed]: 'arm_away',
34087
34094
  [sdk_1.SecuritySystemMode.NightArmed]: 'arm_night',
34088
34095
  };
34089
- /** Common incoming synonyms SecuritySystemMode
34090
- * (transitori non alterano la modalità corrente) */
34096
+ /** Parse incoming payload -> final mode (ignore transition states) */
34091
34097
  function payloadToMode(payload) {
34092
34098
  if (payload == null)
34093
34099
  return;
34094
34100
  const p = normalize(payload.toString());
34095
- // final modes
34096
34101
  if (['disarm', 'disarmed', 'off', '0', 'idle', 'ready'].includes(p))
34097
34102
  return sdk_1.SecuritySystemMode.Disarmed;
34098
34103
  if (['arm_home', 'home', 'stay', 'armed_home'].includes(p))
@@ -34106,9 +34111,83 @@ function payloadToMode(payload) {
34106
34111
  return undefined;
34107
34112
  return undefined;
34108
34113
  }
34114
+ class BaseMqttSensor extends sdk_1.ScryptedDeviceBase {
34115
+ constructor(nativeId, cfg) {
34116
+ super(nativeId);
34117
+ this.cfg = cfg;
34118
+ this.online = this.online ?? true;
34119
+ }
34120
+ /** Called by parent on each MQTT message */
34121
+ handleMqtt(topic, payload) {
34122
+ const p = payload?.toString() ?? '';
34123
+ const np = normalize(p);
34124
+ // online
34125
+ if (topic === this.cfg.topics.online) {
34126
+ if (truthy(np) || np === 'online')
34127
+ this.online = true;
34128
+ if (falsy(np) || np === 'offline')
34129
+ this.online = false;
34130
+ }
34131
+ // tamper
34132
+ if (topic === this.cfg.topics.tamper) {
34133
+ if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
34134
+ this.tampered = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34135
+ }
34136
+ else if (falsy(np)) {
34137
+ this.tampered = false;
34138
+ }
34139
+ }
34140
+ // battery
34141
+ if (topic === this.cfg.topics.batteryLevel) {
34142
+ const n = clamp(parseFloat(p), 0, 100);
34143
+ if (isFinite(n))
34144
+ this.batteryLevel = n;
34145
+ }
34146
+ else if (topic === this.cfg.topics.lowBattery && !this.cfg.topics.batteryLevel) {
34147
+ // sintetizza se non c'è batteryLevel
34148
+ this.batteryLevel = truthy(np) ? 10 : 100;
34149
+ }
34150
+ // primary handled by subclasses
34151
+ this.handlePrimary(topic, np, p);
34152
+ }
34153
+ }
34154
+ class ContactMqttSensor extends BaseMqttSensor {
34155
+ constructor(nativeId, cfg) {
34156
+ super(nativeId, cfg);
34157
+ }
34158
+ handlePrimary(topic, np, _raw) {
34159
+ if (topic === this.cfg.topics.contact) {
34160
+ this.entryOpen = truthy(np);
34161
+ }
34162
+ }
34163
+ }
34164
+ class MotionMqttSensor extends BaseMqttSensor {
34165
+ constructor(nativeId, cfg) {
34166
+ super(nativeId, cfg);
34167
+ }
34168
+ handlePrimary(topic, np, _raw) {
34169
+ if (topic === this.cfg.topics.motion) {
34170
+ this.motionDetected = truthy(np);
34171
+ }
34172
+ }
34173
+ }
34174
+ class OccupancyMqttSensor extends BaseMqttSensor {
34175
+ constructor(nativeId, cfg) {
34176
+ super(nativeId, cfg);
34177
+ }
34178
+ handlePrimary(topic, np, _raw) {
34179
+ if (topic === this.cfg.topics.occupancy) {
34180
+ this.occupied = truthy(np);
34181
+ }
34182
+ }
34183
+ }
34184
+ /** ----------------- Main Plugin ----------------- */
34109
34185
  class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34110
34186
  constructor() {
34111
34187
  super();
34188
+ // sensor management
34189
+ this.sensorsCfg = [];
34190
+ this.devices = new Map();
34112
34191
  // (facoltativo) Imposta il device type in UI
34113
34192
  setTimeout(() => {
34114
34193
  try {
@@ -34127,6 +34206,9 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34127
34206
  ],
34128
34207
  };
34129
34208
  this.online = this.online ?? false;
34209
+ // Load sensors config and announce devices
34210
+ this.loadSensorsFromStorage();
34211
+ this.discoverSensors();
34130
34212
  // Connect on start
34131
34213
  this.connectMqtt().catch(e => this.console.error('MQTT connect error:', e));
34132
34214
  // chiusura pulita del client MQTT ai reload/stop del plugin
@@ -34146,33 +34228,152 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34146
34228
  }
34147
34229
  catch { }
34148
34230
  }
34149
- // --- Settings UI ---
34231
+ /** ---- Settings UI ---- */
34150
34232
  async getSettings() {
34151
34233
  return [
34234
+ // MQTT Core
34152
34235
  { 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' },
34153
34236
  { group: 'MQTT', key: 'username', title: 'Username', type: 'string', value: this.storage.getItem('username') || '' },
34154
34237
  { group: 'MQTT', key: 'password', title: 'Password', type: 'password', value: this.storage.getItem('password') || '' },
34155
34238
  { group: 'MQTT', key: 'clientId', title: 'Client ID', placeholder: 'scrypted-paradox', value: this.storage.getItem('clientId') || 'scrypted-paradox' },
34156
34239
  { group: 'MQTT', key: 'tls', title: 'Use TLS', type: 'boolean', value: this.storage.getItem('tls') === 'true' },
34157
34240
  { group: 'MQTT', key: 'rejectUnauthorized', title: 'Reject Unauthorized (TLS)', type: 'boolean', value: this.storage.getItem('rejectUnauthorized') !== 'false', description: 'Disattiva solo con broker self-signed.' },
34158
- { group: 'Topics', key: 'topicSetTarget', title: 'Set Target State (publish)', placeholder: 'paradox/control/partitions/Area_1', value: this.storage.getItem('topicSetTarget') || '' },
34159
- { group: 'Topics', key: 'topicGetTarget', title: 'Get Target State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/target_state', value: this.storage.getItem('topicGetTarget') || '' },
34160
- { group: 'Topics', key: 'topicGetCurrent', title: 'Get Current State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/current_state', value: this.storage.getItem('topicGetCurrent') || '' },
34161
- { group: 'Topics', key: 'topicTamper', title: 'Get Status Tampered (subscribe)', placeholder: 'paradox/states/system/troubles/zone_tamper_trouble', value: this.storage.getItem('topicTamper') || '' },
34162
- { group: 'Topics', key: 'topicOnline', title: 'Get Online (subscribe)', placeholder: 'paradox/interface/availability', value: this.storage.getItem('topicOnline') || '' },
34241
+ // Alarm Topics
34242
+ { group: 'Alarm Topics', key: 'topicSetTarget', title: 'Set Target State (publish)', placeholder: 'paradox/control/partitions/Area_1', value: this.storage.getItem('topicSetTarget') || '' },
34243
+ { group: 'Alarm Topics', key: 'topicGetTarget', title: 'Get Target State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/target_state', value: this.storage.getItem('topicGetTarget') || '' },
34244
+ { group: 'Alarm Topics', key: 'topicGetCurrent', title: 'Get Current State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/current_state', value: this.storage.getItem('topicGetCurrent') || '' },
34245
+ { group: 'Alarm Topics', key: 'topicTamper', title: 'Get Status Tampered (subscribe)', placeholder: 'paradox/states/system/troubles/zone_tamper_trouble', value: this.storage.getItem('topicTamper') || '' },
34246
+ { group: 'Alarm Topics', key: 'topicOnline', title: 'Get Online (subscribe)', placeholder: 'paradox/interface/availability', value: this.storage.getItem('topicOnline') || '' },
34163
34247
  { group: 'Publish Options', key: 'qos', title: 'QoS', type: 'integer', value: parseInt(this.storage.getItem('qos') || '0') },
34164
34248
  { group: 'Publish Options', key: 'retain', title: 'Retain', type: 'boolean', value: this.storage.getItem('retain') === 'true' },
34165
34249
  { group: 'Outgoing Payloads', key: 'payloadDisarm', title: 'Payload Disarm', value: this.storage.getItem('payloadDisarm') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.Disarmed] },
34166
34250
  { group: 'Outgoing Payloads', key: 'payloadHome', title: 'Payload HomeArmed', value: this.storage.getItem('payloadHome') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.HomeArmed] },
34167
34251
  { group: 'Outgoing Payloads', key: 'payloadAway', title: 'Payload AwayArmed', value: this.storage.getItem('payloadAway') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.AwayArmed] },
34168
34252
  { group: 'Outgoing Payloads', key: 'payloadNight', title: 'Payload NightArmed', value: this.storage.getItem('payloadNight') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.NightArmed] },
34253
+ // Sensors config (JSON)
34254
+ {
34255
+ group: 'Sensors',
34256
+ key: 'sensorsJson',
34257
+ title: 'Sensors JSON (contact/motion/occupancy)',
34258
+ description: 'Definisci i sensori e i topic MQTT (vedi README). Incolla JSON; le interruzioni di riga sono accettate.',
34259
+ type: 'string',
34260
+ value: this.storage.getItem('sensorsJson') || '[\n {\n "id": "front-door",\n "name": "Front Door",\n "kind": "contact",\n "topics": { "contact": "SYSTEM/zones/front/contact" }\n }\n]'
34261
+ },
34169
34262
  ];
34170
34263
  }
34171
34264
  async putSetting(key, value) {
34172
34265
  this.storage.setItem(key, String(value));
34173
- await this.connectMqtt(true);
34266
+ if (key === 'sensorsJson') {
34267
+ this.loadSensorsFromStorage();
34268
+ await this.discoverSensors();
34269
+ await this.connectMqtt(true);
34270
+ }
34271
+ else {
34272
+ await this.connectMqtt(true);
34273
+ }
34274
+ }
34275
+ /** ---- DeviceProvider ---- */
34276
+ async getDevice(nativeId) {
34277
+ return this.devices.get(nativeId);
34278
+ }
34279
+ async releaseDevice(id, nativeId) {
34280
+ try {
34281
+ const dev = this.devices.get(nativeId);
34282
+ if (dev) {
34283
+ this.devices.delete(nativeId);
34284
+ }
34285
+ try {
34286
+ deviceManager.onDeviceRemoved?.(nativeId);
34287
+ }
34288
+ catch { }
34289
+ }
34290
+ catch (e) {
34291
+ this.console.warn('releaseDevice error', e);
34292
+ }
34293
+ }
34294
+ loadSensorsFromStorage() {
34295
+ try {
34296
+ const raw = this.storage.getItem('sensorsJson') || '[]';
34297
+ const parsed = JSON.parse(raw);
34298
+ // sanitize
34299
+ this.sensorsCfg = (parsed || []).filter(x => x && x.id && x.name && x.kind && x.topics);
34300
+ }
34301
+ catch (e) {
34302
+ this.console.error('Invalid sensorsJson:', e);
34303
+ this.sensorsCfg = [];
34304
+ }
34305
+ }
34306
+ /** ===== discoverSensors con batch/fallback (FIX type) ===== */
34307
+ async discoverSensors() {
34308
+ // Prepara i manifest e istanzia/aggiorna le classi locali
34309
+ const manifests = this.sensorsCfg.map(cfg => {
34310
+ const nativeId = `sensor:${cfg.id}`;
34311
+ let interfaces = [
34312
+ sdk_1.ScryptedInterface.Online,
34313
+ sdk_1.ScryptedInterface.TamperSensor,
34314
+ sdk_1.ScryptedInterface.Battery,
34315
+ ];
34316
+ // Il tipo RESTA generico "Sensor" per compatibilità SDK,
34317
+ // le capacità sono date dalle interfacce.
34318
+ const type = sdk_1.ScryptedDeviceType.Sensor;
34319
+ switch (cfg.kind) {
34320
+ case 'contact':
34321
+ interfaces = [sdk_1.ScryptedInterface.EntrySensor, ...interfaces];
34322
+ break;
34323
+ case 'motion':
34324
+ interfaces = [sdk_1.ScryptedInterface.MotionSensor, ...interfaces];
34325
+ break;
34326
+ case 'occupancy':
34327
+ interfaces = [sdk_1.ScryptedInterface.OccupancySensor, ...interfaces];
34328
+ break;
34329
+ }
34330
+ let dev = this.devices.get(nativeId);
34331
+ if (!dev) {
34332
+ if (cfg.kind === 'contact')
34333
+ dev = new ContactMqttSensor(nativeId, cfg);
34334
+ else if (cfg.kind === 'motion')
34335
+ dev = new MotionMqttSensor(nativeId, cfg);
34336
+ else
34337
+ dev = new OccupancyMqttSensor(nativeId, cfg);
34338
+ this.devices.set(nativeId, dev);
34339
+ }
34340
+ else {
34341
+ dev.cfg = cfg;
34342
+ }
34343
+ return {
34344
+ nativeId,
34345
+ name: cfg.name,
34346
+ type,
34347
+ interfaces,
34348
+ };
34349
+ });
34350
+ // Annuncio in batch se disponibile, altrimenti uno per volta con un piccolo delay
34351
+ const dm = deviceManager;
34352
+ if (typeof dm.onDevicesChanged === 'function') {
34353
+ dm.onDevicesChanged({ devices: manifests });
34354
+ this.console.log('Annunciati (batch):', manifests.map(m => m.nativeId).join(', '));
34355
+ }
34356
+ else {
34357
+ for (const m of manifests) {
34358
+ deviceManager.onDeviceDiscovered(m);
34359
+ this.console.log('Annunciato:', m.nativeId);
34360
+ await new Promise(res => setTimeout(res, 50));
34361
+ }
34362
+ }
34363
+ // Rimuovi eventuali sensori non più presenti
34364
+ const announced = new Set(manifests.map(m => m.nativeId));
34365
+ for (const [nativeId] of this.devices) {
34366
+ if (!announced.has(nativeId)) {
34367
+ try {
34368
+ this.devices.delete(nativeId);
34369
+ deviceManager.onDeviceRemoved?.(nativeId);
34370
+ this.console.log('Rimosso:', nativeId);
34371
+ }
34372
+ catch { }
34373
+ }
34374
+ }
34174
34375
  }
34175
- // --- MQTT ---
34376
+ /** ---- MQTT ---- */
34176
34377
  getMqttOptions() {
34177
34378
  const url = this.storage.getItem('brokerUrl') || 'mqtt://127.0.0.1:1883';
34178
34379
  const username = this.storage.getItem('username') || undefined;
@@ -34193,12 +34394,25 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34193
34394
  }
34194
34395
  return { url, opts };
34195
34396
  }
34397
+ collectAllSubscriptions() {
34398
+ const subs = new Set();
34399
+ // alarm
34400
+ for (const k of ['topicGetTarget', 'topicGetCurrent', 'topicTamper', 'topicOnline']) {
34401
+ const v = this.storage.getItem(k);
34402
+ if (v)
34403
+ subs.add(v);
34404
+ }
34405
+ // sensors
34406
+ for (const s of this.sensorsCfg) {
34407
+ const t = s.topics;
34408
+ [t.contact, t.motion, t.occupancy, t.batteryLevel, t.lowBattery, t.tamper, t.online]
34409
+ .filter(Boolean)
34410
+ .forEach(x => subs.add(String(x)));
34411
+ }
34412
+ return Array.from(subs);
34413
+ }
34196
34414
  async connectMqtt(reconnect = false) {
34197
- const tTarget = this.storage.getItem('topicGetTarget') || '';
34198
- const tCurrent = this.storage.getItem('topicGetCurrent') || '';
34199
- const tTamper = this.storage.getItem('topicTamper') || '';
34200
- const tOnline = this.storage.getItem('topicOnline') || '';
34201
- const subs = [tTarget, tCurrent, tTamper, tOnline].filter(Boolean);
34415
+ const subs = this.collectAllSubscriptions();
34202
34416
  if (!subs.length && !this.storage.getItem('topicSetTarget')) {
34203
34417
  this.console.warn('Configura almeno un topic nelle impostazioni.');
34204
34418
  }
@@ -34213,6 +34427,11 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34213
34427
  this.console.log(`Connecting MQTT ${url} ...`);
34214
34428
  const client = mqtt_1.default.connect(url, opts);
34215
34429
  this.client = client;
34430
+ // cache alarm topics for fast compare
34431
+ const tTarget = this.storage.getItem('topicGetTarget') || '';
34432
+ const tCurrent = this.storage.getItem('topicGetCurrent') || '';
34433
+ const tTamper = this.storage.getItem('topicTamper') || '';
34434
+ const tOnline = this.storage.getItem('topicOnline') || '';
34216
34435
  client.on('connect', () => {
34217
34436
  this.console.log('MQTT connected');
34218
34437
  this.online = true;
@@ -34229,29 +34448,27 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34229
34448
  client.on('message', (topic, payload) => {
34230
34449
  try {
34231
34450
  const p = payload?.toString() ?? '';
34232
- // Online
34451
+ const np = normalize(p);
34452
+ // ---- Alarm handling ----
34233
34453
  if (topic === tOnline) {
34234
- if (truthy(p) || p.toLowerCase() === 'online')
34454
+ if (truthy(np) || np === 'online')
34235
34455
  this.online = true;
34236
- if (falsy(p) || p.toLowerCase() === 'offline')
34456
+ if (falsy(np) || np === 'offline')
34237
34457
  this.online = false;
34238
34458
  return;
34239
34459
  }
34240
- // Tamper
34241
34460
  if (topic === tTamper) {
34242
- const np = normalize(p);
34243
- if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
34244
- this.tampered = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34461
+ if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np)) {
34462
+ this.tampered = ['cover', 'intrusion'].find(x => x === np) || true;
34245
34463
  }
34246
34464
  else if (falsy(np)) {
34247
34465
  this.tampered = false;
34248
34466
  }
34249
34467
  return;
34250
34468
  }
34251
- // CURRENT state → aggiorna il mode mostrato
34252
34469
  if (topic === tCurrent) {
34253
34470
  const mode = payloadToMode(payload);
34254
- const isAlarm = ['alarm', 'triggered'].includes(normalize(p));
34471
+ const isAlarm = ['alarm', 'triggered'].includes(np);
34255
34472
  const current = this.securitySystemState || { mode: sdk_1.SecuritySystemMode.Disarmed };
34256
34473
  const newState = {
34257
34474
  mode: mode ?? current.mode,
@@ -34266,19 +34483,22 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34266
34483
  this.securitySystemState = newState;
34267
34484
  return;
34268
34485
  }
34269
- // TARGET state → NON cambia il mode; lo memorizziamo solo come pending
34270
34486
  if (topic === tTarget) {
34271
34487
  this.pendingTarget = payloadToMode(payload);
34272
34488
  this.console.log('Target state reported:', p, '->', this.pendingTarget);
34273
34489
  return;
34274
34490
  }
34491
+ // ---- Sensor dispatch ----
34492
+ for (const dev of this.devices.values()) {
34493
+ dev.handleMqtt(topic, payload);
34494
+ }
34275
34495
  }
34276
34496
  catch (e) {
34277
34497
  this.console.error('MQTT message handler error', e);
34278
34498
  }
34279
34499
  });
34280
34500
  }
34281
- // --- SecuritySystem commands ---
34501
+ /** ---- SecuritySystem commands ---- */
34282
34502
  publishSetTarget(payload) {
34283
34503
  const topic = this.storage.getItem('topicSetTarget');
34284
34504
  if (!topic || !this.client) {
@@ -34298,14 +34518,12 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34298
34518
  this.console.log('armSecuritySystem', mode, '->', payload);
34299
34519
  this.pendingTarget = mode; // memorizza target, ma NON cambiare il current
34300
34520
  this.publishSetTarget(payload);
34301
- // niente update ottimistico: HomeKit vedrà Target ≠ Current e mostrerà "Arming..."
34302
34521
  }
34303
34522
  async disarmSecuritySystem() {
34304
34523
  const payload = this.getOutgoing(sdk_1.SecuritySystemMode.Disarmed);
34305
34524
  this.console.log('disarmSecuritySystem ->', payload);
34306
34525
  this.pendingTarget = sdk_1.SecuritySystemMode.Disarmed;
34307
34526
  this.publishSetTarget(payload);
34308
- // niente update ottimistico: aspetta il feedback CURRENT
34309
34527
  }
34310
34528
  getOutgoing(mode) {
34311
34529
  const map = {
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.15",
3
+ "version": "1.0.18",
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",