@rfranzoi/scrypted-mqtt-securitysystem 1.0.15 → 1.0.17

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,21 @@ 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
+ /** SecuritySystem outgoing defaults (PAI-like) */
34083
34087
  const DEFAULT_OUTGOING = {
34084
34088
  [sdk_1.SecuritySystemMode.Disarmed]: 'disarm',
34085
34089
  [sdk_1.SecuritySystemMode.HomeArmed]: 'arm_home',
34086
34090
  [sdk_1.SecuritySystemMode.AwayArmed]: 'arm_away',
34087
34091
  [sdk_1.SecuritySystemMode.NightArmed]: 'arm_night',
34088
34092
  };
34089
- /** Common incoming synonyms SecuritySystemMode
34090
- * (transitori non alterano la modalità corrente) */
34093
+ /** Parse incoming payload -> final mode (ignore transition states) */
34091
34094
  function payloadToMode(payload) {
34092
34095
  if (payload == null)
34093
34096
  return;
34094
34097
  const p = normalize(payload.toString());
34095
- // final modes
34096
34098
  if (['disarm', 'disarmed', 'off', '0', 'idle', 'ready'].includes(p))
34097
34099
  return sdk_1.SecuritySystemMode.Disarmed;
34098
34100
  if (['arm_home', 'home', 'stay', 'armed_home'].includes(p))
@@ -34106,9 +34108,83 @@ function payloadToMode(payload) {
34106
34108
  return undefined;
34107
34109
  return undefined;
34108
34110
  }
34111
+ class BaseMqttSensor extends sdk_1.ScryptedDeviceBase {
34112
+ constructor(nativeId, cfg) {
34113
+ super(nativeId);
34114
+ this.cfg = cfg;
34115
+ this.online = this.online ?? true;
34116
+ }
34117
+ /** Called by parent on each MQTT message */
34118
+ handleMqtt(topic, payload) {
34119
+ const p = payload?.toString() ?? '';
34120
+ const np = normalize(p);
34121
+ // online
34122
+ if (topic === this.cfg.topics.online) {
34123
+ if (truthy(np) || np === 'online')
34124
+ this.online = true;
34125
+ if (falsy(np) || np === 'offline')
34126
+ this.online = false;
34127
+ }
34128
+ // tamper
34129
+ if (topic === this.cfg.topics.tamper) {
34130
+ if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
34131
+ this.tampered = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34132
+ }
34133
+ else if (falsy(np)) {
34134
+ this.tampered = false;
34135
+ }
34136
+ }
34137
+ // battery
34138
+ if (topic === this.cfg.topics.batteryLevel) {
34139
+ const n = clamp(parseFloat(p), 0, 100);
34140
+ if (isFinite(n))
34141
+ this.batteryLevel = n;
34142
+ }
34143
+ else if (topic === this.cfg.topics.lowBattery && !this.cfg.topics.batteryLevel) {
34144
+ // sintetizza se non c'è batteryLevel
34145
+ this.batteryLevel = truthy(np) ? 10 : 100;
34146
+ }
34147
+ // primary handled by subclasses
34148
+ this.handlePrimary(topic, np, p);
34149
+ }
34150
+ }
34151
+ class ContactMqttSensor extends BaseMqttSensor {
34152
+ constructor(nativeId, cfg) {
34153
+ super(nativeId, cfg);
34154
+ }
34155
+ handlePrimary(topic, np, _raw) {
34156
+ if (topic === this.cfg.topics.contact) {
34157
+ this.entryOpen = truthy(np);
34158
+ }
34159
+ }
34160
+ }
34161
+ class MotionMqttSensor extends BaseMqttSensor {
34162
+ constructor(nativeId, cfg) {
34163
+ super(nativeId, cfg);
34164
+ }
34165
+ handlePrimary(topic, np, _raw) {
34166
+ if (topic === this.cfg.topics.motion) {
34167
+ this.motionDetected = truthy(np);
34168
+ }
34169
+ }
34170
+ }
34171
+ class OccupancyMqttSensor extends BaseMqttSensor {
34172
+ constructor(nativeId, cfg) {
34173
+ super(nativeId, cfg);
34174
+ }
34175
+ handlePrimary(topic, np, _raw) {
34176
+ if (topic === this.cfg.topics.occupancy) {
34177
+ this.occupied = truthy(np);
34178
+ }
34179
+ }
34180
+ }
34181
+ /** ----------------- Main Plugin ----------------- */
34109
34182
  class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34110
34183
  constructor() {
34111
34184
  super();
34185
+ // sensor management
34186
+ this.sensorsCfg = [];
34187
+ this.devices = new Map();
34112
34188
  // (facoltativo) Imposta il device type in UI
34113
34189
  setTimeout(() => {
34114
34190
  try {
@@ -34127,6 +34203,9 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34127
34203
  ],
34128
34204
  };
34129
34205
  this.online = this.online ?? false;
34206
+ // Load sensors config and announce devices
34207
+ this.loadSensorsFromStorage();
34208
+ this.discoverSensors();
34130
34209
  // Connect on start
34131
34210
  this.connectMqtt().catch(e => this.console.error('MQTT connect error:', e));
34132
34211
  // chiusura pulita del client MQTT ai reload/stop del plugin
@@ -34146,33 +34225,134 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34146
34225
  }
34147
34226
  catch { }
34148
34227
  }
34149
- // --- Settings UI ---
34228
+ /** ---- Settings UI ---- */
34150
34229
  async getSettings() {
34151
34230
  return [
34231
+ // MQTT Core
34152
34232
  { 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
34233
  { group: 'MQTT', key: 'username', title: 'Username', type: 'string', value: this.storage.getItem('username') || '' },
34154
34234
  { group: 'MQTT', key: 'password', title: 'Password', type: 'password', value: this.storage.getItem('password') || '' },
34155
34235
  { group: 'MQTT', key: 'clientId', title: 'Client ID', placeholder: 'scrypted-paradox', value: this.storage.getItem('clientId') || 'scrypted-paradox' },
34156
34236
  { group: 'MQTT', key: 'tls', title: 'Use TLS', type: 'boolean', value: this.storage.getItem('tls') === 'true' },
34157
34237
  { 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') || '' },
34238
+ // Alarm Topics
34239
+ { group: 'Alarm Topics', key: 'topicSetTarget', title: 'Set Target State (publish)', placeholder: 'paradox/control/partitions/Area_1', value: this.storage.getItem('topicSetTarget') || '' },
34240
+ { group: 'Alarm Topics', key: 'topicGetTarget', title: 'Get Target State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/target_state', value: this.storage.getItem('topicGetTarget') || '' },
34241
+ { group: 'Alarm Topics', key: 'topicGetCurrent', title: 'Get Current State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/current_state', value: this.storage.getItem('topicGetCurrent') || '' },
34242
+ { group: 'Alarm Topics', key: 'topicTamper', title: 'Get Status Tampered (subscribe)', placeholder: 'paradox/states/system/troubles/zone_tamper_trouble', value: this.storage.getItem('topicTamper') || '' },
34243
+ { group: 'Alarm Topics', key: 'topicOnline', title: 'Get Online (subscribe)', placeholder: 'paradox/interface/availability', value: this.storage.getItem('topicOnline') || '' },
34163
34244
  { group: 'Publish Options', key: 'qos', title: 'QoS', type: 'integer', value: parseInt(this.storage.getItem('qos') || '0') },
34164
34245
  { group: 'Publish Options', key: 'retain', title: 'Retain', type: 'boolean', value: this.storage.getItem('retain') === 'true' },
34165
34246
  { group: 'Outgoing Payloads', key: 'payloadDisarm', title: 'Payload Disarm', value: this.storage.getItem('payloadDisarm') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.Disarmed] },
34166
34247
  { group: 'Outgoing Payloads', key: 'payloadHome', title: 'Payload HomeArmed', value: this.storage.getItem('payloadHome') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.HomeArmed] },
34167
34248
  { group: 'Outgoing Payloads', key: 'payloadAway', title: 'Payload AwayArmed', value: this.storage.getItem('payloadAway') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.AwayArmed] },
34168
34249
  { group: 'Outgoing Payloads', key: 'payloadNight', title: 'Payload NightArmed', value: this.storage.getItem('payloadNight') || DEFAULT_OUTGOING[sdk_1.SecuritySystemMode.NightArmed] },
34250
+ // Sensors config (JSON)
34251
+ {
34252
+ group: 'Sensors',
34253
+ key: 'sensorsJson',
34254
+ title: 'Sensors JSON (contact/motion/occupancy)',
34255
+ description: 'Definisci i sensori e i topic MQTT (vedi README). Incolla JSON; le interruzioni di riga sono accettate.',
34256
+ type: 'string',
34257
+ 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]'
34258
+ },
34169
34259
  ];
34170
34260
  }
34171
34261
  async putSetting(key, value) {
34172
34262
  this.storage.setItem(key, String(value));
34173
- await this.connectMqtt(true);
34263
+ if (key === 'sensorsJson') {
34264
+ this.loadSensorsFromStorage();
34265
+ await this.discoverSensors();
34266
+ await this.connectMqtt(true);
34267
+ }
34268
+ else {
34269
+ await this.connectMqtt(true);
34270
+ }
34271
+ }
34272
+ /** ---- DeviceProvider ---- */
34273
+ async getDevice(nativeId) {
34274
+ return this.devices.get(nativeId);
34174
34275
  }
34175
- // --- MQTT ---
34276
+ async releaseDevice(id, nativeId) {
34277
+ try {
34278
+ // chiudi e rimuovi l’istanza locale se esiste
34279
+ const dev = this.devices.get(nativeId);
34280
+ if (dev) {
34281
+ this.devices.delete(nativeId);
34282
+ }
34283
+ // notifica (best effort) la rimozione al device manager
34284
+ try {
34285
+ deviceManager.onDeviceRemoved?.(nativeId);
34286
+ }
34287
+ catch { }
34288
+ }
34289
+ catch (e) {
34290
+ this.console.warn('releaseDevice error', e);
34291
+ }
34292
+ }
34293
+ loadSensorsFromStorage() {
34294
+ try {
34295
+ const raw = this.storage.getItem('sensorsJson') || '[]';
34296
+ const parsed = JSON.parse(raw);
34297
+ // sanitize
34298
+ this.sensorsCfg = (parsed || []).filter(x => x && x.id && x.name && x.kind && x.topics);
34299
+ }
34300
+ catch (e) {
34301
+ this.console.error('Invalid sensorsJson:', e);
34302
+ this.sensorsCfg = [];
34303
+ }
34304
+ }
34305
+ async discoverSensors() {
34306
+ const announced = new Set();
34307
+ for (const cfg of this.sensorsCfg) {
34308
+ const nativeId = `sensor:${cfg.id}`;
34309
+ announced.add(nativeId);
34310
+ let interfaces = [sdk_1.ScryptedInterface.Online, sdk_1.ScryptedInterface.TamperSensor, sdk_1.ScryptedInterface.Battery];
34311
+ switch (cfg.kind) {
34312
+ case 'contact':
34313
+ interfaces = [sdk_1.ScryptedInterface.EntrySensor, ...interfaces];
34314
+ break;
34315
+ case 'motion':
34316
+ interfaces = [sdk_1.ScryptedInterface.MotionSensor, ...interfaces];
34317
+ break;
34318
+ case 'occupancy':
34319
+ interfaces = [sdk_1.ScryptedInterface.OccupancySensor, ...interfaces];
34320
+ break;
34321
+ }
34322
+ deviceManager.onDeviceDiscovered({
34323
+ nativeId,
34324
+ name: cfg.name,
34325
+ type: sdk_1.ScryptedDeviceType.Sensor,
34326
+ interfaces,
34327
+ });
34328
+ // create/update instance
34329
+ let dev = this.devices.get(nativeId);
34330
+ if (!dev) {
34331
+ if (cfg.kind === 'contact')
34332
+ dev = new ContactMqttSensor(nativeId, cfg);
34333
+ else if (cfg.kind === 'motion')
34334
+ dev = new MotionMqttSensor(nativeId, cfg);
34335
+ else
34336
+ dev = new OccupancyMqttSensor(nativeId, cfg);
34337
+ this.devices.set(nativeId, dev);
34338
+ }
34339
+ else {
34340
+ // update config reference
34341
+ dev.cfg = cfg;
34342
+ }
34343
+ }
34344
+ // drop removed sensors
34345
+ for (const [nativeId] of this.devices) {
34346
+ if (!announced.has(nativeId)) {
34347
+ try {
34348
+ this.devices.delete(nativeId);
34349
+ deviceManager.onDeviceRemoved?.(nativeId);
34350
+ }
34351
+ catch { }
34352
+ }
34353
+ }
34354
+ }
34355
+ /** ---- MQTT ---- */
34176
34356
  getMqttOptions() {
34177
34357
  const url = this.storage.getItem('brokerUrl') || 'mqtt://127.0.0.1:1883';
34178
34358
  const username = this.storage.getItem('username') || undefined;
@@ -34193,12 +34373,25 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34193
34373
  }
34194
34374
  return { url, opts };
34195
34375
  }
34376
+ collectAllSubscriptions() {
34377
+ const subs = new Set();
34378
+ // alarm
34379
+ for (const k of ['topicGetTarget', 'topicGetCurrent', 'topicTamper', 'topicOnline']) {
34380
+ const v = this.storage.getItem(k);
34381
+ if (v)
34382
+ subs.add(v);
34383
+ }
34384
+ // sensors
34385
+ for (const s of this.sensorsCfg) {
34386
+ const t = s.topics;
34387
+ [t.contact, t.motion, t.occupancy, t.batteryLevel, t.lowBattery, t.tamper, t.online]
34388
+ .filter(Boolean)
34389
+ .forEach(x => subs.add(String(x)));
34390
+ }
34391
+ return Array.from(subs);
34392
+ }
34196
34393
  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);
34394
+ const subs = this.collectAllSubscriptions();
34202
34395
  if (!subs.length && !this.storage.getItem('topicSetTarget')) {
34203
34396
  this.console.warn('Configura almeno un topic nelle impostazioni.');
34204
34397
  }
@@ -34213,6 +34406,11 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34213
34406
  this.console.log(`Connecting MQTT ${url} ...`);
34214
34407
  const client = mqtt_1.default.connect(url, opts);
34215
34408
  this.client = client;
34409
+ // cache alarm topics for fast compare
34410
+ const tTarget = this.storage.getItem('topicGetTarget') || '';
34411
+ const tCurrent = this.storage.getItem('topicGetCurrent') || '';
34412
+ const tTamper = this.storage.getItem('topicTamper') || '';
34413
+ const tOnline = this.storage.getItem('topicOnline') || '';
34216
34414
  client.on('connect', () => {
34217
34415
  this.console.log('MQTT connected');
34218
34416
  this.online = true;
@@ -34229,29 +34427,27 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34229
34427
  client.on('message', (topic, payload) => {
34230
34428
  try {
34231
34429
  const p = payload?.toString() ?? '';
34232
- // Online
34430
+ const np = normalize(p);
34431
+ // ---- Alarm handling ----
34233
34432
  if (topic === tOnline) {
34234
- if (truthy(p) || p.toLowerCase() === 'online')
34433
+ if (truthy(np) || np === 'online')
34235
34434
  this.online = true;
34236
- if (falsy(p) || p.toLowerCase() === 'offline')
34435
+ if (falsy(np) || np === 'offline')
34237
34436
  this.online = false;
34238
34437
  return;
34239
34438
  }
34240
- // Tamper
34241
34439
  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;
34440
+ if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np)) {
34441
+ this.tampered = ['cover', 'intrusion'].find(x => x === np) || true;
34245
34442
  }
34246
34443
  else if (falsy(np)) {
34247
34444
  this.tampered = false;
34248
34445
  }
34249
34446
  return;
34250
34447
  }
34251
- // CURRENT state → aggiorna il mode mostrato
34252
34448
  if (topic === tCurrent) {
34253
34449
  const mode = payloadToMode(payload);
34254
- const isAlarm = ['alarm', 'triggered'].includes(normalize(p));
34450
+ const isAlarm = ['alarm', 'triggered'].includes(np);
34255
34451
  const current = this.securitySystemState || { mode: sdk_1.SecuritySystemMode.Disarmed };
34256
34452
  const newState = {
34257
34453
  mode: mode ?? current.mode,
@@ -34266,19 +34462,22 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34266
34462
  this.securitySystemState = newState;
34267
34463
  return;
34268
34464
  }
34269
- // TARGET state → NON cambia il mode; lo memorizziamo solo come pending
34270
34465
  if (topic === tTarget) {
34271
34466
  this.pendingTarget = payloadToMode(payload);
34272
34467
  this.console.log('Target state reported:', p, '->', this.pendingTarget);
34273
34468
  return;
34274
34469
  }
34470
+ // ---- Sensor dispatch ----
34471
+ for (const dev of this.devices.values()) {
34472
+ dev.handleMqtt(topic, payload);
34473
+ }
34275
34474
  }
34276
34475
  catch (e) {
34277
34476
  this.console.error('MQTT message handler error', e);
34278
34477
  }
34279
34478
  });
34280
34479
  }
34281
- // --- SecuritySystem commands ---
34480
+ /** ---- SecuritySystem commands ---- */
34282
34481
  publishSetTarget(payload) {
34283
34482
  const topic = this.storage.getItem('topicSetTarget');
34284
34483
  if (!topic || !this.client) {
@@ -34298,14 +34497,12 @@ class ParadoxMqttSecuritySystem extends sdk_1.ScryptedDeviceBase {
34298
34497
  this.console.log('armSecuritySystem', mode, '->', payload);
34299
34498
  this.pendingTarget = mode; // memorizza target, ma NON cambiare il current
34300
34499
  this.publishSetTarget(payload);
34301
- // niente update ottimistico: HomeKit vedrà Target ≠ Current e mostrerà "Arming..."
34302
34500
  }
34303
34501
  async disarmSecuritySystem() {
34304
34502
  const payload = this.getOutgoing(sdk_1.SecuritySystemMode.Disarmed);
34305
34503
  this.console.log('disarmSecuritySystem ->', payload);
34306
34504
  this.pendingTarget = sdk_1.SecuritySystemMode.Disarmed;
34307
34505
  this.publishSetTarget(payload);
34308
- // niente update ottimistico: aspetta il feedback CURRENT
34309
34506
  }
34310
34507
  getOutgoing(mode) {
34311
34508
  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.17",
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",