@mega-yfue/eufy-sdk 0.6.0-beta.46 → 0.6.0-beta.48
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/core/contracts.d.ts +4 -4
- package/dist/index.js +412 -239
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/battery.d.ts +19 -3
- package/dist/model/capabilities/camera.d.ts +14 -22
- package/dist/model/capabilities/contact.d.ts +129 -55
- package/dist/model/capabilities/index.d.ts +1 -2
- package/dist/model/capabilities/leak.d.ts +51 -13
- package/dist/transport/p2p/command-router.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3471,11 +3471,11 @@ function propertiesOf(members, ctx) {
|
|
|
3471
3471
|
}
|
|
3472
3472
|
var BOOL_VALUES = /* @__PURE__ */ new Set([true, false, 0, 1, "0", "1", "true", "false"]);
|
|
3473
3473
|
function memberWrite(name, m, value, ctx) {
|
|
3474
|
-
if (!inDomain(m, value))
|
|
3475
|
-
throw new Error(rejection(name, m, value));
|
|
3474
|
+
if (!inDomain(m, value, ctx))
|
|
3475
|
+
throw new Error(rejection(name, m, value, ctx));
|
|
3476
3476
|
const cmd = m.write?.(value, ctx);
|
|
3477
3477
|
if (!cmd)
|
|
3478
|
-
throw new Error(rejection(name, m, value));
|
|
3478
|
+
throw new Error(rejection(name, m, value, ctx));
|
|
3479
3479
|
const reflected = m.observation?.reflects(value, ctx);
|
|
3480
3480
|
return m.observation && reflected ? observeCommand(cmd, {
|
|
3481
3481
|
event: m.observation.event,
|
|
@@ -3487,10 +3487,11 @@ function memberWrite(name, m, value, ctx) {
|
|
|
3487
3487
|
timeoutMs: m.observation.timeoutMs
|
|
3488
3488
|
}) : cmd;
|
|
3489
3489
|
}
|
|
3490
|
-
function writeDomain(m) {
|
|
3491
|
-
|
|
3490
|
+
function writeDomain(m, ctx) {
|
|
3491
|
+
const values = resolvedEnum(m, ctx).values;
|
|
3492
|
+
return m.args?.[0]?.values ?? m.decodedValues ?? (values && Object.keys(values).map(Number));
|
|
3492
3493
|
}
|
|
3493
|
-
function inDomain(m, value) {
|
|
3494
|
+
function inDomain(m, value, ctx) {
|
|
3494
3495
|
if (m.type === "bool")
|
|
3495
3496
|
return BOOL_VALUES.has(typeof value === "string" ? value.toLowerCase() : value);
|
|
3496
3497
|
if (typeof value === "number") {
|
|
@@ -3499,11 +3500,11 @@ function inDomain(m, value) {
|
|
|
3499
3500
|
if (m.max !== void 0 && value > m.max)
|
|
3500
3501
|
return false;
|
|
3501
3502
|
}
|
|
3502
|
-
const domain = writeDomain(m)?.filter((v) => typeof v === typeof value);
|
|
3503
|
+
const domain = writeDomain(m, ctx)?.filter((v) => typeof v === typeof value);
|
|
3503
3504
|
return !domain?.length || domain.includes(value);
|
|
3504
3505
|
}
|
|
3505
|
-
function rejection(name, m, value) {
|
|
3506
|
-
const options = writeDomain(m);
|
|
3506
|
+
function rejection(name, m, value, ctx) {
|
|
3507
|
+
const options = writeDomain(m, ctx);
|
|
3507
3508
|
const domain = options?.length ? `one of ${options.join("/")}` : m.min !== void 0 && m.max !== void 0 ? `in ${m.min}..${m.max}` : m.min !== void 0 ? `>= ${m.min}` : void 0;
|
|
3508
3509
|
return `${name}: ${JSON.stringify(value)} is not a valid value` + (domain ? ` (must be ${domain})` : "");
|
|
3509
3510
|
}
|
|
@@ -4817,8 +4818,10 @@ function cellGated(members) {
|
|
|
4817
4818
|
return Object.fromEntries(gated);
|
|
4818
4819
|
}
|
|
4819
4820
|
var notSensor = (ctx) => ctx.codec !== "sensor";
|
|
4820
|
-
function notOnSensor(members) {
|
|
4821
|
+
function notOnSensor(members, sensorOnly) {
|
|
4821
4822
|
const gated = Object.entries(members).map(([name, m]) => {
|
|
4823
|
+
if (sensorOnly.includes(name))
|
|
4824
|
+
return [name, m];
|
|
4822
4825
|
const own = m.available;
|
|
4823
4826
|
const available = own ? (ctx) => notSensor(ctx) && own(ctx) : notSensor;
|
|
4824
4827
|
return [name, { ...m, available }];
|
|
@@ -5054,12 +5057,26 @@ var CAMERA_BATTERY_MEMBERS = cellGated({
|
|
|
5054
5057
|
provenance: "apk",
|
|
5055
5058
|
unexposed: true,
|
|
5056
5059
|
description: "Raw GET_CAMERA_INFO value (1103), meaning unevidenced \u2014 read live as the constant 5 on a T8170 at 92% and a T8171 at 27%, so it is NOT a low-battery flag."
|
|
5060
|
+
},
|
|
5061
|
+
/**
|
|
5062
|
+
* A sensor's battery is low. The app reads the entry sensor's 1552 and the motion sensor's 1601 into the
|
|
5063
|
+
* same field (`sensor_is_low_power`, `asBoolean`), so one member reads whichever the sensor reports.
|
|
5064
|
+
* Present on the `sensor` codec only: a camera or a lock reports a level instead.
|
|
5065
|
+
*/
|
|
5066
|
+
batteryLow: {
|
|
5067
|
+
param: 1552,
|
|
5068
|
+
readAliases: [{ paramType: 1601 }],
|
|
5069
|
+
type: "bool",
|
|
5070
|
+
kind: "boolean",
|
|
5071
|
+
provenance: "verified",
|
|
5072
|
+
available: (ctx) => !notSensor(ctx),
|
|
5073
|
+
description: "Sensor battery low (1552 entry sensor, 1601 motion sensor; sensor_is_low_power)."
|
|
5057
5074
|
}
|
|
5058
5075
|
});
|
|
5059
|
-
var BATTERY_MEMBERS = notOnSensor(CAMERA_BATTERY_MEMBERS);
|
|
5076
|
+
var BATTERY_MEMBERS = notOnSensor(CAMERA_BATTERY_MEMBERS, ["batteryLow"]);
|
|
5060
5077
|
var BATTERY = {
|
|
5061
5078
|
capability: "battery",
|
|
5062
|
-
description: "Battery level, charging, health, temperature
|
|
5079
|
+
description: "Battery level, charging, health, temperature and solar input; a sensor's low-battery flag.",
|
|
5063
5080
|
members: BATTERY_MEMBERS,
|
|
5064
5081
|
properties: propertiesOf(BATTERY_MEMBERS),
|
|
5065
5082
|
/** A reported battery-level param (1101) is the verified proof the device is battery-powered. */
|
|
@@ -5104,161 +5121,6 @@ function resolveWorkingModeValue(model, name) {
|
|
|
5104
5121
|
return hit ? Number(hit[0]) : void 0;
|
|
5105
5122
|
}
|
|
5106
5123
|
|
|
5107
|
-
// dist/model/capabilities/contact.js
|
|
5108
|
-
var CONTACT_CMD = {
|
|
5109
|
-
/** Alarm sound/tone selection (app `APP_CMD_ALARM_SOUND_TYPE`). See {@link EntryAlarmTone}. */
|
|
5110
|
-
ALARM_SOUND_TYPE: 1507,
|
|
5111
|
-
/** Alarm volume (app `APP_CMD_ALARM_VOLUME_VALUE`). A device level 1-26, not a percentage. */
|
|
5112
|
-
ALARM_VOLUME: 1508
|
|
5113
|
-
};
|
|
5114
|
-
var EntryAlarmTone = {
|
|
5115
|
-
/** Silent — no alarm tone. */
|
|
5116
|
-
None: 0,
|
|
5117
|
-
Water: 1,
|
|
5118
|
-
Classic: 2,
|
|
5119
|
-
Light: 3,
|
|
5120
|
-
Ding: 4
|
|
5121
|
-
};
|
|
5122
|
-
var ALARM_VOLUME_MIN = 1;
|
|
5123
|
-
var ALARM_VOLUME_MAX = 26;
|
|
5124
|
-
var SENSOR_STATUS_CMD = 1829;
|
|
5125
|
-
var CONTACT_PARAM = 1550;
|
|
5126
|
-
function statusParam(signal, paramType) {
|
|
5127
|
-
if (signal.source !== "p2p-frame" || signal.json?.cmd !== SENSOR_STATUS_CMD)
|
|
5128
|
-
return void 0;
|
|
5129
|
-
const params = signal.json.payload?.params;
|
|
5130
|
-
if (!Array.isArray(params))
|
|
5131
|
-
return void 0;
|
|
5132
|
-
const hit = params.find((p) => p?.param_type === paramType);
|
|
5133
|
-
return hit?.param_value == null ? void 0 : String(hit.param_value);
|
|
5134
|
-
}
|
|
5135
|
-
function contactOpen(raw) {
|
|
5136
|
-
return raw == null || raw === "" ? {} : { open: String(raw) === "1" };
|
|
5137
|
-
}
|
|
5138
|
-
var CONTACT_MEMBERS = {
|
|
5139
|
-
/**
|
|
5140
|
-
* The headline read: `true` when the door or window is open. Polarity is the V6 app's own
|
|
5141
|
-
* `"1".equals(e)` — see `contactOpen`, which normalises every inbound source through one
|
|
5142
|
-
* predicate so the poll, the push and the station notify cannot disagree about what open means.
|
|
5143
|
-
* Reporting 1550 is also this capability's detection evidence, so a device with this getter is
|
|
5144
|
-
* confirmed to be a contact sensor.
|
|
5145
|
-
*/
|
|
5146
|
-
open: {
|
|
5147
|
-
param: CONTACT_PARAM,
|
|
5148
|
-
property: "contact",
|
|
5149
|
-
type: "bool",
|
|
5150
|
-
kind: "boolean",
|
|
5151
|
-
provenance: "verified",
|
|
5152
|
-
description: "Contact state, true=open (verified: param 1550 entry-sensor contact)."
|
|
5153
|
-
},
|
|
5154
|
-
/**
|
|
5155
|
-
* Rejected, not clamped, outside the tone enum: a fire-and-forget write of a bogus tone would look like
|
|
5156
|
-
* it worked. The wire is a direct-binary scalar `[ch][value][acct]` at signCode 8 — the app's own
|
|
5157
|
-
* captured frame.
|
|
5158
|
-
*/
|
|
5159
|
-
alarmSoundType: {
|
|
5160
|
-
param: CONTACT_CMD.ALARM_SOUND_TYPE,
|
|
5161
|
-
type: "enum",
|
|
5162
|
-
kind: "enum",
|
|
5163
|
-
enumValues: enumLabels(EntryAlarmTone),
|
|
5164
|
-
provenance: "verified",
|
|
5165
|
-
requires: [CONTACT_CMD.ALARM_SOUND_TYPE],
|
|
5166
|
-
args: [{ name: "tone", kind: "enum", description: "Silent (0) or one of the four chimes." }],
|
|
5167
|
-
description: "Entry-sensor alarm tone (1507): 0=None,1=Water,2=Classic,3=Light,4=Ding. Write verified live on a T90E0 (direct-binary [ch][value][acct]). See CONTACT_CMD.ALARM_SOUND_TYPE.",
|
|
5168
|
-
write: (v, ctx) => {
|
|
5169
|
-
const tone = coerceEnumValue(EntryAlarmTone, v);
|
|
5170
|
-
return tone === void 0 ? void 0 : setScalar(CONTACT_CMD.ALARM_SOUND_TYPE, tone, ctx, "auto");
|
|
5171
|
-
}
|
|
5172
|
-
},
|
|
5173
|
-
/**
|
|
5174
|
-
* `1350` SET_PAYLOAD, `mChannel` = the device channel, `mValue3` 0, payload carrying the channel and a
|
|
5175
|
-
* transaction stamp — the byte-shape of the app's own captured frame. Out of range is refused rather
|
|
5176
|
-
* than clamped: the app's slider has no values outside it, so one is a caller error, not a nudge.
|
|
5177
|
-
*
|
|
5178
|
-
* Level-2 only, though the `mValue3` 0 would allow `"auto"`: an entry sensor's session IS its
|
|
5179
|
-
* HomeBase's, which always holds a key — the second of `setPayload`'s two conditions, not an oversight
|
|
5180
|
-
* of the first.
|
|
5181
|
-
*/
|
|
5182
|
-
alarmVolume: {
|
|
5183
|
-
param: CONTACT_CMD.ALARM_VOLUME,
|
|
5184
|
-
type: "number",
|
|
5185
|
-
kind: "scalar",
|
|
5186
|
-
provenance: "verified",
|
|
5187
|
-
requires: [CONTACT_CMD.ALARM_VOLUME],
|
|
5188
|
-
min: ALARM_VOLUME_MIN,
|
|
5189
|
-
max: ALARM_VOLUME_MAX,
|
|
5190
|
-
args: [
|
|
5191
|
-
{
|
|
5192
|
-
name: "level",
|
|
5193
|
-
kind: "scalar",
|
|
5194
|
-
min: ALARM_VOLUME_MIN,
|
|
5195
|
-
max: ALARM_VOLUME_MAX,
|
|
5196
|
-
description: "A device level, not a percentage."
|
|
5197
|
-
}
|
|
5198
|
-
],
|
|
5199
|
-
description: "Entry-sensor alarm volume (1508), a device level 1-26 (NOT a percentage). Write verified live on a T90E0 (1350 SET_PAYLOAD {channel,volume,transaction}). See CONTACT_CMD.ALARM_VOLUME.",
|
|
5200
|
-
write: (v, ctx) => {
|
|
5201
|
-
const level = Number(v);
|
|
5202
|
-
return Number.isInteger(level) && level >= ALARM_VOLUME_MIN && level <= ALARM_VOLUME_MAX ? setPayload(CONTACT_CMD.ALARM_VOLUME, { channel: ctx.channel, volume: level, transaction: String(Date.now()) }, ctx, 0) : void 0;
|
|
5203
|
-
}
|
|
5204
|
-
}
|
|
5205
|
-
};
|
|
5206
|
-
var CONTACT = {
|
|
5207
|
-
capability: "contact",
|
|
5208
|
-
description: "Entry (door/window) sensor: contact state and the built-in alarm tone/volume.",
|
|
5209
|
-
members: CONTACT_MEMBERS,
|
|
5210
|
-
properties: propertiesOf(CONTACT_MEMBERS),
|
|
5211
|
-
/** A reported entry-sensor contact param (1550) is the verified proof of a contact sensor. */
|
|
5212
|
-
detection: { evidenceParams: [1550] },
|
|
5213
|
-
/**
|
|
5214
|
-
* Inbound `contactState`: the contact param (1550) changing on a cloud poll (open/closed), AND the FCM
|
|
5215
|
-
* door-sensor push (`CusPushEvent.DOOR_SENSOR` = 3) — verified live on a T8900 (both fire).
|
|
5216
|
-
*
|
|
5217
|
-
* Both sources carry the state under a different key, so each normalises to `open`: the poll's `to` is
|
|
5218
|
-
* the 1550 param value, and the push's is the wire's single-letter `e`. Polarity for the push is the
|
|
5219
|
-
* V6 app's own `CusPushMode.isSensorOpen()`, which is exactly `"1".equals(e)` — anything else,
|
|
5220
|
-
* including absent, is closed — read under its `a == PUSH_DOOR_SENSOR_EVT` branch to choose the
|
|
5221
|
-
* "sensor is opened" vs "sensor is closed" notification string.
|
|
5222
|
-
*/
|
|
5223
|
-
events: [
|
|
5224
|
-
{
|
|
5225
|
-
source: "poll",
|
|
5226
|
-
match: 1550,
|
|
5227
|
-
emit: "contactState",
|
|
5228
|
-
derive: (s) => contactOpen(s.source === "poll" ? s.to : void 0)
|
|
5229
|
-
},
|
|
5230
|
-
{
|
|
5231
|
-
source: "push",
|
|
5232
|
-
match: CusPushEvent.DOOR_SENSOR,
|
|
5233
|
-
emit: "contactState",
|
|
5234
|
-
derive: (s) => contactOpen(s.source === "push" ? s.payload?.["e"] : void 0)
|
|
5235
|
-
}
|
|
5236
|
-
],
|
|
5237
|
-
/**
|
|
5238
|
-
* The contact is a settled state reported by three transports, so one door movement would otherwise
|
|
5239
|
-
* be announced up to three times — the station notify, then the same value as an FCM push ~2 s
|
|
5240
|
-
* later, then again on the next cloud poll.
|
|
5241
|
-
*/
|
|
5242
|
-
stateEvents: [{ event: "contactState", field: "open" }],
|
|
5243
|
-
/**
|
|
5244
|
-
* The station's own notify — the fastest contact signal there is, and the only one that needs no
|
|
5245
|
-
* cloud at all (see {@link SENSOR_STATUS_CMD} for the measured lead over the push).
|
|
5246
|
-
*
|
|
5247
|
-
* A declarative `events` row can't express this: those match a push event id or a poll param id,
|
|
5248
|
-
* while the state here is nested inside a frame's `params` array under an inner command id. Hence
|
|
5249
|
-
* the decode hook, which is the documented escape hatch for exactly that.
|
|
5250
|
-
*
|
|
5251
|
-
* Emits nothing when the notify carries no contact param, so a status frame reporting only battery
|
|
5252
|
-
* or signal strength doesn't fabricate a contact event.
|
|
5253
|
-
*/
|
|
5254
|
-
decodeEvent(signal) {
|
|
5255
|
-
const raw = statusParam(signal, CONTACT_PARAM);
|
|
5256
|
-
if (raw === void 0)
|
|
5257
|
-
return null;
|
|
5258
|
-
return { event: "contactState", payload: contactOpen(raw) };
|
|
5259
|
-
}
|
|
5260
|
-
};
|
|
5261
|
-
|
|
5262
5124
|
// dist/model/device-family.js
|
|
5263
5125
|
var INDOOR_CAMERA_TYPES = /* @__PURE__ */ new Set([
|
|
5264
5126
|
DeviceType.INDOOR_CAMERA,
|
|
@@ -5902,7 +5764,7 @@ var CAMERA_CMD = {
|
|
|
5902
5764
|
* (1013). (Some models omit full colour; enum in {@link NightVision}.)
|
|
5903
5765
|
*/
|
|
5904
5766
|
NIGHT_VISION_TYPE: 1277,
|
|
5905
|
-
/**
|
|
5767
|
+
/** Night vision on a device reporting 2002: direct level-1 [channel][value]. */
|
|
5906
5768
|
IC_NIGHT_VISION_TYPE: 1013,
|
|
5907
5769
|
/**
|
|
5908
5770
|
* Push-notification STYLE — how much a detection push carries: text alone, a thumbnail, or text
|
|
@@ -6013,10 +5875,12 @@ var SoundDetectionType = {
|
|
|
6013
5875
|
var NightVision = {
|
|
6014
5876
|
/** Off — never use infrared. */
|
|
6015
5877
|
Off: 0,
|
|
6016
|
-
/** Infrared / "B&W Auto" — black-and-white night vision. */
|
|
5878
|
+
/** Infrared / "B&W Auto" — black-and-white night vision, switched on when it gets dark. */
|
|
6017
5879
|
Infrared: 1,
|
|
6018
5880
|
/** Full colour night vision (models with a spotlight / starlight sensor). */
|
|
6019
|
-
FullColor: 2
|
|
5881
|
+
FullColor: 2,
|
|
5882
|
+
/** Infrared held on — black-and-white night vision, not switched by the light level. */
|
|
5883
|
+
InfraredOn: 3
|
|
6020
5884
|
};
|
|
6021
5885
|
var RecordingQuality = {
|
|
6022
5886
|
HD720: "HD (720P)",
|
|
@@ -6094,8 +5958,8 @@ function powerValue(on, ctx) {
|
|
|
6094
5958
|
function isT8410HomeBase3(ctx) {
|
|
6095
5959
|
return ctx.model === "T8410" && ctx.stationSerial?.startsWith("T8030") === true;
|
|
6096
5960
|
}
|
|
6097
|
-
function
|
|
6098
|
-
return ctx.
|
|
5961
|
+
function readsNightVisionOn2002(ctx) {
|
|
5962
|
+
return ctx.paramIds?.has(2002) === true && !ctx.paramIds.has(CAMERA_CMD.NIGHT_VISION_TYPE);
|
|
6099
5963
|
}
|
|
6100
5964
|
function usesHomeBasePowerPayload(ctx) {
|
|
6101
5965
|
if (!isT8410HomeBase3(ctx) || !ctx.firmwareVersion || !/^\d+(?:\.\d+){2,3}$/.test(ctx.firmwareVersion))
|
|
@@ -6310,36 +6174,27 @@ var CAMERA_MEMBERS = {
|
|
|
6310
6174
|
return style == null ? void 0 : setJson(CAMERA_CMD.PUSH_NOTIFY_TYPE, { value: style, transaction: String(Date.now()) }, ctx);
|
|
6311
6175
|
}
|
|
6312
6176
|
},
|
|
6313
|
-
/**
|
|
6314
|
-
* Three modes, and `FullColor` is the conditional one: a model without a spotlight or starlight
|
|
6315
|
-
* sensor omits it, and the wire accepts the value regardless — the device's own reported set is the
|
|
6316
|
-
* only statement of which of the three it has. `coerceEnumValue` rejects a value outside
|
|
6317
|
-
* {@link NightVision} rather than coercing it. The payload key on the wire is `night_sion`.
|
|
6318
|
-
*
|
|
6319
|
-
* 1277 carries more than these three on some models: the V6 app's per-model mappings put modes on
|
|
6320
|
-
* 3, 5, 6 and 8 as well, and which model takes which mapping is not in the app bundle. A reported value
|
|
6321
|
-
* outside the published options reads as `undefined` rather than as an unnamed number.
|
|
6322
|
-
*/
|
|
6177
|
+
/** Modes come from the thing description (`thing`); unlisted values read as `undefined`. */
|
|
6323
6178
|
nightVision: {
|
|
6324
6179
|
param: CAMERA_CMD.NIGHT_VISION_TYPE,
|
|
6325
6180
|
type: "enum",
|
|
6326
6181
|
kind: "enum",
|
|
6327
|
-
enumValues: { 0: "Off", 1: "Infrared", 2: "Full Color" },
|
|
6328
|
-
|
|
6182
|
+
enumValues: { 0: "Off", 1: "Infrared", 2: "Full Color", 3: "Infrared On" },
|
|
6183
|
+
thing: ["nightvision_type_new", "nightvision_type"],
|
|
6329
6184
|
provenance: "verified",
|
|
6330
6185
|
available: isCameraCodec,
|
|
6331
|
-
readAvailable: (ctx) => !
|
|
6332
|
-
readAliases: [{ paramType: 2002, available:
|
|
6333
|
-
|
|
6186
|
+
readAvailable: (ctx) => !readsNightVisionOn2002(ctx),
|
|
6187
|
+
readAliases: [{ paramType: 2002, available: readsNightVisionOn2002 }],
|
|
6188
|
+
coerce: (raw) => raw === "true" ? 1 : raw === "false" ? 0 : Number(raw),
|
|
6189
|
+
description: "Night-vision mode: 0 = off, 1 = infrared by light level, 2 = full colour, 3 = infrared held on. Written on 1277 (1350 SET_PAYLOAD, night_sion), or on 1013 as a bare [channel][value] when the device reports 2002.",
|
|
6334
6190
|
write: (v, ctx) => {
|
|
6335
6191
|
const nv = coerceEnumValue(NightVision, v);
|
|
6336
6192
|
if (nv == null)
|
|
6337
6193
|
return void 0;
|
|
6338
|
-
if (
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
}
|
|
6194
|
+
if (readsNightVisionOn2002(ctx))
|
|
6195
|
+
return setScalar(CAMERA_CMD.IC_NIGHT_VISION_TYPE, nv, ctx, "int-pair");
|
|
6196
|
+
if (nv === NightVision.InfraredOn)
|
|
6197
|
+
throw new Error("nightVision: Infrared On has no verified write on this model");
|
|
6343
6198
|
return setPayload(CAMERA_CMD.NIGHT_VISION_TYPE, { channel: ctx.channel, night_sion: nv }, ctx, 0, 0, "auto");
|
|
6344
6199
|
}
|
|
6345
6200
|
},
|
|
@@ -7573,29 +7428,330 @@ var PTZ = {
|
|
|
7573
7428
|
}
|
|
7574
7429
|
};
|
|
7575
7430
|
|
|
7431
|
+
// dist/model/capabilities/contact.js
|
|
7432
|
+
var CONTACT_CMD = {
|
|
7433
|
+
/** Contact state (`APP_CMD_ENTRY_SENSOR_STATUS`, identifier `sensor_door_status`). */
|
|
7434
|
+
STATUS: 1550,
|
|
7435
|
+
/** Removed from its mount (`APP_CMD_DEV_DISASSEMBLE`, identifier `sensor_disassemble_status`). */
|
|
7436
|
+
TAMPER: 61015,
|
|
7437
|
+
/** Bypassed by the alarm (`APP_CMD_DEV_ENTRY_BYPASS`, identifier `sensor_door_bypass_status`). */
|
|
7438
|
+
BYPASS: 61016,
|
|
7439
|
+
/** Left-open alert (`APP_CMD_SENSOR_OPEN_STATUS_ALERT`, identifier `sensor_door_open_status_alert_config`). */
|
|
7440
|
+
OPEN_ALERT: 1290,
|
|
7441
|
+
/** Daily state check (`APP_CMD_SENSOR_DOOR_DAILY_CHECK`, identifier `sensor_door_daily_check_status_config`). */
|
|
7442
|
+
DAILY_CHECK: 1291,
|
|
7443
|
+
/** Alarm tone (`APP_CMD_ALARM_SOUND_TYPE`, identifier `device_alarm_sound_type`). */
|
|
7444
|
+
ALARM_SOUND_TYPE: 1507,
|
|
7445
|
+
/** Alarm volume (`APP_CMD_ALARM_VOLUME_VALUE`, identifier `alarm_volume_value`). */
|
|
7446
|
+
ALARM_VOLUME: 1508
|
|
7447
|
+
};
|
|
7448
|
+
var SENSOR_STATUS_CMD = 1829;
|
|
7449
|
+
var OPEN_ALERT_MINUTES = [5, 10, 15, 30, 60];
|
|
7450
|
+
var ALARM_TONES = { None: 0, Water: 1, Classics: 2, Light: 3, Ding: 4 };
|
|
7451
|
+
var ALARM_VOLUME_MIN = 1;
|
|
7452
|
+
var ALARM_VOLUME_MAX = 26;
|
|
7453
|
+
var described = (identifier) => (ctx) => !ctx.things || identifier in ctx.things.properties;
|
|
7454
|
+
function statusParam(signal, paramType) {
|
|
7455
|
+
if (signal.source !== "p2p-frame" || signal.json?.cmd !== SENSOR_STATUS_CMD)
|
|
7456
|
+
return void 0;
|
|
7457
|
+
const params = signal.json.payload?.params;
|
|
7458
|
+
if (!Array.isArray(params))
|
|
7459
|
+
return void 0;
|
|
7460
|
+
const hit = params.find((p) => p?.param_type === paramType);
|
|
7461
|
+
return hit?.param_value == null ? void 0 : String(hit.param_value);
|
|
7462
|
+
}
|
|
7463
|
+
function contactOpen(raw) {
|
|
7464
|
+
return raw == null || raw === "" ? {} : { open: String(raw) === "1" };
|
|
7465
|
+
}
|
|
7466
|
+
function config(raw) {
|
|
7467
|
+
if (typeof raw !== "string" || !raw)
|
|
7468
|
+
return void 0;
|
|
7469
|
+
try {
|
|
7470
|
+
const parsed = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
|
|
7471
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
7472
|
+
} catch {
|
|
7473
|
+
return void 0;
|
|
7474
|
+
}
|
|
7475
|
+
}
|
|
7476
|
+
var hhmm = (minutes) => `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
|
|
7477
|
+
function minutesOf(time) {
|
|
7478
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(time);
|
|
7479
|
+
if (!m)
|
|
7480
|
+
return void 0;
|
|
7481
|
+
const [h, min] = [Number(m[1]), Number(m[2])];
|
|
7482
|
+
return h < 24 && min < 60 ? h * 60 + min : void 0;
|
|
7483
|
+
}
|
|
7484
|
+
var CONTACT_MEMBERS = {
|
|
7485
|
+
/**
|
|
7486
|
+
* The headline read: `true` when the door or window is open. Every inbound source normalises through
|
|
7487
|
+
* `contactOpen`, so the poll, the push and the station notify cannot disagree about what open means.
|
|
7488
|
+
* Reporting 1550 is also this capability's detection evidence.
|
|
7489
|
+
*/
|
|
7490
|
+
open: {
|
|
7491
|
+
param: CONTACT_CMD.STATUS,
|
|
7492
|
+
property: "contact",
|
|
7493
|
+
type: "bool",
|
|
7494
|
+
kind: "boolean",
|
|
7495
|
+
provenance: "verified",
|
|
7496
|
+
description: "Contact state, true=open (1550, sensor_door_status)."
|
|
7497
|
+
},
|
|
7498
|
+
/** The sensor was removed from its mount. Reported by a T8900; the meaning of a set value is the product bundle's. */
|
|
7499
|
+
tampered: {
|
|
7500
|
+
param: CONTACT_CMD.TAMPER,
|
|
7501
|
+
type: "bool",
|
|
7502
|
+
kind: "boolean",
|
|
7503
|
+
provenance: "apk",
|
|
7504
|
+
description: "Removed from its mount (61015, sensor_disassemble_status)."
|
|
7505
|
+
},
|
|
7506
|
+
/** The alarm currently skips this sensor. Reported by a T8900; the meaning of a set value is the product bundle's. */
|
|
7507
|
+
bypassed: {
|
|
7508
|
+
param: CONTACT_CMD.BYPASS,
|
|
7509
|
+
type: "bool",
|
|
7510
|
+
kind: "boolean",
|
|
7511
|
+
provenance: "apk",
|
|
7512
|
+
description: "Bypassed by the alarm (61016, sensor_door_bypass_status)."
|
|
7513
|
+
},
|
|
7514
|
+
/**
|
|
7515
|
+
* The left-open alert config, whole as the sensor stores it (base64 JSON). Its two fields are the
|
|
7516
|
+
* members below; both are set together by `setOpenAlert`, since one frame carries both.
|
|
7517
|
+
*/
|
|
7518
|
+
openAlertConfig: {
|
|
7519
|
+
param: CONTACT_CMD.OPEN_ALERT,
|
|
7520
|
+
type: "string",
|
|
7521
|
+
kind: "text",
|
|
7522
|
+
provenance: "verified",
|
|
7523
|
+
unexposed: true,
|
|
7524
|
+
writtenElsewhere: true,
|
|
7525
|
+
description: "Left-open alert config, base64 JSON {door_enable,door_time_point} (1290)."
|
|
7526
|
+
},
|
|
7527
|
+
/** Whether a notification fires when the door stays open longer than {@link CONTACT_MEMBERS.openAlertMinutes}. */
|
|
7528
|
+
openAlert: {
|
|
7529
|
+
readsFrom: "openAlertConfig",
|
|
7530
|
+
type: "bool",
|
|
7531
|
+
kind: "boolean",
|
|
7532
|
+
provenance: "verified",
|
|
7533
|
+
decode: (raw) => {
|
|
7534
|
+
const c = config(raw);
|
|
7535
|
+
return c === void 0 ? void 0 : asBool(c.door_enable);
|
|
7536
|
+
},
|
|
7537
|
+
description: "Left-open alert on (1290 door_enable). Set with setOpenAlert."
|
|
7538
|
+
},
|
|
7539
|
+
/** How long the door may stay open before the alert fires: one of 5, 10, 15, 30 or 60 minutes. */
|
|
7540
|
+
openAlertMinutes: {
|
|
7541
|
+
readsFrom: "openAlertConfig",
|
|
7542
|
+
type: "number",
|
|
7543
|
+
kind: "scalar",
|
|
7544
|
+
provenance: "verified",
|
|
7545
|
+
decode: (raw) => {
|
|
7546
|
+
const n = Number(config(raw)?.door_time_point);
|
|
7547
|
+
return Number.isInteger(n) ? n : void 0;
|
|
7548
|
+
},
|
|
7549
|
+
description: "Left-open alert delay, minutes: 5, 10, 15, 30 or 60 (1290 door_time_point)."
|
|
7550
|
+
},
|
|
7551
|
+
/** The daily state check config, whole (base64 JSON); its fields are the members below. */
|
|
7552
|
+
dailyCheckConfig: {
|
|
7553
|
+
param: CONTACT_CMD.DAILY_CHECK,
|
|
7554
|
+
type: "string",
|
|
7555
|
+
kind: "text",
|
|
7556
|
+
provenance: "verified",
|
|
7557
|
+
unexposed: true,
|
|
7558
|
+
writtenElsewhere: true,
|
|
7559
|
+
description: "Daily check config, base64 JSON {door_enable,door_time_interval} (1291)."
|
|
7560
|
+
},
|
|
7561
|
+
/** Whether the sensor's state is notified at each of {@link CONTACT_MEMBERS.dailyCheckTimes}. */
|
|
7562
|
+
dailyCheck: {
|
|
7563
|
+
readsFrom: "dailyCheckConfig",
|
|
7564
|
+
type: "bool",
|
|
7565
|
+
kind: "boolean",
|
|
7566
|
+
provenance: "verified",
|
|
7567
|
+
decode: (raw) => {
|
|
7568
|
+
const c = config(raw);
|
|
7569
|
+
return c === void 0 ? void 0 : asBool(c.door_enable);
|
|
7570
|
+
},
|
|
7571
|
+
description: "Daily state check on (1291 door_enable). Set with setDailyCheck."
|
|
7572
|
+
},
|
|
7573
|
+
/** The times of day the state is notified, `HH:MM` comma-separated in the order the sensor holds them. */
|
|
7574
|
+
dailyCheckTimes: {
|
|
7575
|
+
readsFrom: "dailyCheckConfig",
|
|
7576
|
+
type: "string",
|
|
7577
|
+
kind: "text",
|
|
7578
|
+
provenance: "verified",
|
|
7579
|
+
decode: (raw) => {
|
|
7580
|
+
const times = config(raw)?.door_time_interval;
|
|
7581
|
+
return Array.isArray(times) ? times.map((t) => hhmm(Number(t) || 0)).join(",") : void 0;
|
|
7582
|
+
},
|
|
7583
|
+
description: "Daily check times, HH:MM comma-separated (1291 door_time_interval, minutes since midnight)."
|
|
7584
|
+
},
|
|
7585
|
+
/**
|
|
7586
|
+
* Turn the left-open alert on or off, with its delay. One frame carries both: `1350` SET_PAYLOAD,
|
|
7587
|
+
* sub-command 1290, `{door_enable, door_time_point}` — the product's settings bundle's own frame.
|
|
7588
|
+
* Verified live on a T8900: param 1290 read back the written value and the app showed it.
|
|
7589
|
+
*/
|
|
7590
|
+
setOpenAlert: method(({ ctx, sink }) => async (on, minutes) => {
|
|
7591
|
+
if (!OPEN_ALERT_MINUTES.includes(minutes)) {
|
|
7592
|
+
throw new Error(`openAlertMinutes: ${String(minutes)} is not one of ${OPEN_ALERT_MINUTES.join(", ")}`);
|
|
7593
|
+
}
|
|
7594
|
+
await sink.dispatch(setPayload(CONTACT_CMD.OPEN_ALERT, { door_enable: asBool(on) ? 1 : 0, door_time_point: minutes }, ctx, 0));
|
|
7595
|
+
}, "Set the left-open alert and its delay (5, 10, 15, 30 or 60 minutes), which share one wire."),
|
|
7596
|
+
/**
|
|
7597
|
+
* Turn the daily state check on or off, with its times of day. One frame carries both: `1350`
|
|
7598
|
+
* SET_PAYLOAD, sub-command 1291, `{door_enable, door_time_interval}` with each time as minutes since
|
|
7599
|
+
* midnight — the product's settings bundle's own frame.
|
|
7600
|
+
* Verified live on a T8900: param 1291 read back the written times and the app showed them.
|
|
7601
|
+
*/
|
|
7602
|
+
setDailyCheck: method(({ ctx, sink }) => async (on, times) => {
|
|
7603
|
+
const minutes = times.map(minutesOf);
|
|
7604
|
+
if (minutes.some((m) => m === void 0)) {
|
|
7605
|
+
throw new Error(`dailyCheckTimes: ${times.join(",")} must all be HH:MM`);
|
|
7606
|
+
}
|
|
7607
|
+
await sink.dispatch(setPayload(CONTACT_CMD.DAILY_CHECK, { door_enable: asBool(on) ? 1 : 0, door_time_interval: minutes }, ctx, 0));
|
|
7608
|
+
}, "Set the daily state check and its times of day (HH:MM), which share one wire."),
|
|
7609
|
+
/**
|
|
7610
|
+
* The built-in alarm tone, offered where the product's description lists it: a sensor may report the
|
|
7611
|
+
* param while the app offers no such setting on it. Rejected, not clamped, outside the listed tones.
|
|
7612
|
+
* Write: a direct-binary scalar `[ch][value][acct]` at signCode 8, captured live on a T90E0.
|
|
7613
|
+
*/
|
|
7614
|
+
alarmSoundType: {
|
|
7615
|
+
param: CONTACT_CMD.ALARM_SOUND_TYPE,
|
|
7616
|
+
type: "enum",
|
|
7617
|
+
kind: "enum",
|
|
7618
|
+
enumValues: enumLabels(ALARM_TONES),
|
|
7619
|
+
thing: ["device_alarm_sound_type"],
|
|
7620
|
+
provenance: "verified",
|
|
7621
|
+
available: described("device_alarm_sound_type"),
|
|
7622
|
+
args: [{ name: "tone", kind: "enum", description: "Silent (0) or one of the four tones." }],
|
|
7623
|
+
description: "Alarm tone (1507, device_alarm_sound_type): 0=None,1=Water,2=Classics,3=Light,4=Ding.",
|
|
7624
|
+
write: (v, ctx) => {
|
|
7625
|
+
const tone = coerceEnumValue(ALARM_TONES, v);
|
|
7626
|
+
return tone === void 0 ? void 0 : setScalar(CONTACT_CMD.ALARM_SOUND_TYPE, tone, ctx, "auto");
|
|
7627
|
+
}
|
|
7628
|
+
},
|
|
7629
|
+
/**
|
|
7630
|
+
* The built-in alarm volume, a device level 1-26, offered where the product's description lists it.
|
|
7631
|
+
* Write: `1350` SET_PAYLOAD with `mChannel` the device channel, `mValue3` 0, payload `{channel, volume,
|
|
7632
|
+
* transaction}` — the app's own captured frame on a T90E0. Out of range is refused, not clamped.
|
|
7633
|
+
*/
|
|
7634
|
+
alarmVolume: {
|
|
7635
|
+
param: CONTACT_CMD.ALARM_VOLUME,
|
|
7636
|
+
type: "number",
|
|
7637
|
+
kind: "scalar",
|
|
7638
|
+
provenance: "verified",
|
|
7639
|
+
available: described("alarm_volume_value"),
|
|
7640
|
+
min: ALARM_VOLUME_MIN,
|
|
7641
|
+
max: ALARM_VOLUME_MAX,
|
|
7642
|
+
args: [
|
|
7643
|
+
{ name: "level", kind: "scalar", min: ALARM_VOLUME_MIN, max: ALARM_VOLUME_MAX, description: "A device level." }
|
|
7644
|
+
],
|
|
7645
|
+
description: "Alarm volume (1508, alarm_volume_value), a device level 1-26, not a percentage.",
|
|
7646
|
+
write: (v, ctx) => {
|
|
7647
|
+
const level = Number(v);
|
|
7648
|
+
return Number.isInteger(level) && level >= ALARM_VOLUME_MIN && level <= ALARM_VOLUME_MAX ? setPayload(CONTACT_CMD.ALARM_VOLUME, { channel: ctx.channel, volume: level, transaction: String(Date.now()) }, ctx, 0) : void 0;
|
|
7649
|
+
}
|
|
7650
|
+
}
|
|
7651
|
+
};
|
|
7652
|
+
var CONTACT = {
|
|
7653
|
+
capability: "contact",
|
|
7654
|
+
description: "Entry (door/window) sensor: contact state, tamper and bypass, the left-open alert and the daily state check, and the built-in alarm where the product has one.",
|
|
7655
|
+
members: CONTACT_MEMBERS,
|
|
7656
|
+
properties: propertiesOf(CONTACT_MEMBERS),
|
|
7657
|
+
/** A reported entry-sensor contact param (1550) is the verified proof of a contact sensor. */
|
|
7658
|
+
detection: { evidenceParams: [CONTACT_CMD.STATUS] },
|
|
7659
|
+
/**
|
|
7660
|
+
* Inbound `contactState`: the contact param changing on a cloud poll, and the FCM door-sensor push
|
|
7661
|
+
* (`CusPushEvent.DOOR_SENSOR`), whose state is the single-letter `e` — both verified live on a T8900.
|
|
7662
|
+
*/
|
|
7663
|
+
events: [
|
|
7664
|
+
{
|
|
7665
|
+
source: "poll",
|
|
7666
|
+
match: CONTACT_CMD.STATUS,
|
|
7667
|
+
emit: "contactState",
|
|
7668
|
+
derive: (s) => contactOpen(s.source === "poll" ? s.to : void 0)
|
|
7669
|
+
},
|
|
7670
|
+
{
|
|
7671
|
+
source: "push",
|
|
7672
|
+
match: CusPushEvent.DOOR_SENSOR,
|
|
7673
|
+
emit: "contactState",
|
|
7674
|
+
derive: (s) => contactOpen(s.source === "push" ? s.payload?.["e"] : void 0)
|
|
7675
|
+
}
|
|
7676
|
+
],
|
|
7677
|
+
/**
|
|
7678
|
+
* One door movement reaches the SDK up to three times — the station notify, the same value as a push
|
|
7679
|
+
* ~2 s later, then again on the next poll — so `contactState` is announced on a change only.
|
|
7680
|
+
*/
|
|
7681
|
+
stateEvents: [{ event: "contactState", field: "open" }],
|
|
7682
|
+
/**
|
|
7683
|
+
* The station's own notify, the fastest contact signal and the only one that needs no cloud. A
|
|
7684
|
+
* declarative `events` row matches a push id or a poll param, not a param nested in a frame's
|
|
7685
|
+
* `params` array under an inner command, hence the decode hook. A notify without the contact param
|
|
7686
|
+
* (battery or signal only) emits nothing.
|
|
7687
|
+
*/
|
|
7688
|
+
decodeEvent(signal) {
|
|
7689
|
+
const raw = statusParam(signal, CONTACT_CMD.STATUS);
|
|
7690
|
+
if (raw === void 0)
|
|
7691
|
+
return null;
|
|
7692
|
+
return { event: "contactState", payload: contactOpen(raw) };
|
|
7693
|
+
}
|
|
7694
|
+
};
|
|
7695
|
+
|
|
7576
7696
|
// dist/model/capabilities/leak.js
|
|
7697
|
+
var LEAK_PARAM = {
|
|
7698
|
+
/** Water detected (`APP_CMD_WATER_SENSOR_WATER_STATE`, identifier `sensor_water_status`). */
|
|
7699
|
+
WATER: 1820,
|
|
7700
|
+
/** Temperature below the threshold (`APP_CMD_WATER_SENSOR_TEMPERATE_LOW`, `sensor_temperature_low_status`). */
|
|
7701
|
+
LOW_TEMPERATURE: 1822,
|
|
7702
|
+
/** Low-temperature alarm threshold, °F (`APP_CMD_WATER_SENSOR_TEMPERATURE_ALARM`). */
|
|
7703
|
+
LOW_TEMPERATURE_THRESHOLD: 1873
|
|
7704
|
+
};
|
|
7577
7705
|
var LEAK_MEMBERS = {
|
|
7578
7706
|
/**
|
|
7579
|
-
*
|
|
7580
|
-
*
|
|
7581
|
-
* leave this getter uninstalled. The push event is the reliable channel for a leak; this read is
|
|
7582
|
-
* here so the flag has a home once a capture pins a real id.
|
|
7707
|
+
* Water is detected. The app's own `event_sensor_water_detected_notify` is this param changing, so
|
|
7708
|
+
* the state and the alert are one fact.
|
|
7583
7709
|
*/
|
|
7584
7710
|
leakDetected: {
|
|
7585
|
-
param:
|
|
7711
|
+
param: LEAK_PARAM.WATER,
|
|
7586
7712
|
type: "bool",
|
|
7587
7713
|
kind: "boolean",
|
|
7588
|
-
provenance: "
|
|
7589
|
-
description: "Water
|
|
7714
|
+
provenance: "apk",
|
|
7715
|
+
description: "Water detected (1820, sensor_water_status)."
|
|
7716
|
+
},
|
|
7717
|
+
/** The temperature is below {@link LEAK_MEMBERS.lowTemperatureThreshold}: the freeze alarm. */
|
|
7718
|
+
lowTemperature: {
|
|
7719
|
+
param: LEAK_PARAM.LOW_TEMPERATURE,
|
|
7720
|
+
type: "bool",
|
|
7721
|
+
kind: "boolean",
|
|
7722
|
+
provenance: "apk",
|
|
7723
|
+
description: "Temperature below the alarm threshold (1822, sensor_temperature_low_status)."
|
|
7724
|
+
},
|
|
7725
|
+
/**
|
|
7726
|
+
* The temperature, in °F, below which the freeze alarm fires: one of 32, 33, 35, 37 or 40 as the
|
|
7727
|
+
* product's description lists them. The write is the bundle's `1350` SET_PAYLOAD on 1873, not yet run
|
|
7728
|
+
* on a device, so no setter is installed.
|
|
7729
|
+
*/
|
|
7730
|
+
lowTemperatureThreshold: {
|
|
7731
|
+
param: LEAK_PARAM.LOW_TEMPERATURE_THRESHOLD,
|
|
7732
|
+
type: "number",
|
|
7733
|
+
kind: "enum",
|
|
7734
|
+
enumValues: { 32: "32\xB0F", 33: "33\xB0F", 35: "35\xB0F", 37: "37\xB0F", 40: "40\xB0F" },
|
|
7735
|
+
thing: ["sensor_low_temperature_alarm_threshold"],
|
|
7736
|
+
provenance: "apk",
|
|
7737
|
+
unverified: true,
|
|
7738
|
+
description: "Freeze alarm threshold, \xB0F: 32, 33, 35, 37 or 40 (1873, sensor_low_temperature_alarm_threshold)."
|
|
7590
7739
|
}
|
|
7591
7740
|
};
|
|
7592
7741
|
var LEAK = {
|
|
7593
7742
|
capability: "leak",
|
|
7594
|
-
description: "Water-leak
|
|
7743
|
+
description: "Water-leak and freeze sensor: water and low-temperature state, and the freeze threshold.",
|
|
7595
7744
|
members: LEAK_MEMBERS,
|
|
7596
7745
|
properties: propertiesOf(LEAK_MEMBERS),
|
|
7597
|
-
|
|
7598
|
-
|
|
7746
|
+
/**
|
|
7747
|
+
* A reported water-state param (1820) proves a water sensor, as does a product description listing
|
|
7748
|
+
* `sensor_water_status` — the app's own definition of one, which holds before the sensor has reported.
|
|
7749
|
+
* Never the device's name: that is what its owner typed.
|
|
7750
|
+
*/
|
|
7751
|
+
detection: {
|
|
7752
|
+
evidenceParams: [LEAK_PARAM.WATER],
|
|
7753
|
+
detect: (rec) => rec.things !== void 0 && "sensor_water_status" in rec.things.properties
|
|
7754
|
+
}
|
|
7599
7755
|
};
|
|
7600
7756
|
|
|
7601
7757
|
// dist/model/capabilities/smoke.js
|
|
@@ -8153,16 +8309,16 @@ function armingCommand(mode, ctx) {
|
|
|
8153
8309
|
}
|
|
8154
8310
|
return setPayload(ARMING_CMD.SET_ARMING, { mode_type: ARMING_MODE_WIRE[mode], user_name: ctx.accountName }, ctx, 0, void 0, "auto");
|
|
8155
8311
|
}
|
|
8156
|
-
function alarmDelayCommand(mode,
|
|
8312
|
+
function alarmDelayCommand(mode, config2, ctx) {
|
|
8157
8313
|
const data = {
|
|
8158
8314
|
mode_id: ARMING_MODE_WIRE[mode],
|
|
8159
8315
|
count_down_alarm: {
|
|
8160
|
-
channel_list:
|
|
8161
|
-
delay_time:
|
|
8316
|
+
channel_list: config2.countDownAlarm.channelList,
|
|
8317
|
+
delay_time: config2.countDownAlarm.delaySeconds
|
|
8162
8318
|
},
|
|
8163
|
-
count_down_arm: { channel_list:
|
|
8164
|
-
devices:
|
|
8165
|
-
siren_sensor_action:
|
|
8319
|
+
count_down_arm: { channel_list: config2.countDownArm.channelList, delay_time: config2.countDownArm.delaySeconds },
|
|
8320
|
+
devices: config2.devices.map((d) => ({ action: d.action, device_channel: d.deviceChannel })),
|
|
8321
|
+
siren_sensor_action: config2.sirenSensorAction.map((d) => ({ action: d.action, device_channel: d.deviceChannel }))
|
|
8166
8322
|
};
|
|
8167
8323
|
return setJsonRaw(ARMING_CMD.SET_ALL_ACTION, data, ctx, STATION_CHANNEL2);
|
|
8168
8324
|
}
|
|
@@ -8232,9 +8388,9 @@ var ARMING_MEMBERS = {
|
|
|
8232
8388
|
* that integer in `mode_id` with no runtime validation and no readback, so a mode outside this union
|
|
8233
8389
|
* would be the same unverified guess `setMode` refuses.
|
|
8234
8390
|
*/
|
|
8235
|
-
setAlarmDelayConfig: method(({ ctx, sink }) => (mode,
|
|
8391
|
+
setAlarmDelayConfig: method(({ ctx, sink }) => (mode, config2) => {
|
|
8236
8392
|
try {
|
|
8237
|
-
return sink.dispatch(alarmDelayCommand(mode,
|
|
8393
|
+
return sink.dispatch(alarmDelayCommand(mode, config2, ctx));
|
|
8238
8394
|
} catch (e) {
|
|
8239
8395
|
return Promise.reject(e);
|
|
8240
8396
|
}
|
|
@@ -12857,6 +13013,16 @@ var SECURITY_PARAMS = {
|
|
|
12857
13013
|
type: "bool",
|
|
12858
13014
|
provenance: "apk"
|
|
12859
13015
|
},
|
|
13016
|
+
1820: {
|
|
13017
|
+
name: "waterSensorWaterState",
|
|
13018
|
+
type: "bool",
|
|
13019
|
+
provenance: "apk"
|
|
13020
|
+
},
|
|
13021
|
+
1822: {
|
|
13022
|
+
name: "waterSensorTemperatureLow",
|
|
13023
|
+
type: "bool",
|
|
13024
|
+
provenance: "apk"
|
|
13025
|
+
},
|
|
12860
13026
|
1825: {
|
|
12861
13027
|
name: "sirenSensorSetAlarmVol",
|
|
12862
13028
|
type: "number",
|
|
@@ -12867,6 +13033,11 @@ var SECURITY_PARAMS = {
|
|
|
12867
13033
|
type: "bool",
|
|
12868
13034
|
provenance: "apk"
|
|
12869
13035
|
},
|
|
13036
|
+
1873: {
|
|
13037
|
+
name: "waterSensorTemperatureAlarm",
|
|
13038
|
+
type: "number",
|
|
13039
|
+
provenance: "apk"
|
|
13040
|
+
},
|
|
12870
13041
|
2001: {
|
|
12871
13042
|
name: "paramTypeOpenDevice",
|
|
12872
13043
|
type: "string",
|
|
@@ -22106,7 +22277,7 @@ var Fmp4Muxer = class {
|
|
|
22106
22277
|
return box("stbl", box("stsd", Buffer.concat([u32(0), u32(1), this.sampleEntry()])), box("stts", Buffer.concat([u32(0), u32(0)])), box("stsc", Buffer.concat([u32(0), u32(0)])), box("stsz", Buffer.concat([u32(0), u32(0), u32(0)])), box("stco", Buffer.concat([u32(0), u32(0)])));
|
|
22107
22278
|
}
|
|
22108
22279
|
sampleEntry() {
|
|
22109
|
-
const { sample, config } = CODEC_BOXES[this.codec];
|
|
22280
|
+
const { sample, config: config2 } = CODEC_BOXES[this.codec];
|
|
22110
22281
|
const head = Buffer.alloc(78);
|
|
22111
22282
|
head.writeUInt16BE(1, 6);
|
|
22112
22283
|
head.writeUInt16BE(this.width, 24);
|
|
@@ -22116,7 +22287,7 @@ var Fmp4Muxer = class {
|
|
|
22116
22287
|
head.writeUInt16BE(1, 40);
|
|
22117
22288
|
head.writeUInt16BE(24, 74);
|
|
22118
22289
|
head.writeInt16BE(-1, 76);
|
|
22119
|
-
const cfg = box(
|
|
22290
|
+
const cfg = box(config2, this.codec === "h264" ? this.avcC() : this.hvcC());
|
|
22120
22291
|
return box(sample, Buffer.concat([head, cfg]));
|
|
22121
22292
|
}
|
|
22122
22293
|
avcC() {
|
|
@@ -22263,8 +22434,8 @@ var Fmp4Muxer = class {
|
|
|
22263
22434
|
return box("mp4a", head, this.esds());
|
|
22264
22435
|
}
|
|
22265
22436
|
esds() {
|
|
22266
|
-
const
|
|
22267
|
-
const specific = descriptor(5,
|
|
22437
|
+
const config2 = this.audioConfig ?? Buffer.from([20, 8]);
|
|
22438
|
+
const specific = descriptor(5, config2);
|
|
22268
22439
|
const decoder = descriptor(4, Buffer.concat([Buffer.from([64, 21, 0, 0, 0]), u32(64e3), u32(32e3), specific]));
|
|
22269
22440
|
const sl = descriptor(6, Buffer.from([2]));
|
|
22270
22441
|
return box("esds", u32(0), descriptor(3, Buffer.concat([u16(2), Buffer.from([0]), decoder, sl])));
|
|
@@ -22497,6 +22668,7 @@ function stationChannels(devices) {
|
|
|
22497
22668
|
|
|
22498
22669
|
// dist/transport/p2p/command-router.js
|
|
22499
22670
|
var DIRECT_CMD_SENDS = 5;
|
|
22671
|
+
var STATION_TABLE_QUERY = Buffer.from("ff00000087030000", "hex");
|
|
22500
22672
|
var LEVEL2_GRACE_MS = 25e3;
|
|
22501
22673
|
var KEEP_LEVEL1_SESSION_MODELS = /* @__PURE__ */ new Set(["T8423"]);
|
|
22502
22674
|
var P2P_STATION_WAITS = {
|
|
@@ -22881,7 +23053,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22881
23053
|
return;
|
|
22882
23054
|
this.tearDownStation(stationSn);
|
|
22883
23055
|
});
|
|
22884
|
-
session.on("level2Ready", ({ cipherId }) =>
|
|
23056
|
+
session.on("level2Ready", ({ cipherId }) => {
|
|
23057
|
+
session.sendRawLevel2Bytes(STATION_TABLE_QUERY, 0, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
|
|
23058
|
+
this.deps.onLevel2Ready(stationSn, cipherId);
|
|
23059
|
+
});
|
|
22885
23060
|
session.on("peerAnswer", (atMs) => {
|
|
22886
23061
|
if (this.manager.get(stationSn) === session)
|
|
22887
23062
|
this.deps.onPeerAnswer?.(stationSn, atMs);
|
|
@@ -23717,11 +23892,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23717
23892
|
* Resolve a scalar `"set-param"` intent to a concrete P2P frame — the ONE place that maps a
|
|
23718
23893
|
* capability's *what* (param + value + {@link ScalarForm}) to the *how* (encryption level + wire):
|
|
23719
23894
|
* `"auto"` defers the level to {@link sendBySessionLevel} — the ONE decision point — while
|
|
23720
|
-
* `"int-string"`
|
|
23895
|
+
* `"int-string"` and `"int-pair"` pin L1 and `"direct-binary"` pins L2.
|
|
23721
23896
|
*/
|
|
23722
23897
|
async resolveScalarParam(sn, param, value, form) {
|
|
23723
|
-
if (form === "int-string") {
|
|
23724
|
-
await this.sendIntStringCommand(sn, param, value);
|
|
23898
|
+
if (form === "int-string" || form === "int-pair") {
|
|
23899
|
+
await this.sendIntStringCommand(sn, param, value, form);
|
|
23725
23900
|
return;
|
|
23726
23901
|
}
|
|
23727
23902
|
if (form === "direct-binary") {
|
|
@@ -24348,10 +24523,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
24348
24523
|
* SoloCam-spotlight / Cam2C-3). No level-2 key needed (standalone level-1 ECB). Fire-and-forget,
|
|
24349
24524
|
* repeated for RF resilience.
|
|
24350
24525
|
*/
|
|
24351
|
-
async sendIntStringCommand(sn, outerCmd, value) {
|
|
24526
|
+
async sendIntStringCommand(sn, outerCmd, value, form) {
|
|
24352
24527
|
const { session, channel, accountId } = await this.resolveSession(sn);
|
|
24353
24528
|
for (let i = 0; i < DIRECT_CMD_SENDS; i++) {
|
|
24354
|
-
session.sendIntStringCommand(outerCmd, value, channel, accountId, channel);
|
|
24529
|
+
session.sendIntStringCommand(outerCmd, value, channel, form === "int-pair" ? "" : accountId, channel);
|
|
24355
24530
|
await sleep2(200);
|
|
24356
24531
|
}
|
|
24357
24532
|
}
|
|
@@ -24371,9 +24546,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
24371
24546
|
mValue3: 0,
|
|
24372
24547
|
payload: { switch: enabled ? 1 : 0 }
|
|
24373
24548
|
}), "utf-8");
|
|
24374
|
-
const camInfoPre = Buffer.from("ff00000087030000", "hex");
|
|
24375
24549
|
const gap = () => sleep2(150);
|
|
24376
|
-
session.sendRawLevel2Bytes(
|
|
24550
|
+
session.sendRawLevel2Bytes(STATION_TABLE_QUERY, 255, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
|
|
24377
24551
|
await gap();
|
|
24378
24552
|
for (let i = 0; i < 2; i++) {
|
|
24379
24553
|
session.sendRawLevel2Bytes(setJson2, 0, P2P_ENVELOPE.SET_PAYLOAD, 8);
|
|
@@ -24383,7 +24557,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
24383
24557
|
session.sendRawLevel2Bytes(setJson2, channel, P2P_ENVELOPE.SET_PAYLOAD, 8);
|
|
24384
24558
|
await gap();
|
|
24385
24559
|
}
|
|
24386
|
-
session.sendRawLevel2Bytes(
|
|
24560
|
+
session.sendRawLevel2Bytes(STATION_TABLE_QUERY, 0, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
|
|
24387
24561
|
}
|
|
24388
24562
|
/**
|
|
24389
24563
|
* Route a control command (`{commandType, data}`) to a device over P2P: HomeBase-attached →
|
|
@@ -25136,7 +25310,7 @@ async function loadNativePeerFactory() {
|
|
|
25136
25310
|
const message = UNBUILT_ARCHES.has(process.arch) ? `the WebRTC transport for a HomeBase S1 Pro (T9000) needs 'node-datachannel', which has no build for ${process.arch}; a 64-bit OS is needed to drive a T9000` : "the WebRTC transport for a HomeBase S1 Pro (T9000) could not load 'node-datachannel'";
|
|
25137
25311
|
throw new Error(message, { cause: err });
|
|
25138
25312
|
}
|
|
25139
|
-
return (name,
|
|
25313
|
+
return (name, config2) => new ndc.PeerConnection(name, config2);
|
|
25140
25314
|
}
|
|
25141
25315
|
var ANSWER_TIMEOUT_MS = 15e3;
|
|
25142
25316
|
var RtcPeer = class extends EventEmitter9 {
|
|
@@ -25168,13 +25342,13 @@ var RtcPeer = class extends EventEmitter9 {
|
|
|
25168
25342
|
if (this.pc)
|
|
25169
25343
|
return;
|
|
25170
25344
|
const createPeer = this.opts.createPeer ?? await loadNativePeerFactory();
|
|
25171
|
-
const
|
|
25345
|
+
const config2 = {
|
|
25172
25346
|
iceServers: turnServers(turn),
|
|
25173
25347
|
iceTransportPolicy: this.opts.iceTransportPolicy ?? "relay",
|
|
25174
25348
|
maxMessageSize: ANKER_MAX_MESSAGE_SIZE,
|
|
25175
25349
|
enableIceTcp: true
|
|
25176
25350
|
};
|
|
25177
|
-
const pc = createPeer("eufy-sdk",
|
|
25351
|
+
const pc = createPeer("eufy-sdk", config2);
|
|
25178
25352
|
this.pc = pc;
|
|
25179
25353
|
pc.onLocalDescription((sdp, type) => {
|
|
25180
25354
|
if (String(type).toLowerCase() === "answer" && this.localAnswer) {
|
|
@@ -27753,15 +27927,15 @@ var TuyaClient = class {
|
|
|
27753
27927
|
endpoint;
|
|
27754
27928
|
http;
|
|
27755
27929
|
session;
|
|
27756
|
-
constructor(
|
|
27757
|
-
this.signer =
|
|
27758
|
-
this.env =
|
|
27759
|
-
this.endpoint =
|
|
27760
|
-
this.http =
|
|
27930
|
+
constructor(config2 = {}) {
|
|
27931
|
+
this.signer = config2.signer ?? new HmacSigner();
|
|
27932
|
+
this.env = config2.env ?? DEFAULT_TUYA_ENV;
|
|
27933
|
+
this.endpoint = config2.endpoint;
|
|
27934
|
+
this.http = config2.http;
|
|
27761
27935
|
this.session = {
|
|
27762
|
-
sid:
|
|
27763
|
-
deviceId:
|
|
27764
|
-
chKey:
|
|
27936
|
+
sid: config2.sid ?? "",
|
|
27937
|
+
deviceId: config2.deviceId ?? genDeviceId(),
|
|
27938
|
+
chKey: config2.chKey ?? TUYA_CHKEY
|
|
27765
27939
|
};
|
|
27766
27940
|
}
|
|
27767
27941
|
/** The current per-install session identity (sid empty until {@link login}). */
|
|
@@ -27924,9 +28098,9 @@ var TuyaCommandRouter = class {
|
|
|
27924
28098
|
* `virtualId`, or the device's serial when the record carries none of them.
|
|
27925
28099
|
*/
|
|
27926
28100
|
snMap = /* @__PURE__ */ new Map();
|
|
27927
|
-
constructor(
|
|
27928
|
-
this.allowUnverified =
|
|
27929
|
-
this.http =
|
|
28101
|
+
constructor(config2 = {}) {
|
|
28102
|
+
this.allowUnverified = config2.allowUnverified ?? false;
|
|
28103
|
+
this.http = config2.http;
|
|
27930
28104
|
}
|
|
27931
28105
|
/**
|
|
27932
28106
|
* Supply credentials for lazy Tuya login. Called by the facade after a successful mega login.
|
|
@@ -30804,7 +30978,7 @@ var EufyMega = class _EufyMega extends EventEmitter14 {
|
|
|
30804
30978
|
return false;
|
|
30805
30979
|
if (!device)
|
|
30806
30980
|
return true;
|
|
30807
|
-
|
|
30981
|
+
this.applyAndAnnounce(device, rawValue === void 0 ? {} : { [refresh.param]: String(rawValue) });
|
|
30808
30982
|
return this.matchesObservation(device.getProperty(refresh.property)?.value, refresh, before);
|
|
30809
30983
|
};
|
|
30810
30984
|
if (refresh.expected !== void 0 && settled())
|
|
@@ -33653,7 +33827,6 @@ export {
|
|
|
33653
33827
|
EMPTY_DP_CATALOG,
|
|
33654
33828
|
EUFYLIFE_LOCAL_KEY_HEX,
|
|
33655
33829
|
EUFY_MEGA_LOCAL_KEY_HEX,
|
|
33656
|
-
EntryAlarmTone,
|
|
33657
33830
|
EufyCloudErrorCode,
|
|
33658
33831
|
EufyMega,
|
|
33659
33832
|
FCM,
|