@rfranzoi/scrypted-mqtt-securitysystem 1.0.39 → 1.0.41

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.
@@ -34028,84 +34028,54 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
34028
34028
  return (mod && mod.__esModule) ? mod : { "default": mod };
34029
34029
  };
34030
34030
  Object.defineProperty(exports, "__esModule", ({ value: true }));
34031
- // --- Preload: silenzia SOLO il warning facoltativo di sdk.json ---
34032
- (() => {
34033
- const swallow = (orig) => (...args) => {
34034
- const txt = args.map(a => typeof a === 'string' ? a : (a?.message || '')).join(' ');
34035
- if (txt.includes('failed to load custom interface descriptors'))
34036
- return;
34037
- return orig(...args);
34038
- };
34039
- console.error = swallow(console.error.bind(console));
34040
- console.warn = swallow(console.warn.bind(console));
34041
- })();
34042
- // Runtime SDK via require (evita esecuzione anticipata del bundler)
34031
+ // --- Silence ONLY the optional sdk.json warning from @scrypted/sdk ---
34032
+ const __origConsoleError = console.error.bind(console);
34033
+ console.error = (...args) => {
34034
+ const first = args?.[0];
34035
+ const msg = typeof first === 'string' ? first : (first?.message || '');
34036
+ if (typeof msg === 'string' && msg.includes('failed to load custom interface descriptors')) {
34037
+ // swallow just this warning
34038
+ return;
34039
+ }
34040
+ __origConsoleError(...args);
34041
+ };
34042
+ // Carica lo SDK (non tipizziamo qui per non perdere le proprietà runtime)
34043
34043
  const sdk = __webpack_require__(/*! @scrypted/sdk */ "./node_modules/@scrypted/sdk/dist/src/index.js");
34044
- const { ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, SecuritySystemMode, systemManager, } = sdk;
34044
+ // Valori runtime (enum/classi/manager) dal modulo SDK
34045
+ const { ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, // valore (enum)
34046
+ SecuritySystemMode, // valore (enum)
34047
+ systemManager,
34048
+ // ⚠️ NON destrutturare deviceManager: va letto sempre "al volo" da sdk.deviceManager.
34049
+ } = sdk;
34045
34050
  const mqtt_1 = __importDefault(__webpack_require__(/*! mqtt */ "./node_modules/mqtt/build/index.js"));
34046
34051
  /** utils */
34047
- function normalize(s) { return (s || '').trim().toLowerCase(); }
34048
34052
  function truthy(v) {
34049
34053
  if (!v)
34050
34054
  return false;
34051
- const s = normalize(v);
34052
- return s === '1' || s === 'true' || s === 'online' || s === 'yes' || s === 'on' || s === 'ok' || s === 'open';
34055
+ const s = v.toString().trim().toLowerCase();
34056
+ return s === '1' || s === 'true' || s === 'online' || s === 'yes' || s === 'on' || s === 'ok';
34053
34057
  }
34054
34058
  function falsy(v) {
34055
34059
  if (!v)
34056
34060
  return false;
34057
- const s = normalize(v);
34058
- return s === '0' || s === 'false' || s === 'offline' || s === 'no' || s === 'off' || s === 'closed' || s === 'close';
34059
- }
34060
- function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
34061
- function deepEqual(a, b) { try {
34062
- return JSON.stringify(a) === JSON.stringify(b);
34063
- }
34064
- catch {
34065
- return a === b;
34066
- } }
34067
- /** Match topic con wildcard MQTT (+, #) oppure confronto esatto */
34068
- function topicMatches(topic, pattern) {
34069
- if (!pattern)
34070
- return false;
34071
- if (pattern === topic)
34072
- return true;
34073
- if (!pattern.includes('+') && !pattern.includes('#'))
34074
- return false;
34075
- // Escapa tutto tranne '/'
34076
- const esc = pattern.replace(/[-/\\^$*?.()|[\]{}]/g, '\\$&');
34077
- const rx = '^' + esc
34078
- .replace(/\\\+/g, '[^/]+') // '+' => un segmento
34079
- .replace(/\\\#/g, '.+'); // '#" => qualsiasi suffisso
34080
- try {
34081
- return new RegExp(rx + '$').test(topic);
34082
- }
34083
- catch {
34084
- return false;
34085
- }
34061
+ const s = v.toString().trim().toLowerCase();
34062
+ return s === '0' || s === 'false' || s === 'offline' || s === 'no' || s === 'off';
34086
34063
  }
34087
- /** set + emit evento scrypted */
34088
- function setAndEmit(dev, key, value, iface, log) {
34089
- if (dev[key] === value)
34090
- return;
34091
- dev[key] = value;
34092
- try {
34093
- dev.onDeviceEvent?.(iface, value);
34094
- }
34095
- catch { }
34096
- try {
34097
- if (log)
34098
- dev.console?.log?.(log);
34099
- }
34100
- catch { }
34064
+ function normalize(s) {
34065
+ return (s || '').trim().toLowerCase();
34066
+ }
34067
+ function clamp(n, min, max) {
34068
+ return Math.max(min, Math.min(max, n));
34101
34069
  }
34102
- /** Outgoing predefiniti (PAI-like). Chiavi numeriche per compat enum */
34070
+ /** SecuritySystem outgoing defaults (PAI-like)
34071
+ * Nota: usare number come chiave evita stranezze con gli enum in TS. */
34103
34072
  const DEFAULT_OUTGOING = {
34104
34073
  [SecuritySystemMode.Disarmed]: 'disarm',
34105
34074
  [SecuritySystemMode.HomeArmed]: 'arm_home',
34106
34075
  [SecuritySystemMode.AwayArmed]: 'arm_away',
34107
34076
  [SecuritySystemMode.NightArmed]: 'arm_night',
34108
34077
  };
34078
+ /** Parse incoming payload -> final mode (ignore transition states) */
34109
34079
  function payloadToMode(payload) {
34110
34080
  if (payload == null)
34111
34081
  return;
@@ -34118,6 +34088,7 @@ function payloadToMode(payload) {
34118
34088
  return SecuritySystemMode.AwayArmed;
34119
34089
  if (['arm_night', 'night', 'armed_night', 'sleep', 'arm_sleep', 'armed_sleep'].includes(p))
34120
34090
  return SecuritySystemMode.NightArmed;
34091
+ // transitori: non cambiano il mode
34121
34092
  if (['entry_delay', 'exit_delay', 'pending', 'arming', 'disarming'].includes(p))
34122
34093
  return undefined;
34123
34094
  return undefined;
@@ -34127,148 +34098,58 @@ class BaseMqttSensor extends ScryptedDeviceBase {
34127
34098
  super(nativeId);
34128
34099
  this.cfg = cfg;
34129
34100
  }
34101
+ /** Called by parent on each MQTT message */
34130
34102
  handleMqtt(topic, payload) {
34131
- const raw = payload?.toString() ?? '';
34132
- const np = normalize(raw);
34133
- // Online
34134
- if (topicMatches(topic, this.cfg.topics.online)) {
34103
+ const p = payload?.toString() ?? '';
34104
+ const np = normalize(p);
34105
+ // online
34106
+ if (topic === this.cfg.topics.online) {
34135
34107
  if (truthy(np) || np === 'online')
34136
- setAndEmit(this, 'online', true, ScryptedInterface.Online, `[${this.name}] online=true`);
34137
- else if (falsy(np) || np === 'offline')
34138
- setAndEmit(this, 'online', false, ScryptedInterface.Online, `[${this.name}] online=false`);
34108
+ this.online = true;
34109
+ if (falsy(np) || np === 'offline')
34110
+ this.online = false;
34139
34111
  }
34140
- // Tamper
34141
- if (topicMatches(topic, this.cfg.topics.tamper)) {
34112
+ // tamper
34113
+ if (topic === this.cfg.topics.tamper) {
34142
34114
  if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
34143
- const t = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34144
- setAndEmit(this, 'tampered', t, ScryptedInterface.TamperSensor, `[${this.name}] tampered=${t}`);
34115
+ this.tampered = ['cover', 'intrusion', 'motion', 'magnetic'].find(x => x === np) || true;
34145
34116
  }
34146
34117
  else if (falsy(np)) {
34147
- setAndEmit(this, 'tampered', false, ScryptedInterface.TamperSensor, `[${this.name}] tampered=false`);
34118
+ this.tampered = false;
34148
34119
  }
34149
34120
  }
34150
- // Battery
34151
- if (topicMatches(topic, this.cfg.topics.batteryLevel)) {
34152
- const n = clamp(parseFloat(raw), 0, 100);
34121
+ // battery
34122
+ if (topic === this.cfg.topics.batteryLevel) {
34123
+ const n = clamp(parseFloat(p), 0, 100);
34153
34124
  if (isFinite(n))
34154
- setAndEmit(this, 'batteryLevel', n, ScryptedInterface.Battery, `[${this.name}] batteryLevel=${n}`);
34125
+ this.batteryLevel = n;
34155
34126
  }
34156
- else if (topicMatches(topic, this.cfg.topics.lowBattery) && !this.cfg.topics.batteryLevel) {
34157
- const n = truthy(np) ? 10 : 100;
34158
- setAndEmit(this, 'batteryLevel', n, ScryptedInterface.Battery, `[${this.name}] batteryLevel=${n} (lowBattery)`);
34127
+ else if (topic === this.cfg.topics.lowBattery && !this.cfg.topics.batteryLevel) {
34128
+ // se abbiamo SOLO lowBattery (bool):
34129
+ this.batteryLevel = truthy(np) ? 10 : 100;
34159
34130
  }
34160
- // Primario
34161
- this.handlePrimary(topic, np, raw);
34131
+ // primary handled by subclasses
34132
+ this.handlePrimary(topic, np, p);
34162
34133
  }
34163
34134
  }
34164
- /** === SENSORI: parsing robusto + eventi === */
34165
34135
  class ContactMqttSensor extends BaseMqttSensor {
34166
- handlePrimary(topic, np, raw) {
34167
- if (!topicMatches(topic, this.cfg.topics.contact))
34168
- return;
34169
- let val;
34170
- // stringhe comuni (True/False compresi via normalize)
34171
- if (['open', 'opened', '1', 'true', 'on', 'yes'].includes(np))
34172
- val = true;
34173
- else if (['closed', 'close', '0', 'false', 'off', 'no', 'shut'].includes(np))
34174
- val = false;
34175
- // JSON comuni
34176
- if (val === undefined) {
34177
- try {
34178
- const j = JSON.parse(raw);
34179
- if (typeof j?.open === 'boolean')
34180
- val = !!j.open;
34181
- else if (typeof j?.opened === 'boolean')
34182
- val = !!j.opened;
34183
- else if (typeof j?.contact === 'boolean')
34184
- val = !j.contact; // contact:false => aperto
34185
- else if (typeof j?.state === 'string') {
34186
- const s = normalize(j.state);
34187
- if (s === 'open')
34188
- val = true;
34189
- if (s === 'closed')
34190
- val = false;
34191
- }
34192
- }
34193
- catch { }
34194
- }
34195
- if (val !== undefined) {
34196
- setAndEmit(this, 'entryOpen', val, ScryptedInterface.EntrySensor, `[${this.name}] entryOpen=${val} (${topic})`);
34197
- }
34198
- else {
34199
- this.console?.debug?.(`Contact payload non gestito (${this.cfg.id}) topic=${topic} raw="${raw}"`);
34136
+ handlePrimary(topic, np) {
34137
+ if (topic === this.cfg.topics.contact) {
34138
+ this.entryOpen = truthy(np);
34200
34139
  }
34201
34140
  }
34202
34141
  }
34203
34142
  class MotionMqttSensor extends BaseMqttSensor {
34204
- handlePrimary(topic, np, raw) {
34205
- if (!topicMatches(topic, this.cfg.topics.motion))
34206
- return;
34207
- let val;
34208
- if (['motion', 'detected', 'active', '1', 'true', 'on', 'yes'].includes(np))
34209
- val = true;
34210
- else if (['clear', 'inactive', 'no_motion', 'none', '0', 'false', 'off', 'no'].includes(np))
34211
- val = false;
34212
- if (val === undefined) {
34213
- try {
34214
- const j = JSON.parse(raw);
34215
- if (typeof j?.motion === 'boolean')
34216
- val = !!j.motion;
34217
- else if (typeof j?.occupancy === 'boolean')
34218
- val = !!j.occupancy;
34219
- else if (typeof j?.presence === 'boolean')
34220
- val = !!j.presence;
34221
- else if (typeof j?.state === 'string') {
34222
- const s = normalize(j.state);
34223
- if (['on', 'motion', 'detected', 'active'].includes(s))
34224
- val = true;
34225
- if (['off', 'clear', 'inactive'].includes(s))
34226
- val = false;
34227
- }
34228
- }
34229
- catch { }
34230
- }
34231
- if (val !== undefined) {
34232
- setAndEmit(this, 'motionDetected', val, ScryptedInterface.MotionSensor, `[${this.name}] motionDetected=${val} (${topic})`);
34233
- }
34234
- else {
34235
- this.console?.debug?.(`Motion payload non gestito (${this.cfg.id}) topic=${topic} raw="${raw}"`);
34143
+ handlePrimary(topic, np) {
34144
+ if (topic === this.cfg.topics.motion) {
34145
+ this.motionDetected = truthy(np);
34236
34146
  }
34237
34147
  }
34238
34148
  }
34239
34149
  class OccupancyMqttSensor extends BaseMqttSensor {
34240
- handlePrimary(topic, np, raw) {
34241
- if (!topicMatches(topic, this.cfg.topics.occupancy))
34242
- return;
34243
- let val;
34244
- if (['occupied', 'presence', 'present', '1', 'true', 'on', 'yes'].includes(np))
34245
- val = true;
34246
- else if (['unoccupied', 'vacant', 'absent', '0', 'false', 'off', 'no', 'clear'].includes(np))
34247
- val = false;
34248
- if (val === undefined) {
34249
- try {
34250
- const j = JSON.parse(raw);
34251
- if (typeof j?.occupied === 'boolean')
34252
- val = !!j.occupied;
34253
- else if (typeof j?.presence === 'boolean')
34254
- val = !!j.presence;
34255
- else if (typeof j?.occupancy === 'boolean')
34256
- val = !!j.occupancy;
34257
- else if (typeof j?.state === 'string') {
34258
- const s = normalize(j.state);
34259
- if (['occupied', 'presence', 'present', 'on'].includes(s))
34260
- val = true;
34261
- if (['vacant', 'absent', 'clear', 'off'].includes(s))
34262
- val = false;
34263
- }
34264
- }
34265
- catch { }
34266
- }
34267
- if (val !== undefined) {
34268
- setAndEmit(this, 'occupied', val, ScryptedInterface.OccupancySensor, `[${this.name}] occupied=${val} (${topic})`);
34269
- }
34270
- else {
34271
- this.console?.debug?.(`Occupancy payload non gestito (${this.cfg.id}) topic=${topic} raw="${raw}"`);
34150
+ handlePrimary(topic, np) {
34151
+ if (topic === this.cfg.topics.occupancy) {
34152
+ this.occupied = truthy(np);
34272
34153
  }
34273
34154
  }
34274
34155
  }
@@ -34276,17 +34157,17 @@ class OccupancyMqttSensor extends BaseMqttSensor {
34276
34157
  class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34277
34158
  constructor() {
34278
34159
  super();
34160
+ // sensor management
34279
34161
  this.sensorsCfg = [];
34280
34162
  this.devices = new Map();
34281
- this.discoveryPostponed = false;
34282
- // Tipo in UI (best-effort)
34163
+ // (facoltativo) Imposta il device type in UI
34283
34164
  setTimeout(() => {
34284
34165
  try {
34285
34166
  systemManager.getDeviceById(this.id)?.setType?.(ScryptedDeviceType.SecuritySystem);
34286
34167
  }
34287
34168
  catch { }
34288
34169
  });
34289
- // Stato di default
34170
+ // Default state
34290
34171
  this.securitySystemState = this.securitySystemState || {
34291
34172
  mode: SecuritySystemMode.Disarmed,
34292
34173
  supportedModes: [
@@ -34297,9 +34178,12 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34297
34178
  ],
34298
34179
  };
34299
34180
  this.online = this.online ?? false;
34181
+ // Load sensors config and announce devices
34300
34182
  this.loadSensorsFromStorage();
34301
- this.safeDiscoverSensors();
34183
+ this.discoverSensors().catch((e) => this.console.error('discoverSensors error', e));
34184
+ // Connect on start
34302
34185
  this.connectMqtt().catch((e) => this.console.error('MQTT connect error:', e));
34186
+ // chiusura pulita del client MQTT ai reload/stop del plugin
34303
34187
  try {
34304
34188
  process.once('SIGTERM', () => { try {
34305
34189
  this.client?.end(true);
@@ -34316,7 +34200,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34316
34200
  }
34317
34201
  catch { }
34318
34202
  }
34319
- /** ---- Settings ---- */
34203
+ // helpers persistenza
34320
34204
  saveSensorsToStorage() {
34321
34205
  try {
34322
34206
  this.storage.setItem('sensorsJson', JSON.stringify(this.sensorsCfg));
@@ -34325,14 +34209,17 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34325
34209
  this.console.error('saveSensorsToStorage error', e);
34326
34210
  }
34327
34211
  }
34212
+ /** ---- Settings UI ---- */
34328
34213
  async getSettings() {
34329
34214
  const out = [
34215
+ // MQTT Core
34330
34216
  { 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' },
34331
34217
  { group: 'MQTT', key: 'username', title: 'Username', type: 'string', value: this.storage.getItem('username') || '' },
34332
34218
  { group: 'MQTT', key: 'password', title: 'Password', type: 'password', value: this.storage.getItem('password') || '' },
34333
34219
  { group: 'MQTT', key: 'clientId', title: 'Client ID', placeholder: 'scrypted-paradox', value: this.storage.getItem('clientId') || 'scrypted-paradox' },
34334
34220
  { group: 'MQTT', key: 'tls', title: 'Use TLS', type: 'boolean', value: this.storage.getItem('tls') === 'true' },
34335
34221
  { group: 'MQTT', key: 'rejectUnauthorized', title: 'Reject Unauthorized (TLS)', type: 'boolean', value: this.storage.getItem('rejectUnauthorized') !== 'false', description: 'Disattiva solo con broker self-signed.' },
34222
+ // Alarm Topics
34336
34223
  { group: 'Alarm Topics', key: 'topicSetTarget', title: 'Set Target State (publish)', placeholder: 'paradox/control/partitions/Area_1', value: this.storage.getItem('topicSetTarget') || '' },
34337
34224
  { group: 'Alarm Topics', key: 'topicGetTarget', title: 'Get Target State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/target_state', value: this.storage.getItem('topicGetTarget') || '' },
34338
34225
  { group: 'Alarm Topics', key: 'topicGetCurrent', title: 'Get Current State (subscribe)', placeholder: 'paradox/states/partitions/Area_1/current_state', value: this.storage.getItem('topicGetCurrent') || '' },
@@ -34341,23 +34228,31 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34341
34228
  { group: 'Publish Options', key: 'qos', title: 'QoS', type: 'integer', value: parseInt(this.storage.getItem('qos') || '0') },
34342
34229
  { group: 'Publish Options', key: 'retain', title: 'Retain', type: 'boolean', value: this.storage.getItem('retain') === 'true' },
34343
34230
  ];
34344
- // Add Sensor
34345
- 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: 'Toggle ON to create the sensor.' });
34231
+ // ---- UI Add Sensor ----
34232
+ 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.' });
34233
+ // ---- UI per sensori esistenti ----
34346
34234
  for (const cfg of this.sensorsCfg) {
34347
34235
  const gid = `Sensor: ${cfg.name} [${cfg.id}]`;
34348
34236
  out.push({ group: gid, key: `sensor.${cfg.id}.name`, title: 'Name', value: cfg.name }, { group: gid, key: `sensor.${cfg.id}.kind`, title: 'Type', value: cfg.kind, choices: ['contact', 'motion', 'occupancy'] });
34349
- if (cfg.kind === 'contact')
34350
- out.push({ group: gid, key: `sensor.${cfg.id}.topic.contact`, title: 'Contact State Topic', value: cfg.topics.contact || '', placeholder: 'paradox/states/zones/XYZ/open (supporta +/#)' });
34351
- else if (cfg.kind === 'motion')
34352
- out.push({ group: gid, key: `sensor.${cfg.id}.topic.motion`, title: 'Motion Detected Topic', value: cfg.topics.motion || '' });
34353
- else
34354
- out.push({ group: gid, key: `sensor.${cfg.id}.topic.occupancy`, title: 'Occupancy Detected Topic', value: cfg.topics.occupancy || '' });
34237
+ // primary per tipo
34238
+ if (cfg.kind === 'contact') {
34239
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.contact`, title: 'Contact State Topic', value: cfg.topics.contact || '', placeholder: 'paradox/states/zones/XYZ/open' });
34240
+ }
34241
+ else if (cfg.kind === 'motion') {
34242
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.motion`, title: 'Motion Detected Topic', value: cfg.topics.motion || '', placeholder: 'paradox/states/zones/XYZ/open' });
34243
+ }
34244
+ else {
34245
+ out.push({ group: gid, key: `sensor.${cfg.id}.topic.occupancy`, title: 'Occupancy Detected Topic', value: cfg.topics.occupancy || '', placeholder: 'paradox/states/zones/XYZ/open' });
34246
+ }
34247
+ // extra opzionali
34355
34248
  out.push({ group: gid, key: `sensor.${cfg.id}.topic.batteryLevel`, title: 'Battery Level Topic (0..100)', value: cfg.topics.batteryLevel || '' }, { group: gid, key: `sensor.${cfg.id}.topic.lowBattery`, title: 'Low Battery Topic (bool)', value: cfg.topics.lowBattery || '' }, { group: gid, key: `sensor.${cfg.id}.topic.tamper`, title: 'Tamper Topic', value: cfg.topics.tamper || '' }, { group: gid, key: `sensor.${cfg.id}.topic.online`, title: 'Online Topic', value: cfg.topics.online || '' }, { group: gid, key: `sensor.${cfg.id}.remove`, title: 'Remove sensor', type: 'boolean' });
34356
34249
  }
34357
34250
  return out;
34358
34251
  }
34359
34252
  async putSetting(key, value) {
34253
+ // salva sempre nella storage la value del campo (così resta in UI)
34360
34254
  this.storage.setItem(key, String(value));
34255
+ // --- Add Sensor workflow ---
34361
34256
  if (key === 'new.create' && String(value) === 'true') {
34362
34257
  const id = (this.storage.getItem('new.id') || '').trim();
34363
34258
  const name = (this.storage.getItem('new.name') || '').trim() || id;
@@ -34372,14 +34267,16 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34372
34267
  }
34373
34268
  this.sensorsCfg.push({ id, name, kind, topics: {} });
34374
34269
  this.saveSensorsToStorage();
34270
+ // pulisci i campi "new.*"
34375
34271
  this.storage.removeItem('new.id');
34376
34272
  this.storage.removeItem('new.name');
34377
34273
  this.storage.removeItem('new.kind');
34378
34274
  this.storage.removeItem('new.create');
34379
- this.safeDiscoverSensors(true);
34275
+ await this.discoverSensors();
34380
34276
  await this.connectMqtt(true);
34381
34277
  return;
34382
34278
  }
34279
+ // --- Edit/Remove sensore esistente ---
34383
34280
  const m = key.match(/^sensor\.([^\.]+)\.(.+)$/);
34384
34281
  if (m) {
34385
34282
  const sid = m[1];
@@ -34390,33 +34287,40 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34390
34287
  return;
34391
34288
  }
34392
34289
  if (prop === 'remove' && String(value) === 'true') {
34290
+ // elimina
34393
34291
  this.sensorsCfg = this.sensorsCfg.filter(s => s.id !== sid);
34394
34292
  this.saveSensorsToStorage();
34395
34293
  try {
34294
+ this.devices.delete(`sensor:${sid}`);
34396
34295
  sdk?.deviceManager?.onDeviceRemoved?.(`sensor:${sid}`);
34397
34296
  }
34398
34297
  catch { }
34298
+ // pulisci flag
34399
34299
  this.storage.removeItem(key);
34400
- this.safeDiscoverSensors(true);
34300
+ await this.discoverSensors();
34401
34301
  await this.connectMqtt(true);
34402
34302
  return;
34403
34303
  }
34404
- if (prop === 'name')
34304
+ if (prop === 'name') {
34405
34305
  cfg.name = String(value);
34406
- else if (prop === 'kind')
34306
+ }
34307
+ else if (prop === 'kind') {
34407
34308
  cfg.kind = String(value);
34309
+ }
34408
34310
  else if (prop.startsWith('topic.')) {
34409
34311
  const tk = prop.substring('topic.'.length);
34410
34312
  cfg.topics[tk] = String(value).trim();
34411
34313
  }
34412
34314
  this.saveSensorsToStorage();
34413
- this.safeDiscoverSensors(true);
34315
+ await this.discoverSensors();
34414
34316
  await this.connectMqtt(true);
34415
34317
  return;
34416
34318
  }
34319
+ // --- Altro (MQTT / Alarm settings) ---
34417
34320
  if (key === 'sensorsJson') {
34321
+ // non più mostrato, ma se presente da vecchie versioni
34418
34322
  this.loadSensorsFromStorage();
34419
- this.safeDiscoverSensors(true);
34323
+ await this.discoverSensors();
34420
34324
  await this.connectMqtt(true);
34421
34325
  }
34422
34326
  else {
@@ -34424,12 +34328,15 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34424
34328
  }
34425
34329
  }
34426
34330
  /** ---- DeviceProvider ---- */
34427
- async getDevice(nativeId) { return this.devices.get(nativeId); }
34331
+ async getDevice(nativeId) {
34332
+ return this.devices.get(nativeId);
34333
+ }
34428
34334
  async releaseDevice(_id, nativeId) {
34429
34335
  try {
34430
34336
  const dev = this.devices.get(nativeId);
34431
- if (dev)
34337
+ if (dev) {
34432
34338
  this.devices.delete(nativeId);
34339
+ }
34433
34340
  try {
34434
34341
  sdk?.deviceManager?.onDeviceRemoved?.(nativeId);
34435
34342
  }
@@ -34443,6 +34350,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34443
34350
  try {
34444
34351
  const raw = this.storage.getItem('sensorsJson') || '[]';
34445
34352
  const parsed = JSON.parse(raw);
34353
+ // sanitize
34446
34354
  this.sensorsCfg = (parsed || []).filter(x => x && x.id && x.name && x.kind && x.topics);
34447
34355
  }
34448
34356
  catch (e) {
@@ -34450,21 +34358,15 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34450
34358
  this.sensorsCfg = [];
34451
34359
  }
34452
34360
  }
34453
- safeDiscoverSensors(triggeredByChange = false) {
34361
+ /** ===== discoverSensors: annuncia PRIMA, istanzia DOPO (con retry se manager non pronto) ===== */
34362
+ async discoverSensors() {
34454
34363
  const dmAny = sdk?.deviceManager;
34455
34364
  if (!dmAny) {
34456
- if (!this.discoveryPostponed) {
34457
- this.console.log('Device discovery postponed: deviceManager not ready yet.');
34458
- this.discoveryPostponed = true;
34459
- }
34365
+ this.console.warn('deviceManager not ready yet, retrying in 1s…');
34366
+ setTimeout(() => this.discoverSensors().catch(e => this.console.error('discoverSensors retry error', e)), 1000);
34460
34367
  return;
34461
34368
  }
34462
- this.discoveryPostponed = false;
34463
- this.discoverSensors(dmAny);
34464
- if (triggeredByChange)
34465
- this.console.log('Sensors discovered/updated.');
34466
- }
34467
- discoverSensors(dmAny) {
34369
+ // 1) Prepara i manifest
34468
34370
  const manifests = this.sensorsCfg.map(cfg => {
34469
34371
  const nativeId = `sensor:${cfg.id}`;
34470
34372
  const t = cfg.topics || {};
@@ -34481,11 +34383,23 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34481
34383
  interfaces.push(ScryptedInterface.Battery);
34482
34384
  return { nativeId, name: cfg.name, type: ScryptedDeviceType.Sensor, interfaces };
34483
34385
  });
34484
- if (typeof dmAny.onDevicesChanged === 'function')
34386
+ // 2) Annuncio
34387
+ if (typeof dmAny.onDevicesChanged === 'function') {
34485
34388
  dmAny.onDevicesChanged({ devices: manifests });
34486
- else if (typeof dmAny.onDeviceDiscovered === 'function')
34487
- for (const m of manifests)
34389
+ this.console.log('Annunciati (batch):', manifests.map(m => m.nativeId).join(', '));
34390
+ }
34391
+ else if (typeof dmAny.onDeviceDiscovered === 'function') {
34392
+ for (const m of manifests) {
34488
34393
  dmAny.onDeviceDiscovered(m);
34394
+ this.console.log('Annunciato:', m.nativeId);
34395
+ }
34396
+ }
34397
+ else {
34398
+ this.console.warn('deviceManager has no discovery methods yet, retrying in 1s…');
34399
+ setTimeout(() => this.discoverSensors().catch(e => this.console.error('discoverSensors retry error', e)), 1000);
34400
+ return;
34401
+ }
34402
+ // 3) Istanzia/aggiorna
34489
34403
  for (const cfg of this.sensorsCfg) {
34490
34404
  const nativeId = `sensor:${cfg.id}`;
34491
34405
  let dev = this.devices.get(nativeId);
@@ -34502,15 +34416,18 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34502
34416
  dev.cfg = cfg;
34503
34417
  }
34504
34418
  const hasBattery = !!(cfg.topics.batteryLevel || cfg.topics.lowBattery);
34505
- if (hasBattery && dev.batteryLevel === undefined)
34506
- setAndEmit(dev, 'batteryLevel', 100, ScryptedInterface.Battery, `[${cfg.name}] batteryLevel=100 (default)`);
34419
+ if (hasBattery && dev.batteryLevel === undefined) {
34420
+ dev.batteryLevel = 100;
34421
+ }
34507
34422
  }
34423
+ // 4) Rimuovi quelli spariti
34508
34424
  const announced = new Set(manifests.map(m => m.nativeId));
34509
34425
  for (const [nativeId] of this.devices) {
34510
34426
  if (!announced.has(nativeId)) {
34511
34427
  try {
34512
34428
  this.devices.delete(nativeId);
34513
- dmAny.onDeviceRemoved?.(nativeId);
34429
+ sdk?.deviceManager?.onDeviceRemoved?.(nativeId);
34430
+ this.console.log('Rimosso:', nativeId);
34514
34431
  }
34515
34432
  catch { }
34516
34433
  }
@@ -34524,7 +34441,13 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34524
34441
  const clientId = this.storage.getItem('clientId') || 'scrypted-paradox';
34525
34442
  const tls = this.storage.getItem('tls') === 'true';
34526
34443
  const rejectUnauthorized = this.storage.getItem('rejectUnauthorized') !== 'false';
34527
- const opts = { clientId, username, password, clean: true, reconnectPeriod: 3000 };
34444
+ const opts = {
34445
+ clientId,
34446
+ username,
34447
+ password,
34448
+ clean: true,
34449
+ reconnectPeriod: 3000,
34450
+ };
34528
34451
  if (tls) {
34529
34452
  opts.protocol = 'mqtts';
34530
34453
  opts.rejectUnauthorized = rejectUnauthorized;
@@ -34533,71 +34456,77 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34533
34456
  }
34534
34457
  collectAllSubscriptions() {
34535
34458
  const subs = new Set();
34459
+ // alarm
34536
34460
  for (const k of ['topicGetTarget', 'topicGetCurrent', 'topicTamper', 'topicOnline']) {
34537
34461
  const v = this.storage.getItem(k);
34538
34462
  if (v)
34539
34463
  subs.add(v);
34540
34464
  }
34465
+ // sensors
34541
34466
  for (const s of this.sensorsCfg) {
34542
34467
  const t = s.topics || {};
34543
34468
  [t.contact, t.motion, t.occupancy, t.batteryLevel, t.lowBattery, t.tamper, t.online]
34544
- .filter(Boolean).forEach(x => subs.add(String(x)));
34469
+ .filter(Boolean)
34470
+ .forEach(x => subs.add(String(x)));
34545
34471
  }
34546
34472
  return Array.from(subs);
34547
34473
  }
34548
34474
  async connectMqtt(_reconnect = false) {
34549
34475
  const subs = this.collectAllSubscriptions();
34550
- if (!subs.length && !this.storage.getItem('topicSetTarget'))
34476
+ if (!subs.length && !this.storage.getItem('topicSetTarget')) {
34551
34477
  this.console.warn('Configura almeno un topic nelle impostazioni.');
34478
+ }
34552
34479
  if (this.client) {
34553
34480
  try {
34554
34481
  this.client.end(true);
34555
34482
  }
34556
34483
  catch { }
34557
- ;
34558
34484
  this.client = undefined;
34559
34485
  }
34560
34486
  const { url, opts } = this.getMqttOptions();
34561
34487
  this.console.log(`Connecting MQTT ${url} ...`);
34562
34488
  const client = mqtt_1.default.connect(url, opts);
34563
34489
  this.client = client;
34490
+ // cache alarm topics for fast compare
34564
34491
  const tTarget = this.storage.getItem('topicGetTarget') || '';
34565
34492
  const tCurrent = this.storage.getItem('topicGetCurrent') || '';
34566
34493
  const tTamper = this.storage.getItem('topicTamper') || '';
34567
34494
  const tOnline = this.storage.getItem('topicOnline') || '';
34568
34495
  client.on('connect', () => {
34569
34496
  this.console.log('MQTT connected');
34570
- setAndEmit(this, 'online', true, ScryptedInterface.Online, `[Alarm] online=true`);
34571
- if (subs.length)
34572
- client.subscribe(subs, { qos: 0 }, (err) => { if (err)
34573
- this.console.error('subscribe error', err); });
34574
- this.safeDiscoverSensors(true);
34497
+ this.online = true;
34498
+ if (subs.length) {
34499
+ client.subscribe(subs, { qos: 0 }, (err) => {
34500
+ if (err)
34501
+ this.console.error('subscribe error', err);
34502
+ });
34503
+ }
34575
34504
  });
34576
34505
  client.on('reconnect', () => this.console.log('MQTT reconnecting...'));
34577
- client.on('close', () => { this.console.log('MQTT closed'); setAndEmit(this, 'online', false, ScryptedInterface.Online, `[Alarm] online=false`); });
34506
+ client.on('close', () => { this.console.log('MQTT closed'); this.online = false; });
34578
34507
  client.on('error', (e) => { this.console.error('MQTT error', e); });
34579
34508
  client.on('message', (topic, payload) => {
34580
34509
  try {
34581
- const raw = payload?.toString() ?? '';
34582
- const np = normalize(raw);
34583
- if (topicMatches(topic, tOnline)) {
34510
+ const p = payload?.toString() ?? '';
34511
+ const np = normalize(p);
34512
+ // ---- Alarm handling ----
34513
+ if (topic === tOnline) {
34584
34514
  if (truthy(np) || np === 'online')
34585
- setAndEmit(this, 'online', true, ScryptedInterface.Online, `[Alarm] online=true (${topic})`);
34586
- else if (falsy(np) || np === 'offline')
34587
- setAndEmit(this, 'online', false, ScryptedInterface.Online, `[Alarm] online=false (${topic})`);
34515
+ this.online = true;
34516
+ if (falsy(np) || np === 'offline')
34517
+ this.online = false;
34588
34518
  return;
34589
34519
  }
34590
- if (topicMatches(topic, tTamper)) {
34520
+ if (topic === tTamper) {
34591
34521
  if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np)) {
34592
- const t = ['cover', 'intrusion'].find(x => x === np) || true;
34593
- setAndEmit(this, 'tampered', t, ScryptedInterface.TamperSensor, `[Alarm] tampered=${t} (${topic})`);
34522
+ this.tampered = ['cover', 'intrusion'].find(x => x === np) || true;
34594
34523
  }
34595
34524
  else if (falsy(np)) {
34596
- setAndEmit(this, 'tampered', false, ScryptedInterface.TamperSensor, `[Alarm] tampered=false (${topic})`);
34525
+ this.tampered = false;
34597
34526
  }
34598
34527
  return;
34599
34528
  }
34600
- if (topicMatches(topic, tCurrent)) {
34529
+ if (topic === tCurrent) {
34601
34530
  const mode = payloadToMode(payload);
34602
34531
  const isAlarm = ['alarm', 'triggered'].includes(np);
34603
34532
  const current = this.securitySystemState || { mode: SecuritySystemMode.Disarmed };
@@ -34611,26 +34540,18 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34611
34540
  ],
34612
34541
  triggered: isAlarm || undefined,
34613
34542
  };
34614
- if (!deepEqual(this.securitySystemState, newState)) {
34615
- this.securitySystemState = newState;
34616
- try {
34617
- this.onDeviceEvent?.(ScryptedInterface.SecuritySystem, newState);
34618
- }
34619
- catch { }
34620
- this.console.log(`[Alarm] currentState=${JSON.stringify(newState)} (${topic})`);
34621
- }
34543
+ this.securitySystemState = newState;
34622
34544
  return;
34623
34545
  }
34624
- if (topicMatches(topic, tTarget)) {
34546
+ if (topic === tTarget) {
34625
34547
  this.pendingTarget = payloadToMode(payload);
34626
- this.console.log(`[Alarm] target reported: "${raw}" -> ${this.pendingTarget} (${topic})`);
34548
+ this.console.log('Target state reported:', p, '->', this.pendingTarget);
34627
34549
  return;
34628
34550
  }
34629
- // Sensors: se discovery rimandata, riprova
34630
- if (this.discoveryPostponed)
34631
- this.safeDiscoverSensors(true);
34632
- for (const dev of this.devices.values())
34551
+ // ---- Sensor dispatch ----
34552
+ for (const dev of this.devices.values()) {
34633
34553
  dev.handleMqtt(topic, payload);
34554
+ }
34634
34555
  }
34635
34556
  catch (e) {
34636
34557
  this.console.error('MQTT message handler error', e);
@@ -34655,7 +34576,7 @@ class ParadoxMqttSecuritySystem extends ScryptedDeviceBase {
34655
34576
  async armSecuritySystem(mode) {
34656
34577
  const payload = this.getOutgoing(mode);
34657
34578
  this.console.log('armSecuritySystem', mode, '->', payload);
34658
- this.pendingTarget = mode;
34579
+ this.pendingTarget = mode; // memorizza target, ma NON cambiare il current
34659
34580
  this.publishSetTarget(payload);
34660
34581
  }
34661
34582
  async disarmSecuritySystem() {
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.39",
3
+ "version": "1.0.41",
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",