@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.
- package/dist/main.nodejs.js +175 -254
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
package/dist/main.nodejs.js
CHANGED
|
@@ -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
|
-
// ---
|
|
34032
|
-
(
|
|
34033
|
-
|
|
34034
|
-
|
|
34035
|
-
|
|
34036
|
-
|
|
34037
|
-
|
|
34038
|
-
|
|
34039
|
-
|
|
34040
|
-
|
|
34041
|
-
}
|
|
34042
|
-
//
|
|
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
|
-
|
|
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 =
|
|
34052
|
-
return s === '1' || s === 'true' || s === 'online' || s === 'yes' || s === 'on' || s === 'ok'
|
|
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 =
|
|
34058
|
-
return s === '0' || s === 'false' || s === 'offline' || s === 'no' || s === 'off'
|
|
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
|
-
|
|
34088
|
-
|
|
34089
|
-
|
|
34090
|
-
|
|
34091
|
-
|
|
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
|
-
/**
|
|
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
|
|
34132
|
-
const np = normalize(
|
|
34133
|
-
//
|
|
34134
|
-
if (
|
|
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
|
-
|
|
34137
|
-
|
|
34138
|
-
|
|
34108
|
+
this.online = true;
|
|
34109
|
+
if (falsy(np) || np === 'offline')
|
|
34110
|
+
this.online = false;
|
|
34139
34111
|
}
|
|
34140
|
-
//
|
|
34141
|
-
if (
|
|
34112
|
+
// tamper
|
|
34113
|
+
if (topic === this.cfg.topics.tamper) {
|
|
34142
34114
|
if (truthy(np) || ['tamper', 'intrusion', 'cover', 'motion', 'magnetic'].includes(np)) {
|
|
34143
|
-
|
|
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
|
-
|
|
34118
|
+
this.tampered = false;
|
|
34148
34119
|
}
|
|
34149
34120
|
}
|
|
34150
|
-
//
|
|
34151
|
-
if (
|
|
34152
|
-
const n = clamp(parseFloat(
|
|
34121
|
+
// battery
|
|
34122
|
+
if (topic === this.cfg.topics.batteryLevel) {
|
|
34123
|
+
const n = clamp(parseFloat(p), 0, 100);
|
|
34153
34124
|
if (isFinite(n))
|
|
34154
|
-
|
|
34125
|
+
this.batteryLevel = n;
|
|
34155
34126
|
}
|
|
34156
|
-
else if (
|
|
34157
|
-
|
|
34158
|
-
|
|
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
|
-
//
|
|
34161
|
-
this.handlePrimary(topic, np,
|
|
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
|
|
34167
|
-
if (
|
|
34168
|
-
|
|
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
|
|
34205
|
-
if (
|
|
34206
|
-
|
|
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
|
|
34241
|
-
if (
|
|
34242
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
34350
|
-
|
|
34351
|
-
|
|
34352
|
-
|
|
34353
|
-
else
|
|
34354
|
-
out.push({ group: gid, key: `sensor.${cfg.id}.topic.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
34457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34386
|
+
// 2) Annuncio
|
|
34387
|
+
if (typeof dmAny.onDevicesChanged === 'function') {
|
|
34485
34388
|
dmAny.onDevicesChanged({ devices: manifests });
|
|
34486
|
-
|
|
34487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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)
|
|
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
|
-
|
|
34571
|
-
if (subs.length)
|
|
34572
|
-
client.subscribe(subs, { qos: 0 }, (err) => {
|
|
34573
|
-
|
|
34574
|
-
|
|
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');
|
|
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
|
|
34582
|
-
const np = normalize(
|
|
34583
|
-
|
|
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
|
-
|
|
34586
|
-
|
|
34587
|
-
|
|
34515
|
+
this.online = true;
|
|
34516
|
+
if (falsy(np) || np === 'offline')
|
|
34517
|
+
this.online = false;
|
|
34588
34518
|
return;
|
|
34589
34519
|
}
|
|
34590
|
-
if (
|
|
34520
|
+
if (topic === tTamper) {
|
|
34591
34521
|
if (truthy(np) || ['tamper', 'intrusion', 'cover'].includes(np)) {
|
|
34592
|
-
|
|
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
|
-
|
|
34525
|
+
this.tampered = false;
|
|
34597
34526
|
}
|
|
34598
34527
|
return;
|
|
34599
34528
|
}
|
|
34600
|
-
if (
|
|
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
|
-
|
|
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 (
|
|
34546
|
+
if (topic === tTarget) {
|
|
34625
34547
|
this.pendingTarget = payloadToMode(payload);
|
|
34626
|
-
this.console.log(
|
|
34548
|
+
this.console.log('Target state reported:', p, '->', this.pendingTarget);
|
|
34627
34549
|
return;
|
|
34628
34550
|
}
|
|
34629
|
-
//
|
|
34630
|
-
|
|
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