@camstack/addon-provider-dreame 0.2.70 → 0.2.71
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/addon.js +2757 -273
- package/dist/addon.mjs +2739 -255
- package/dist/index.mjs +1 -1
- package/package.json +5 -2
package/dist/addon.js
CHANGED
|
@@ -695,7 +695,7 @@ var require_util$9 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
695
695
|
var { kDestroyed, kBodyUsed, kListeners, kBody } = require_symbols$4();
|
|
696
696
|
var { IncomingMessage } = require("node:http");
|
|
697
697
|
var stream$1 = require("node:stream");
|
|
698
|
-
var net$
|
|
698
|
+
var net$7 = require("node:net");
|
|
699
699
|
var { Blob: Blob$3 } = require("node:buffer");
|
|
700
700
|
var nodeUtil$3 = require("node:util");
|
|
701
701
|
var { stringify } = require("node:querystring");
|
|
@@ -800,7 +800,7 @@ var require_util$9 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
800
800
|
if (!host) return null;
|
|
801
801
|
assert$27(typeof host === "string");
|
|
802
802
|
const servername = getHostname(host);
|
|
803
|
-
if (net$
|
|
803
|
+
if (net$7.isIP(servername)) return "";
|
|
804
804
|
return servername;
|
|
805
805
|
}
|
|
806
806
|
function deepClone(obj) {
|
|
@@ -1967,7 +1967,7 @@ var require_timers = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
1967
1967
|
//#endregion
|
|
1968
1968
|
//#region ../../node_modules/@apocaliss92/nodedreame/node_modules/undici/lib/core/connect.js
|
|
1969
1969
|
var require_connect$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
1970
|
-
var net$
|
|
1970
|
+
var net$6 = require("node:net");
|
|
1971
1971
|
var assert$25 = require("node:assert");
|
|
1972
1972
|
var util = require_util$9();
|
|
1973
1973
|
var { InvalidArgumentError, ConnectTimeoutError } = require_errors$1();
|
|
@@ -2047,7 +2047,7 @@ var require_connect$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
2047
2047
|
} else {
|
|
2048
2048
|
assert$25(!httpSocket, "httpSocket can only be sent on TLS update");
|
|
2049
2049
|
port = port || 80;
|
|
2050
|
-
socket = net$
|
|
2050
|
+
socket = net$6.connect({
|
|
2051
2051
|
highWaterMark: 64 * 1024,
|
|
2052
2052
|
...options,
|
|
2053
2053
|
localAddress,
|
|
@@ -6264,7 +6264,7 @@ var require_redirect_interceptor = /* @__PURE__ */ __commonJSMin(((exports, modu
|
|
|
6264
6264
|
//#region ../../node_modules/@apocaliss92/nodedreame/node_modules/undici/lib/dispatcher/client.js
|
|
6265
6265
|
var require_client$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
6266
6266
|
var assert$17 = require("node:assert");
|
|
6267
|
-
var net$
|
|
6267
|
+
var net$5 = require("node:net");
|
|
6268
6268
|
var http$2 = require("node:http");
|
|
6269
6269
|
var util = require_util$9();
|
|
6270
6270
|
var { channels } = require_diagnostics();
|
|
@@ -6308,7 +6308,7 @@ var require_client$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
6308
6308
|
if (connect != null && typeof connect !== "function" && typeof connect !== "object") throw new InvalidArgumentError("connect must be a function or an object");
|
|
6309
6309
|
if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) throw new InvalidArgumentError("maxRedirections must be a positive number");
|
|
6310
6310
|
if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) throw new InvalidArgumentError("maxRequestsPerClient must be a positive number");
|
|
6311
|
-
if (localAddress != null && (typeof localAddress !== "string" || net$
|
|
6311
|
+
if (localAddress != null && (typeof localAddress !== "string" || net$5.isIP(localAddress) === 0)) throw new InvalidArgumentError("localAddress must be valid string IP address");
|
|
6312
6312
|
if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) throw new InvalidArgumentError("maxResponseSize must be a positive number");
|
|
6313
6313
|
if (autoSelectFamilyAttemptTimeout != null && (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1)) throw new InvalidArgumentError("autoSelectFamilyAttemptTimeout must be a positive number");
|
|
6314
6314
|
if (allowH2 != null && typeof allowH2 !== "boolean") throw new InvalidArgumentError("allowH2 must be a valid boolean value");
|
|
@@ -6449,7 +6449,7 @@ var require_client$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
6449
6449
|
const idx = hostname.indexOf("]");
|
|
6450
6450
|
assert$17(idx !== -1);
|
|
6451
6451
|
const ip = hostname.substring(1, idx);
|
|
6452
|
-
assert$17(net$
|
|
6452
|
+
assert$17(net$5.isIP(ip));
|
|
6453
6453
|
hostname = ip;
|
|
6454
6454
|
}
|
|
6455
6455
|
client[kConnecting] = true;
|
|
@@ -36633,7 +36633,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
36633
36633
|
var EventEmitter$2 = require("events");
|
|
36634
36634
|
var https = require("https");
|
|
36635
36635
|
var http$1 = require("http");
|
|
36636
|
-
var net$
|
|
36636
|
+
var net$4 = require("net");
|
|
36637
36637
|
var tls = require("tls");
|
|
36638
36638
|
var { randomBytes: randomBytes$1, createHash: createHash$2 } = require("crypto");
|
|
36639
36639
|
var { Duplex: Duplex$2, Readable } = require("stream");
|
|
@@ -37379,7 +37379,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
37379
37379
|
*/
|
|
37380
37380
|
function netConnect(options) {
|
|
37381
37381
|
options.path = options.socketPath;
|
|
37382
|
-
return net$
|
|
37382
|
+
return net$4.connect(options);
|
|
37383
37383
|
}
|
|
37384
37384
|
/**
|
|
37385
37385
|
* Create a `tls.TLSSocket` and initiate a connection.
|
|
@@ -37390,7 +37390,7 @@ var require_websocket = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
37390
37390
|
*/
|
|
37391
37391
|
function tlsConnect(options) {
|
|
37392
37392
|
options.path = void 0;
|
|
37393
|
-
if (!options.servername && options.servername !== "") options.servername = net$
|
|
37393
|
+
if (!options.servername && options.servername !== "") options.servername = net$4.isIP(options.host) ? "" : options.host;
|
|
37394
37394
|
return tls.connect(options);
|
|
37395
37395
|
}
|
|
37396
37396
|
/**
|
|
@@ -41473,7 +41473,7 @@ var require_helpers = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
41473
41473
|
var constants_1 = require_constants$3();
|
|
41474
41474
|
var stream = require("stream");
|
|
41475
41475
|
var ip_address_1 = require_ip_address();
|
|
41476
|
-
var net$
|
|
41476
|
+
var net$3 = require("net");
|
|
41477
41477
|
/**
|
|
41478
41478
|
* Validates the provided SocksClientOptions
|
|
41479
41479
|
* @param options { SocksClientOptions }
|
|
@@ -41551,10 +41551,10 @@ var require_helpers = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
41551
41551
|
}
|
|
41552
41552
|
exports.int32ToIpv4 = int32ToIpv4;
|
|
41553
41553
|
function ipToBuffer(ip) {
|
|
41554
|
-
if (net$
|
|
41554
|
+
if (net$3.isIPv4(ip)) {
|
|
41555
41555
|
const address = new ip_address_1.Address4(ip);
|
|
41556
41556
|
return Buffer.from(address.toArray());
|
|
41557
|
-
} else if (net$
|
|
41557
|
+
} else if (net$3.isIPv6(ip)) {
|
|
41558
41558
|
const address = new ip_address_1.Address6(ip);
|
|
41559
41559
|
return Buffer.from(address.canonicalForm().split(":").map((segment) => segment.padStart(4, "0")).join(""), "hex");
|
|
41560
41560
|
} else throw new Error("Invalid IP address format");
|
|
@@ -41633,7 +41633,7 @@ var require_socksclient = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
41633
41633
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41634
41634
|
exports.SocksClientError = exports.SocksClient = void 0;
|
|
41635
41635
|
var events_1 = require("events");
|
|
41636
|
-
var net = require("net");
|
|
41636
|
+
var net$2 = require("net");
|
|
41637
41637
|
var smart_buffer_1 = require_smartbuffer();
|
|
41638
41638
|
var constants_1 = require_constants$3();
|
|
41639
41639
|
var helpers_1 = require_helpers();
|
|
@@ -41745,10 +41745,10 @@ var require_socksclient = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
41745
41745
|
const buff = new smart_buffer_1.SmartBuffer();
|
|
41746
41746
|
buff.writeUInt16BE(0);
|
|
41747
41747
|
buff.writeUInt8(options.frameNumber || 0);
|
|
41748
|
-
if (net.isIPv4(options.remoteHost.host)) {
|
|
41748
|
+
if (net$2.isIPv4(options.remoteHost.host)) {
|
|
41749
41749
|
buff.writeUInt8(constants_1.Socks5HostType.IPv4);
|
|
41750
41750
|
buff.writeUInt32BE((0, helpers_1.ipv4ToInt32)(options.remoteHost.host));
|
|
41751
|
-
} else if (net.isIPv6(options.remoteHost.host)) {
|
|
41751
|
+
} else if (net$2.isIPv6(options.remoteHost.host)) {
|
|
41752
41752
|
buff.writeUInt8(constants_1.Socks5HostType.IPv6);
|
|
41753
41753
|
buff.writeBuffer((0, helpers_1.ipToBuffer)(options.remoteHost.host));
|
|
41754
41754
|
} else {
|
|
@@ -41801,7 +41801,7 @@ var require_socksclient = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
41801
41801
|
const timer = setTimeout(() => this.onEstablishedTimeout(), this.options.timeout || constants_1.DEFAULT_TIMEOUT);
|
|
41802
41802
|
if (timer.unref && typeof timer.unref === "function") timer.unref();
|
|
41803
41803
|
if (existingSocket) this.socket = existingSocket;
|
|
41804
|
-
else this.socket = new net.Socket();
|
|
41804
|
+
else this.socket = new net$2.Socket();
|
|
41805
41805
|
this.socket.once("close", this.onClose);
|
|
41806
41806
|
this.socket.once("error", this.onError);
|
|
41807
41807
|
this.socket.once("connect", this.onConnect);
|
|
@@ -41913,7 +41913,7 @@ var require_socksclient = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
41913
41913
|
buff.writeUInt8(4);
|
|
41914
41914
|
buff.writeUInt8(constants_1.SocksCommand[this.options.command]);
|
|
41915
41915
|
buff.writeUInt16BE(this.options.destination.port);
|
|
41916
|
-
if (net.isIPv4(this.options.destination.host)) {
|
|
41916
|
+
if (net$2.isIPv4(this.options.destination.host)) {
|
|
41917
41917
|
buff.writeBuffer((0, helpers_1.ipToBuffer)(this.options.destination.host));
|
|
41918
41918
|
buff.writeStringNT(userId);
|
|
41919
41919
|
} else {
|
|
@@ -42072,10 +42072,10 @@ var require_socksclient = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
42072
42072
|
buff.writeUInt8(5);
|
|
42073
42073
|
buff.writeUInt8(constants_1.SocksCommand[this.options.command]);
|
|
42074
42074
|
buff.writeUInt8(0);
|
|
42075
|
-
if (net.isIPv4(this.options.destination.host)) {
|
|
42075
|
+
if (net$2.isIPv4(this.options.destination.host)) {
|
|
42076
42076
|
buff.writeUInt8(constants_1.Socks5HostType.IPv4);
|
|
42077
42077
|
buff.writeBuffer((0, helpers_1.ipToBuffer)(this.options.destination.host));
|
|
42078
|
-
} else if (net.isIPv6(this.options.destination.host)) {
|
|
42078
|
+
} else if (net$2.isIPv6(this.options.destination.host)) {
|
|
42079
42079
|
buff.writeUInt8(constants_1.Socks5HostType.IPv6);
|
|
42080
42080
|
buff.writeBuffer((0, helpers_1.ipToBuffer)(this.options.destination.host));
|
|
42081
42081
|
} else {
|
|
@@ -44955,6 +44955,38 @@ async function postForToken(ctx, body) {
|
|
|
44955
44955
|
if (typeof data.refresh_token === "string") session.refreshToken = data.refresh_token;
|
|
44956
44956
|
return session;
|
|
44957
44957
|
}
|
|
44958
|
+
var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
|
|
44959
|
+
var numOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
44960
|
+
function parseBroker(bindDomain) {
|
|
44961
|
+
const s = str(bindDomain);
|
|
44962
|
+
if (!s) return null;
|
|
44963
|
+
const idx = s.lastIndexOf(":");
|
|
44964
|
+
if (idx < 0) return {
|
|
44965
|
+
host: s,
|
|
44966
|
+
port: null
|
|
44967
|
+
};
|
|
44968
|
+
const port = Number(s.slice(idx + 1));
|
|
44969
|
+
return {
|
|
44970
|
+
host: s.slice(0, idx),
|
|
44971
|
+
port: Number.isFinite(port) ? port : null
|
|
44972
|
+
};
|
|
44973
|
+
}
|
|
44974
|
+
function parseConnectivity(raw) {
|
|
44975
|
+
const deviceInfo = raw["deviceInfo"] ?? {};
|
|
44976
|
+
return {
|
|
44977
|
+
online: raw["online"] === true,
|
|
44978
|
+
connectionType: str(deviceInfo["scType"]),
|
|
44979
|
+
mac: str(raw["mac"]),
|
|
44980
|
+
broker: parseBroker(raw["bindDomain"]),
|
|
44981
|
+
region: str(raw["region"]),
|
|
44982
|
+
cloudVendor: str(raw["vendor"]),
|
|
44983
|
+
firmwareVersion: str(raw["ver"]),
|
|
44984
|
+
serialNumber: str(raw["sn"]),
|
|
44985
|
+
subModel: str(raw["subModel"]),
|
|
44986
|
+
battery: numOrNull(raw["battery"]),
|
|
44987
|
+
statusCode: numOrNull(raw["latestStatus"])
|
|
44988
|
+
};
|
|
44989
|
+
}
|
|
44958
44990
|
async function listDevices(input) {
|
|
44959
44991
|
const ctx = input.ctx ?? RequestContext.from({
|
|
44960
44992
|
...input,
|
|
@@ -44994,7 +45026,8 @@ function toDevice(rawIn) {
|
|
|
44994
45026
|
name,
|
|
44995
45027
|
online: rawIn.online === true || rawIn.lwt === 1 || lwtFlag === 1,
|
|
44996
45028
|
raw,
|
|
44997
|
-
cloudState: parseCloudState(raw)
|
|
45029
|
+
cloudState: parseCloudState(raw),
|
|
45030
|
+
connectivity: parseConnectivity(raw)
|
|
44998
45031
|
};
|
|
44999
45032
|
if (rawIn.mac) device.mac = String(rawIn.mac);
|
|
45000
45033
|
if (typeof raw["ver"] === "string") device.firmwareVersion = raw["ver"];
|
|
@@ -46611,6 +46644,953 @@ var VacuumCapabilityResolver = class {
|
|
|
46611
46644
|
return new VacuumCapabilitySet(model, tokensFor(getVacuumCapabilities(model)));
|
|
46612
46645
|
}
|
|
46613
46646
|
};
|
|
46647
|
+
objectType({
|
|
46648
|
+
code: numberType().optional(),
|
|
46649
|
+
msg: stringType().nullish(),
|
|
46650
|
+
data: objectType({ data: objectType({
|
|
46651
|
+
token: stringType(),
|
|
46652
|
+
userId: unionType([stringType(), numberType()]).optional(),
|
|
46653
|
+
expireAt: numberType().optional()
|
|
46654
|
+
}).passthrough() }).passthrough()
|
|
46655
|
+
}).passthrough();
|
|
46656
|
+
var AuthCodeResponseSchema = objectType({
|
|
46657
|
+
code: numberType().optional(),
|
|
46658
|
+
msg: stringType().nullish(),
|
|
46659
|
+
data: stringType()
|
|
46660
|
+
}).passthrough();
|
|
46661
|
+
objectType({
|
|
46662
|
+
code: numberType().optional(),
|
|
46663
|
+
msg: stringType().nullish(),
|
|
46664
|
+
data: objectType({ data: objectType({ familyId: stringType() }).passthrough() }).passthrough()
|
|
46665
|
+
}).passthrough();
|
|
46666
|
+
var DeviceVideoInfoSchema = objectType({
|
|
46667
|
+
model: stringType().optional(),
|
|
46668
|
+
displayName: stringType().optional(),
|
|
46669
|
+
permit: stringType().optional(),
|
|
46670
|
+
videoDynamicVendor: booleanType().optional(),
|
|
46671
|
+
defaultVendors: arrayType(stringType()).optional(),
|
|
46672
|
+
productId: unionType([stringType(), numberType()]).optional()
|
|
46673
|
+
}).passthrough();
|
|
46674
|
+
var DeviceInfoRecordSchema = objectType({
|
|
46675
|
+
did: unionType([stringType(), numberType()]).optional(),
|
|
46676
|
+
model: stringType().optional(),
|
|
46677
|
+
customName: stringType().optional(),
|
|
46678
|
+
vendor: stringType().optional(),
|
|
46679
|
+
online: booleanType().optional(),
|
|
46680
|
+
lwt: numberType().optional(),
|
|
46681
|
+
property: stringType().optional(),
|
|
46682
|
+
bindDomain: stringType().optional(),
|
|
46683
|
+
deviceInfo: DeviceVideoInfoSchema.optional()
|
|
46684
|
+
}).passthrough();
|
|
46685
|
+
var DeviceInfoResponseSchema = objectType({
|
|
46686
|
+
code: numberType().optional(),
|
|
46687
|
+
msg: stringType().nullish(),
|
|
46688
|
+
data: DeviceInfoRecordSchema
|
|
46689
|
+
}).passthrough();
|
|
46690
|
+
var KNOWN_VENDORS = ["tx", "ali"];
|
|
46691
|
+
function asVendor(v) {
|
|
46692
|
+
return v === "tx" || v === "ali" ? v : null;
|
|
46693
|
+
}
|
|
46694
|
+
function parseProperty(property) {
|
|
46695
|
+
if (!property) return {
|
|
46696
|
+
iotId: null,
|
|
46697
|
+
lwt: null
|
|
46698
|
+
};
|
|
46699
|
+
try {
|
|
46700
|
+
const p = JSON.parse(property);
|
|
46701
|
+
return {
|
|
46702
|
+
iotId: typeof p.iotId === "string" && p.iotId.length > 0 ? p.iotId : null,
|
|
46703
|
+
lwt: typeof p.lwt === "number" ? p.lwt : null
|
|
46704
|
+
};
|
|
46705
|
+
} catch {
|
|
46706
|
+
return {
|
|
46707
|
+
iotId: null,
|
|
46708
|
+
lwt: null
|
|
46709
|
+
};
|
|
46710
|
+
}
|
|
46711
|
+
}
|
|
46712
|
+
function toVideoProfile(record) {
|
|
46713
|
+
const info = record.deviceInfo ?? {};
|
|
46714
|
+
const supportedVendors = (info.defaultVendors ?? []).filter((v) => KNOWN_VENDORS.includes(v));
|
|
46715
|
+
const prop = parseProperty(record.property);
|
|
46716
|
+
const online = record.online === true || record.lwt === 1 || prop.lwt === 1;
|
|
46717
|
+
return {
|
|
46718
|
+
did: String(record.did ?? ""),
|
|
46719
|
+
model: String(record.model ?? info.model ?? ""),
|
|
46720
|
+
displayName: info.displayName ?? null,
|
|
46721
|
+
videoCapable: info.permit === "video" || supportedVendors.length > 0,
|
|
46722
|
+
currentVendor: asVendor(record.vendor),
|
|
46723
|
+
supportedVendors,
|
|
46724
|
+
dynamicVendor: info.videoDynamicVendor === true,
|
|
46725
|
+
iotId: prop.iotId,
|
|
46726
|
+
online
|
|
46727
|
+
};
|
|
46728
|
+
}
|
|
46729
|
+
var P_SMARTHOME = "/dreame-smarthome";
|
|
46730
|
+
var P_USER_IOT = "/dreame-user-iot";
|
|
46731
|
+
var DEFAULT_OS = "android";
|
|
46732
|
+
function resolveCtx(input) {
|
|
46733
|
+
return input.ctx ?? RequestContext.from({
|
|
46734
|
+
...input,
|
|
46735
|
+
host: input.apiHost
|
|
46736
|
+
});
|
|
46737
|
+
}
|
|
46738
|
+
function passthrough(input) {
|
|
46739
|
+
return {
|
|
46740
|
+
accessToken: input.session.accessToken,
|
|
46741
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
46742
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {}
|
|
46743
|
+
};
|
|
46744
|
+
}
|
|
46745
|
+
async function getAliyunAuthCode(input) {
|
|
46746
|
+
const raw = await httpPostJsonBody({
|
|
46747
|
+
ctx: resolveCtx(input),
|
|
46748
|
+
path: `${P_SMARTHOME}/aliIot/getAuthCodeV3`,
|
|
46749
|
+
body: { os: DEFAULT_OS },
|
|
46750
|
+
context: "aliyun authCode",
|
|
46751
|
+
...passthrough(input)
|
|
46752
|
+
});
|
|
46753
|
+
return AuthCodeResponseSchema.parse(raw).data;
|
|
46754
|
+
}
|
|
46755
|
+
async function getDeviceVideoProfile(input) {
|
|
46756
|
+
const raw = await httpPostJsonBody({
|
|
46757
|
+
ctx: resolveCtx(input),
|
|
46758
|
+
path: `${P_USER_IOT}/iotuserbind/device/info`,
|
|
46759
|
+
body: { did: input.did },
|
|
46760
|
+
context: "device video info",
|
|
46761
|
+
...passthrough(input)
|
|
46762
|
+
});
|
|
46763
|
+
return toVideoProfile(DeviceInfoResponseSchema.parse(raw).data);
|
|
46764
|
+
}
|
|
46765
|
+
var ALIYUN_APP_KEY = "33974273";
|
|
46766
|
+
var ALIYUN_APP_SECRET = "65a62f03ae5eda97af962fecc2e3efa6";
|
|
46767
|
+
var ALIYUN_REGION_ID = {
|
|
46768
|
+
eu: "eu-central-1",
|
|
46769
|
+
de: "eu-central-1",
|
|
46770
|
+
us: "us-east-1",
|
|
46771
|
+
cn: "cn-shanghai",
|
|
46772
|
+
ru: "eu-central-1",
|
|
46773
|
+
sg: "ap-southeast-1",
|
|
46774
|
+
in: "ap-southeast-1",
|
|
46775
|
+
tw: "cn-shanghai"
|
|
46776
|
+
};
|
|
46777
|
+
function livingAccountHost(regionId) {
|
|
46778
|
+
return `living-account.${regionId}.aliyuncs.com`;
|
|
46779
|
+
}
|
|
46780
|
+
function apiIotHost(regionId) {
|
|
46781
|
+
return `${regionId}.api-iot.aliyuncs.com`;
|
|
46782
|
+
}
|
|
46783
|
+
var VISION_STREAM_QUERY = "/vision/customer/stream/query";
|
|
46784
|
+
var CT_JSON = "application/json; charset=UTF-8";
|
|
46785
|
+
var CT_FORM = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
46786
|
+
function md5Base64(s) {
|
|
46787
|
+
return (0, crypto$1.createHash)("md5").update(s, "utf8").digest("base64");
|
|
46788
|
+
}
|
|
46789
|
+
function signApiGatewayRequest(input) {
|
|
46790
|
+
const accept = CT_JSON;
|
|
46791
|
+
const contentType = input.json !== void 0 ? CT_JSON : CT_FORM;
|
|
46792
|
+
const date = input.date ?? (/* @__PURE__ */ new Date()).toUTCString();
|
|
46793
|
+
let body;
|
|
46794
|
+
let contentMd5 = "";
|
|
46795
|
+
let formParams;
|
|
46796
|
+
if (input.json !== void 0) {
|
|
46797
|
+
body = JSON.stringify(input.json);
|
|
46798
|
+
contentMd5 = md5Base64(body);
|
|
46799
|
+
} else {
|
|
46800
|
+
const form = input.form ?? {};
|
|
46801
|
+
body = Object.entries(form).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
46802
|
+
formParams = form;
|
|
46803
|
+
}
|
|
46804
|
+
const caHeaders = {
|
|
46805
|
+
"X-Ca-Key": input.appKey,
|
|
46806
|
+
"X-Ca-Nonce": input.nonce ?? crypto.randomUUID().toUpperCase(),
|
|
46807
|
+
"X-Ca-Timestamp": String(input.timestamp ?? Date.now()),
|
|
46808
|
+
"X-Ca-Version": "1",
|
|
46809
|
+
...input.extraCaHeaders ?? {}
|
|
46810
|
+
};
|
|
46811
|
+
const signedHeaderNames = Object.keys(caHeaders).sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0);
|
|
46812
|
+
const headerBlock = signedHeaderNames.map((k) => `${k}:${caHeaders[k]}
|
|
46813
|
+
`).join("");
|
|
46814
|
+
let resource = input.path;
|
|
46815
|
+
if (formParams && Object.keys(formParams).length > 0) {
|
|
46816
|
+
const sorted = Object.keys(formParams).sort();
|
|
46817
|
+
resource += "?" + sorted.map((k) => `${k}=${formParams[k]}`).join("&");
|
|
46818
|
+
}
|
|
46819
|
+
const stringToSign = `${input.method}
|
|
46820
|
+
${accept}
|
|
46821
|
+
${contentMd5}
|
|
46822
|
+
${contentType}
|
|
46823
|
+
${date}
|
|
46824
|
+
${headerBlock}${resource}`;
|
|
46825
|
+
const headers = {
|
|
46826
|
+
"Content-Type": contentType,
|
|
46827
|
+
Accept: accept,
|
|
46828
|
+
Date: date,
|
|
46829
|
+
"X-Ca-Signature": (0, crypto$1.createHmac)("sha1", input.appSecret).update(stringToSign, "utf8").digest("base64"),
|
|
46830
|
+
"X-Ca-Signature-Method": "HmacSHA1",
|
|
46831
|
+
"X-Ca-Signature-Headers": signedHeaderNames.join(","),
|
|
46832
|
+
...caHeaders
|
|
46833
|
+
};
|
|
46834
|
+
if (contentMd5) headers["Content-MD5"] = contentMd5;
|
|
46835
|
+
return {
|
|
46836
|
+
headers,
|
|
46837
|
+
body
|
|
46838
|
+
};
|
|
46839
|
+
}
|
|
46840
|
+
var LoginByOauthSchema = objectType({ data: objectType({ data: objectType({ loginSuccessResult: objectType({
|
|
46841
|
+
sid: stringType(),
|
|
46842
|
+
refreshToken: stringType().optional(),
|
|
46843
|
+
token: stringType().optional(),
|
|
46844
|
+
openAccount: objectType({ openId: stringType().optional() }).passthrough().optional()
|
|
46845
|
+
}).passthrough() }).passthrough() }).passthrough() }).passthrough();
|
|
46846
|
+
var CreateSessionSchema = objectType({
|
|
46847
|
+
code: numberType().optional(),
|
|
46848
|
+
message: stringType().nullish(),
|
|
46849
|
+
data: objectType({
|
|
46850
|
+
iotToken: stringType(),
|
|
46851
|
+
refreshToken: stringType().optional(),
|
|
46852
|
+
identityId: stringType().optional(),
|
|
46853
|
+
iotTokenExpire: numberType().optional()
|
|
46854
|
+
}).passthrough()
|
|
46855
|
+
}).passthrough();
|
|
46856
|
+
async function postJson(url, headers, body, fetchImpl, ctx) {
|
|
46857
|
+
const res = await fetchImpl(url, {
|
|
46858
|
+
method: "POST",
|
|
46859
|
+
headers,
|
|
46860
|
+
body
|
|
46861
|
+
});
|
|
46862
|
+
const text = await res.text();
|
|
46863
|
+
try {
|
|
46864
|
+
return JSON.parse(text);
|
|
46865
|
+
} catch {
|
|
46866
|
+
throw new DreameApiError(`${ctx} returned non-JSON (status ${res.status})`, res.status);
|
|
46867
|
+
}
|
|
46868
|
+
}
|
|
46869
|
+
async function loginByOauth(input) {
|
|
46870
|
+
const host = livingAccountHost(input.regionId);
|
|
46871
|
+
const riskControlInfo = {
|
|
46872
|
+
createIfNotExist: "true",
|
|
46873
|
+
sdkVersion: "3.4.0.27",
|
|
46874
|
+
openaccountVersion: "3.4.0.27",
|
|
46875
|
+
platformName: "iOS",
|
|
46876
|
+
netType: "wifi",
|
|
46877
|
+
brand: "Apple",
|
|
46878
|
+
USE_H5_NC: "true",
|
|
46879
|
+
deviceId: input.deviceId ?? (0, crypto$1.randomUUID)(),
|
|
46880
|
+
locale: "en-US",
|
|
46881
|
+
platformVersion: "26.5.2",
|
|
46882
|
+
platform: "iPhone",
|
|
46883
|
+
appKey: ALIYUN_APP_KEY,
|
|
46884
|
+
USE_OA_PWD_ENCRYPT: "true",
|
|
46885
|
+
model: "iPhone16,2",
|
|
46886
|
+
utdid: input.utdid ?? (0, crypto$1.randomUUID)().replace(/-/g, "").slice(0, 20),
|
|
46887
|
+
appVersion: "1895"
|
|
46888
|
+
};
|
|
46889
|
+
const signed = signApiGatewayRequest({
|
|
46890
|
+
method: "POST",
|
|
46891
|
+
host,
|
|
46892
|
+
path: "/api/prd/loginbyoauth.json",
|
|
46893
|
+
appKey: ALIYUN_APP_KEY,
|
|
46894
|
+
appSecret: ALIYUN_APP_SECRET,
|
|
46895
|
+
form: { loginByOauthRequest: JSON.stringify({
|
|
46896
|
+
riskControlInfo,
|
|
46897
|
+
oauthPlateform: 23,
|
|
46898
|
+
oauthAppKey: ALIYUN_APP_KEY,
|
|
46899
|
+
authCode: input.authCode
|
|
46900
|
+
}) }
|
|
46901
|
+
});
|
|
46902
|
+
const json = await postJson(`https://${host}/api/prd/loginbyoauth.json`, signed.headers, signed.body, input.fetchImpl ?? defaultFetch, "loginbyoauth");
|
|
46903
|
+
const r = LoginByOauthSchema.parse(json).data.data.loginSuccessResult;
|
|
46904
|
+
return {
|
|
46905
|
+
sid: r.sid,
|
|
46906
|
+
refreshToken: r.refreshToken ?? "",
|
|
46907
|
+
openId: r.openAccount?.openId ?? ""
|
|
46908
|
+
};
|
|
46909
|
+
}
|
|
46910
|
+
async function createIotSession(input) {
|
|
46911
|
+
const host = apiIotHost(input.regionId);
|
|
46912
|
+
const path = "/account/createSessionByAuthCode";
|
|
46913
|
+
const signed = signApiGatewayRequest({
|
|
46914
|
+
method: "POST",
|
|
46915
|
+
host,
|
|
46916
|
+
path,
|
|
46917
|
+
appKey: ALIYUN_APP_KEY,
|
|
46918
|
+
appSecret: ALIYUN_APP_SECRET,
|
|
46919
|
+
json: {
|
|
46920
|
+
id: (0, crypto$1.randomUUID)().toUpperCase(),
|
|
46921
|
+
params: { request: {
|
|
46922
|
+
appKey: ALIYUN_APP_KEY,
|
|
46923
|
+
authCode: input.sid,
|
|
46924
|
+
accountType: "OA_SESSION"
|
|
46925
|
+
} },
|
|
46926
|
+
request: {
|
|
46927
|
+
apiVer: "1.0.4",
|
|
46928
|
+
language: "en-US",
|
|
46929
|
+
appKey: ALIYUN_APP_KEY
|
|
46930
|
+
},
|
|
46931
|
+
version: "1.0.0"
|
|
46932
|
+
},
|
|
46933
|
+
extraCaHeaders: { "X-Ca-Stage": "RELEASE" }
|
|
46934
|
+
});
|
|
46935
|
+
const json = await postJson(`https://${host}${path}`, signed.headers, signed.body, input.fetchImpl ?? defaultFetch, "createSessionByAuthCode");
|
|
46936
|
+
const parsed = CreateSessionSchema.parse(json);
|
|
46937
|
+
if (parsed.code !== 200 && parsed.code !== 0) throw new DreameApiError(`createSession rejected: code=${parsed.code} msg=${parsed.message ?? "?"}`, 200, parsed);
|
|
46938
|
+
const d = parsed.data;
|
|
46939
|
+
return {
|
|
46940
|
+
iotToken: d.iotToken,
|
|
46941
|
+
refreshToken: d.refreshToken ?? "",
|
|
46942
|
+
identityId: d.identityId ?? "",
|
|
46943
|
+
expiresAt: Date.now() + (d.iotTokenExpire ?? 72e3) * 1e3
|
|
46944
|
+
};
|
|
46945
|
+
}
|
|
46946
|
+
var VISION_DEVICE_OFFLINE = 9201;
|
|
46947
|
+
var StreamQueryResponseSchema = objectType({
|
|
46948
|
+
code: numberType().optional(),
|
|
46949
|
+
message: stringType().nullish(),
|
|
46950
|
+
data: objectType({
|
|
46951
|
+
typeList: arrayType(stringType()).optional(),
|
|
46952
|
+
relayUrl: stringType().optional(),
|
|
46953
|
+
relayDecryptKey: objectType({
|
|
46954
|
+
iv: stringType().optional(),
|
|
46955
|
+
key: stringType().optional()
|
|
46956
|
+
}).passthrough().optional(),
|
|
46957
|
+
p2pInfo: objectType({
|
|
46958
|
+
stunUrl: stringType().optional(),
|
|
46959
|
+
alterStunUrl: stringType().optional(),
|
|
46960
|
+
signalUrl: stringType().optional(),
|
|
46961
|
+
portDetectStunUrlList: arrayType(stringType()).optional()
|
|
46962
|
+
}).passthrough().optional(),
|
|
46963
|
+
supportVisionPtz: booleanType().optional()
|
|
46964
|
+
}).passthrough().optional()
|
|
46965
|
+
}).passthrough();
|
|
46966
|
+
async function streamQuery(input) {
|
|
46967
|
+
const appKey = input.appKey ?? ALIYUN_APP_KEY;
|
|
46968
|
+
const appSecret = input.appSecret ?? ALIYUN_APP_SECRET;
|
|
46969
|
+
const host = apiIotHost(input.regionId);
|
|
46970
|
+
const signed = signApiGatewayRequest({
|
|
46971
|
+
method: "POST",
|
|
46972
|
+
host,
|
|
46973
|
+
path: VISION_STREAM_QUERY,
|
|
46974
|
+
appKey,
|
|
46975
|
+
appSecret,
|
|
46976
|
+
json: {
|
|
46977
|
+
id: crypto.randomUUID().toUpperCase(),
|
|
46978
|
+
params: {
|
|
46979
|
+
streamType: input.streamType ?? 0,
|
|
46980
|
+
needDomainName: true,
|
|
46981
|
+
relayEncryptType: 0,
|
|
46982
|
+
clientType: "iOS",
|
|
46983
|
+
iotId: input.iotId,
|
|
46984
|
+
enablePortPredict: true,
|
|
46985
|
+
enableWebSocket: true,
|
|
46986
|
+
forceIFrame: true,
|
|
46987
|
+
cacheDuration: 3e3,
|
|
46988
|
+
relayEncrypted: input.relayEncrypted ?? false
|
|
46989
|
+
},
|
|
46990
|
+
request: {
|
|
46991
|
+
language: "en-US",
|
|
46992
|
+
appKey,
|
|
46993
|
+
iotToken: input.iotToken,
|
|
46994
|
+
apiVer: "2.2.0"
|
|
46995
|
+
},
|
|
46996
|
+
version: "1.0.0"
|
|
46997
|
+
},
|
|
46998
|
+
extraCaHeaders: { "X-Ca-Stage": "RELEASE" }
|
|
46999
|
+
});
|
|
47000
|
+
const res = await (input.fetchImpl ?? defaultFetch)(`https://${host}${VISION_STREAM_QUERY}`, {
|
|
47001
|
+
method: "POST",
|
|
47002
|
+
headers: signed.headers,
|
|
47003
|
+
body: signed.body,
|
|
47004
|
+
...input.signal ? { signal: input.signal } : {}
|
|
47005
|
+
});
|
|
47006
|
+
const text = await res.text();
|
|
47007
|
+
let json;
|
|
47008
|
+
try {
|
|
47009
|
+
json = JSON.parse(text);
|
|
47010
|
+
} catch {
|
|
47011
|
+
throw new DreameApiError(`stream/query returned non-JSON (status ${res.status})`, res.status);
|
|
47012
|
+
}
|
|
47013
|
+
const parsed = StreamQueryResponseSchema.parse(json);
|
|
47014
|
+
if (parsed.code === VISION_DEVICE_OFFLINE) throw new DreameDeviceOfflineError(`stream/query: device video agent offline (code ${VISION_DEVICE_OFFLINE}) \u2014 retry to wake`, res.status, parsed);
|
|
47015
|
+
if (parsed.code !== 200 && parsed.code !== 0) throw new DreameApiError(`stream/query rejected: code=${parsed.code} msg=${parsed.message ?? "?"}`, res.status, parsed);
|
|
47016
|
+
const d = parsed.data ?? {};
|
|
47017
|
+
const dk = d.relayDecryptKey;
|
|
47018
|
+
const hasKey = dk && dk.iv && dk.key;
|
|
47019
|
+
const p = d.p2pInfo;
|
|
47020
|
+
const p2pInfo = p ? {
|
|
47021
|
+
...p.stunUrl !== void 0 ? { stunUrl: p.stunUrl } : {},
|
|
47022
|
+
...p.alterStunUrl !== void 0 ? { alterStunUrl: p.alterStunUrl } : {},
|
|
47023
|
+
...p.signalUrl !== void 0 ? { signalUrl: p.signalUrl } : {},
|
|
47024
|
+
...p.portDetectStunUrlList !== void 0 ? { portDetectStunUrlList: p.portDetectStunUrlList } : {}
|
|
47025
|
+
} : null;
|
|
47026
|
+
return {
|
|
47027
|
+
typeList: d.typeList ?? [],
|
|
47028
|
+
relayUrl: d.relayUrl ?? null,
|
|
47029
|
+
relayDecryptKey: hasKey ? {
|
|
47030
|
+
iv: dk.iv,
|
|
47031
|
+
key: dk.key
|
|
47032
|
+
} : null,
|
|
47033
|
+
p2pInfo,
|
|
47034
|
+
...d.supportVisionPtz !== void 0 ? { supportVisionPtz: d.supportVisionPtz } : {}
|
|
47035
|
+
};
|
|
47036
|
+
}
|
|
47037
|
+
var DEFAULT_WAKE_TIMEOUT_MS = 3e4;
|
|
47038
|
+
var DEFAULT_WAKE_POLL_MS = 2e3;
|
|
47039
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
47040
|
+
var REFRESH_LEEWAY_MS = 6e4;
|
|
47041
|
+
var DreameVideoSession = class {
|
|
47042
|
+
#session;
|
|
47043
|
+
#region;
|
|
47044
|
+
#regionId;
|
|
47045
|
+
#fetchImpl;
|
|
47046
|
+
#iot = null;
|
|
47047
|
+
#inFlight = null;
|
|
47048
|
+
constructor(input) {
|
|
47049
|
+
this.#session = input.session;
|
|
47050
|
+
this.#region = input.region;
|
|
47051
|
+
this.#regionId = ALIYUN_REGION_ID[input.region];
|
|
47052
|
+
this.#fetchImpl = input.fetchImpl;
|
|
47053
|
+
}
|
|
47054
|
+
/** The resolved Aliyun IoT region id (e.g. `eu-central-1`). */
|
|
47055
|
+
get regionId() {
|
|
47056
|
+
return this.#regionId;
|
|
47057
|
+
}
|
|
47058
|
+
/** Mint (or reuse) a valid Aliyun IoT session, refreshing before expiry. */
|
|
47059
|
+
async ensureIotSession() {
|
|
47060
|
+
const cur = this.#iot;
|
|
47061
|
+
if (cur && Date.now() < cur.expiresAt - REFRESH_LEEWAY_MS) return cur;
|
|
47062
|
+
if (this.#inFlight) return this.#inFlight;
|
|
47063
|
+
this.#inFlight = this.#mint();
|
|
47064
|
+
try {
|
|
47065
|
+
this.#iot = await this.#inFlight;
|
|
47066
|
+
return this.#iot;
|
|
47067
|
+
} finally {
|
|
47068
|
+
this.#inFlight = null;
|
|
47069
|
+
}
|
|
47070
|
+
}
|
|
47071
|
+
async #mint() {
|
|
47072
|
+
return createIotSession({
|
|
47073
|
+
sid: (await loginByOauth({
|
|
47074
|
+
authCode: await getAliyunAuthCode({
|
|
47075
|
+
session: this.#session,
|
|
47076
|
+
region: this.#region,
|
|
47077
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
47078
|
+
}),
|
|
47079
|
+
regionId: this.#regionId,
|
|
47080
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
47081
|
+
})).sid,
|
|
47082
|
+
regionId: this.#regionId,
|
|
47083
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
47084
|
+
});
|
|
47085
|
+
}
|
|
47086
|
+
/**
|
|
47087
|
+
* Mint a fresh LinkVisual stream descriptor for a device. `encrypted:false`
|
|
47088
|
+
* (default) yields a plain RTMP relay URL directly playable by ffmpeg.
|
|
47089
|
+
*
|
|
47090
|
+
* A camera that has been idle reports "device offline" until the query wakes
|
|
47091
|
+
* its video agent via the LinkVisual cloud. This transparently retries on that
|
|
47092
|
+
* (up to `wakeTimeoutMs`, default 30s) so a single call returns a live stream;
|
|
47093
|
+
* pass `wakeTimeoutMs: 0` to fail fast instead.
|
|
47094
|
+
*/
|
|
47095
|
+
async getStreamInfo(iotId, opts) {
|
|
47096
|
+
const deadline = Date.now() + (opts?.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS);
|
|
47097
|
+
const pollMs = opts?.pollMs ?? DEFAULT_WAKE_POLL_MS;
|
|
47098
|
+
for (;;) {
|
|
47099
|
+
const iot = await this.ensureIotSession();
|
|
47100
|
+
try {
|
|
47101
|
+
return await streamQuery({
|
|
47102
|
+
regionId: this.#regionId,
|
|
47103
|
+
iotToken: iot.iotToken,
|
|
47104
|
+
iotId,
|
|
47105
|
+
relayEncrypted: opts?.encrypted ?? false,
|
|
47106
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
47107
|
+
});
|
|
47108
|
+
} catch (err) {
|
|
47109
|
+
if (err instanceof DreameDeviceOfflineError && Date.now() + pollMs < deadline) {
|
|
47110
|
+
await sleep(pollMs);
|
|
47111
|
+
continue;
|
|
47112
|
+
}
|
|
47113
|
+
throw err;
|
|
47114
|
+
}
|
|
47115
|
+
}
|
|
47116
|
+
}
|
|
47117
|
+
/**
|
|
47118
|
+
* Convenience: mint a fresh plain RTMP relay URL for a device, or `null` when
|
|
47119
|
+
* the device is not currently streaming (no relay available).
|
|
47120
|
+
*/
|
|
47121
|
+
async getRtmpUrl(iotId) {
|
|
47122
|
+
return (await this.getStreamInfo(iotId, { encrypted: false })).relayUrl;
|
|
47123
|
+
}
|
|
47124
|
+
};
|
|
47125
|
+
var MONITOR_SIID = 10001;
|
|
47126
|
+
var MONITOR_AIID = {
|
|
47127
|
+
/** Stream lifecycle + keep-alive (monitor start/end, keep_alive). */
|
|
47128
|
+
CAMERA_OPERATE: 1,
|
|
47129
|
+
/** Two-way intercom start/end. */
|
|
47130
|
+
VOICE_OPERATE: 2,
|
|
47131
|
+
/** Generic property-scoped operations. */
|
|
47132
|
+
PROPERTY_OPERATE: 3,
|
|
47133
|
+
/** Privacy access-code gate (launch/verify/set/reset). */
|
|
47134
|
+
ACCESS_CODE_OPERATE: 4,
|
|
47135
|
+
/** Video-vendor SDK init / switch. */
|
|
47136
|
+
VIDEO_VENDOR: 7
|
|
47137
|
+
};
|
|
47138
|
+
var MONITOR_PIID = {
|
|
47139
|
+
MONITOR_STATUS: 1,
|
|
47140
|
+
MONITOR_AUDIO_STATUS: 2,
|
|
47141
|
+
MONITOR_RECORD_STATUS: 4,
|
|
47142
|
+
TAKE_PHOTO: 5,
|
|
47143
|
+
KEEP_ALIVE: 6,
|
|
47144
|
+
MONITOR_FAULTS: 7,
|
|
47145
|
+
FILL_LIGHT_SET: 9,
|
|
47146
|
+
CAMERA_LIGHT_SWITCH: 10,
|
|
47147
|
+
VIDEO_VENDOR_STATUS: 11,
|
|
47148
|
+
GET_PROPERTY: 99,
|
|
47149
|
+
PERSON_DATA: 110,
|
|
47150
|
+
VIDEO_OBSTACLE_SWITCH: 111,
|
|
47151
|
+
VIDEO_OBSTACLE_DATA: 112,
|
|
47152
|
+
MONITOR_TASK_STATUS: 103,
|
|
47153
|
+
UPLOAD_STATUS: 1003,
|
|
47154
|
+
GET_ACCESS_CODE: 1100,
|
|
47155
|
+
SET_ACCESS_CODE: 1101,
|
|
47156
|
+
VERIFY_ACCESS_CODE: 1102,
|
|
47157
|
+
MONITOR_REST_MODE: 1103,
|
|
47158
|
+
SET_ACCESS_HOT_STATUS: 1104,
|
|
47159
|
+
MONITOR_SPACE: 2003
|
|
47160
|
+
};
|
|
47161
|
+
var MONITOR_VENDOR_TOKEN = {
|
|
47162
|
+
ali: "alify",
|
|
47163
|
+
tx: "tx"
|
|
47164
|
+
};
|
|
47165
|
+
var VACUUM_MOVE = {
|
|
47166
|
+
/** Remote-drive state property (set_properties), sent ~1 Hz while held. */
|
|
47167
|
+
REMOTE_STATE_PIID: 15,
|
|
47168
|
+
/** Work-mode action instance (go-to-point, cruise, person-follow start). */
|
|
47169
|
+
WORK_AIID: 1,
|
|
47170
|
+
/** Work-mode stop action instance (person-follow stop). */
|
|
47171
|
+
STOP_AIID: 2,
|
|
47172
|
+
/** piid carrying the work-mode selector value in the WORK action. */
|
|
47173
|
+
MODE_PIID: 1,
|
|
47174
|
+
/** piid carrying the point-info JSON in the WORK action. */
|
|
47175
|
+
POINT_PIID: 10,
|
|
47176
|
+
/** Work-mode selector values. */
|
|
47177
|
+
MODE_SPOT: 23,
|
|
47178
|
+
MODE_CRUISE: 22,
|
|
47179
|
+
MODE_PERSON_FOLLOW: 26,
|
|
47180
|
+
MODE_SPOT_CLEAN: 20,
|
|
47181
|
+
/** Auto-switch setter property (fill-light auto/full toggle lives here). */
|
|
47182
|
+
AUTO_SWITCH_PIID: 50
|
|
47183
|
+
};
|
|
47184
|
+
var VACUUM_CHARGE = {
|
|
47185
|
+
siid: 3,
|
|
47186
|
+
aiid: 1
|
|
47187
|
+
};
|
|
47188
|
+
var VOICE_SIID = 7;
|
|
47189
|
+
var VOICE_LOCATE_AIID = 1;
|
|
47190
|
+
var VOICE_PLAY_SOUND_AIID = 2;
|
|
47191
|
+
var VOICE_SOUND_PIID = 4;
|
|
47192
|
+
var VACUUM_LOCATE = {
|
|
47193
|
+
siid: VOICE_SIID,
|
|
47194
|
+
aiid: VOICE_LOCATE_AIID
|
|
47195
|
+
};
|
|
47196
|
+
var PET_SOUNDS = {
|
|
47197
|
+
meow: 684,
|
|
47198
|
+
bark: 685,
|
|
47199
|
+
footsteps: 686,
|
|
47200
|
+
purring: 687,
|
|
47201
|
+
tickTock: 688
|
|
47202
|
+
};
|
|
47203
|
+
var VACUUM_ACTIONS = {
|
|
47204
|
+
startClean: {
|
|
47205
|
+
siid: 2,
|
|
47206
|
+
aiid: 1
|
|
47207
|
+
},
|
|
47208
|
+
pauseClean: {
|
|
47209
|
+
siid: 2,
|
|
47210
|
+
aiid: 2
|
|
47211
|
+
},
|
|
47212
|
+
stopClean: {
|
|
47213
|
+
siid: 4,
|
|
47214
|
+
aiid: 2
|
|
47215
|
+
},
|
|
47216
|
+
dockWash: {
|
|
47217
|
+
siid: 4,
|
|
47218
|
+
aiid: 4
|
|
47219
|
+
},
|
|
47220
|
+
autoEmpty: {
|
|
47221
|
+
siid: 15,
|
|
47222
|
+
aiid: 1
|
|
47223
|
+
}
|
|
47224
|
+
};
|
|
47225
|
+
function makeMonitorSession(accountId, now = Date.now()) {
|
|
47226
|
+
return (0, crypto$1.createHash)("md5").update(`${accountId}_${now}`).digest("hex");
|
|
47227
|
+
}
|
|
47228
|
+
function hashAccessCode(code) {
|
|
47229
|
+
return (0, crypto$1.createHash)("sha256").update(code).digest("hex");
|
|
47230
|
+
}
|
|
47231
|
+
function buildActionInput(piid, params, session) {
|
|
47232
|
+
return {
|
|
47233
|
+
piid,
|
|
47234
|
+
value: JSON.stringify({
|
|
47235
|
+
...params ?? {},
|
|
47236
|
+
session
|
|
47237
|
+
})
|
|
47238
|
+
};
|
|
47239
|
+
}
|
|
47240
|
+
function startMonitorParams(iotId, vendor, area) {
|
|
47241
|
+
return {
|
|
47242
|
+
token: MONITOR_VENDOR_TOKEN[vendor],
|
|
47243
|
+
channelId: iotId,
|
|
47244
|
+
area,
|
|
47245
|
+
operType: "monitor",
|
|
47246
|
+
operation: "start"
|
|
47247
|
+
};
|
|
47248
|
+
}
|
|
47249
|
+
function stopMonitorParams() {
|
|
47250
|
+
return {
|
|
47251
|
+
operType: "monitor",
|
|
47252
|
+
operation: "end"
|
|
47253
|
+
};
|
|
47254
|
+
}
|
|
47255
|
+
function keepAliveParams(videoStatus) {
|
|
47256
|
+
return {
|
|
47257
|
+
operType: "keep_alive",
|
|
47258
|
+
videoStatus
|
|
47259
|
+
};
|
|
47260
|
+
}
|
|
47261
|
+
function accessCodeLaunchParams() {
|
|
47262
|
+
return { open: true };
|
|
47263
|
+
}
|
|
47264
|
+
function verifyAccessCodeParams(code, lazymode = 0) {
|
|
47265
|
+
return {
|
|
47266
|
+
oldcode: hashAccessCode(code),
|
|
47267
|
+
lazymode
|
|
47268
|
+
};
|
|
47269
|
+
}
|
|
47270
|
+
function intercomStartParams(opts) {
|
|
47271
|
+
return {
|
|
47272
|
+
operType: "intercom",
|
|
47273
|
+
operation: "start",
|
|
47274
|
+
...opts?.needRecordSound !== void 0 ? { need_record_sound: opts.needRecordSound } : {},
|
|
47275
|
+
...opts?.videoCall ? { phone: 1 } : {}
|
|
47276
|
+
};
|
|
47277
|
+
}
|
|
47278
|
+
function intercomStopParams() {
|
|
47279
|
+
return {
|
|
47280
|
+
operType: "intercom",
|
|
47281
|
+
operation: "end"
|
|
47282
|
+
};
|
|
47283
|
+
}
|
|
47284
|
+
function fillLightParams(value) {
|
|
47285
|
+
return { value: String(value) };
|
|
47286
|
+
}
|
|
47287
|
+
function takePhotoParams() {
|
|
47288
|
+
return {
|
|
47289
|
+
operType: "takephoto",
|
|
47290
|
+
operation: "start"
|
|
47291
|
+
};
|
|
47292
|
+
}
|
|
47293
|
+
function remoteDriveValue(spdv, spdw, now = Date.now()) {
|
|
47294
|
+
return JSON.stringify({
|
|
47295
|
+
spdv,
|
|
47296
|
+
spdw,
|
|
47297
|
+
audio: "false",
|
|
47298
|
+
random: Math.floor(Math.random() * 1e3),
|
|
47299
|
+
timestamp: now
|
|
47300
|
+
});
|
|
47301
|
+
}
|
|
47302
|
+
var DRIVE_DIRECTIONS = {
|
|
47303
|
+
forward: [200, 0],
|
|
47304
|
+
left: [0, 45],
|
|
47305
|
+
right: [0, -45],
|
|
47306
|
+
turnAround: [0, 180],
|
|
47307
|
+
stop: [0, 0]
|
|
47308
|
+
};
|
|
47309
|
+
function actionCode(res) {
|
|
47310
|
+
const r = res;
|
|
47311
|
+
return typeof r?.code === "number" ? r.code : null;
|
|
47312
|
+
}
|
|
47313
|
+
function firstOutValue(res) {
|
|
47314
|
+
const r = res;
|
|
47315
|
+
const out = r?.out ?? r?.result?.out;
|
|
47316
|
+
return Array.isArray(out) && out.length > 0 ? out[0]?.value : void 0;
|
|
47317
|
+
}
|
|
47318
|
+
var DEFAULT_KEEP_ALIVE_MS = 1e4;
|
|
47319
|
+
var DEFAULT_AREA = "4";
|
|
47320
|
+
var DreameCameraController = class {
|
|
47321
|
+
#device;
|
|
47322
|
+
#relay;
|
|
47323
|
+
#iotId;
|
|
47324
|
+
#accountId;
|
|
47325
|
+
#vendor;
|
|
47326
|
+
#accessCode;
|
|
47327
|
+
#area;
|
|
47328
|
+
#keepAliveIntervalMs;
|
|
47329
|
+
#wakeTimeoutMs;
|
|
47330
|
+
#onKeepAliveError;
|
|
47331
|
+
#log;
|
|
47332
|
+
#session = null;
|
|
47333
|
+
#keepAliveTimer = null;
|
|
47334
|
+
#opened = false;
|
|
47335
|
+
constructor(input) {
|
|
47336
|
+
this.#device = input.device;
|
|
47337
|
+
this.#relay = input.relay;
|
|
47338
|
+
this.#iotId = input.iotId;
|
|
47339
|
+
this.#accountId = input.accountId;
|
|
47340
|
+
this.#vendor = input.vendor ?? "ali";
|
|
47341
|
+
this.#accessCode = input.accessCode;
|
|
47342
|
+
this.#area = input.area ?? DEFAULT_AREA;
|
|
47343
|
+
this.#keepAliveIntervalMs = input.keepAliveIntervalMs ?? DEFAULT_KEEP_ALIVE_MS;
|
|
47344
|
+
this.#wakeTimeoutMs = input.wakeTimeoutMs;
|
|
47345
|
+
this.#onKeepAliveError = input.onKeepAliveError;
|
|
47346
|
+
this.#log = input.log ?? (() => {});
|
|
47347
|
+
}
|
|
47348
|
+
#logLine(msg) {
|
|
47349
|
+
this.#log(`[nodedreame:monitor] ${msg}`);
|
|
47350
|
+
}
|
|
47351
|
+
/** True while a monitor session is active (between {@link open} and {@link close}). */
|
|
47352
|
+
get isOpen() {
|
|
47353
|
+
return this.#opened;
|
|
47354
|
+
}
|
|
47355
|
+
/** Send one camera action, resolving to the raw device reply. */
|
|
47356
|
+
async #action(aiid, piid, params) {
|
|
47357
|
+
if (!this.#session) throw new DreameError("camera controller has no session; call open() first");
|
|
47358
|
+
const input = buildActionInput(piid, params, this.#session);
|
|
47359
|
+
return this.#device.callAction(MONITOR_SIID, aiid, [input]);
|
|
47360
|
+
}
|
|
47361
|
+
/**
|
|
47362
|
+
* Run the full cold-start sequence and return a live relay URL. Starts the
|
|
47363
|
+
* keep-alive loop; the caller MUST {@link close} when done to release the
|
|
47364
|
+
* device. Throws if the monitor cannot be started or no relay comes up.
|
|
47365
|
+
*/
|
|
47366
|
+
async open() {
|
|
47367
|
+
if (this.#opened) throw new DreameError("camera stream already open");
|
|
47368
|
+
this.#session = makeMonitorSession(this.#accountId);
|
|
47369
|
+
this.#logLine(`open: vendor=${this.#vendor} iotId=${this.#iotId} area=${this.#area}`);
|
|
47370
|
+
await this.#tryPrime();
|
|
47371
|
+
const encryptionKey = asString(firstOutValue(await this.#startMonitorWithGate()));
|
|
47372
|
+
this.#logLine(`startMonitor accepted (encryptionKey=${encryptionKey ? "yes" : "no"})`);
|
|
47373
|
+
this.#opened = true;
|
|
47374
|
+
this.#startKeepAlive();
|
|
47375
|
+
let relayUrl;
|
|
47376
|
+
try {
|
|
47377
|
+
relayUrl = (await this.#relay.getStreamInfo(this.#iotId, {
|
|
47378
|
+
encrypted: false,
|
|
47379
|
+
...this.#wakeTimeoutMs !== void 0 ? { wakeTimeoutMs: this.#wakeTimeoutMs } : {}
|
|
47380
|
+
})).relayUrl;
|
|
47381
|
+
} catch (err) {
|
|
47382
|
+
this.#logLine(`relay mint failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
47383
|
+
await this.close();
|
|
47384
|
+
throw err;
|
|
47385
|
+
}
|
|
47386
|
+
if (!relayUrl) {
|
|
47387
|
+
await this.close();
|
|
47388
|
+
throw new DreameError("monitor started but no relay URL was returned");
|
|
47389
|
+
}
|
|
47390
|
+
this.#logLine("relay up");
|
|
47391
|
+
return {
|
|
47392
|
+
rtmpUrl: relayUrl,
|
|
47393
|
+
encryptionKey
|
|
47394
|
+
};
|
|
47395
|
+
}
|
|
47396
|
+
async #tryPrime() {
|
|
47397
|
+
try {
|
|
47398
|
+
await this.#action(MONITOR_AIID.ACCESS_CODE_OPERATE, MONITOR_PIID.GET_ACCESS_CODE, accessCodeLaunchParams());
|
|
47399
|
+
} catch {}
|
|
47400
|
+
try {
|
|
47401
|
+
await this.#action(MONITOR_AIID.VIDEO_VENDOR, MONITOR_PIID.VIDEO_VENDOR_STATUS, null);
|
|
47402
|
+
} catch {}
|
|
47403
|
+
}
|
|
47404
|
+
async #startMonitorWithGate() {
|
|
47405
|
+
const params = startMonitorParams(this.#iotId, this.#vendor, this.#area);
|
|
47406
|
+
let res = await this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.MONITOR_STATUS, params);
|
|
47407
|
+
if (actionCode(res) === 0) return res;
|
|
47408
|
+
this.#logLine(`startMonitor refused (code ${actionCode(res) ?? "?"}); verifying access code`);
|
|
47409
|
+
if (this.#accessCode) {
|
|
47410
|
+
await this.#action(MONITOR_AIID.ACCESS_CODE_OPERATE, MONITOR_PIID.VERIFY_ACCESS_CODE, verifyAccessCodeParams(this.#accessCode));
|
|
47411
|
+
res = await this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.MONITOR_STATUS, params);
|
|
47412
|
+
}
|
|
47413
|
+
if (actionCode(res) !== 0) throw new DreameError(`startMonitor refused (code ${actionCode(res) ?? "?"})` + (this.#accessCode ? "" : "; device likely requires an accessCode"));
|
|
47414
|
+
return res;
|
|
47415
|
+
}
|
|
47416
|
+
#startKeepAlive() {
|
|
47417
|
+
this.#stopKeepAlive();
|
|
47418
|
+
this.#keepAliveTimer = setInterval(() => {
|
|
47419
|
+
this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.KEEP_ALIVE, keepAliveParams("opened")).catch((err) => this.#onKeepAliveError?.(err));
|
|
47420
|
+
}, this.#keepAliveIntervalMs);
|
|
47421
|
+
this.#keepAliveTimer.unref?.();
|
|
47422
|
+
}
|
|
47423
|
+
#stopKeepAlive() {
|
|
47424
|
+
if (this.#keepAliveTimer) {
|
|
47425
|
+
clearInterval(this.#keepAliveTimer);
|
|
47426
|
+
this.#keepAliveTimer = null;
|
|
47427
|
+
}
|
|
47428
|
+
}
|
|
47429
|
+
/**
|
|
47430
|
+
* Mint a FRESH single-use RTMP relay URL for the already-open monitor. Each
|
|
47431
|
+
* consumer needs its own relay URL, so call this once per ffmpeg/consumer.
|
|
47432
|
+
* Throws if the stream is not open.
|
|
47433
|
+
*/
|
|
47434
|
+
async mintRelayUrl() {
|
|
47435
|
+
if (!this.#opened) throw new DreameError("camera stream is not open; call open() first");
|
|
47436
|
+
const info = await this.#relay.getStreamInfo(this.#iotId, {
|
|
47437
|
+
encrypted: false,
|
|
47438
|
+
...this.#wakeTimeoutMs !== void 0 ? { wakeTimeoutMs: this.#wakeTimeoutMs } : {}
|
|
47439
|
+
});
|
|
47440
|
+
if (!info.relayUrl) throw new DreameError("no relay URL available for the open monitor");
|
|
47441
|
+
return info.relayUrl;
|
|
47442
|
+
}
|
|
47443
|
+
/** Start the two-way intercom session (control plane only; see module docs on mic uplink). */
|
|
47444
|
+
async startIntercom(opts) {
|
|
47445
|
+
return this.#action(MONITOR_AIID.VOICE_OPERATE, MONITOR_PIID.MONITOR_AUDIO_STATUS, intercomStartParams(opts));
|
|
47446
|
+
}
|
|
47447
|
+
/** Stop the two-way intercom session. */
|
|
47448
|
+
async stopIntercom() {
|
|
47449
|
+
return this.#action(MONITOR_AIID.VOICE_OPERATE, MONITOR_PIID.MONITOR_AUDIO_STATUS, intercomStopParams());
|
|
47450
|
+
}
|
|
47451
|
+
/** Set the fill-light brightness (40–100 = manual %, below 40 / 101 = auto). */
|
|
47452
|
+
async setFillLight(value) {
|
|
47453
|
+
return this.#action(MONITOR_AIID.PROPERTY_OPERATE, MONITOR_PIID.FILL_LIGHT_SET, fillLightParams(value));
|
|
47454
|
+
}
|
|
47455
|
+
/**
|
|
47456
|
+
* Device-side snapshot: the robot captures a still and uploads it to cloud
|
|
47457
|
+
* storage (read the result back from the `uploadStatus` property). Prefer a
|
|
47458
|
+
* frame-grab from the live stream for an instant image.
|
|
47459
|
+
*/
|
|
47460
|
+
async takePhoto() {
|
|
47461
|
+
return this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.TAKE_PHOTO, takePhotoParams());
|
|
47462
|
+
}
|
|
47463
|
+
/**
|
|
47464
|
+
* Remote-drive the robot ("PTZ" substitute — the camera is fixed, the robot
|
|
47465
|
+
* moves). `spdv` = forward speed, `spdw` = turn; send repeatedly (~1 Hz) while
|
|
47466
|
+
* a direction is held, then `stop`. Only valid while the stream is open.
|
|
47467
|
+
*/
|
|
47468
|
+
async drive(spdv, spdw) {
|
|
47469
|
+
this.#requireSession();
|
|
47470
|
+
if (!this.#device.setProperty) throw new DreameError("device does not support setProperty; cannot remote-drive");
|
|
47471
|
+
return this.#device.setProperty({
|
|
47472
|
+
siid: 4,
|
|
47473
|
+
piid: VACUUM_MOVE.REMOTE_STATE_PIID,
|
|
47474
|
+
value: remoteDriveValue(spdv, spdw)
|
|
47475
|
+
});
|
|
47476
|
+
}
|
|
47477
|
+
/** Remote-drive by named direction (forward/left/right/turnAround/stop). */
|
|
47478
|
+
async driveDirection(direction) {
|
|
47479
|
+
const [spdv, spdw] = DRIVE_DIRECTIONS[direction];
|
|
47480
|
+
return this.drive(spdv, spdw);
|
|
47481
|
+
}
|
|
47482
|
+
/** Start person-follow mode (robot tracks a detected person). */
|
|
47483
|
+
async startPersonFollow() {
|
|
47484
|
+
this.#requireSession();
|
|
47485
|
+
return this.#device.callAction(4, VACUUM_MOVE.WORK_AIID, [{
|
|
47486
|
+
piid: VACUUM_MOVE.MODE_PIID,
|
|
47487
|
+
value: VACUUM_MOVE.MODE_PERSON_FOLLOW
|
|
47488
|
+
}]);
|
|
47489
|
+
}
|
|
47490
|
+
/**
|
|
47491
|
+
* Universal stop for the current work mode — stops person-follow, spot-clean,
|
|
47492
|
+
* go-to-point, cruise, etc. (siid 4 / aiid 2).
|
|
47493
|
+
*/
|
|
47494
|
+
async stopWork() {
|
|
47495
|
+
return this.#device.callAction(4, VACUUM_MOVE.STOP_AIID, []);
|
|
47496
|
+
}
|
|
47497
|
+
/** Stop person-follow mode (alias for {@link stopWork}). */
|
|
47498
|
+
async stopPersonFollow() {
|
|
47499
|
+
return this.stopWork();
|
|
47500
|
+
}
|
|
47501
|
+
/** Spot-clean the robot's current position (WorkMode SpotClean). */
|
|
47502
|
+
async spotClean() {
|
|
47503
|
+
return this.#device.callAction(4, VACUUM_MOVE.WORK_AIID, [{
|
|
47504
|
+
piid: VACUUM_MOVE.MODE_PIID,
|
|
47505
|
+
value: VACUUM_MOVE.MODE_SPOT_CLEAN
|
|
47506
|
+
}]);
|
|
47507
|
+
}
|
|
47508
|
+
/** Play one of the robot's sound clips (e.g. pet sounds) by id. */
|
|
47509
|
+
async playSound(soundId) {
|
|
47510
|
+
return this.#device.callAction(VOICE_SIID, VOICE_PLAY_SOUND_AIID, [{
|
|
47511
|
+
piid: VOICE_SOUND_PIID,
|
|
47512
|
+
value: soundId
|
|
47513
|
+
}]);
|
|
47514
|
+
}
|
|
47515
|
+
/** Play a named pet-teasing sound (meow/bark/footsteps/purring/tickTock). */
|
|
47516
|
+
async playPetSound(sound) {
|
|
47517
|
+
return this.playSound(PET_SOUNDS[sound]);
|
|
47518
|
+
}
|
|
47519
|
+
/** Run a common whole-robot action (startClean/pauseClean/stopClean/dockWash/autoEmpty). */
|
|
47520
|
+
async runVacuumAction(action) {
|
|
47521
|
+
const a = VACUUM_ACTIONS[action];
|
|
47522
|
+
return this.#device.callAction(a.siid, a.aiid, []);
|
|
47523
|
+
}
|
|
47524
|
+
/** Find-pet: cruise the home looking for the pet. */
|
|
47525
|
+
async findPet() {
|
|
47526
|
+
this.#requireSession();
|
|
47527
|
+
return this.#device.callAction(4, VACUUM_MOVE.WORK_AIID, [{
|
|
47528
|
+
piid: VACUUM_MOVE.MODE_PIID,
|
|
47529
|
+
value: VACUUM_MOVE.MODE_CRUISE
|
|
47530
|
+
}, {
|
|
47531
|
+
piid: VACUUM_MOVE.POINT_PIID,
|
|
47532
|
+
value: JSON.stringify({ findpet: "" })
|
|
47533
|
+
}]);
|
|
47534
|
+
}
|
|
47535
|
+
/** Send the robot to a map point (`spoint` current, `tpoint` targets). */
|
|
47536
|
+
async goToPoint(spoint, tpoint) {
|
|
47537
|
+
this.#requireSession();
|
|
47538
|
+
return this.#device.callAction(4, VACUUM_MOVE.WORK_AIID, [{
|
|
47539
|
+
piid: VACUUM_MOVE.MODE_PIID,
|
|
47540
|
+
value: VACUUM_MOVE.MODE_SPOT
|
|
47541
|
+
}, {
|
|
47542
|
+
piid: VACUUM_MOVE.POINT_PIID,
|
|
47543
|
+
value: JSON.stringify({
|
|
47544
|
+
spoint,
|
|
47545
|
+
tpoint
|
|
47546
|
+
})
|
|
47547
|
+
}]);
|
|
47548
|
+
}
|
|
47549
|
+
/** Toggle the fill light between auto (`full=false`) and full-on (`full=true`). */
|
|
47550
|
+
async setFillLightAuto(full) {
|
|
47551
|
+
if (!this.#device.setProperty) throw new DreameError("device does not support setProperty; cannot toggle fill light");
|
|
47552
|
+
return this.#device.setProperty({
|
|
47553
|
+
siid: 4,
|
|
47554
|
+
piid: VACUUM_MOVE.AUTO_SWITCH_PIID,
|
|
47555
|
+
value: JSON.stringify({
|
|
47556
|
+
k: "FillinLight",
|
|
47557
|
+
v: full ? 1 : 0
|
|
47558
|
+
})
|
|
47559
|
+
});
|
|
47560
|
+
}
|
|
47561
|
+
/**
|
|
47562
|
+
* Send the robot back to its dock to charge ("go home"). Works regardless of
|
|
47563
|
+
* stream state — usable as a standalone command or a PTZ "home" preset.
|
|
47564
|
+
*/
|
|
47565
|
+
async returnToDock() {
|
|
47566
|
+
return this.#device.callAction(VACUUM_CHARGE.siid, VACUUM_CHARGE.aiid, []);
|
|
47567
|
+
}
|
|
47568
|
+
/** Locate the robot — it beeps. Works regardless of stream state. */
|
|
47569
|
+
async locate() {
|
|
47570
|
+
return this.#device.callAction(VACUUM_LOCATE.siid, VACUUM_LOCATE.aiid, []);
|
|
47571
|
+
}
|
|
47572
|
+
#requireSession() {
|
|
47573
|
+
if (!this.#opened) throw new DreameError("camera stream is not open; call open() first");
|
|
47574
|
+
}
|
|
47575
|
+
/** Stop the keep-alive loop and release the monitor on the device. Idempotent. */
|
|
47576
|
+
async close() {
|
|
47577
|
+
this.#stopKeepAlive();
|
|
47578
|
+
if (!this.#opened) {
|
|
47579
|
+
this.#session = null;
|
|
47580
|
+
return;
|
|
47581
|
+
}
|
|
47582
|
+
this.#opened = false;
|
|
47583
|
+
this.#logLine("close: stopping monitor");
|
|
47584
|
+
try {
|
|
47585
|
+
await this.#action(MONITOR_AIID.CAMERA_OPERATE, MONITOR_PIID.MONITOR_STATUS, stopMonitorParams());
|
|
47586
|
+
} finally {
|
|
47587
|
+
this.#session = null;
|
|
47588
|
+
}
|
|
47589
|
+
}
|
|
47590
|
+
};
|
|
47591
|
+
function asString(v) {
|
|
47592
|
+
return typeof v === "string" ? v : null;
|
|
47593
|
+
}
|
|
46614
47594
|
var HEADER_SIZE = 27;
|
|
46615
47595
|
var ANGLE_ABSENT = 32767;
|
|
46616
47596
|
var FRAME_TYPE = {
|
|
@@ -48311,6 +49291,134 @@ function drawLabel(png, cx, cy, text, color, px) {
|
|
|
48311
49291
|
originX += glyphW + gap;
|
|
48312
49292
|
}
|
|
48313
49293
|
}
|
|
49294
|
+
var NIBBLE_TO_BARS = {
|
|
49295
|
+
10: 0,
|
|
49296
|
+
11: 1,
|
|
49297
|
+
12: 2,
|
|
49298
|
+
13: 3,
|
|
49299
|
+
14: 4
|
|
49300
|
+
};
|
|
49301
|
+
var isWifiNibble = (n) => n >= 10 && n <= 14;
|
|
49302
|
+
var NIBBLE_RGBA = {
|
|
49303
|
+
2: [
|
|
49304
|
+
176,
|
|
49305
|
+
182,
|
|
49306
|
+
190,
|
|
49307
|
+
255
|
|
49308
|
+
],
|
|
49309
|
+
3: [
|
|
49310
|
+
242,
|
|
49311
|
+
244,
|
|
49312
|
+
247,
|
|
49313
|
+
255
|
|
49314
|
+
],
|
|
49315
|
+
10: [
|
|
49316
|
+
229,
|
|
49317
|
+
234,
|
|
49318
|
+
238,
|
|
49319
|
+
255
|
|
49320
|
+
],
|
|
49321
|
+
11: [
|
|
49322
|
+
217,
|
|
49323
|
+
226,
|
|
49324
|
+
239,
|
|
49325
|
+
255
|
|
49326
|
+
],
|
|
49327
|
+
12: [
|
|
49328
|
+
205,
|
|
49329
|
+
218,
|
|
49330
|
+
239,
|
|
49331
|
+
255
|
|
49332
|
+
],
|
|
49333
|
+
13: [
|
|
49334
|
+
161,
|
|
49335
|
+
189,
|
|
49336
|
+
242,
|
|
49337
|
+
255
|
|
49338
|
+
],
|
|
49339
|
+
14: [
|
|
49340
|
+
129,
|
|
49341
|
+
168,
|
|
49342
|
+
245,
|
|
49343
|
+
255
|
|
49344
|
+
]
|
|
49345
|
+
};
|
|
49346
|
+
var poseOrNull = (x, y, a) => x === 0 && y === 0 ? null : {
|
|
49347
|
+
x,
|
|
49348
|
+
y,
|
|
49349
|
+
angle: a === ANGLE_ABSENT ? 0 : a
|
|
49350
|
+
};
|
|
49351
|
+
function decodeWifiSignalMap(input, opts = {}) {
|
|
49352
|
+
const inflated = typeof input === "string" ? unwrapEnvelope(input, opts) : looksLikeBase64Zlib(input) ? unwrapEnvelope(input.toString("latin1"), opts) : input;
|
|
49353
|
+
const header = parseMapHeader(inflated);
|
|
49354
|
+
const { width, height, gridSize, left, top } = header;
|
|
49355
|
+
const pixelEnd = HEADER_SIZE + width * height;
|
|
49356
|
+
const grid = inflated.subarray(HEADER_SIZE, pixelEnd);
|
|
49357
|
+
const cells = new Uint8Array(width * height);
|
|
49358
|
+
for (let i = 0; i < cells.length; i++) cells[i] = (grid[i] ?? 0) & 15;
|
|
49359
|
+
const dimensions = {
|
|
49360
|
+
left,
|
|
49361
|
+
top,
|
|
49362
|
+
width,
|
|
49363
|
+
height,
|
|
49364
|
+
gridSize
|
|
49365
|
+
};
|
|
49366
|
+
const robot = poseOrNull(header.robotX, header.robotY, header.robotA);
|
|
49367
|
+
const dock = poseOrNull(header.chargerX, header.chargerY, header.chargerA);
|
|
49368
|
+
const signalAt = (worldX, worldY) => {
|
|
49369
|
+
if (gridSize <= 0) return null;
|
|
49370
|
+
const col = Math.floor((worldX - left) / gridSize);
|
|
49371
|
+
const row = Math.floor((worldY - top) / gridSize);
|
|
49372
|
+
if (col < 0 || col >= width || row < 0 || row >= height) return null;
|
|
49373
|
+
const nibble = cells[row * width + col] ?? 0;
|
|
49374
|
+
return isWifiNibble(nibble) ? NIBBLE_TO_BARS[nibble] : null;
|
|
49375
|
+
};
|
|
49376
|
+
return {
|
|
49377
|
+
dimensions,
|
|
49378
|
+
robot,
|
|
49379
|
+
dock,
|
|
49380
|
+
cells,
|
|
49381
|
+
signalAt,
|
|
49382
|
+
currentSignal: robot ? signalAt(robot.x, robot.y) : null
|
|
49383
|
+
};
|
|
49384
|
+
}
|
|
49385
|
+
function renderWifiSignalPng(map, opts = {}) {
|
|
49386
|
+
const scale = Math.max(1, Math.floor(opts.scale ?? 4));
|
|
49387
|
+
const { width, height, left, top, gridSize } = map.dimensions;
|
|
49388
|
+
const png = new import_png.PNG({
|
|
49389
|
+
width: width * scale,
|
|
49390
|
+
height: height * scale,
|
|
49391
|
+
colorType: 6
|
|
49392
|
+
});
|
|
49393
|
+
for (let row = 0; row < height; row++) for (let col = 0; col < width; col++) {
|
|
49394
|
+
const rgba = NIBBLE_RGBA[map.cells[row * width + col] ?? 0];
|
|
49395
|
+
if (!rgba) continue;
|
|
49396
|
+
for (let dy = 0; dy < scale; dy++) for (let dx = 0; dx < scale; dx++) {
|
|
49397
|
+
const o = col * scale + dx + (row * scale + dy) * width * scale << 2;
|
|
49398
|
+
png.data[o] = rgba[0];
|
|
49399
|
+
png.data[o + 1] = rgba[1];
|
|
49400
|
+
png.data[o + 2] = rgba[2];
|
|
49401
|
+
png.data[o + 3] = rgba[3];
|
|
49402
|
+
}
|
|
49403
|
+
}
|
|
49404
|
+
if ((opts.markRobot ?? true) && map.robot && gridSize > 0) {
|
|
49405
|
+
const cx = Math.floor((map.robot.x - left) / gridSize) * scale + Math.floor(scale / 2);
|
|
49406
|
+
const cy = Math.floor((map.robot.y - top) / gridSize) * scale + Math.floor(scale / 2);
|
|
49407
|
+
const r = Math.max(2, scale);
|
|
49408
|
+
for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
|
|
49409
|
+
if (dx * dx + dy * dy > r * r) continue;
|
|
49410
|
+
const x = cx + dx;
|
|
49411
|
+
const y = cy + dy;
|
|
49412
|
+
if (x < 0 || y < 0 || x >= width * scale || y >= height * scale) continue;
|
|
49413
|
+
const o = x + y * width * scale << 2;
|
|
49414
|
+
png.data[o] = 255;
|
|
49415
|
+
png.data[o + 1] = 69;
|
|
49416
|
+
png.data[o + 2] = 0;
|
|
49417
|
+
png.data[o + 3] = 255;
|
|
49418
|
+
}
|
|
49419
|
+
}
|
|
49420
|
+
return import_png.PNG.sync.write(png);
|
|
49421
|
+
}
|
|
48314
49422
|
var OSS_DOWNLOAD_PATH = "/dreame-user-iot/iotfile/getDownloadUrl";
|
|
48315
49423
|
var DEFAULT_TTL_MS = 1800 * 1e3;
|
|
48316
49424
|
var OssFetcher = class {
|
|
@@ -48789,11 +49897,11 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice$1 {
|
|
|
48789
49897
|
if (zones.length === 0) throw new RangeError("cleanZones: zones must not be empty");
|
|
48790
49898
|
this.#requireCap(this.#caps.canCleanPerRoom, "cleanZones", "per-room cleaning");
|
|
48791
49899
|
const { repeats, fan, water } = this.#resolveCleanOpts(opts);
|
|
48792
|
-
const areas = zones.map((
|
|
48793
|
-
Math.round(
|
|
48794
|
-
Math.round(
|
|
48795
|
-
Math.round(
|
|
48796
|
-
Math.round(
|
|
49900
|
+
const areas = zones.map((z7) => [
|
|
49901
|
+
Math.round(z7.x0),
|
|
49902
|
+
Math.round(z7.y0),
|
|
49903
|
+
Math.round(z7.x1),
|
|
49904
|
+
Math.round(z7.y1),
|
|
48797
49905
|
repeats,
|
|
48798
49906
|
fan,
|
|
48799
49907
|
water
|
|
@@ -48929,6 +50037,60 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice$1 {
|
|
|
48929
50037
|
};
|
|
48930
50038
|
}
|
|
48931
50039
|
/**
|
|
50040
|
+
* Fetch the robot's Wi-Fi signal map — the last stored coverage heatmap, with
|
|
50041
|
+
* NO robot movement. Requests it (map service `wifiMap` action, siid 6 aiid 4),
|
|
50042
|
+
* polls `PropWifiMap` (6/15) for the OSS object it advertises, fetches + decodes
|
|
50043
|
+
* it into a per-cell signal grid + geometry + robot/dock poses. The signal at
|
|
50044
|
+
* any world point (incl. the robot's own position) is a lookup — see
|
|
50045
|
+
* {@link WifiSignalMap.signalAt} / {@link WifiSignalMap.currentSignal}.
|
|
50046
|
+
*
|
|
50047
|
+
* Pass `requestFresh: false` to skip the request and read whatever is already
|
|
50048
|
+
* advertised. Throws if no Wi-Fi map exists yet (never run on this device).
|
|
50049
|
+
*/
|
|
50050
|
+
async getWifiSignalMap(input = {}) {
|
|
50051
|
+
if (input.requestFresh !== false) try {
|
|
50052
|
+
await this.callAction(6, 4, []);
|
|
50053
|
+
} catch {}
|
|
50054
|
+
const pollMs = input.pollMs ?? 2500;
|
|
50055
|
+
const deadline = Date.now() + (input.maxWaitMs ?? 2e4);
|
|
50056
|
+
let objectName = "";
|
|
50057
|
+
for (;;) {
|
|
50058
|
+
const val = (await this.refreshProperties([{
|
|
50059
|
+
siid: 6,
|
|
50060
|
+
piid: 15
|
|
50061
|
+
}]))[0]?.value;
|
|
50062
|
+
if (typeof val === "string" && val.length > 0) {
|
|
50063
|
+
objectName = val;
|
|
50064
|
+
break;
|
|
50065
|
+
}
|
|
50066
|
+
if (Date.now() + pollMs >= deadline) throw new DreameError("wifi signal map not available (none stored yet, or request timed out)");
|
|
50067
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
50068
|
+
}
|
|
50069
|
+
const filename = objectName.replace(/^\d+,/, "");
|
|
50070
|
+
return decodeWifiSignalMap(await this.#fetchMapBlob({
|
|
50071
|
+
filename,
|
|
50072
|
+
...input.fetcher ? { fetcher: input.fetcher } : {},
|
|
50073
|
+
...input.host ? { host: input.host } : {},
|
|
50074
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {},
|
|
50075
|
+
...input.signal ? { signal: input.signal } : {}
|
|
50076
|
+
}), this.#decodeOpts(input));
|
|
50077
|
+
}
|
|
50078
|
+
/**
|
|
50079
|
+
* Render the Wi-Fi signal map as a simple PNG heatmap image (weakest→strongest,
|
|
50080
|
+
* unreached grey, robot marker) — ready to display. Convenience over
|
|
50081
|
+
* {@link getWifiSignalMap} + {@link renderWifiSignalPng}.
|
|
50082
|
+
*/
|
|
50083
|
+
async getWifiSignalImage(input = {}) {
|
|
50084
|
+
return renderWifiSignalPng(await this.getWifiSignalMap(input), input);
|
|
50085
|
+
}
|
|
50086
|
+
/**
|
|
50087
|
+
* The one-call "current Wi-Fi signal": fetches the signal map and returns the
|
|
50088
|
+
* bars (1–4, 0 = unreached) at the robot's own position, or null when unknown.
|
|
50089
|
+
*/
|
|
50090
|
+
async getCurrentSignal(input = {}) {
|
|
50091
|
+
return (await this.getWifiSignalMap(input)).currentSignal;
|
|
50092
|
+
}
|
|
50093
|
+
/**
|
|
48932
50094
|
* The merge base for live P-frame streaming — always an INFLATED frame buffer.
|
|
48933
50095
|
* An I-frame (re)seeds it; each P-frame merge replaces it with the merged
|
|
48934
50096
|
* inflated buffer (so further P-frames stack). `null` until the first I-frame
|
|
@@ -48994,6 +50156,44 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice$1 {
|
|
|
48994
50156
|
this.emit("map", frame);
|
|
48995
50157
|
return frame;
|
|
48996
50158
|
}
|
|
50159
|
+
/**
|
|
50160
|
+
* Build a {@link DreameCameraController} for this device's camera, wired to
|
|
50161
|
+
* both planes it needs: the MIoT action transport (this device handle) and a
|
|
50162
|
+
* fresh Aliyun {@link DreameVideoSession} for the relay. Call
|
|
50163
|
+
* `controller.open()` to autonomously cold-start the stream and get a pullable
|
|
50164
|
+
* RTMP URL, then `controller.close()` to release it.
|
|
50165
|
+
*
|
|
50166
|
+
* The camera's per-session privacy gate means a coded camera needs its PIN:
|
|
50167
|
+
* pass `accessCode` (the code set at pairing). `iotId` is resolved from the
|
|
50168
|
+
* device's cloud video profile when not supplied.
|
|
50169
|
+
*/
|
|
50170
|
+
async createCameraController(opts) {
|
|
50171
|
+
const session = this.currentSession();
|
|
50172
|
+
const region = this.region;
|
|
50173
|
+
const relay = opts?.relay ?? new DreameVideoSession({
|
|
50174
|
+
session,
|
|
50175
|
+
region
|
|
50176
|
+
});
|
|
50177
|
+
const iotId = opts?.iotId ?? (await getDeviceVideoProfile({
|
|
50178
|
+
session,
|
|
50179
|
+
region,
|
|
50180
|
+
did: this.deviceId
|
|
50181
|
+
})).iotId;
|
|
50182
|
+
if (!iotId) throw new DreameError("device has no LinkVisual iotId; not a camera device or not provisioned");
|
|
50183
|
+
return new DreameCameraController({
|
|
50184
|
+
device: this,
|
|
50185
|
+
relay,
|
|
50186
|
+
iotId,
|
|
50187
|
+
accountId: session.uid,
|
|
50188
|
+
...opts?.vendor ? { vendor: opts.vendor } : {},
|
|
50189
|
+
...opts?.accessCode !== void 0 ? { accessCode: opts.accessCode } : {},
|
|
50190
|
+
...opts?.area ? { area: opts.area } : {},
|
|
50191
|
+
...opts?.keepAliveIntervalMs !== void 0 ? { keepAliveIntervalMs: opts.keepAliveIntervalMs } : {},
|
|
50192
|
+
...opts?.wakeTimeoutMs !== void 0 ? { wakeTimeoutMs: opts.wakeTimeoutMs } : {},
|
|
50193
|
+
...opts?.onKeepAliveError ? { onKeepAliveError: opts.onKeepAliveError } : {},
|
|
50194
|
+
...opts?.log ? { log: opts.log } : {}
|
|
50195
|
+
});
|
|
50196
|
+
}
|
|
48997
50197
|
/** Props worth seeding on start() / polling — exported for the facade. */
|
|
48998
50198
|
static DEFAULT_PROPS = [
|
|
48999
50199
|
VACUUM_PROP.STATE,
|
|
@@ -49554,7 +50754,7 @@ function isRecord3(v) {
|
|
|
49554
50754
|
function asNumber(v, fallback) {
|
|
49555
50755
|
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
49556
50756
|
}
|
|
49557
|
-
function
|
|
50757
|
+
function asString2(v, fallback) {
|
|
49558
50758
|
return typeof v === "string" ? v : fallback;
|
|
49559
50759
|
}
|
|
49560
50760
|
function mapIdFromIndex(mapIndex) {
|
|
@@ -49634,7 +50834,7 @@ function parseZones(list, includeTimes) {
|
|
|
49634
50834
|
out.push({
|
|
49635
50835
|
zoneId: asNumber(id, 0),
|
|
49636
50836
|
path: extractPathCoords(data["path"]),
|
|
49637
|
-
name:
|
|
50837
|
+
name: asString2(data["name"], ""),
|
|
49638
50838
|
zoneType: asNumber(data["type"], 0),
|
|
49639
50839
|
shapeType: includeTimes ? asNumber(data["shapeType"], 0) : 0,
|
|
49640
50840
|
area: includeTimes ? asNumber(data["area"], 0) : 0,
|
|
@@ -49653,7 +50853,7 @@ function parseSpotAreas(list) {
|
|
|
49653
50853
|
out.push({
|
|
49654
50854
|
areaId: Math.trunc(asNumber(id, 0)),
|
|
49655
50855
|
path: extractPathCoords(data["path"]),
|
|
49656
|
-
name:
|
|
50856
|
+
name: asString2(data["name"], ""),
|
|
49657
50857
|
shapeType: asNumber(data["shapeType"], 0),
|
|
49658
50858
|
area: asNumber(data["area"], 0)
|
|
49659
50859
|
});
|
|
@@ -49712,7 +50912,7 @@ function parseMowerMap(mapJsonStr) {
|
|
|
49712
50912
|
contours: parseContours(parsePolygonList(data["contours"])),
|
|
49713
50913
|
boundary: parseBoundary(data["boundary"]),
|
|
49714
50914
|
totalArea: asNumber(data["totalArea"], 0),
|
|
49715
|
-
name:
|
|
50915
|
+
name: asString2(data["name"], ""),
|
|
49716
50916
|
mapIndex,
|
|
49717
50917
|
mapId: mapIdFromIndex(mapIndex),
|
|
49718
50918
|
mowPaths: [],
|
|
@@ -50111,7 +51311,7 @@ var MowerDevice = class _MowerDevice extends BaseDevice$1 {
|
|
|
50111
51311
|
async startMowingZones(zoneIds) {
|
|
50112
51312
|
this.#requireCap(this.#caps.canMowZones, "startMowingZones", "zone mowing");
|
|
50113
51313
|
if (zoneIds.length === 0) throw new RangeError("startMowingZones: zoneIds must not be empty");
|
|
50114
|
-
return this.#sendTask(buildZonePayload(zoneIds.map((
|
|
51314
|
+
return this.#sendTask(buildZonePayload(zoneIds.map((z7) => Math.trunc(z7))));
|
|
50115
51315
|
}
|
|
50116
51316
|
/** Edge / contour mowing (2:50 o:101). Contour ids are two-int pairs [[1,0]]. */
|
|
50117
51317
|
async startMowingEdges(contourIds) {
|
|
@@ -50503,6 +51703,12 @@ objectType({
|
|
|
50503
51703
|
generatedAt: numberType()
|
|
50504
51704
|
})
|
|
50505
51705
|
});
|
|
51706
|
+
Buffer.from([
|
|
51707
|
+
0,
|
|
51708
|
+
0,
|
|
51709
|
+
0,
|
|
51710
|
+
1
|
|
51711
|
+
]);
|
|
50506
51712
|
//#endregion
|
|
50507
51713
|
//#region ../types/dist/event-category-zAv7pMUz.mjs
|
|
50508
51714
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
@@ -74306,6 +75512,21 @@ var NetworkLinkStatusSchema = object({
|
|
|
74306
75512
|
/** Ms epoch of the last observation. Lets consumers reason about freshness. */
|
|
74307
75513
|
lastUpdated: number()
|
|
74308
75514
|
});
|
|
75515
|
+
/** The slice a provider seeds before its first read: nothing is known yet. */
|
|
75516
|
+
var NETWORK_LINK_UNKNOWN = {
|
|
75517
|
+
type: "unknown",
|
|
75518
|
+
signalPercent: null,
|
|
75519
|
+
lastUpdated: 0
|
|
75520
|
+
};
|
|
75521
|
+
/**
|
|
75522
|
+
* Normalise a signal the firmware reports as BARS (0..`maxBars`) to 0..100.
|
|
75523
|
+
* Out-of-range or non-finite input is not a reading: `null`.
|
|
75524
|
+
*/
|
|
75525
|
+
function signalPercentFromBars(bars, maxBars) {
|
|
75526
|
+
if (bars === void 0 || !Number.isFinite(bars) || maxBars <= 0) return null;
|
|
75527
|
+
if (bars < 0 || bars > maxBars) return null;
|
|
75528
|
+
return Math.round(bars / maxBars * 100);
|
|
75529
|
+
}
|
|
74309
75530
|
var networkLinkCapability = {
|
|
74310
75531
|
name: "network-link",
|
|
74311
75532
|
scope: "device",
|
|
@@ -74334,11 +75555,7 @@ onStatusChanged: { data: object({
|
|
|
74334
75555
|
status: {
|
|
74335
75556
|
schema: NetworkLinkStatusSchema,
|
|
74336
75557
|
kind: "push",
|
|
74337
|
-
empty:
|
|
74338
|
-
type: "unknown",
|
|
74339
|
-
signalPercent: null,
|
|
74340
|
-
lastUpdated: 0
|
|
74341
|
-
}
|
|
75558
|
+
empty: NETWORK_LINK_UNKNOWN
|
|
74342
75559
|
},
|
|
74343
75560
|
/**
|
|
74344
75561
|
* Runtime-state slice — every provider stores the same shape under
|
|
@@ -79336,7 +80553,10 @@ var PtzMoveCommandSchema = object({
|
|
|
79336
80553
|
zoom: number().optional(),
|
|
79337
80554
|
speed: number().optional()
|
|
79338
80555
|
});
|
|
79339
|
-
|
|
80556
|
+
/** PTZ status — current position plus the camera's autofocus state.
|
|
80557
|
+
* `getPosition` / `move` still operate on the bare position; the
|
|
80558
|
+
* auto-injected `getStatus` returns this richer block. */
|
|
80559
|
+
var PtzStatusSchema = PtzPositionSchema.extend({ autofocus: boolean() });
|
|
79340
80560
|
/** Per-camera PTZ availability descriptor — drives the UI. */
|
|
79341
80561
|
var PtzOptionsSchema = object({
|
|
79342
80562
|
hasPan: boolean(),
|
|
@@ -79349,26 +80569,65 @@ var PtzOptionsSchema = object({
|
|
|
79349
80569
|
* (boolean `hasX` per the getOptions availability convention). */
|
|
79350
80570
|
hasAutofocus: boolean()
|
|
79351
80571
|
});
|
|
79352
|
-
|
|
79353
|
-
|
|
79354
|
-
|
|
79355
|
-
|
|
79356
|
-
|
|
79357
|
-
|
|
79358
|
-
|
|
79359
|
-
|
|
79360
|
-
|
|
79361
|
-
|
|
79362
|
-
|
|
79363
|
-
|
|
79364
|
-
|
|
79365
|
-
}
|
|
79366
|
-
|
|
79367
|
-
|
|
79368
|
-
|
|
79369
|
-
|
|
79370
|
-
|
|
79371
|
-
|
|
80572
|
+
var ptzCapability = {
|
|
80573
|
+
name: "ptz",
|
|
80574
|
+
scope: "device",
|
|
80575
|
+
deviceNative: true,
|
|
80576
|
+
mode: "singleton",
|
|
80577
|
+
deviceTypes: [DeviceType.Camera],
|
|
80578
|
+
deviceConfig: { ui: {
|
|
80579
|
+
kind: "widget",
|
|
80580
|
+
widgetId: "host/ptz-panel",
|
|
80581
|
+
tab: "ptz",
|
|
80582
|
+
topTab: true,
|
|
80583
|
+
label: "PTZ",
|
|
80584
|
+
order: 0
|
|
80585
|
+
} },
|
|
80586
|
+
methods: {
|
|
80587
|
+
move: method(PtzMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
80588
|
+
continuousMove: method(PtzMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
80589
|
+
stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
80590
|
+
getPresets: method(object({ deviceId: number() }), array(PtzPresetSchema)),
|
|
80591
|
+
goToPreset: method(object({
|
|
80592
|
+
deviceId: number(),
|
|
80593
|
+
presetId: string()
|
|
80594
|
+
}), _void(), { kind: "mutation" }),
|
|
80595
|
+
savePreset: method(object({
|
|
80596
|
+
deviceId: number(),
|
|
80597
|
+
presetId: string(),
|
|
80598
|
+
name: string()
|
|
80599
|
+
}), _void(), {
|
|
80600
|
+
kind: "mutation",
|
|
80601
|
+
auth: "admin"
|
|
80602
|
+
}),
|
|
80603
|
+
deletePreset: method(object({
|
|
80604
|
+
deviceId: number(),
|
|
80605
|
+
presetId: string()
|
|
80606
|
+
}), _void(), {
|
|
80607
|
+
kind: "mutation",
|
|
80608
|
+
auth: "admin"
|
|
80609
|
+
}),
|
|
80610
|
+
getOptions: method(object({ deviceId: number() }), PtzOptionsSchema),
|
|
80611
|
+
goHome: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
80612
|
+
/**
|
|
80613
|
+
* Pull the current PTZ position. Redundant with the auto-injected
|
|
80614
|
+
* `getStatus` method (see `status` below); kept for callers that
|
|
80615
|
+
* haven't migrated. Will be folded into `getStatus` in a future
|
|
80616
|
+
* cleanup pass once all consumers use the typed status surface.
|
|
80617
|
+
*/
|
|
80618
|
+
getPosition: method(object({ deviceId: number() }), PtzPositionSchema),
|
|
80619
|
+
/** Toggle the camera's autofocus. Only meaningful when
|
|
80620
|
+
* `getOptions().hasAutofocus` is true. */
|
|
80621
|
+
setAutofocus: method(object({
|
|
80622
|
+
deviceId: number(),
|
|
80623
|
+
enabled: boolean()
|
|
80624
|
+
}), _void(), { kind: "mutation" })
|
|
80625
|
+
},
|
|
80626
|
+
status: {
|
|
80627
|
+
schema: PtzStatusSchema,
|
|
80628
|
+
kind: "command-driven"
|
|
80629
|
+
}
|
|
80630
|
+
};
|
|
79372
80631
|
var PtzAutotrackSettingsSchema = object({
|
|
79373
80632
|
targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
|
|
79374
80633
|
stopDelaySeconds: number().int().min(0).max(300),
|
|
@@ -79494,6 +80753,424 @@ var ptzAutotrackCapability = {
|
|
|
79494
80753
|
*/
|
|
79495
80754
|
durability: "session"
|
|
79496
80755
|
};
|
|
80756
|
+
/**
|
|
80757
|
+
* `navigation` — a device-scoped capability that natively expresses the FULL
|
|
80758
|
+
* navigation / action surface of a robot that DRIVES ITSELF and carries an
|
|
80759
|
+
* on-board camera (the Dreame robot-vacuum camera is the first provider).
|
|
80760
|
+
*
|
|
80761
|
+
* Why a NEW cap rather than overloading `ptz`:
|
|
80762
|
+
* - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
|
|
80763
|
+
* robot vacuum has no gimbal — the whole chassis drives, turns and spins.
|
|
80764
|
+
* The two are different physical models: PTZ is absolute-position + presets,
|
|
80765
|
+
* navigation is momentary drive nudges + discrete robot ACTIONS
|
|
80766
|
+
* (dock / spot-clean / follow-pet / go-to-point / …).
|
|
80767
|
+
* - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
|
|
80768
|
+
* the reverse:
|
|
80769
|
+
* 1. a native CamStack navigation panel (data-driven from `listActions`
|
|
80770
|
+
* / `getOptions`), and
|
|
80771
|
+
* 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
|
|
80772
|
+
* robot camera shows up in the existing PTZ control path without every
|
|
80773
|
+
* PTZ provider learning about robots. The mapping lives in the adapter,
|
|
80774
|
+
* not here (see the addon design note):
|
|
80775
|
+
* ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
|
|
80776
|
+
* ptz.stop() → navigation.stop()
|
|
80777
|
+
* ptz.goHome() → navigation.runAction('goHome')
|
|
80778
|
+
* ptz.getPresets() → navigation.listActions() (id→preset)
|
|
80779
|
+
* ptz.goToPreset(id) → navigation.runAction(id)
|
|
80780
|
+
*
|
|
80781
|
+
* ## Continuous drive
|
|
80782
|
+
*
|
|
80783
|
+
* `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
|
|
80784
|
+
* sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
|
|
80785
|
+
* one `stop()` on release — exactly like the robot app's remote-drive joystick.
|
|
80786
|
+
* The provider forwards EACH `move` to one drive write; it must NOT debounce or
|
|
80787
|
+
* coalesce them. The UI owns the cadence.
|
|
80788
|
+
*
|
|
80789
|
+
* ## The action dictionary
|
|
80790
|
+
*
|
|
80791
|
+
* The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
|
|
80792
|
+
* are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
|
|
80793
|
+
* carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
|
|
80794
|
+
* native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
|
|
80795
|
+
* vendor-specific list. `kind: 'action'` entries are triggered with
|
|
80796
|
+
* `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
|
|
80797
|
+
* (the entry carries the `soundId` to pass). The general primitives — `move`,
|
|
80798
|
+
* `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
|
|
80799
|
+
*
|
|
80800
|
+
* NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
|
|
80801
|
+
* `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
|
|
80802
|
+
* that the currently-published `@apocaliss92/nodedreame` already exposes on
|
|
80803
|
+
* every device handle. A future nodedreame publish adds a typed
|
|
80804
|
+
* `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
|
|
80805
|
+
* provider can then swap the raw calls for the typed methods with no change to
|
|
80806
|
+
* THIS contract.
|
|
80807
|
+
*/
|
|
80808
|
+
/**
|
|
80809
|
+
* A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
|
|
80810
|
+
* keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
|
|
80811
|
+
* halts it.
|
|
80812
|
+
*
|
|
80813
|
+
* - `pan` — turn: negative = left, positive = right, 0 = straight.
|
|
80814
|
+
* - `tilt` — throttle: positive = forward, negative = spin / turn-around.
|
|
80815
|
+
* - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
|
|
80816
|
+
* vector by it (drivers without proportional drive ignore it).
|
|
80817
|
+
*
|
|
80818
|
+
* `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
|
|
80819
|
+
* axis alone; an all-undefined nudge is a no-op.
|
|
80820
|
+
*/
|
|
80821
|
+
var NavigationMoveCommandSchema = object({
|
|
80822
|
+
pan: number().min(-1).max(1).optional(),
|
|
80823
|
+
tilt: number().min(-1).max(1).optional(),
|
|
80824
|
+
speed: number().min(0).max(1).optional()
|
|
80825
|
+
});
|
|
80826
|
+
/**
|
|
80827
|
+
* The enumerated discrete actions a navigation-capable robot can perform via
|
|
80828
|
+
* `runAction`. This is the CLOSED vocabulary; a given device advertises the
|
|
80829
|
+
* subset it supports through `listActions`. Sounds are NOT here — they go through
|
|
80830
|
+
* `playSound` (see the `sound` dictionary entries).
|
|
80831
|
+
*/
|
|
80832
|
+
var NavigationActionIdSchema = _enum([
|
|
80833
|
+
"goHome",
|
|
80834
|
+
"locate",
|
|
80835
|
+
"spotClean",
|
|
80836
|
+
"findPet",
|
|
80837
|
+
"personFollow",
|
|
80838
|
+
"stop",
|
|
80839
|
+
"startClean",
|
|
80840
|
+
"pauseClean",
|
|
80841
|
+
"dockWash",
|
|
80842
|
+
"autoEmpty",
|
|
80843
|
+
"flashOn",
|
|
80844
|
+
"flashOff"
|
|
80845
|
+
]);
|
|
80846
|
+
/** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
|
|
80847
|
+
var NavigationEntryKindSchema = _enum(["action", "sound"]);
|
|
80848
|
+
/**
|
|
80849
|
+
* One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
|
|
80850
|
+
* native panel and the PTZ mimic render as a button.
|
|
80851
|
+
*
|
|
80852
|
+
* - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
|
|
80853
|
+
* (pass to `runAction`); for `kind:'sound'` it is a namespaced id
|
|
80854
|
+
* (`sound:meow`) whose `soundId` is passed to `playSound`.
|
|
80855
|
+
* - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
|
|
80856
|
+
* - `label` — operator-facing English label.
|
|
80857
|
+
* - `soundId` — wire sound id, present only on `kind:'sound'` entries.
|
|
80858
|
+
* - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
|
|
80859
|
+
* PTZ render ONLY enabled entries. Data-driven: the provider
|
|
80860
|
+
* flips it from config, never by editing code.
|
|
80861
|
+
*/
|
|
80862
|
+
var NavigationActionEntrySchema = object({
|
|
80863
|
+
id: string(),
|
|
80864
|
+
kind: NavigationEntryKindSchema,
|
|
80865
|
+
label: string(),
|
|
80866
|
+
icon: string(),
|
|
80867
|
+
/** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
|
|
80868
|
+
soundId: number().int().optional(),
|
|
80869
|
+
/** Per-device feature flag — render this entry only when true. */
|
|
80870
|
+
enabled: boolean()
|
|
80871
|
+
});
|
|
80872
|
+
/**
|
|
80873
|
+
* Canonical dictionary of every navigation control (discrete actions + sounds)
|
|
80874
|
+
* with its default label + icon hint. A provider filters this to the subset a
|
|
80875
|
+
* device supports (and may override a label). Exported so the provider AND the
|
|
80876
|
+
* UI share ONE list instead of re-deriving labels / ids independently.
|
|
80877
|
+
*
|
|
80878
|
+
* Icons are lucide-style hints; the viewer maps them to its own icon set. Every
|
|
80879
|
+
* catalog default is `enabled: true`; a provider overrides per-device from
|
|
80880
|
+
* config (data-driven — enabling / disabling one later is a config change, not a
|
|
80881
|
+
* code change).
|
|
80882
|
+
*/
|
|
80883
|
+
var NAVIGATION_ACTION_CATALOG = [
|
|
80884
|
+
{
|
|
80885
|
+
id: "startClean",
|
|
80886
|
+
kind: "action",
|
|
80887
|
+
label: "Start cleaning",
|
|
80888
|
+
icon: "play",
|
|
80889
|
+
enabled: true
|
|
80890
|
+
},
|
|
80891
|
+
{
|
|
80892
|
+
id: "pauseClean",
|
|
80893
|
+
kind: "action",
|
|
80894
|
+
label: "Pause cleaning",
|
|
80895
|
+
icon: "pause",
|
|
80896
|
+
enabled: true
|
|
80897
|
+
},
|
|
80898
|
+
{
|
|
80899
|
+
id: "stop",
|
|
80900
|
+
kind: "action",
|
|
80901
|
+
label: "Stop",
|
|
80902
|
+
icon: "hand",
|
|
80903
|
+
enabled: true
|
|
80904
|
+
},
|
|
80905
|
+
{
|
|
80906
|
+
id: "goHome",
|
|
80907
|
+
kind: "action",
|
|
80908
|
+
label: "Return to dock",
|
|
80909
|
+
icon: "home",
|
|
80910
|
+
enabled: true
|
|
80911
|
+
},
|
|
80912
|
+
{
|
|
80913
|
+
id: "locate",
|
|
80914
|
+
kind: "action",
|
|
80915
|
+
label: "Locate",
|
|
80916
|
+
icon: "bell",
|
|
80917
|
+
enabled: true
|
|
80918
|
+
},
|
|
80919
|
+
{
|
|
80920
|
+
id: "spotClean",
|
|
80921
|
+
kind: "action",
|
|
80922
|
+
label: "Spot clean here",
|
|
80923
|
+
icon: "crosshair",
|
|
80924
|
+
enabled: true
|
|
80925
|
+
},
|
|
80926
|
+
{
|
|
80927
|
+
id: "findPet",
|
|
80928
|
+
kind: "action",
|
|
80929
|
+
label: "Find pet",
|
|
80930
|
+
icon: "paw-print",
|
|
80931
|
+
enabled: true
|
|
80932
|
+
},
|
|
80933
|
+
{
|
|
80934
|
+
id: "personFollow",
|
|
80935
|
+
kind: "action",
|
|
80936
|
+
label: "Follow me",
|
|
80937
|
+
icon: "footprints",
|
|
80938
|
+
enabled: true
|
|
80939
|
+
},
|
|
80940
|
+
{
|
|
80941
|
+
id: "dockWash",
|
|
80942
|
+
kind: "action",
|
|
80943
|
+
label: "Wash mop",
|
|
80944
|
+
icon: "droplets",
|
|
80945
|
+
enabled: true
|
|
80946
|
+
},
|
|
80947
|
+
{
|
|
80948
|
+
id: "autoEmpty",
|
|
80949
|
+
kind: "action",
|
|
80950
|
+
label: "Empty dustbin",
|
|
80951
|
+
icon: "trash-2",
|
|
80952
|
+
enabled: true
|
|
80953
|
+
},
|
|
80954
|
+
{
|
|
80955
|
+
id: "flashOn",
|
|
80956
|
+
kind: "action",
|
|
80957
|
+
label: "Flash on",
|
|
80958
|
+
icon: "flashlight",
|
|
80959
|
+
enabled: true
|
|
80960
|
+
},
|
|
80961
|
+
{
|
|
80962
|
+
id: "flashOff",
|
|
80963
|
+
kind: "action",
|
|
80964
|
+
label: "Flash off",
|
|
80965
|
+
icon: "flashlight-off",
|
|
80966
|
+
enabled: true
|
|
80967
|
+
},
|
|
80968
|
+
{
|
|
80969
|
+
id: "sound:meow",
|
|
80970
|
+
kind: "sound",
|
|
80971
|
+
label: "Meow",
|
|
80972
|
+
icon: "cat",
|
|
80973
|
+
soundId: 684,
|
|
80974
|
+
enabled: true
|
|
80975
|
+
},
|
|
80976
|
+
{
|
|
80977
|
+
id: "sound:bark",
|
|
80978
|
+
kind: "sound",
|
|
80979
|
+
label: "Woof",
|
|
80980
|
+
icon: "dog",
|
|
80981
|
+
soundId: 685,
|
|
80982
|
+
enabled: true
|
|
80983
|
+
},
|
|
80984
|
+
{
|
|
80985
|
+
id: "sound:footsteps",
|
|
80986
|
+
kind: "sound",
|
|
80987
|
+
label: "Footsteps",
|
|
80988
|
+
icon: "footprints",
|
|
80989
|
+
soundId: 686,
|
|
80990
|
+
enabled: true
|
|
80991
|
+
},
|
|
80992
|
+
{
|
|
80993
|
+
id: "sound:purring",
|
|
80994
|
+
kind: "sound",
|
|
80995
|
+
label: "Purring",
|
|
80996
|
+
icon: "cat",
|
|
80997
|
+
soundId: 687,
|
|
80998
|
+
enabled: true
|
|
80999
|
+
},
|
|
81000
|
+
{
|
|
81001
|
+
id: "sound:tickTock",
|
|
81002
|
+
kind: "sound",
|
|
81003
|
+
label: "Tick-tock",
|
|
81004
|
+
icon: "clock",
|
|
81005
|
+
soundId: 688,
|
|
81006
|
+
enabled: true
|
|
81007
|
+
}
|
|
81008
|
+
];
|
|
81009
|
+
/** Coordinates for `goToPoint` — a point on the robot's live map. */
|
|
81010
|
+
var NavigationPointSchema = object({
|
|
81011
|
+
x: number(),
|
|
81012
|
+
y: number()
|
|
81013
|
+
});
|
|
81014
|
+
/**
|
|
81015
|
+
* Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
|
|
81016
|
+
* The cap reports which are enabled so the UI / PTZ render only the controls
|
|
81017
|
+
* that are turned on for THIS device. Data-driven: the provider derives these
|
|
81018
|
+
* from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
|
|
81019
|
+
* live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
|
|
81020
|
+
* that are not dictionary entries.
|
|
81021
|
+
*
|
|
81022
|
+
* - `move` / `stop` — the momentary drive joystick.
|
|
81023
|
+
* - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
|
|
81024
|
+
* map-coordinate plumbing is wired.
|
|
81025
|
+
* - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
|
|
81026
|
+
* - `playSound` — the sound buttons (dictionary `kind:'sound'`).
|
|
81027
|
+
* - `light` — the on/off fill-light toggle (works anytime).
|
|
81028
|
+
* - `lightMode` — the auto/manual selector + manual level slider (a
|
|
81029
|
+
* camera-service control; needs an active stream).
|
|
81030
|
+
*/
|
|
81031
|
+
var NavigationFeaturesSchema = object({
|
|
81032
|
+
move: boolean(),
|
|
81033
|
+
stop: boolean(),
|
|
81034
|
+
goToPoint: boolean(),
|
|
81035
|
+
runAction: boolean(),
|
|
81036
|
+
playSound: boolean(),
|
|
81037
|
+
light: boolean(),
|
|
81038
|
+
lightMode: boolean()
|
|
81039
|
+
});
|
|
81040
|
+
/** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
|
|
81041
|
+
var NavigationLightModeSchema = _enum(["auto", "manual"]);
|
|
81042
|
+
/**
|
|
81043
|
+
* Live navigation state so the UI can reflect what the robot is doing:
|
|
81044
|
+
* - `mode` — coarse activity (idle / cleaning / following / …).
|
|
81045
|
+
* - `following` — person/pet follow is currently armed.
|
|
81046
|
+
* - `flash` — the on-camera fill light is on.
|
|
81047
|
+
* - `lightMode` — auto vs manual fill-light mode.
|
|
81048
|
+
* - `lightLevel` — manual fill-light level (40..100); meaningful when
|
|
81049
|
+
* `lightMode === 'manual'`.
|
|
81050
|
+
*/
|
|
81051
|
+
var NavigationStatusSchema = object({
|
|
81052
|
+
mode: _enum([
|
|
81053
|
+
"idle",
|
|
81054
|
+
"cleaning",
|
|
81055
|
+
"spot",
|
|
81056
|
+
"following",
|
|
81057
|
+
"goto",
|
|
81058
|
+
"returning",
|
|
81059
|
+
"paused",
|
|
81060
|
+
"unknown"
|
|
81061
|
+
]),
|
|
81062
|
+
following: boolean(),
|
|
81063
|
+
flash: boolean(),
|
|
81064
|
+
lightMode: NavigationLightModeSchema,
|
|
81065
|
+
lightLevel: number().min(40).max(100),
|
|
81066
|
+
/** Ms epoch when the slice was last updated. */
|
|
81067
|
+
lastChangedAt: number()
|
|
81068
|
+
});
|
|
81069
|
+
/**
|
|
81070
|
+
* Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
|
|
81071
|
+
* observable). Adds `lastFetchedAt` on top of the status shape per the
|
|
81072
|
+
* convention.
|
|
81073
|
+
*/
|
|
81074
|
+
var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: number() });
|
|
81075
|
+
var navigationCapability = {
|
|
81076
|
+
name: "navigation",
|
|
81077
|
+
scope: "device",
|
|
81078
|
+
deviceNative: true,
|
|
81079
|
+
mode: "singleton",
|
|
81080
|
+
deviceTypes: [DeviceType.Camera],
|
|
81081
|
+
deviceConfig: { ui: {
|
|
81082
|
+
kind: "widget",
|
|
81083
|
+
widgetId: "host/navigation-panel",
|
|
81084
|
+
tab: "navigation",
|
|
81085
|
+
topTab: true,
|
|
81086
|
+
label: "Navigation",
|
|
81087
|
+
order: 0
|
|
81088
|
+
} },
|
|
81089
|
+
methods: {
|
|
81090
|
+
/**
|
|
81091
|
+
* Momentary drive nudge (the robot moves). `protected` — mirrors
|
|
81092
|
+
* `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
|
|
81093
|
+
* path) works for any authenticated user, not admin-only. The UI sends
|
|
81094
|
+
* these at ~1 Hz while a control is held; the provider forwards each one to
|
|
81095
|
+
* a single drive write WITHOUT debouncing.
|
|
81096
|
+
*/
|
|
81097
|
+
move: method(NavigationMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
81098
|
+
/** Halt all motion immediately (zero drive vector). */
|
|
81099
|
+
stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
81100
|
+
/** Send the robot to a point on its live map. */
|
|
81101
|
+
goToPoint: method(NavigationPointSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
|
|
81102
|
+
/**
|
|
81103
|
+
* Enumerate the discrete controls THIS device supports (data-driven UI +
|
|
81104
|
+
* PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
|
|
81105
|
+
*/
|
|
81106
|
+
listActions: method(object({ deviceId: number() }), array(NavigationActionEntrySchema)),
|
|
81107
|
+
/**
|
|
81108
|
+
* Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
|
|
81109
|
+
* unsupported action ids are rejected by the provider.
|
|
81110
|
+
*/
|
|
81111
|
+
runAction: method(object({
|
|
81112
|
+
deviceId: number(),
|
|
81113
|
+
actionId: NavigationActionIdSchema
|
|
81114
|
+
}), _void(), { kind: "mutation" }),
|
|
81115
|
+
/** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
|
|
81116
|
+
playSound: method(object({
|
|
81117
|
+
deviceId: number(),
|
|
81118
|
+
soundId: number().int()
|
|
81119
|
+
}), _void(), { kind: "mutation" }),
|
|
81120
|
+
/**
|
|
81121
|
+
* Turn the on-camera fill light on / off (the `OpenFullLight` control —
|
|
81122
|
+
* works anytime, no active stream required).
|
|
81123
|
+
*/
|
|
81124
|
+
setLightOn: method(object({
|
|
81125
|
+
deviceId: number(),
|
|
81126
|
+
on: boolean()
|
|
81127
|
+
}), _void(), { kind: "mutation" }),
|
|
81128
|
+
/**
|
|
81129
|
+
* Set the fill-light mode (auto vs manual). `manual` optionally carries the
|
|
81130
|
+
* initial `level`. The auto/manual + level control is a CAMERA-service
|
|
81131
|
+
* action that generally needs an active camera stream/monitor session — the
|
|
81132
|
+
* UI shows the manual level slider ONLY when `mode === 'manual'`.
|
|
81133
|
+
*/
|
|
81134
|
+
setLightMode: method(object({
|
|
81135
|
+
deviceId: number(),
|
|
81136
|
+
mode: NavigationLightModeSchema,
|
|
81137
|
+
level: number().min(40).max(100).optional()
|
|
81138
|
+
}), _void(), { kind: "mutation" }),
|
|
81139
|
+
/** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
|
|
81140
|
+
setLightLevel: method(object({
|
|
81141
|
+
deviceId: number(),
|
|
81142
|
+
level: number().min(40).max(100)
|
|
81143
|
+
}), _void(), { kind: "mutation" }),
|
|
81144
|
+
/**
|
|
81145
|
+
* Per-device FEATURE-FLAG report for the general primitives — drives which
|
|
81146
|
+
* controls the UI shows (the per-entry flags for the dictionary come back on
|
|
81147
|
+
* `listActions`).
|
|
81148
|
+
*/
|
|
81149
|
+
getFeatures: method(object({ deviceId: number() }), NavigationFeaturesSchema)
|
|
81150
|
+
},
|
|
81151
|
+
events: { onStatusChanged: { data: object({
|
|
81152
|
+
deviceId: number(),
|
|
81153
|
+
status: NavigationStatusSchema
|
|
81154
|
+
}) } },
|
|
81155
|
+
status: {
|
|
81156
|
+
schema: NavigationStatusSchema,
|
|
81157
|
+
kind: "push"
|
|
81158
|
+
},
|
|
81159
|
+
/**
|
|
81160
|
+
* Runtime-state slice mirrored by the kernel. The navigation panel watches it
|
|
81161
|
+
* for live mode / follow / flash changes.
|
|
81162
|
+
*/
|
|
81163
|
+
runtimeState: NavigationRuntimeStateSchema,
|
|
81164
|
+
/**
|
|
81165
|
+
* Runtime-state durability: **session** — like `vacuum-control`, a restored
|
|
81166
|
+
* `mode: cleaning` / `following: true` is a robot that is not actually doing
|
|
81167
|
+
* that. The live handle re-publishes on connect.
|
|
81168
|
+
*
|
|
81169
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
81170
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
81171
|
+
*/
|
|
81172
|
+
durability: "session"
|
|
81173
|
+
};
|
|
79497
81174
|
DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
|
|
79498
81175
|
kind: "mutation",
|
|
79499
81176
|
auth: "admin"
|
|
@@ -82769,6 +84446,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
82769
84446
|
motionTrigger: motionTriggerCapability,
|
|
82770
84447
|
motionZones: motionZonesCapability,
|
|
82771
84448
|
nativeObjectDetection: nativeObjectDetectionCapability,
|
|
84449
|
+
navigation: navigationCapability,
|
|
82772
84450
|
networkLink: networkLinkCapability,
|
|
82773
84451
|
notifier: notifierCapability,
|
|
82774
84452
|
numericSensor: numericSensorCapability,
|
|
@@ -86623,6 +88301,66 @@ Object.freeze({
|
|
|
86623
88301
|
addonId: null,
|
|
86624
88302
|
access: "create"
|
|
86625
88303
|
},
|
|
88304
|
+
"navigation.getFeatures": {
|
|
88305
|
+
capName: "navigation",
|
|
88306
|
+
capScope: "device",
|
|
88307
|
+
addonId: null,
|
|
88308
|
+
access: "view"
|
|
88309
|
+
},
|
|
88310
|
+
"navigation.goToPoint": {
|
|
88311
|
+
capName: "navigation",
|
|
88312
|
+
capScope: "device",
|
|
88313
|
+
addonId: null,
|
|
88314
|
+
access: "create"
|
|
88315
|
+
},
|
|
88316
|
+
"navigation.listActions": {
|
|
88317
|
+
capName: "navigation",
|
|
88318
|
+
capScope: "device",
|
|
88319
|
+
addonId: null,
|
|
88320
|
+
access: "view"
|
|
88321
|
+
},
|
|
88322
|
+
"navigation.move": {
|
|
88323
|
+
capName: "navigation",
|
|
88324
|
+
capScope: "device",
|
|
88325
|
+
addonId: null,
|
|
88326
|
+
access: "create"
|
|
88327
|
+
},
|
|
88328
|
+
"navigation.playSound": {
|
|
88329
|
+
capName: "navigation",
|
|
88330
|
+
capScope: "device",
|
|
88331
|
+
addonId: null,
|
|
88332
|
+
access: "create"
|
|
88333
|
+
},
|
|
88334
|
+
"navigation.runAction": {
|
|
88335
|
+
capName: "navigation",
|
|
88336
|
+
capScope: "device",
|
|
88337
|
+
addonId: null,
|
|
88338
|
+
access: "create"
|
|
88339
|
+
},
|
|
88340
|
+
"navigation.setLightLevel": {
|
|
88341
|
+
capName: "navigation",
|
|
88342
|
+
capScope: "device",
|
|
88343
|
+
addonId: null,
|
|
88344
|
+
access: "create"
|
|
88345
|
+
},
|
|
88346
|
+
"navigation.setLightMode": {
|
|
88347
|
+
capName: "navigation",
|
|
88348
|
+
capScope: "device",
|
|
88349
|
+
addonId: null,
|
|
88350
|
+
access: "create"
|
|
88351
|
+
},
|
|
88352
|
+
"navigation.setLightOn": {
|
|
88353
|
+
capName: "navigation",
|
|
88354
|
+
capScope: "device",
|
|
88355
|
+
addonId: null,
|
|
88356
|
+
access: "create"
|
|
88357
|
+
},
|
|
88358
|
+
"navigation.stop": {
|
|
88359
|
+
capName: "navigation",
|
|
88360
|
+
capScope: "device",
|
|
88361
|
+
addonId: null,
|
|
88362
|
+
access: "create"
|
|
88363
|
+
},
|
|
86626
88364
|
"networkAccess.getEndpoint": {
|
|
86627
88365
|
capName: "network-access",
|
|
86628
88366
|
capScope: "system",
|
|
@@ -90718,6 +92456,56 @@ Object.freeze({
|
|
|
90718
92456
|
form: "single",
|
|
90719
92457
|
optional: false
|
|
90720
92458
|
}],
|
|
92459
|
+
"navigation.getFeatures": [{
|
|
92460
|
+
name: "deviceId",
|
|
92461
|
+
form: "single",
|
|
92462
|
+
optional: false
|
|
92463
|
+
}],
|
|
92464
|
+
"navigation.goToPoint": [{
|
|
92465
|
+
name: "deviceId",
|
|
92466
|
+
form: "single",
|
|
92467
|
+
optional: false
|
|
92468
|
+
}],
|
|
92469
|
+
"navigation.listActions": [{
|
|
92470
|
+
name: "deviceId",
|
|
92471
|
+
form: "single",
|
|
92472
|
+
optional: false
|
|
92473
|
+
}],
|
|
92474
|
+
"navigation.move": [{
|
|
92475
|
+
name: "deviceId",
|
|
92476
|
+
form: "single",
|
|
92477
|
+
optional: false
|
|
92478
|
+
}],
|
|
92479
|
+
"navigation.playSound": [{
|
|
92480
|
+
name: "deviceId",
|
|
92481
|
+
form: "single",
|
|
92482
|
+
optional: false
|
|
92483
|
+
}],
|
|
92484
|
+
"navigation.runAction": [{
|
|
92485
|
+
name: "deviceId",
|
|
92486
|
+
form: "single",
|
|
92487
|
+
optional: false
|
|
92488
|
+
}],
|
|
92489
|
+
"navigation.setLightLevel": [{
|
|
92490
|
+
name: "deviceId",
|
|
92491
|
+
form: "single",
|
|
92492
|
+
optional: false
|
|
92493
|
+
}],
|
|
92494
|
+
"navigation.setLightMode": [{
|
|
92495
|
+
name: "deviceId",
|
|
92496
|
+
form: "single",
|
|
92497
|
+
optional: false
|
|
92498
|
+
}],
|
|
92499
|
+
"navigation.setLightOn": [{
|
|
92500
|
+
name: "deviceId",
|
|
92501
|
+
form: "single",
|
|
92502
|
+
optional: false
|
|
92503
|
+
}],
|
|
92504
|
+
"navigation.stop": [{
|
|
92505
|
+
name: "deviceId",
|
|
92506
|
+
form: "single",
|
|
92507
|
+
optional: false
|
|
92508
|
+
}],
|
|
90721
92509
|
"networkQuality.getDeviceStats": [{
|
|
90722
92510
|
name: "deviceId",
|
|
90723
92511
|
form: "single",
|
|
@@ -93900,6 +95688,23 @@ var dreameChildSchema = object({
|
|
|
93900
95688
|
mapShowPath: boolean().optional(),
|
|
93901
95689
|
mapShowRobot: boolean().optional(),
|
|
93902
95690
|
mapShowCharger: boolean().optional(),
|
|
95691
|
+
/**
|
|
95692
|
+
* `navigation` per-device overrides (camera child only): a feature flag set
|
|
95693
|
+
* here wins over the addon defaults, an entry id listed here is switched off.
|
|
95694
|
+
* Data, not code — see `dreame-navigation-config.ts`.
|
|
95695
|
+
*/
|
|
95696
|
+
navigationFeatures: object({
|
|
95697
|
+
move: boolean().optional(),
|
|
95698
|
+
stop: boolean().optional(),
|
|
95699
|
+
goToPoint: boolean().optional(),
|
|
95700
|
+
runAction: boolean().optional(),
|
|
95701
|
+
playSound: boolean().optional(),
|
|
95702
|
+
light: boolean().optional(),
|
|
95703
|
+
lightMode: boolean().optional()
|
|
95704
|
+
}).optional(),
|
|
95705
|
+
navigationDisabledEntryIds: array(string()).optional(),
|
|
95706
|
+
/** Privacy PIN set at pairing — required to wake a coded camera (camera child only). */
|
|
95707
|
+
navigationAccessCode: string().optional(),
|
|
93903
95708
|
mapShowNoGo: boolean().optional(),
|
|
93904
95709
|
mapShowVirtualWalls: boolean().optional(),
|
|
93905
95710
|
mapShowObstacles: boolean().optional(),
|
|
@@ -93992,7 +95797,7 @@ var DreameChildDevice = class extends BaseDevice {
|
|
|
93992
95797
|
};
|
|
93993
95798
|
//#endregion
|
|
93994
95799
|
//#region src/devices/dreame-ai-switch-device.ts
|
|
93995
|
-
var CAP_NAME$
|
|
95800
|
+
var CAP_NAME$15 = "switch";
|
|
93996
95801
|
var COLD$4 = {
|
|
93997
95802
|
on: false,
|
|
93998
95803
|
lastChangedAt: 0
|
|
@@ -94054,26 +95859,26 @@ var DreameAiSwitchDevice = class extends DreameChildDevice {
|
|
|
94054
95859
|
}
|
|
94055
95860
|
registerControlCap() {
|
|
94056
95861
|
this.ctx.registerNativeCap(switchCapability, {
|
|
94057
|
-
getStatus: async () => this.runtimeState.getCapState(CAP_NAME$
|
|
95862
|
+
getStatus: async () => this.runtimeState.getCapState(CAP_NAME$15) ?? COLD$4,
|
|
94058
95863
|
setState: async ({ on }) => {
|
|
94059
95864
|
const vacuum = this.resolveVacuum();
|
|
94060
95865
|
if (vacuum === null) throw new Error(`DreameAiSwitchDevice: vacuum handle unavailable (device=${this.dreameDeviceId}, feature=${this.#feature})`);
|
|
94061
95866
|
await vacuum.setAiFeature(this.#feature, on);
|
|
94062
|
-
this.runtimeState.setCapState(CAP_NAME$
|
|
95867
|
+
this.runtimeState.setCapState(CAP_NAME$15, {
|
|
94063
95868
|
on,
|
|
94064
95869
|
lastChangedAt: Date.now()
|
|
94065
95870
|
});
|
|
94066
95871
|
}
|
|
94067
95872
|
});
|
|
94068
|
-
this.runtimeState.setCapState(CAP_NAME$
|
|
95873
|
+
this.runtimeState.setCapState(CAP_NAME$15, COLD$4);
|
|
94069
95874
|
}
|
|
94070
95875
|
recomputeSlice() {
|
|
94071
95876
|
const vacuum = this.resolveVacuum();
|
|
94072
95877
|
if (vacuum === null) return;
|
|
94073
95878
|
const on = vacuum.aiFeature(this.#feature);
|
|
94074
95879
|
if (on === null) return;
|
|
94075
|
-
if (this.runtimeState.getCapState(CAP_NAME$
|
|
94076
|
-
this.runtimeState.setCapState(CAP_NAME$
|
|
95880
|
+
if (this.runtimeState.getCapState(CAP_NAME$15)?.on === on) return;
|
|
95881
|
+
this.runtimeState.setCapState(CAP_NAME$15, {
|
|
94077
95882
|
on,
|
|
94078
95883
|
lastChangedAt: Date.now()
|
|
94079
95884
|
});
|
|
@@ -94098,6 +95903,14 @@ function vacuumSupportsAi(caps) {
|
|
|
94098
95903
|
return caps !== null && caps.hasAiObstacleDetection && caps.aiFeatureCount > 0;
|
|
94099
95904
|
}
|
|
94100
95905
|
/**
|
|
95906
|
+
* Whether a vacuum carries an on-board camera (LinkVisual video) — gates the
|
|
95907
|
+
* separate camera + navigation child. `false` when the handle has not resolved
|
|
95908
|
+
* (confirmed capability only, never optimistic).
|
|
95909
|
+
*/
|
|
95910
|
+
function vacuumSupportsCamera(caps) {
|
|
95911
|
+
return caps?.hasCamera === true;
|
|
95912
|
+
}
|
|
95913
|
+
/**
|
|
94101
95914
|
* Whether a mower supports a map. `false` when the handle has not resolved.
|
|
94102
95915
|
*/
|
|
94103
95916
|
function mowerSupportsMap(caps) {
|
|
@@ -94131,7 +95944,7 @@ function deriveContainerFeatures(input) {
|
|
|
94131
95944
|
}
|
|
94132
95945
|
//#endregion
|
|
94133
95946
|
//#region src/devices/dreame-auto-switch-device.ts
|
|
94134
|
-
var CAP_NAME$
|
|
95947
|
+
var CAP_NAME$14 = "switch";
|
|
94135
95948
|
var COLD$3 = {
|
|
94136
95949
|
on: false,
|
|
94137
95950
|
lastChangedAt: 0
|
|
@@ -94179,19 +95992,19 @@ var DreameAutoSwitchDevice = class extends DreameChildDevice {
|
|
|
94179
95992
|
}
|
|
94180
95993
|
registerControlCap() {
|
|
94181
95994
|
this.ctx.registerNativeCap(switchCapability, {
|
|
94182
|
-
getStatus: async () => this.runtimeState.getCapState(CAP_NAME$
|
|
95995
|
+
getStatus: async () => this.runtimeState.getCapState(CAP_NAME$14) ?? COLD$3,
|
|
94183
95996
|
setState: async ({ on }) => {
|
|
94184
95997
|
const vacuum = this.#vacuum();
|
|
94185
95998
|
if (vacuum === null) throw new Error(`DreameAutoSwitchDevice: handle unavailable (device=${this.dreameDeviceId}, ${this.#key})`);
|
|
94186
95999
|
await vacuum.setAutoSwitchProperty(this.#key, on ? 1 : 0);
|
|
94187
|
-
this.runtimeState.setCapState(CAP_NAME$
|
|
96000
|
+
this.runtimeState.setCapState(CAP_NAME$14, {
|
|
94188
96001
|
on,
|
|
94189
96002
|
lastChangedAt: Date.now()
|
|
94190
96003
|
});
|
|
94191
96004
|
}
|
|
94192
96005
|
});
|
|
94193
96006
|
const on = this.#read();
|
|
94194
|
-
this.runtimeState.setCapState(CAP_NAME$
|
|
96007
|
+
this.runtimeState.setCapState(CAP_NAME$14, on === null ? COLD$3 : {
|
|
94195
96008
|
on,
|
|
94196
96009
|
lastChangedAt: Date.now()
|
|
94197
96010
|
});
|
|
@@ -94199,8 +96012,8 @@ var DreameAutoSwitchDevice = class extends DreameChildDevice {
|
|
|
94199
96012
|
recomputeSlice() {
|
|
94200
96013
|
const on = this.#read();
|
|
94201
96014
|
if (on === null) return;
|
|
94202
|
-
if (this.runtimeState.getCapState(CAP_NAME$
|
|
94203
|
-
this.runtimeState.setCapState(CAP_NAME$
|
|
96015
|
+
if (this.runtimeState.getCapState(CAP_NAME$14)?.on === on) return;
|
|
96016
|
+
this.runtimeState.setCapState(CAP_NAME$14, {
|
|
94204
96017
|
on,
|
|
94205
96018
|
lastChangedAt: Date.now()
|
|
94206
96019
|
});
|
|
@@ -94208,7 +96021,7 @@ var DreameAutoSwitchDevice = class extends DreameChildDevice {
|
|
|
94208
96021
|
};
|
|
94209
96022
|
//#endregion
|
|
94210
96023
|
//#region src/devices/dreame-auto-select-device.ts
|
|
94211
|
-
var CAP_NAME$
|
|
96024
|
+
var CAP_NAME$13 = "control";
|
|
94212
96025
|
var dreameAutoSelectOptionSchema = object({
|
|
94213
96026
|
value: number().int(),
|
|
94214
96027
|
label: string().min(1)
|
|
@@ -94263,7 +96076,7 @@ var DreameAutoSelectDevice = class extends DreameChildDevice {
|
|
|
94263
96076
|
}
|
|
94264
96077
|
registerControlCap() {
|
|
94265
96078
|
this.ctx.registerNativeCap(controlCapability, {
|
|
94266
|
-
getStatus: async () => this.runtimeState.getCapState(CAP_NAME$
|
|
96079
|
+
getStatus: async () => this.runtimeState.getCapState(CAP_NAME$13) ?? this.#slice(this.#readLabel() ?? ""),
|
|
94267
96080
|
setValue: async ({ control }) => {
|
|
94268
96081
|
if (control.kind !== "select") throw new Error(`DreameAutoSelectDevice: expected a select value (device=${this.dreameDeviceId}, ${this.#key})`);
|
|
94269
96082
|
const option = this.#options.find((o) => o.label === control.value);
|
|
@@ -94271,16 +96084,16 @@ var DreameAutoSelectDevice = class extends DreameChildDevice {
|
|
|
94271
96084
|
const vacuum = this.#vacuum();
|
|
94272
96085
|
if (vacuum === null) throw new Error(`DreameAutoSelectDevice: handle unavailable (device=${this.dreameDeviceId}, ${this.#key})`);
|
|
94273
96086
|
await vacuum.setAutoSwitchProperty(this.#key, option.value);
|
|
94274
|
-
this.runtimeState.setCapState(CAP_NAME$
|
|
96087
|
+
this.runtimeState.setCapState(CAP_NAME$13, this.#slice(option.label));
|
|
94275
96088
|
}
|
|
94276
96089
|
});
|
|
94277
|
-
this.runtimeState.setCapState(CAP_NAME$
|
|
96090
|
+
this.runtimeState.setCapState(CAP_NAME$13, this.#slice(this.#readLabel() ?? ""));
|
|
94278
96091
|
}
|
|
94279
96092
|
recomputeSlice() {
|
|
94280
96093
|
const label = this.#readLabel();
|
|
94281
96094
|
if (label === null) return;
|
|
94282
|
-
if (this.runtimeState.getCapState(CAP_NAME$
|
|
94283
|
-
this.runtimeState.setCapState(CAP_NAME$
|
|
96095
|
+
if (this.runtimeState.getCapState(CAP_NAME$13)?.value === label) return;
|
|
96096
|
+
this.runtimeState.setCapState(CAP_NAME$13, this.#slice(label));
|
|
94284
96097
|
}
|
|
94285
96098
|
};
|
|
94286
96099
|
//#endregion
|
|
@@ -94323,6 +96136,779 @@ var DreameButtonDevice = class extends DreameChildDevice {
|
|
|
94323
96136
|
recomputeSlice() {}
|
|
94324
96137
|
};
|
|
94325
96138
|
//#endregion
|
|
96139
|
+
//#region src/dreame-vacuum-decode.ts
|
|
96140
|
+
/**
|
|
96141
|
+
* Pure decode helpers for the Dreame vacuum. Translate the library's MIoT enum
|
|
96142
|
+
* values into the camstack `vacuum-control` slice shape. Unit-tested in
|
|
96143
|
+
* isolation (no facade, no network) — mirrors `homematic-decode.ts`.
|
|
96144
|
+
*/
|
|
96145
|
+
/**
|
|
96146
|
+
* Map a Dreame {@link MiotState} (siid 2 piid 1) onto the camstack canonical
|
|
96147
|
+
* `VacuumState` lifecycle (`idle` / `cleaning` / `paused` / `returning` /
|
|
96148
|
+
* `docked` / `error`). A `null` state (not yet observed) settles on `idle`.
|
|
96149
|
+
*
|
|
96150
|
+
* The MIoT state space is large (~50 values); we collapse it onto the six
|
|
96151
|
+
* canonical states by family. Anything unrecognised falls through to `idle`
|
|
96152
|
+
* (the safe neutral) so a firmware-added code never throws.
|
|
96153
|
+
*/
|
|
96154
|
+
function mapMiotStateToVacuumState(state) {
|
|
96155
|
+
if (state === null) return "idle";
|
|
96156
|
+
switch (state) {
|
|
96157
|
+
case MiotState.Charging:
|
|
96158
|
+
case MiotState.ChargingComplete:
|
|
96159
|
+
case MiotState.IntelligentCharging:
|
|
96160
|
+
case MiotState.CleanWashboardBase:
|
|
96161
|
+
case MiotState.AutoRepairBase:
|
|
96162
|
+
case MiotState.WaterSupplyDrainTest:
|
|
96163
|
+
case MiotState.CleanMopRefillWater:
|
|
96164
|
+
case MiotState.AutoEmptying:
|
|
96165
|
+
case MiotState.Emptying:
|
|
96166
|
+
case MiotState.Draining:
|
|
96167
|
+
case MiotState.AutoWaterDraining:
|
|
96168
|
+
case MiotState.Sanitizing: return "docked";
|
|
96169
|
+
case MiotState.MopDrying:
|
|
96170
|
+
case MiotState.DustBagDrying:
|
|
96171
|
+
case MiotState.SanitizingWithDry: return "drying";
|
|
96172
|
+
case MiotState.ReturningToCharge:
|
|
96173
|
+
case MiotState.ReturningToWash:
|
|
96174
|
+
case MiotState.ReturnInstallMop:
|
|
96175
|
+
case MiotState.ReturnRemoveMop:
|
|
96176
|
+
case MiotState.ReturnToEmpty:
|
|
96177
|
+
case MiotState.ReturningToDrain: return "returning";
|
|
96178
|
+
case MiotState.Paused:
|
|
96179
|
+
case MiotState.PausedAlt:
|
|
96180
|
+
case MiotState.PausedCleaning:
|
|
96181
|
+
case MiotState.DustBagDryingPaused:
|
|
96182
|
+
case MiotState.CameraMonitoringPaused:
|
|
96183
|
+
case MiotState.FindingPetPaused:
|
|
96184
|
+
case MiotState.InitialDeepCleanPaused:
|
|
96185
|
+
case MiotState.ChangingMopPaused:
|
|
96186
|
+
case MiotState.FloorMaintainingPaused: return "paused";
|
|
96187
|
+
case MiotState.Standby:
|
|
96188
|
+
case MiotState.WaitingForTask: return "idle";
|
|
96189
|
+
default: return "cleaning";
|
|
96190
|
+
}
|
|
96191
|
+
}
|
|
96192
|
+
/**
|
|
96193
|
+
* The camstack `fanSpeed` token for a Dreame {@link SuctionLevel} — the enum
|
|
96194
|
+
* member NAME, lower-cased (`Quiet` → `'quiet'`). Returns `null` for an
|
|
96195
|
+
* unobserved / unknown level.
|
|
96196
|
+
*/
|
|
96197
|
+
function suctionLevelToken(level) {
|
|
96198
|
+
if (level === null) return null;
|
|
96199
|
+
const name = SuctionLevel[level];
|
|
96200
|
+
return typeof name === "string" ? name.toLowerCase() : null;
|
|
96201
|
+
}
|
|
96202
|
+
/**
|
|
96203
|
+
* Resolve a camstack `fanSpeed` token back to the Dreame {@link SuctionLevel}
|
|
96204
|
+
* enum value, validated against the model's `supported` levels. Returns null
|
|
96205
|
+
* when the token doesn't match a supported level (the caller rejects the set).
|
|
96206
|
+
*/
|
|
96207
|
+
function tokenToSuctionLevel(token, supported) {
|
|
96208
|
+
const wanted = token.trim().toLowerCase();
|
|
96209
|
+
for (const level of supported) if (suctionLevelToken(level) === wanted) return level;
|
|
96210
|
+
return null;
|
|
96211
|
+
}
|
|
96212
|
+
/** The list of accepted `fanSpeed` tokens for a model's supported suction set. */
|
|
96213
|
+
function supportedFanSpeedTokens(supported) {
|
|
96214
|
+
const tokens = [];
|
|
96215
|
+
for (const level of supported) {
|
|
96216
|
+
const token = suctionLevelToken(level);
|
|
96217
|
+
if (token !== null) tokens.push(token);
|
|
96218
|
+
}
|
|
96219
|
+
return tokens;
|
|
96220
|
+
}
|
|
96221
|
+
/**
|
|
96222
|
+
* Clamp a raw battery reading (already a 0..100 percentage from the library)
|
|
96223
|
+
* to the `vacuum-control` slice's `batteryLevel` field. Returns null for a
|
|
96224
|
+
* null/NaN/out-of-band reading so the slice stays valid (the schema rejects
|
|
96225
|
+
* a number outside 0..100).
|
|
96226
|
+
*/
|
|
96227
|
+
function clampBatteryLevel$1(value) {
|
|
96228
|
+
if (value === null || !Number.isFinite(value)) return null;
|
|
96229
|
+
return Math.max(0, Math.min(100, value));
|
|
96230
|
+
}
|
|
96231
|
+
/** Clamp a 0..100 percentage (progress, coverage, …), null-safe. */
|
|
96232
|
+
function clampPercent$1(value) {
|
|
96233
|
+
if (value === null || !Number.isFinite(value)) return null;
|
|
96234
|
+
return Math.max(0, Math.min(100, value));
|
|
96235
|
+
}
|
|
96236
|
+
/**
|
|
96237
|
+
* Clean-water tank `status`: prefer the richer clean-water-tank-status (27/1),
|
|
96238
|
+
* fall back to the coarser water-tank presence (4/6). `null` when neither maps
|
|
96239
|
+
* to a known good/low reading (NOT_INSTALLED / CHECKING / unknown). Pure.
|
|
96240
|
+
*/
|
|
96241
|
+
function cleanWaterTankStatus(props) {
|
|
96242
|
+
if (props.cleanWaterTankStatus !== null) {
|
|
96243
|
+
if (props.cleanWaterTankStatus === 0) return "ok";
|
|
96244
|
+
if (props.cleanWaterTankStatus === 2) return "low";
|
|
96245
|
+
return null;
|
|
96246
|
+
}
|
|
96247
|
+
if (props.waterTank === 1 || props.waterTank === 10) return "ok";
|
|
96248
|
+
return null;
|
|
96249
|
+
}
|
|
96250
|
+
/** Detergent tank `status` from detergent-status (27/4). Pure. */
|
|
96251
|
+
function detergentTankStatus(value) {
|
|
96252
|
+
if (value === 0) return "ok";
|
|
96253
|
+
if (value === 2) return "low";
|
|
96254
|
+
return null;
|
|
96255
|
+
}
|
|
96256
|
+
/** Dust-bin `status` from dust-collection (15/3); OVER_USE reads as `full`. Pure. */
|
|
96257
|
+
function dustBinTankStatus(value) {
|
|
96258
|
+
if (value === 1) return "ok";
|
|
96259
|
+
if (value === 2) return "full";
|
|
96260
|
+
return null;
|
|
96261
|
+
}
|
|
96262
|
+
/**
|
|
96263
|
+
* The four `vacuum-control` tank fields (clean-water / dirty-water / detergent /
|
|
96264
|
+
* dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
|
|
96265
|
+
* discrete `status` populated from the live MIoT enum props when supplied. Pure.
|
|
96266
|
+
*
|
|
96267
|
+
* The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
|
|
96268
|
+
* (verified against a real `dreame.vacuum.r2538z`) carries only the water-VOLUME
|
|
96269
|
+
* *setting* (a mop-flow level, NOT a tank fill). We therefore never fabricate a
|
|
96270
|
+
* `level`; `level` is always `null`. The cap contract is honoured on PRESENCE,
|
|
96271
|
+
* with `status` derived from the tank-status enums:
|
|
96272
|
+
*
|
|
96273
|
+
* - a field is `null` when the hardware HAS NO such tank (capability flags), and
|
|
96274
|
+
* - `{ level: null, status }` when the tank EXISTS — `status` from the enum
|
|
96275
|
+
* prop ('ok' / 'low' / 'full'), or `null` when the device reports none.
|
|
96276
|
+
*
|
|
96277
|
+
* Presence is derived from the capability record:
|
|
96278
|
+
* - clean-water ← `canMop` (a mopping robot carries a clean-water reservoir)
|
|
96279
|
+
* - dirty-water ← `canMopWash` (a self-wash dock has a recovery/dirty tank)
|
|
96280
|
+
* - detergent ← `hasDetergentReservoir`
|
|
96281
|
+
* - dust-bin ← `canAutoEmpty` (an auto-empty robot has an onboard dust bin)
|
|
96282
|
+
*/
|
|
96283
|
+
function decodeVacuumTanks(caps, props) {
|
|
96284
|
+
const tank = (present, status) => present ? {
|
|
96285
|
+
level: null,
|
|
96286
|
+
status
|
|
96287
|
+
} : null;
|
|
96288
|
+
return {
|
|
96289
|
+
cleanWater: tank(Boolean(caps?.canMop), props ? cleanWaterTankStatus(props) : null),
|
|
96290
|
+
dirtyWater: tank(Boolean(caps?.canMopWash), null),
|
|
96291
|
+
detergent: tank(Boolean(caps?.hasDetergentReservoir), props ? detergentTankStatus(props.detergentStatus) : null),
|
|
96292
|
+
dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection) : null)
|
|
96293
|
+
};
|
|
96294
|
+
}
|
|
96295
|
+
/**
|
|
96296
|
+
* Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
|
|
96297
|
+
* value is absent / non-finite. The library reports remaining life as a 0..100
|
|
96298
|
+
* integer; we clamp defensively so an out-of-range firmware value never violates
|
|
96299
|
+
* the cap schema.
|
|
96300
|
+
*/
|
|
96301
|
+
function clampConsumablePct(value) {
|
|
96302
|
+
if (value === null || !Number.isFinite(value)) return null;
|
|
96303
|
+
return Math.max(0, Math.min(100, value));
|
|
96304
|
+
}
|
|
96305
|
+
/**
|
|
96306
|
+
* Build the `consumables` cap runtime-state slice from a live {@link VacuumDevice}
|
|
96307
|
+
* handle. The returned object satisfies the cap's `runtimeState` schema
|
|
96308
|
+
* (`ConsumablesStatusSchema.extend({ lastFetchedAt: z.number() })`).
|
|
96309
|
+
*
|
|
96310
|
+
* PRESENCE-driven (mirrors HA): the library's `supportedConsumables` enumerates
|
|
96311
|
+
* ONLY the consumables this model reports (a present MIoT life property) — main/
|
|
96312
|
+
* side brush, filter, sensor, tank-filter, mop-pad, silver-ion, detergent,
|
|
96313
|
+
* squeegee, dust-bag — each with its remaining-life % and whether a reset action
|
|
96314
|
+
* exists. No hardcoded per-model list: a consumable the device does not expose
|
|
96315
|
+
* never appears, and a `resettable` item drives the real MIoT reset action.
|
|
96316
|
+
*/
|
|
96317
|
+
function buildVacuumConsumables(vacuum) {
|
|
96318
|
+
const items = vacuum.supportedConsumables.map((c) => ({
|
|
96319
|
+
key: c.key,
|
|
96320
|
+
label: c.label,
|
|
96321
|
+
level: clampConsumablePct(c.leftPct),
|
|
96322
|
+
status: null,
|
|
96323
|
+
lastResetAt: null,
|
|
96324
|
+
resettable: c.resettable
|
|
96325
|
+
}));
|
|
96326
|
+
const now = Date.now();
|
|
96327
|
+
return {
|
|
96328
|
+
items: [...items],
|
|
96329
|
+
lastChangedAt: now,
|
|
96330
|
+
lastFetchedAt: now
|
|
96331
|
+
};
|
|
96332
|
+
}
|
|
96333
|
+
//#endregion
|
|
96334
|
+
//#region src/devices/dreame-navigation-actions.ts
|
|
96335
|
+
/** Forward throttle magnitude for a full-forward nudge (device speed units). */
|
|
96336
|
+
var FORWARD_SPEED = 200;
|
|
96337
|
+
/** Turn magnitudes: left = +45, right = -45, spin/turn-around = +180. */
|
|
96338
|
+
var TURN_LEFT = 45;
|
|
96339
|
+
var TURN_RIGHT = -45;
|
|
96340
|
+
var SPIN = 180;
|
|
96341
|
+
/** The `setFillLight` value that selects auto. */
|
|
96342
|
+
var FILL_LIGHT_AUTO$1 = 101;
|
|
96343
|
+
/**
|
|
96344
|
+
* Map a normalized {@link NavigationMoveCommand} ([-1, 1] per axis) to a discrete
|
|
96345
|
+
* Dreame drive vector. The robot has no proportional joystick, so intensity is
|
|
96346
|
+
* quantized to the RE'd discrete nudges. Priority: forward, then spin
|
|
96347
|
+
* (tilt-down), then turn (pan). An all-zero / empty command is a stop.
|
|
96348
|
+
*/
|
|
96349
|
+
function driveVector(cmd) {
|
|
96350
|
+
const pan = cmd.pan ?? 0;
|
|
96351
|
+
const tilt = cmd.tilt ?? 0;
|
|
96352
|
+
if (tilt > 0) return {
|
|
96353
|
+
spdv: FORWARD_SPEED,
|
|
96354
|
+
spdw: 0
|
|
96355
|
+
};
|
|
96356
|
+
if (tilt < 0) return {
|
|
96357
|
+
spdv: 0,
|
|
96358
|
+
spdw: SPIN
|
|
96359
|
+
};
|
|
96360
|
+
if (pan < 0) return {
|
|
96361
|
+
spdv: 0,
|
|
96362
|
+
spdw: TURN_LEFT
|
|
96363
|
+
};
|
|
96364
|
+
if (pan > 0) return {
|
|
96365
|
+
spdv: 0,
|
|
96366
|
+
spdw: TURN_RIGHT
|
|
96367
|
+
};
|
|
96368
|
+
return {
|
|
96369
|
+
spdv: 0,
|
|
96370
|
+
spdw: 0
|
|
96371
|
+
};
|
|
96372
|
+
}
|
|
96373
|
+
function moveCall(cmd) {
|
|
96374
|
+
const { spdv, spdw } = driveVector(cmd);
|
|
96375
|
+
return {
|
|
96376
|
+
method: "drive",
|
|
96377
|
+
spdv,
|
|
96378
|
+
spdw
|
|
96379
|
+
};
|
|
96380
|
+
}
|
|
96381
|
+
/** The explicit halt: a zero drive vector. */
|
|
96382
|
+
function stopCall() {
|
|
96383
|
+
return {
|
|
96384
|
+
method: "drive",
|
|
96385
|
+
spdv: 0,
|
|
96386
|
+
spdw: 0
|
|
96387
|
+
};
|
|
96388
|
+
}
|
|
96389
|
+
/** Fill light on (full) / off (back to auto) — the control that works anytime. */
|
|
96390
|
+
function lightOnCall(on) {
|
|
96391
|
+
return {
|
|
96392
|
+
method: "setFillLightAuto",
|
|
96393
|
+
full: on
|
|
96394
|
+
};
|
|
96395
|
+
}
|
|
96396
|
+
/** Fill-light mode: auto, or manual at `level` (40..100). Camera-service; needs a stream. */
|
|
96397
|
+
function lightModeCall(mode, level) {
|
|
96398
|
+
return {
|
|
96399
|
+
method: "setFillLight",
|
|
96400
|
+
value: mode === "auto" ? FILL_LIGHT_AUTO$1 : level ?? 100
|
|
96401
|
+
};
|
|
96402
|
+
}
|
|
96403
|
+
function lightLevelCall(level) {
|
|
96404
|
+
return lightModeCall("manual", level);
|
|
96405
|
+
}
|
|
96406
|
+
function playSoundCall(soundId) {
|
|
96407
|
+
return {
|
|
96408
|
+
method: "playSound",
|
|
96409
|
+
soundId
|
|
96410
|
+
};
|
|
96411
|
+
}
|
|
96412
|
+
function goToPointCall(point) {
|
|
96413
|
+
return {
|
|
96414
|
+
method: "goToPoint",
|
|
96415
|
+
x: point.x,
|
|
96416
|
+
y: point.y
|
|
96417
|
+
};
|
|
96418
|
+
}
|
|
96419
|
+
/** A discrete {@link NavigationActionId} → the controller method that performs it. */
|
|
96420
|
+
function actionCall(actionId) {
|
|
96421
|
+
switch (actionId) {
|
|
96422
|
+
case "goHome": return { method: "returnToDock" };
|
|
96423
|
+
case "locate": return { method: "locate" };
|
|
96424
|
+
case "spotClean": return { method: "spotClean" };
|
|
96425
|
+
case "findPet": return { method: "findPet" };
|
|
96426
|
+
case "personFollow": return { method: "startPersonFollow" };
|
|
96427
|
+
case "stop": return { method: "stopWork" };
|
|
96428
|
+
case "startClean": return {
|
|
96429
|
+
method: "runVacuumAction",
|
|
96430
|
+
action: "startClean"
|
|
96431
|
+
};
|
|
96432
|
+
case "pauseClean": return {
|
|
96433
|
+
method: "runVacuumAction",
|
|
96434
|
+
action: "pauseClean"
|
|
96435
|
+
};
|
|
96436
|
+
case "dockWash": return {
|
|
96437
|
+
method: "runVacuumAction",
|
|
96438
|
+
action: "dockWash"
|
|
96439
|
+
};
|
|
96440
|
+
case "autoEmpty": return {
|
|
96441
|
+
method: "runVacuumAction",
|
|
96442
|
+
action: "autoEmpty"
|
|
96443
|
+
};
|
|
96444
|
+
case "flashOn": return lightOnCall(true);
|
|
96445
|
+
case "flashOff": return lightOnCall(false);
|
|
96446
|
+
}
|
|
96447
|
+
}
|
|
96448
|
+
/** Perform one call on a controller. Exhaustive — a new method is a compile error here. */
|
|
96449
|
+
async function runControllerCall(controller, call) {
|
|
96450
|
+
switch (call.method) {
|
|
96451
|
+
case "drive":
|
|
96452
|
+
await controller.drive(call.spdv, call.spdw);
|
|
96453
|
+
return;
|
|
96454
|
+
case "stopWork":
|
|
96455
|
+
await controller.stopWork();
|
|
96456
|
+
return;
|
|
96457
|
+
case "startPersonFollow":
|
|
96458
|
+
await controller.startPersonFollow();
|
|
96459
|
+
return;
|
|
96460
|
+
case "spotClean":
|
|
96461
|
+
await controller.spotClean();
|
|
96462
|
+
return;
|
|
96463
|
+
case "findPet":
|
|
96464
|
+
await controller.findPet();
|
|
96465
|
+
return;
|
|
96466
|
+
case "playSound":
|
|
96467
|
+
await controller.playSound(call.soundId);
|
|
96468
|
+
return;
|
|
96469
|
+
case "runVacuumAction":
|
|
96470
|
+
await controller.runVacuumAction(call.action);
|
|
96471
|
+
return;
|
|
96472
|
+
case "goToPoint":
|
|
96473
|
+
await controller.goToPoint([[call.x, call.y]], [[call.x, call.y]]);
|
|
96474
|
+
return;
|
|
96475
|
+
case "setFillLight":
|
|
96476
|
+
await controller.setFillLight(call.value);
|
|
96477
|
+
return;
|
|
96478
|
+
case "setFillLightAuto":
|
|
96479
|
+
await controller.setFillLightAuto(call.full);
|
|
96480
|
+
return;
|
|
96481
|
+
case "returnToDock":
|
|
96482
|
+
await controller.returnToDock();
|
|
96483
|
+
return;
|
|
96484
|
+
case "locate":
|
|
96485
|
+
await controller.locate();
|
|
96486
|
+
return;
|
|
96487
|
+
}
|
|
96488
|
+
}
|
|
96489
|
+
//#endregion
|
|
96490
|
+
//#region src/devices/dreame-navigation-config.ts
|
|
96491
|
+
/**
|
|
96492
|
+
* DATA-DRIVEN feature flags for the Dreame navigation surface. Keeping these as
|
|
96493
|
+
* data (rather than branches in the device class) is what makes enabling /
|
|
96494
|
+
* disabling a control later a CONFIG change, not a code change — a future step
|
|
96495
|
+
* merges per-device / per-addon overrides on top of these defaults before the
|
|
96496
|
+
* cap reports them.
|
|
96497
|
+
*
|
|
96498
|
+
* `goToPoint` ships OFF: the send-to-map-coordinate path needs map-coordinate
|
|
96499
|
+
* plumbing (resolving a tap on the live map to robot coordinates) that is not
|
|
96500
|
+
* wired yet. Every other primitive defaults ON.
|
|
96501
|
+
*/
|
|
96502
|
+
var DREAME_NAVIGATION_DEFAULT_FEATURES = {
|
|
96503
|
+
move: true,
|
|
96504
|
+
stop: true,
|
|
96505
|
+
goToPoint: false,
|
|
96506
|
+
runAction: true,
|
|
96507
|
+
playSound: true,
|
|
96508
|
+
light: true,
|
|
96509
|
+
lightMode: true
|
|
96510
|
+
};
|
|
96511
|
+
/**
|
|
96512
|
+
* Dictionary-entry ids disabled by default for Dreame (data-driven per-entry
|
|
96513
|
+
* flags). Empty today — every catalog action / sound is enabled. A future
|
|
96514
|
+
* config override drops ids in here (or removes them) without touching code.
|
|
96515
|
+
*/
|
|
96516
|
+
var DREAME_NAVIGATION_DISABLED_ENTRY_IDS = /* @__PURE__ */ new Set();
|
|
96517
|
+
/**
|
|
96518
|
+
* The action dictionary as THIS provider reports it: the shared catalog with the
|
|
96519
|
+
* per-entry `enabled` flag resolved from {@link DREAME_NAVIGATION_DISABLED_ENTRY_IDS}.
|
|
96520
|
+
* Pure — the device method just returns this (a later step layers device config
|
|
96521
|
+
* on top).
|
|
96522
|
+
*/
|
|
96523
|
+
function dreameNavigationCatalog(disabledEntryIds = DREAME_NAVIGATION_DISABLED_ENTRY_IDS) {
|
|
96524
|
+
return NAVIGATION_ACTION_CATALOG.map((entry) => ({
|
|
96525
|
+
...entry,
|
|
96526
|
+
enabled: entry.enabled && !disabledEntryIds.has(entry.id)
|
|
96527
|
+
}));
|
|
96528
|
+
}
|
|
96529
|
+
/** The feature flags in force for ONE device: defaults, then its overrides. */
|
|
96530
|
+
function resolveNavigationFeatures(overrides) {
|
|
96531
|
+
return {
|
|
96532
|
+
...DREAME_NAVIGATION_DEFAULT_FEATURES,
|
|
96533
|
+
...overrides?.features ?? {}
|
|
96534
|
+
};
|
|
96535
|
+
}
|
|
96536
|
+
/** The disabled entry ids in force for ONE device: the addon set plus its own. */
|
|
96537
|
+
function resolveDisabledEntryIds(overrides) {
|
|
96538
|
+
return new Set([...DREAME_NAVIGATION_DISABLED_ENTRY_IDS, ...overrides?.disabledEntryIds ?? []]);
|
|
96539
|
+
}
|
|
96540
|
+
//#endregion
|
|
96541
|
+
//#region src/devices/dreame-navigation-ptz-adapter.ts
|
|
96542
|
+
/** A gimbal descriptor for a robot: the joystick is pan/tilt, there is no lens. */
|
|
96543
|
+
function ptzOptionsFromFeatures(features) {
|
|
96544
|
+
return {
|
|
96545
|
+
hasPan: features.move,
|
|
96546
|
+
hasTilt: features.move,
|
|
96547
|
+
hasZoom: false,
|
|
96548
|
+
supportsPresets: features.runAction || features.playSound,
|
|
96549
|
+
hasAutofocus: false
|
|
96550
|
+
};
|
|
96551
|
+
}
|
|
96552
|
+
/** Every ENABLED dictionary entry is a preset; the label is the preset name. */
|
|
96553
|
+
function presetsFromCatalog(entries, features) {
|
|
96554
|
+
return entries.filter((entry) => entry.enabled).filter((entry) => entry.kind === "action" ? features.runAction : features.playSound).map((entry) => ({
|
|
96555
|
+
id: entry.id,
|
|
96556
|
+
name: entry.label
|
|
96557
|
+
}));
|
|
96558
|
+
}
|
|
96559
|
+
/**
|
|
96560
|
+
* Resolve a preset id back to the navigation call. `null` when the id names
|
|
96561
|
+
* nothing enabled — the caller rejects rather than guessing an action.
|
|
96562
|
+
*/
|
|
96563
|
+
function presetTarget(entries, presetId) {
|
|
96564
|
+
const entry = entries.find((candidate) => candidate.id === presetId && candidate.enabled);
|
|
96565
|
+
if (!entry) return null;
|
|
96566
|
+
if (entry.kind === "sound") return entry.soundId === void 0 ? null : {
|
|
96567
|
+
kind: "sound",
|
|
96568
|
+
soundId: entry.soundId
|
|
96569
|
+
};
|
|
96570
|
+
const parsed = NavigationActionIdSchema.safeParse(entry.id);
|
|
96571
|
+
return parsed.success ? {
|
|
96572
|
+
kind: "action",
|
|
96573
|
+
actionId: parsed.data
|
|
96574
|
+
} : null;
|
|
96575
|
+
}
|
|
96576
|
+
function clampAxis(value) {
|
|
96577
|
+
if (value === void 0 || !Number.isFinite(value)) return void 0;
|
|
96578
|
+
return Math.max(-1, Math.min(1, value));
|
|
96579
|
+
}
|
|
96580
|
+
/**
|
|
96581
|
+
* A PTZ move is a navigation nudge: pan turns, tilt drives, zoom has no
|
|
96582
|
+
* meaning on a chassis and is dropped. Axes are clamped to the cap's [-1, 1].
|
|
96583
|
+
*/
|
|
96584
|
+
function ptzMoveToNavigation(cmd) {
|
|
96585
|
+
const pan = clampAxis(cmd.pan);
|
|
96586
|
+
const tilt = clampAxis(cmd.tilt);
|
|
96587
|
+
const speed = cmd.speed === void 0 || !Number.isFinite(cmd.speed) ? void 0 : Math.max(0, Math.min(1, cmd.speed));
|
|
96588
|
+
return {
|
|
96589
|
+
...pan !== void 0 ? { pan } : {},
|
|
96590
|
+
...tilt !== void 0 ? { tilt } : {},
|
|
96591
|
+
...speed !== void 0 ? { speed } : {}
|
|
96592
|
+
};
|
|
96593
|
+
}
|
|
96594
|
+
/** The camera-service fill-light mode/level property. */
|
|
96595
|
+
var FILL_LIGHT_SET_SIID = 10001;
|
|
96596
|
+
/** The wire value that means "auto" on `fillinLightSet`. */
|
|
96597
|
+
var FILL_LIGHT_AUTO = 101;
|
|
96598
|
+
function asRecord(raw) {
|
|
96599
|
+
if (typeof raw === "string") try {
|
|
96600
|
+
const parsed = JSON.parse(raw);
|
|
96601
|
+
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
96602
|
+
} catch {
|
|
96603
|
+
return null;
|
|
96604
|
+
}
|
|
96605
|
+
return raw !== null && typeof raw === "object" ? raw : null;
|
|
96606
|
+
}
|
|
96607
|
+
/** `4/50` → `{ flash }`, only when the packed key is the fill light. */
|
|
96608
|
+
function decodeFillLight(raw) {
|
|
96609
|
+
const record = asRecord(raw);
|
|
96610
|
+
if (!record || record["k"] !== "FillinLight") return {};
|
|
96611
|
+
const v = record["v"];
|
|
96612
|
+
if (v === 1 || v === "1" || v === true) return { flash: true };
|
|
96613
|
+
if (v === 0 || v === "0" || v === false) return { flash: false };
|
|
96614
|
+
return {};
|
|
96615
|
+
}
|
|
96616
|
+
/** `10001/9` → `{ lightMode, lightLevel? }`: 101 is auto, 40..100 a manual level. */
|
|
96617
|
+
function decodeFillLightSet(raw) {
|
|
96618
|
+
const n = typeof raw === "string" ? Number(raw) : typeof raw === "number" ? raw : NaN;
|
|
96619
|
+
if (!Number.isFinite(n)) return {};
|
|
96620
|
+
if (n === FILL_LIGHT_AUTO) return { lightMode: "auto" };
|
|
96621
|
+
if (n >= 40 && n <= 100) return {
|
|
96622
|
+
lightMode: "manual",
|
|
96623
|
+
lightLevel: n
|
|
96624
|
+
};
|
|
96625
|
+
return {};
|
|
96626
|
+
}
|
|
96627
|
+
/** `true` while the robot's own state says it is following; `null` state = not known. */
|
|
96628
|
+
function decodeFollowing(state) {
|
|
96629
|
+
if (state === null || state === void 0 || !Number.isFinite(state)) return null;
|
|
96630
|
+
return state === 26;
|
|
96631
|
+
}
|
|
96632
|
+
/** Everything the robot currently says about its light, as one patch. */
|
|
96633
|
+
function decodeNavigationReadback(read) {
|
|
96634
|
+
return {
|
|
96635
|
+
...decodeFillLight(read(4, 50)),
|
|
96636
|
+
...decodeFillLightSet(read(FILL_LIGHT_SET_SIID, 9))
|
|
96637
|
+
};
|
|
96638
|
+
}
|
|
96639
|
+
//#endregion
|
|
96640
|
+
//#region src/devices/dreame-camera-device.ts
|
|
96641
|
+
var CAP_NAME$12 = "navigation";
|
|
96642
|
+
var NAV_COLD_START = {
|
|
96643
|
+
mode: "unknown",
|
|
96644
|
+
following: false,
|
|
96645
|
+
flash: false,
|
|
96646
|
+
lightMode: "auto",
|
|
96647
|
+
lightLevel: 100,
|
|
96648
|
+
lastChangedAt: 0
|
|
96649
|
+
};
|
|
96650
|
+
/**
|
|
96651
|
+
* Dreame CAMERA + NAVIGATION device — a SEPARATE camstack device from the
|
|
96652
|
+
* `DreameVacuumDevice`, spawned only for camera-capable vacuums (those whose
|
|
96653
|
+
* `vacuumCapabilities.hasCamera` is true, i.e. a LinkVisual video surface). It
|
|
96654
|
+
* owns the `navigation` cap (drive joystick + discrete robot actions + sounds +
|
|
96655
|
+
* fill light); the vacuum device keeps `vacuum-control` / `consumables` / maps.
|
|
96656
|
+
*
|
|
96657
|
+
* PREBUFFER OFF: this is a BATTERY robot whose camera is expensive to cold-start,
|
|
96658
|
+
* so the device advertises {@link DeviceFeature.BatteryOperated} — the SAME flag
|
|
96659
|
+
* the Reolink battery-camera path uses. The stream-broker's
|
|
96660
|
+
* `getEffectivePreBufferSec` battery rule then sets `preBufferSec = 0`, so the
|
|
96661
|
+
* stream opens on demand and is never continuously prebuffered.
|
|
96662
|
+
*
|
|
96663
|
+
* The whole navigation surface goes through nodedreame's typed
|
|
96664
|
+
* `DreameCameraController` (1.12+, built lazily from the vacuum handle with the
|
|
96665
|
+
* pairing PIN when the camera is coded), mapped by `dreame-navigation-actions.ts`.
|
|
96666
|
+
* The controller is never `open()`ed here — the stream is the broker's; drive is
|
|
96667
|
+
* valid only while a stream is open, which is when the panel is used.
|
|
96668
|
+
*
|
|
96669
|
+
* STATUS: `mode` is mapped from the shared vacuum state; `flash` / `lightMode`
|
|
96670
|
+
* / `lightLevel` are decoded from the robot's own properties on every push
|
|
96671
|
+
* (`dreame-navigation-status.ts`: `4/50` and `10001/9`) and, until the first
|
|
96672
|
+
* read-back lands, from the commands this device issued. `following` has no
|
|
96673
|
+
* confirmed read-back yet and stays command-driven.
|
|
96674
|
+
*
|
|
96675
|
+
* PTZ MIMIC: the same device also registers `ptz`, projected from `navigation`
|
|
96676
|
+
* through `dreame-navigation-ptz-adapter.ts` (pure, tested) — the joystick is
|
|
96677
|
+
* pan/tilt, the enabled actions and sounds are the presets, there is no zoom
|
|
96678
|
+
* and no autofocus. `navigation` stays the source of truth; nothing is stored
|
|
96679
|
+
* on the PTZ side.
|
|
96680
|
+
*/
|
|
96681
|
+
var DreameCameraDevice = class extends DreameChildDevice {
|
|
96682
|
+
features = [DeviceFeature.BatteryOperated];
|
|
96683
|
+
constructor(ctx) {
|
|
96684
|
+
super(ctx);
|
|
96685
|
+
}
|
|
96686
|
+
registerControlCap() {
|
|
96687
|
+
const provider = this.buildNavigationProvider();
|
|
96688
|
+
this.ctx.registerNativeCap(navigationCapability, provider);
|
|
96689
|
+
this.runtimeState.setCapState(CAP_NAME$12, NAV_COLD_START);
|
|
96690
|
+
this.ctx.registerNativeCap(ptzCapability, this.buildPtzMimic(provider));
|
|
96691
|
+
}
|
|
96692
|
+
buildNavigationProvider() {
|
|
96693
|
+
return {
|
|
96694
|
+
getStatus: async () => this.runtimeState.getCapState(CAP_NAME$12) ?? NAV_COLD_START,
|
|
96695
|
+
listActions: async () => dreameNavigationCatalog(resolveDisabledEntryIds(this.overrides())),
|
|
96696
|
+
getFeatures: async () => this.buildFeatures(),
|
|
96697
|
+
move: async ({ pan, tilt, speed }) => {
|
|
96698
|
+
await this.dispatch(moveCall({
|
|
96699
|
+
pan,
|
|
96700
|
+
tilt,
|
|
96701
|
+
speed
|
|
96702
|
+
}));
|
|
96703
|
+
},
|
|
96704
|
+
stop: async () => {
|
|
96705
|
+
await this.dispatch(stopCall());
|
|
96706
|
+
},
|
|
96707
|
+
goToPoint: async ({ x, y }) => {
|
|
96708
|
+
await this.dispatch(goToPointCall({
|
|
96709
|
+
x,
|
|
96710
|
+
y
|
|
96711
|
+
}));
|
|
96712
|
+
},
|
|
96713
|
+
runAction: async ({ actionId }) => {
|
|
96714
|
+
await this.dispatch(actionCall(actionId));
|
|
96715
|
+
this.applyOptimisticAction(actionId);
|
|
96716
|
+
},
|
|
96717
|
+
playSound: async ({ soundId }) => {
|
|
96718
|
+
await this.dispatch(playSoundCall(soundId));
|
|
96719
|
+
},
|
|
96720
|
+
setLightOn: async ({ on }) => {
|
|
96721
|
+
await this.dispatch(lightOnCall(on));
|
|
96722
|
+
this.patchStatus({ flash: on });
|
|
96723
|
+
},
|
|
96724
|
+
setLightMode: async ({ mode, level }) => {
|
|
96725
|
+
await this.dispatch(lightModeCall(mode, level));
|
|
96726
|
+
this.patchStatus(mode === "manual" && level !== void 0 ? {
|
|
96727
|
+
lightMode: mode,
|
|
96728
|
+
lightLevel: level
|
|
96729
|
+
} : { lightMode: mode });
|
|
96730
|
+
},
|
|
96731
|
+
setLightLevel: async ({ level }) => {
|
|
96732
|
+
await this.dispatch(lightLevelCall(level));
|
|
96733
|
+
this.patchStatus({
|
|
96734
|
+
lightMode: "manual",
|
|
96735
|
+
lightLevel: level
|
|
96736
|
+
});
|
|
96737
|
+
}
|
|
96738
|
+
};
|
|
96739
|
+
}
|
|
96740
|
+
/**
|
|
96741
|
+
* `ptz` projected from `navigation` (the PTZ-mimic adapter). Every call
|
|
96742
|
+
* forwards to the navigation provider through the pure mapping; a preset
|
|
96743
|
+
* that names nothing enabled is rejected, never guessed.
|
|
96744
|
+
*/
|
|
96745
|
+
buildPtzMimic(nav) {
|
|
96746
|
+
const deviceId = this.id;
|
|
96747
|
+
const noGimbal = (what) => {
|
|
96748
|
+
throw new Error(`DreameCameraDevice: a robot camera has no ${what}`);
|
|
96749
|
+
};
|
|
96750
|
+
return {
|
|
96751
|
+
getStatus: async () => ({
|
|
96752
|
+
pan: 0,
|
|
96753
|
+
tilt: 0,
|
|
96754
|
+
zoom: 0,
|
|
96755
|
+
autofocus: false
|
|
96756
|
+
}),
|
|
96757
|
+
getPosition: async () => ({
|
|
96758
|
+
pan: 0,
|
|
96759
|
+
tilt: 0,
|
|
96760
|
+
zoom: 0
|
|
96761
|
+
}),
|
|
96762
|
+
getOptions: async () => ptzOptionsFromFeatures(await nav.getFeatures({ deviceId })),
|
|
96763
|
+
move: async (cmd) => {
|
|
96764
|
+
await nav.move({
|
|
96765
|
+
deviceId,
|
|
96766
|
+
...ptzMoveToNavigation(cmd)
|
|
96767
|
+
});
|
|
96768
|
+
},
|
|
96769
|
+
continuousMove: async (cmd) => {
|
|
96770
|
+
await nav.move({
|
|
96771
|
+
deviceId,
|
|
96772
|
+
...ptzMoveToNavigation(cmd)
|
|
96773
|
+
});
|
|
96774
|
+
},
|
|
96775
|
+
stop: async () => {
|
|
96776
|
+
await nav.stop({ deviceId });
|
|
96777
|
+
},
|
|
96778
|
+
goHome: async () => {
|
|
96779
|
+
await nav.runAction({
|
|
96780
|
+
deviceId,
|
|
96781
|
+
actionId: "goHome"
|
|
96782
|
+
});
|
|
96783
|
+
},
|
|
96784
|
+
getPresets: async () => presetsFromCatalog(await nav.listActions({ deviceId }), await nav.getFeatures({ deviceId })),
|
|
96785
|
+
goToPreset: async ({ presetId }) => {
|
|
96786
|
+
const target = presetTarget(await nav.listActions({ deviceId }), presetId);
|
|
96787
|
+
if (target === null) throw new Error(`DreameCameraDevice: unknown or disabled navigation preset "${presetId}"`);
|
|
96788
|
+
if (target.kind === "sound") await nav.playSound({
|
|
96789
|
+
deviceId,
|
|
96790
|
+
soundId: target.soundId
|
|
96791
|
+
});
|
|
96792
|
+
else await nav.runAction({
|
|
96793
|
+
deviceId,
|
|
96794
|
+
actionId: target.actionId
|
|
96795
|
+
});
|
|
96796
|
+
},
|
|
96797
|
+
savePreset: async () => noGimbal("saved preset — its presets are its actions"),
|
|
96798
|
+
deletePreset: async () => noGimbal("deletable preset — its presets are its actions"),
|
|
96799
|
+
setAutofocus: async () => noGimbal("autofocus")
|
|
96800
|
+
};
|
|
96801
|
+
}
|
|
96802
|
+
recomputeSlice() {
|
|
96803
|
+
const handle = this.resolveHandle();
|
|
96804
|
+
if (handle === null) return;
|
|
96805
|
+
const prev = this.runtimeState.getCapState(CAP_NAME$12) ?? NAV_COLD_START;
|
|
96806
|
+
const readback = handle instanceof VacuumDevice ? decodeNavigationReadback((siid, piid) => handle.getProperty(siid, piid)?.value) : {};
|
|
96807
|
+
const following = handle instanceof VacuumDevice ? decodeFollowing(handle.status) : null;
|
|
96808
|
+
this.runtimeState.setCapState(CAP_NAME$12, {
|
|
96809
|
+
...prev,
|
|
96810
|
+
...readback,
|
|
96811
|
+
...following !== null ? { following } : {},
|
|
96812
|
+
mode: following === true ? "following" : this.deriveMode(handle),
|
|
96813
|
+
lastChangedAt: Date.now()
|
|
96814
|
+
});
|
|
96815
|
+
}
|
|
96816
|
+
controller = null;
|
|
96817
|
+
controllerHandle = null;
|
|
96818
|
+
/**
|
|
96819
|
+
* The typed controller, built once per live handle (the build resolves the
|
|
96820
|
+
* cloud video profile, so it is a promise shared by every caller); rebuilt
|
|
96821
|
+
* if the handle changes. Never `open()`ed here.
|
|
96822
|
+
*/
|
|
96823
|
+
requireController() {
|
|
96824
|
+
const handle = this.requireHandle();
|
|
96825
|
+
if (!(handle instanceof VacuumDevice)) throw new Error("DreameCameraDevice: the robot handle is not a vacuum — no camera controller");
|
|
96826
|
+
if (this.controller === null || this.controllerHandle !== handle) {
|
|
96827
|
+
const accessCode = this.config.get("navigationAccessCode");
|
|
96828
|
+
this.controllerHandle = handle;
|
|
96829
|
+
this.controller = handle.createCameraController(accessCode !== void 0 && accessCode !== "" ? { accessCode } : {}).catch((err) => {
|
|
96830
|
+
this.controller = null;
|
|
96831
|
+
this.controllerHandle = null;
|
|
96832
|
+
throw err;
|
|
96833
|
+
});
|
|
96834
|
+
}
|
|
96835
|
+
return this.controller;
|
|
96836
|
+
}
|
|
96837
|
+
/** Perform a translated navigation call on the typed controller. */
|
|
96838
|
+
async dispatch(call) {
|
|
96839
|
+
await runControllerCall(await this.requireController(), call);
|
|
96840
|
+
}
|
|
96841
|
+
async removeDevice() {
|
|
96842
|
+
await super.removeDevice();
|
|
96843
|
+
const pending = this.controller;
|
|
96844
|
+
this.controller = null;
|
|
96845
|
+
this.controllerHandle = null;
|
|
96846
|
+
if (pending !== null) await pending.then((controller) => controller.close()).catch(() => void 0);
|
|
96847
|
+
}
|
|
96848
|
+
/** Per-device general-primitive feature flags: the defaults, then this device's overrides. */
|
|
96849
|
+
buildFeatures() {
|
|
96850
|
+
return resolveNavigationFeatures(this.overrides());
|
|
96851
|
+
}
|
|
96852
|
+
/** This device's navigation overrides, straight from its persisted config. */
|
|
96853
|
+
overrides() {
|
|
96854
|
+
const features = this.config.get("navigationFeatures");
|
|
96855
|
+
const disabledEntryIds = this.config.get("navigationDisabledEntryIds");
|
|
96856
|
+
return {
|
|
96857
|
+
...features !== void 0 ? { features } : {},
|
|
96858
|
+
...disabledEntryIds !== void 0 ? { disabledEntryIds } : {}
|
|
96859
|
+
};
|
|
96860
|
+
}
|
|
96861
|
+
/** Map the shared vacuum lifecycle state to a coarse navigation mode. */
|
|
96862
|
+
deriveMode(handle) {
|
|
96863
|
+
if (!(handle instanceof VacuumDevice)) return "unknown";
|
|
96864
|
+
switch (mapMiotStateToVacuumState(handle.status)) {
|
|
96865
|
+
case "cleaning": return "cleaning";
|
|
96866
|
+
case "paused": return "paused";
|
|
96867
|
+
case "returning": return "returning";
|
|
96868
|
+
case "idle":
|
|
96869
|
+
case "docked":
|
|
96870
|
+
case "drying": return "idle";
|
|
96871
|
+
default: return "unknown";
|
|
96872
|
+
}
|
|
96873
|
+
}
|
|
96874
|
+
/**
|
|
96875
|
+
* Optimistically reflect a just-issued action in the status slice so the UI
|
|
96876
|
+
* updates immediately (there is no confirmed read-back yet).
|
|
96877
|
+
*/
|
|
96878
|
+
applyOptimisticAction(actionId) {
|
|
96879
|
+
switch (actionId) {
|
|
96880
|
+
case "flashOn":
|
|
96881
|
+
this.patchStatus({ flash: true });
|
|
96882
|
+
return;
|
|
96883
|
+
case "flashOff":
|
|
96884
|
+
this.patchStatus({ flash: false });
|
|
96885
|
+
return;
|
|
96886
|
+
case "personFollow":
|
|
96887
|
+
this.patchStatus({
|
|
96888
|
+
following: true,
|
|
96889
|
+
mode: "following"
|
|
96890
|
+
});
|
|
96891
|
+
return;
|
|
96892
|
+
case "stop":
|
|
96893
|
+
this.patchStatus({ following: false });
|
|
96894
|
+
return;
|
|
96895
|
+
default: return;
|
|
96896
|
+
}
|
|
96897
|
+
}
|
|
96898
|
+
/**
|
|
96899
|
+
* Merge a patch into the status slice. Written THROUGH `runtimeState` — never a
|
|
96900
|
+
* parallel private cache (CLAUDE.md).
|
|
96901
|
+
*/
|
|
96902
|
+
patchStatus(patch) {
|
|
96903
|
+
const prev = this.runtimeState.getCapState(CAP_NAME$12) ?? NAV_COLD_START;
|
|
96904
|
+
this.runtimeState.setCapState(CAP_NAME$12, {
|
|
96905
|
+
...prev,
|
|
96906
|
+
...patch,
|
|
96907
|
+
lastChangedAt: Date.now()
|
|
96908
|
+
});
|
|
96909
|
+
}
|
|
96910
|
+
};
|
|
96911
|
+
//#endregion
|
|
94326
96912
|
//#region src/devices/dreame-enum-sensor-device.ts
|
|
94327
96913
|
var CAP_NAME$11 = "enum-sensor";
|
|
94328
96914
|
/** One persisted enum option: the MIoT int value + its display label. */
|
|
@@ -94547,12 +97133,12 @@ function mapMowerStatusToActivity(status) {
|
|
|
94547
97133
|
}
|
|
94548
97134
|
}
|
|
94549
97135
|
/** Clamp a raw battery reading (0..100 from the library) to the slice field. */
|
|
94550
|
-
function clampBatteryLevel
|
|
97136
|
+
function clampBatteryLevel(value) {
|
|
94551
97137
|
if (value === null || !Number.isFinite(value)) return null;
|
|
94552
97138
|
return Math.max(0, Math.min(100, value));
|
|
94553
97139
|
}
|
|
94554
97140
|
/** Clamp a 0..100 percentage (mowing progress), null-safe. */
|
|
94555
|
-
function clampPercent
|
|
97141
|
+
function clampPercent(value) {
|
|
94556
97142
|
if (value === null || !Number.isFinite(value)) return null;
|
|
94557
97143
|
return Math.max(0, Math.min(100, value));
|
|
94558
97144
|
}
|
|
@@ -94726,11 +97312,11 @@ var DreameMowerDevice = class extends DreameChildDevice {
|
|
|
94726
97312
|
if (mower === null) return;
|
|
94727
97313
|
const status = mower.status;
|
|
94728
97314
|
const code = mower.faultRaw;
|
|
94729
|
-
const livePct = clampPercent
|
|
97315
|
+
const livePct = clampPercent(mower.mowingProgressPct);
|
|
94730
97316
|
const prevPct = this.runtimeState.getCapState(CAP_NAME$9)?.progressPercent;
|
|
94731
97317
|
const slice = {
|
|
94732
97318
|
activity: mapMowerStatusToActivity(status),
|
|
94733
|
-
batteryLevel: clampBatteryLevel
|
|
97319
|
+
batteryLevel: clampBatteryLevel(mower.battery),
|
|
94734
97320
|
progressPercent: livePct ?? prevPct ?? null,
|
|
94735
97321
|
currentCode: code,
|
|
94736
97322
|
currentCodeLabel: code !== null ? MowerFault[code] ?? null : null,
|
|
@@ -95373,204 +97959,21 @@ var DreameSwitchDevice = class extends DreameChildDevice {
|
|
|
95373
97959
|
});
|
|
95374
97960
|
}
|
|
95375
97961
|
};
|
|
95376
|
-
|
|
95377
|
-
|
|
95378
|
-
|
|
95379
|
-
* Pure decode helpers for the Dreame vacuum. Translate the library's MIoT enum
|
|
95380
|
-
* values into the camstack `vacuum-control` slice shape. Unit-tested in
|
|
95381
|
-
* isolation (no facade, no network) — mirrors `homematic-decode.ts`.
|
|
95382
|
-
*/
|
|
95383
|
-
/**
|
|
95384
|
-
* Map a Dreame {@link MiotState} (siid 2 piid 1) onto the camstack canonical
|
|
95385
|
-
* `VacuumState` lifecycle (`idle` / `cleaning` / `paused` / `returning` /
|
|
95386
|
-
* `docked` / `error`). A `null` state (not yet observed) settles on `idle`.
|
|
95387
|
-
*
|
|
95388
|
-
* The MIoT state space is large (~50 values); we collapse it onto the six
|
|
95389
|
-
* canonical states by family. Anything unrecognised falls through to `idle`
|
|
95390
|
-
* (the safe neutral) so a firmware-added code never throws.
|
|
95391
|
-
*/
|
|
95392
|
-
function mapMiotStateToVacuumState(state) {
|
|
95393
|
-
if (state === null) return "idle";
|
|
95394
|
-
switch (state) {
|
|
95395
|
-
case MiotState.Charging:
|
|
95396
|
-
case MiotState.ChargingComplete:
|
|
95397
|
-
case MiotState.IntelligentCharging:
|
|
95398
|
-
case MiotState.CleanWashboardBase:
|
|
95399
|
-
case MiotState.AutoRepairBase:
|
|
95400
|
-
case MiotState.WaterSupplyDrainTest:
|
|
95401
|
-
case MiotState.CleanMopRefillWater:
|
|
95402
|
-
case MiotState.AutoEmptying:
|
|
95403
|
-
case MiotState.Emptying:
|
|
95404
|
-
case MiotState.Draining:
|
|
95405
|
-
case MiotState.AutoWaterDraining:
|
|
95406
|
-
case MiotState.Sanitizing: return "docked";
|
|
95407
|
-
case MiotState.MopDrying:
|
|
95408
|
-
case MiotState.DustBagDrying:
|
|
95409
|
-
case MiotState.SanitizingWithDry: return "drying";
|
|
95410
|
-
case MiotState.ReturningToCharge:
|
|
95411
|
-
case MiotState.ReturningToWash:
|
|
95412
|
-
case MiotState.ReturnInstallMop:
|
|
95413
|
-
case MiotState.ReturnRemoveMop:
|
|
95414
|
-
case MiotState.ReturnToEmpty:
|
|
95415
|
-
case MiotState.ReturningToDrain: return "returning";
|
|
95416
|
-
case MiotState.Paused:
|
|
95417
|
-
case MiotState.PausedAlt:
|
|
95418
|
-
case MiotState.PausedCleaning:
|
|
95419
|
-
case MiotState.DustBagDryingPaused:
|
|
95420
|
-
case MiotState.CameraMonitoringPaused:
|
|
95421
|
-
case MiotState.FindingPetPaused:
|
|
95422
|
-
case MiotState.InitialDeepCleanPaused:
|
|
95423
|
-
case MiotState.ChangingMopPaused:
|
|
95424
|
-
case MiotState.FloorMaintainingPaused: return "paused";
|
|
95425
|
-
case MiotState.Standby:
|
|
95426
|
-
case MiotState.WaitingForTask: return "idle";
|
|
95427
|
-
default: return "cleaning";
|
|
95428
|
-
}
|
|
95429
|
-
}
|
|
95430
|
-
/**
|
|
95431
|
-
* The camstack `fanSpeed` token for a Dreame {@link SuctionLevel} — the enum
|
|
95432
|
-
* member NAME, lower-cased (`Quiet` → `'quiet'`). Returns `null` for an
|
|
95433
|
-
* unobserved / unknown level.
|
|
95434
|
-
*/
|
|
95435
|
-
function suctionLevelToken(level) {
|
|
95436
|
-
if (level === null) return null;
|
|
95437
|
-
const name = SuctionLevel[level];
|
|
95438
|
-
return typeof name === "string" ? name.toLowerCase() : null;
|
|
95439
|
-
}
|
|
95440
|
-
/**
|
|
95441
|
-
* Resolve a camstack `fanSpeed` token back to the Dreame {@link SuctionLevel}
|
|
95442
|
-
* enum value, validated against the model's `supported` levels. Returns null
|
|
95443
|
-
* when the token doesn't match a supported level (the caller rejects the set).
|
|
95444
|
-
*/
|
|
95445
|
-
function tokenToSuctionLevel(token, supported) {
|
|
95446
|
-
const wanted = token.trim().toLowerCase();
|
|
95447
|
-
for (const level of supported) if (suctionLevelToken(level) === wanted) return level;
|
|
95448
|
-
return null;
|
|
95449
|
-
}
|
|
95450
|
-
/** The list of accepted `fanSpeed` tokens for a model's supported suction set. */
|
|
95451
|
-
function supportedFanSpeedTokens(supported) {
|
|
95452
|
-
const tokens = [];
|
|
95453
|
-
for (const level of supported) {
|
|
95454
|
-
const token = suctionLevelToken(level);
|
|
95455
|
-
if (token !== null) tokens.push(token);
|
|
95456
|
-
}
|
|
95457
|
-
return tokens;
|
|
95458
|
-
}
|
|
95459
|
-
/**
|
|
95460
|
-
* Clamp a raw battery reading (already a 0..100 percentage from the library)
|
|
95461
|
-
* to the `vacuum-control` slice's `batteryLevel` field. Returns null for a
|
|
95462
|
-
* null/NaN/out-of-band reading so the slice stays valid (the schema rejects
|
|
95463
|
-
* a number outside 0..100).
|
|
95464
|
-
*/
|
|
95465
|
-
function clampBatteryLevel(value) {
|
|
95466
|
-
if (value === null || !Number.isFinite(value)) return null;
|
|
95467
|
-
return Math.max(0, Math.min(100, value));
|
|
95468
|
-
}
|
|
95469
|
-
/** Clamp a 0..100 percentage (progress, coverage, …), null-safe. */
|
|
95470
|
-
function clampPercent(value) {
|
|
95471
|
-
if (value === null || !Number.isFinite(value)) return null;
|
|
95472
|
-
return Math.max(0, Math.min(100, value));
|
|
95473
|
-
}
|
|
95474
|
-
/**
|
|
95475
|
-
* Clean-water tank `status`: prefer the richer clean-water-tank-status (27/1),
|
|
95476
|
-
* fall back to the coarser water-tank presence (4/6). `null` when neither maps
|
|
95477
|
-
* to a known good/low reading (NOT_INSTALLED / CHECKING / unknown). Pure.
|
|
95478
|
-
*/
|
|
95479
|
-
function cleanWaterTankStatus(props) {
|
|
95480
|
-
if (props.cleanWaterTankStatus !== null) {
|
|
95481
|
-
if (props.cleanWaterTankStatus === 0) return "ok";
|
|
95482
|
-
if (props.cleanWaterTankStatus === 2) return "low";
|
|
95483
|
-
return null;
|
|
95484
|
-
}
|
|
95485
|
-
if (props.waterTank === 1 || props.waterTank === 10) return "ok";
|
|
95486
|
-
return null;
|
|
95487
|
-
}
|
|
95488
|
-
/** Detergent tank `status` from detergent-status (27/4). Pure. */
|
|
95489
|
-
function detergentTankStatus(value) {
|
|
95490
|
-
if (value === 0) return "ok";
|
|
95491
|
-
if (value === 2) return "low";
|
|
95492
|
-
return null;
|
|
95493
|
-
}
|
|
95494
|
-
/** Dust-bin `status` from dust-collection (15/3); OVER_USE reads as `full`. Pure. */
|
|
95495
|
-
function dustBinTankStatus(value) {
|
|
95496
|
-
if (value === 1) return "ok";
|
|
95497
|
-
if (value === 2) return "full";
|
|
95498
|
-
return null;
|
|
95499
|
-
}
|
|
95500
|
-
/**
|
|
95501
|
-
* The four `vacuum-control` tank fields (clean-water / dirty-water / detergent /
|
|
95502
|
-
* dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
|
|
95503
|
-
* discrete `status` populated from the live MIoT enum props when supplied. Pure.
|
|
95504
|
-
*
|
|
95505
|
-
* The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
|
|
95506
|
-
* (verified against a real `dreame.vacuum.r2538z`) carries only the water-VOLUME
|
|
95507
|
-
* *setting* (a mop-flow level, NOT a tank fill). We therefore never fabricate a
|
|
95508
|
-
* `level`; `level` is always `null`. The cap contract is honoured on PRESENCE,
|
|
95509
|
-
* with `status` derived from the tank-status enums:
|
|
95510
|
-
*
|
|
95511
|
-
* - a field is `null` when the hardware HAS NO such tank (capability flags), and
|
|
95512
|
-
* - `{ level: null, status }` when the tank EXISTS — `status` from the enum
|
|
95513
|
-
* prop ('ok' / 'low' / 'full'), or `null` when the device reports none.
|
|
95514
|
-
*
|
|
95515
|
-
* Presence is derived from the capability record:
|
|
95516
|
-
* - clean-water ← `canMop` (a mopping robot carries a clean-water reservoir)
|
|
95517
|
-
* - dirty-water ← `canMopWash` (a self-wash dock has a recovery/dirty tank)
|
|
95518
|
-
* - detergent ← `hasDetergentReservoir`
|
|
95519
|
-
* - dust-bin ← `canAutoEmpty` (an auto-empty robot has an onboard dust bin)
|
|
95520
|
-
*/
|
|
95521
|
-
function decodeVacuumTanks(caps, props) {
|
|
95522
|
-
const tank = (present, status) => present ? {
|
|
95523
|
-
level: null,
|
|
95524
|
-
status
|
|
95525
|
-
} : null;
|
|
97962
|
+
/** `null` = nothing learned (keep the previous slice). */
|
|
97963
|
+
function networkLinkFromBars(bars, now) {
|
|
97964
|
+
if (bars === null || !Number.isFinite(bars)) return null;
|
|
95526
97965
|
return {
|
|
95527
|
-
|
|
95528
|
-
|
|
95529
|
-
|
|
95530
|
-
dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection) : null)
|
|
95531
|
-
};
|
|
95532
|
-
}
|
|
95533
|
-
/**
|
|
95534
|
-
* Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
|
|
95535
|
-
* value is absent / non-finite. The library reports remaining life as a 0..100
|
|
95536
|
-
* integer; we clamp defensively so an out-of-range firmware value never violates
|
|
95537
|
-
* the cap schema.
|
|
95538
|
-
*/
|
|
95539
|
-
function clampConsumablePct(value) {
|
|
95540
|
-
if (value === null || !Number.isFinite(value)) return null;
|
|
95541
|
-
return Math.max(0, Math.min(100, value));
|
|
95542
|
-
}
|
|
95543
|
-
/**
|
|
95544
|
-
* Build the `consumables` cap runtime-state slice from a live {@link VacuumDevice}
|
|
95545
|
-
* handle. The returned object satisfies the cap's `runtimeState` schema
|
|
95546
|
-
* (`ConsumablesStatusSchema.extend({ lastFetchedAt: z.number() })`).
|
|
95547
|
-
*
|
|
95548
|
-
* PRESENCE-driven (mirrors HA): the library's `supportedConsumables` enumerates
|
|
95549
|
-
* ONLY the consumables this model reports (a present MIoT life property) — main/
|
|
95550
|
-
* side brush, filter, sensor, tank-filter, mop-pad, silver-ion, detergent,
|
|
95551
|
-
* squeegee, dust-bag — each with its remaining-life % and whether a reset action
|
|
95552
|
-
* exists. No hardcoded per-model list: a consumable the device does not expose
|
|
95553
|
-
* never appears, and a `resettable` item drives the real MIoT reset action.
|
|
95554
|
-
*/
|
|
95555
|
-
function buildVacuumConsumables(vacuum) {
|
|
95556
|
-
const items = vacuum.supportedConsumables.map((c) => ({
|
|
95557
|
-
key: c.key,
|
|
95558
|
-
label: c.label,
|
|
95559
|
-
level: clampConsumablePct(c.leftPct),
|
|
95560
|
-
status: null,
|
|
95561
|
-
lastResetAt: null,
|
|
95562
|
-
resettable: c.resettable
|
|
95563
|
-
}));
|
|
95564
|
-
const now = Date.now();
|
|
95565
|
-
return {
|
|
95566
|
-
items: [...items],
|
|
95567
|
-
lastChangedAt: now,
|
|
95568
|
-
lastFetchedAt: now
|
|
97966
|
+
type: "wifi",
|
|
97967
|
+
signalPercent: bars === 0 ? null : signalPercentFromBars(bars, 4),
|
|
97968
|
+
lastUpdated: now
|
|
95569
97969
|
};
|
|
95570
97970
|
}
|
|
95571
97971
|
//#endregion
|
|
95572
97972
|
//#region src/devices/dreame-vacuum-device.ts
|
|
95573
97973
|
var CAP_NAME$1 = "vacuum-control";
|
|
97974
|
+
var NETWORK_LINK_CAP_NAME = "network-link";
|
|
97975
|
+
/** The Wi-Fi signal map is a cloud round trip; the bars at the dock barely move. */
|
|
97976
|
+
var NETWORK_LINK_REFRESH_MS = 30 * 6e4;
|
|
95574
97977
|
var CONSUMABLES_CAP_NAME = "consumables";
|
|
95575
97978
|
var CONTROL_COLD_START = {
|
|
95576
97979
|
state: "idle",
|
|
@@ -95626,6 +98029,8 @@ var DreameVacuumDevice = class extends DreameChildDevice {
|
|
|
95626
98029
|
}
|
|
95627
98030
|
async onActivate() {
|
|
95628
98031
|
await super.onActivate();
|
|
98032
|
+
this.refreshNetworkLink("activate");
|
|
98033
|
+
this.networkLinkTimer = setInterval(() => void this.refreshNetworkLink("periodic"), NETWORK_LINK_REFRESH_MS);
|
|
95629
98034
|
const vacuum = this.resolveVacuum();
|
|
95630
98035
|
if (vacuum !== null) {
|
|
95631
98036
|
vacuum.refreshFromCache().catch((err) => {
|
|
@@ -95649,11 +98054,60 @@ var DreameVacuumDevice = class extends DreameChildDevice {
|
|
|
95649
98054
|
}
|
|
95650
98055
|
}
|
|
95651
98056
|
/** Narrow the generic handle to the library's typed {@link DreameVacuum}, or null. */
|
|
98057
|
+
networkLinkTimer = null;
|
|
98058
|
+
networkLinkReported = false;
|
|
98059
|
+
networkLinkFailureWarned = false;
|
|
98060
|
+
/** `network-link`: the read path serves the slice; `refreshNetworkLink` writes it. */
|
|
98061
|
+
registerNetworkLink() {
|
|
98062
|
+
this.ctx.registerNativeCap(networkLinkCapability, { getStatus: async () => this.runtimeState.getCapState(NETWORK_LINK_CAP_NAME) ?? NETWORK_LINK_UNKNOWN });
|
|
98063
|
+
if (this.runtimeState.getCapState(NETWORK_LINK_CAP_NAME) === null) this.runtimeState.setCapState(NETWORK_LINK_CAP_NAME, NETWORK_LINK_UNKNOWN);
|
|
98064
|
+
}
|
|
98065
|
+
/**
|
|
98066
|
+
* Read the bars at the robot's position off its Wi-Fi signal map. Bounded,
|
|
98067
|
+
* best-effort; a failure keeps the last slice. The first read per device is
|
|
98068
|
+
* said at INFO with the raw bars, the first failure at WARN.
|
|
98069
|
+
*/
|
|
98070
|
+
async refreshNetworkLink(reason) {
|
|
98071
|
+
const vacuum = this.resolveVacuum();
|
|
98072
|
+
if (vacuum === null) return;
|
|
98073
|
+
try {
|
|
98074
|
+
const bars = await vacuum.getCurrentSignal({ maxWaitMs: 2e4 });
|
|
98075
|
+
const next = networkLinkFromBars(bars, Date.now());
|
|
98076
|
+
this.ctx.logger[this.networkLinkReported ? "debug" : "info"]("network link read", {
|
|
98077
|
+
tags: { deviceId: this.id },
|
|
98078
|
+
meta: {
|
|
98079
|
+
reason,
|
|
98080
|
+
rawBars: bars,
|
|
98081
|
+
...next ?? {}
|
|
98082
|
+
}
|
|
98083
|
+
});
|
|
98084
|
+
this.networkLinkReported = true;
|
|
98085
|
+
if (next !== null) this.runtimeState.setCapState(NETWORK_LINK_CAP_NAME, next);
|
|
98086
|
+
} catch (err) {
|
|
98087
|
+
const level = this.networkLinkFailureWarned ? "debug" : "warn";
|
|
98088
|
+
this.networkLinkFailureWarned = true;
|
|
98089
|
+
this.ctx.logger[level]("network link read failed — keeping the last slice", {
|
|
98090
|
+
tags: { deviceId: this.id },
|
|
98091
|
+
meta: {
|
|
98092
|
+
reason,
|
|
98093
|
+
error: err instanceof Error ? err.message : String(err)
|
|
98094
|
+
}
|
|
98095
|
+
});
|
|
98096
|
+
}
|
|
98097
|
+
}
|
|
98098
|
+
async removeDevice() {
|
|
98099
|
+
if (this.networkLinkTimer !== null) {
|
|
98100
|
+
clearInterval(this.networkLinkTimer);
|
|
98101
|
+
this.networkLinkTimer = null;
|
|
98102
|
+
}
|
|
98103
|
+
await super.removeDevice();
|
|
98104
|
+
}
|
|
95652
98105
|
resolveVacuum() {
|
|
95653
98106
|
const handle = this.resolveHandle();
|
|
95654
98107
|
return handle instanceof VacuumDevice ? handle : null;
|
|
95655
98108
|
}
|
|
95656
98109
|
registerControlCap() {
|
|
98110
|
+
this.registerNetworkLink();
|
|
95657
98111
|
this.ctx.registerNativeCap(vacuumControlCapability, {
|
|
95658
98112
|
getStatus: async () => this.runtimeState.getCapState(CAP_NAME$1) ?? CONTROL_COLD_START,
|
|
95659
98113
|
start: async () => {
|
|
@@ -95710,14 +98164,14 @@ var DreameVacuumDevice = class extends DreameChildDevice {
|
|
|
95710
98164
|
const tanks = decodeVacuumTanks(vacuum.vacuumCapabilities, this.readTankProps());
|
|
95711
98165
|
const controlSlice = {
|
|
95712
98166
|
state: mapMiotStateToVacuumState(vacuum.status),
|
|
95713
|
-
batteryLevel: clampBatteryLevel(vacuum.battery),
|
|
98167
|
+
batteryLevel: clampBatteryLevel$1(vacuum.battery),
|
|
95714
98168
|
fanSpeed: suctionLevelToken(vacuum.suction),
|
|
95715
98169
|
availableFanSpeeds: supportedFanSpeedTokens(vacuum.supportedSuctionLevels),
|
|
95716
98170
|
cleanWater: tanks.cleanWater,
|
|
95717
98171
|
dirtyWater: tanks.dirtyWater,
|
|
95718
98172
|
detergent: tanks.detergent,
|
|
95719
98173
|
dustBin: tanks.dustBin,
|
|
95720
|
-
progressPercent: clampPercent(vacuum.taskProgressPct),
|
|
98174
|
+
progressPercent: clampPercent$1(vacuum.taskProgressPct),
|
|
95721
98175
|
errorCode: vacuum.errorCode,
|
|
95722
98176
|
errorLabel: vacuum.errorCode ? `Error ${vacuum.errorCode}` : null,
|
|
95723
98177
|
lastChangedAt: Date.now()
|
|
@@ -96043,6 +98497,15 @@ function dreameMowerMapSelectChildFactory(ctx) {
|
|
|
96043
98497
|
return new DreameMowerMapSelectDevice(ctx);
|
|
96044
98498
|
}
|
|
96045
98499
|
/**
|
|
98500
|
+
* Factory for the CAMERA + NAVIGATION child (a `DeviceType.Camera` grouped under
|
|
98501
|
+
* the Container), spawned only for camera-capable vacuums. Owns the `navigation`
|
|
98502
|
+
* cap (drive / actions / sounds / fill light). Selected directly by the
|
|
98503
|
+
* Container's `getAccessoryChildren()` for the camera child spec.
|
|
98504
|
+
*/
|
|
98505
|
+
function dreameCameraChildFactory(ctx) {
|
|
98506
|
+
return new DreameCameraDevice(ctx);
|
|
98507
|
+
}
|
|
98508
|
+
/**
|
|
96046
98509
|
* Factory for an AI-obstacle Switch child (a `DeviceType.Switch` grouped under
|
|
96047
98510
|
* the Container). Like {@link dreameMapChildFactory}, selected directly by the
|
|
96048
98511
|
* Container's `getAccessoryChildren()` — the child config carries the
|
|
@@ -96214,6 +98677,20 @@ var DreameContainerDevice = class extends BaseDevice {
|
|
|
96214
98677
|
factory: dreameMapChildFactory
|
|
96215
98678
|
});
|
|
96216
98679
|
}
|
|
98680
|
+
if (kind === "vacuum" && this.modelSupportsCamera()) {
|
|
98681
|
+
const cameraMeta = {
|
|
98682
|
+
type: DeviceType.Camera,
|
|
98683
|
+
name: this.name.length > 0 ? `${this.name} Camera` : "Camera",
|
|
98684
|
+
linkDeviceId: this.id,
|
|
98685
|
+
...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
|
|
98686
|
+
};
|
|
98687
|
+
children.push({
|
|
98688
|
+
stableIdSuffix: "camera",
|
|
98689
|
+
meta: cameraMeta,
|
|
98690
|
+
config,
|
|
98691
|
+
factory: dreameCameraChildFactory
|
|
98692
|
+
});
|
|
98693
|
+
}
|
|
96217
98694
|
if (kind === "vacuum") {
|
|
96218
98695
|
for (const feature of this.supportedAiFeatures()) {
|
|
96219
98696
|
const switchMeta = {
|
|
@@ -96647,6 +99124,7 @@ var DreameContainerDevice = class extends BaseDevice {
|
|
|
96647
99124
|
const caps = handle.vacuumCapabilities;
|
|
96648
99125
|
return {
|
|
96649
99126
|
canMap: caps.canMap,
|
|
99127
|
+
hasCamera: caps.hasCamera,
|
|
96650
99128
|
hasAiObstacleDetection: caps.hasAiObstacleDetection,
|
|
96651
99129
|
aiFeatureCount: caps.hasAiObstacleDetection ? caps.supportedAiFeatures.length : 0
|
|
96652
99130
|
};
|
|
@@ -96674,6 +99152,12 @@ var DreameContainerDevice = class extends BaseDevice {
|
|
|
96674
99152
|
modelSupportsMap() {
|
|
96675
99153
|
return vacuumSupportsMap(this.resolveVacuumCaps());
|
|
96676
99154
|
}
|
|
99155
|
+
/** Whether this vacuum model carries an on-board camera — gates the separate
|
|
99156
|
+
* camera + navigation child. Gated on the RESOLVED handle's confirmed
|
|
99157
|
+
* capability (false when the handle is not up, never optimistic). */
|
|
99158
|
+
modelSupportsCamera() {
|
|
99159
|
+
return vacuumSupportsCamera(this.resolveVacuumCaps());
|
|
99160
|
+
}
|
|
96677
99161
|
/** Whether this mower model supports a map — gated on the RESOLVED handle's
|
|
96678
99162
|
* confirmed capability (false when the handle is not up, never optimistic). */
|
|
96679
99163
|
modelSupportsMowerMap() {
|