flaks-node-hon 1.0.0
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/LICENSE +21 -0
- package/README.md +144 -0
- package/bin/node-hon.js +94 -0
- package/cli/ac_apply_preset.js +36 -0
- package/cli/ac_generate_preset.js +80 -0
- package/cli/ac_turn_off.js +20 -0
- package/cli/ac_turn_on.js +21 -0
- package/cli/config.js +84 -0
- package/cli/purge_cache.js +16 -0
- package/cli/show_my_ac_capabilities.js +36 -0
- package/cli/show_my_ac_devices.js +19 -0
- package/config_example.js +10 -0
- package/package.json +41 -0
- package/presets/preset_auto.json +7 -0
- package/presets/preset_cool.json +8 -0
- package/presets/preset_dry.json +6 -0
- package/presets/preset_fan.json +8 -0
- package/src/ac.js +330 -0
- package/src/api.js +123 -0
- package/src/appliance-identity.js +71 -0
- package/src/appliance.js +282 -0
- package/src/auth.js +424 -0
- package/src/caching/appliance-cache.js +71 -0
- package/src/caching/session-store.js +47 -0
- package/src/client.js +253 -0
- package/src/command.js +314 -0
- package/src/connection.js +73 -0
- package/src/constants.js +17 -0
- package/src/device.js +29 -0
- package/src/errors.js +38 -0
- package/src/index.js +25 -0
- package/src/lib/config.js +22 -0
- package/src/lib/cookie-jar.js +36 -0
- package/src/lib/logger.js +56 -0
- package/src/lib-cli/_format.js +33 -0
- package/src/lib-cli/_get-ac-client.js +29 -0
- package/src/lib-cli/_get-client.js +25 -0
- package/src/lib-cli/_prompt.js +61 -0
- package/src/lib-cli/_run.js +18 -0
- package/src/lib-cli/_select-ac.js +36 -0
- package/src/parameters.js +261 -0
- package/src/preset-generator.js +171 -0
- package/types/global.ts +19 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const constants = require("./constants");
|
|
2
|
+
const { HonAuthError } = require("./errors");
|
|
3
|
+
|
|
4
|
+
class HonConnection {
|
|
5
|
+
constructor(auth, fetchImpl = globalThis.fetch) {
|
|
6
|
+
this.auth = auth;
|
|
7
|
+
this.fetch = fetchImpl;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async get(url, options = {}) {
|
|
11
|
+
return this.request("GET", url, options);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async post(url, options = {}) {
|
|
15
|
+
return this.request("POST", url, options);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async request(method, url, options = {}, loop = 0) {
|
|
19
|
+
const headers = {
|
|
20
|
+
"user-agent": constants.USER_AGENT,
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
...(options.headers || {})
|
|
23
|
+
};
|
|
24
|
+
if (this.auth.refreshToken && this.auth.tokenExpiresSoon() && loop === 0) {
|
|
25
|
+
await this.auth.refresh();
|
|
26
|
+
}
|
|
27
|
+
if (!this.auth.sessionToken || !this.auth.idToken) {
|
|
28
|
+
await this.auth.initialize();
|
|
29
|
+
}
|
|
30
|
+
headers["cognito-token"] = this.auth.sessionToken;
|
|
31
|
+
headers["id-token"] = this.auth.idToken;
|
|
32
|
+
|
|
33
|
+
const response = await this.fetch(url, { ...options, method, headers });
|
|
34
|
+
if ((this.auth.tokenExpiresSoon() || response.status === 401 || response.status === 403) && loop === 0) {
|
|
35
|
+
await this.auth.refresh();
|
|
36
|
+
return this.request(method, url, options, loop + 1);
|
|
37
|
+
}
|
|
38
|
+
if ((this.auth.tokenIsExpired() || response.status === 401 || response.status === 403) && loop === 1) {
|
|
39
|
+
await this.auth.authenticate();
|
|
40
|
+
return this.request(method, url, options, loop + 1);
|
|
41
|
+
}
|
|
42
|
+
if (loop >= 2 && (response.status === 401 || response.status === 403)) {
|
|
43
|
+
throw new HonAuthError("Login failure", { status: response.status, url });
|
|
44
|
+
}
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class HonAnonymousConnection {
|
|
50
|
+
constructor(fetchImpl = globalThis.fetch) {
|
|
51
|
+
this.fetch = fetchImpl;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async get(url, options = {}) {
|
|
55
|
+
return this.request("GET", url, options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async post(url, options = {}) {
|
|
59
|
+
return this.request("POST", url, options);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async request(method, url, options = {}) {
|
|
63
|
+
const headers = {
|
|
64
|
+
"user-agent": constants.USER_AGENT,
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"x-api-key": constants.API_KEY,
|
|
67
|
+
...(options.headers || {})
|
|
68
|
+
};
|
|
69
|
+
return this.fetch(url, { ...options, method, headers });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { HonConnection, HonAnonymousConnection };
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
AUTH_API: "https://account2.hon-smarthome.com",
|
|
3
|
+
API_URL: "https://api-iot.he.services",
|
|
4
|
+
API_KEY: "GRCqFhC6Gk@ikWXm1RmnSmX1cm,MxY-configuration",
|
|
5
|
+
AWS_ENDPOINT: "a30f6tqw0oh1x0-ats.iot.eu-west-1.amazonaws.com",
|
|
6
|
+
AWS_AUTHORIZER: "candy-iot-authorizer",
|
|
7
|
+
APP: "hon",
|
|
8
|
+
CLIENT_ID:
|
|
9
|
+
"3MVG9QDx8IX8nP5T2Ha8ofvlmjLZl5L_gvfbT9." +
|
|
10
|
+
"HJvpHGKoAS_dcMN8LYpTSYeVFCraUnV.2Ag1Ki7m4znVO6",
|
|
11
|
+
APP_VERSION: "2.6.5",
|
|
12
|
+
OS_VERSION: 999,
|
|
13
|
+
OS: "android",
|
|
14
|
+
DEVICE_MODEL: "pyhOn",
|
|
15
|
+
USER_AGENT: "Chrome/999.999.999.999",
|
|
16
|
+
MOBILE_ID: "pyhOn"
|
|
17
|
+
};
|
package/src/device.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const constants = require("./constants");
|
|
2
|
+
|
|
3
|
+
class HonDevice {
|
|
4
|
+
constructor(mobileId = "") {
|
|
5
|
+
this.appVersion = constants.APP_VERSION;
|
|
6
|
+
this.osVersion = constants.OS_VERSION;
|
|
7
|
+
this.os = constants.OS;
|
|
8
|
+
this.deviceModel = constants.DEVICE_MODEL;
|
|
9
|
+
this.mobileId = mobileId || constants.MOBILE_ID;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get(mobile = false) {
|
|
13
|
+
/** @type {{ appVersion: string | number, mobileId: string, os?: string, osVersion: string | number, deviceModel: string, mobileOs?: string }} */
|
|
14
|
+
const result = {
|
|
15
|
+
appVersion: this.appVersion,
|
|
16
|
+
mobileId: this.mobileId,
|
|
17
|
+
os: this.os,
|
|
18
|
+
osVersion: this.osVersion,
|
|
19
|
+
deviceModel: this.deviceModel
|
|
20
|
+
};
|
|
21
|
+
if (mobile) {
|
|
22
|
+
result.mobileOs = result.os;
|
|
23
|
+
delete result.os;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { HonDevice };
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class HonAuthError extends Error {
|
|
2
|
+
constructor(message, details) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "HonAuthError";
|
|
5
|
+
this.details = details;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class HonApiError extends Error {
|
|
10
|
+
constructor(message, details) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "HonApiError";
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class UnsupportedControlError extends Error {
|
|
18
|
+
constructor(message, details) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "UnsupportedControlError";
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class ApplianceNotFoundError extends Error {
|
|
26
|
+
constructor(message, details) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "ApplianceNotFoundError";
|
|
29
|
+
this.details = details;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
HonAuthError,
|
|
35
|
+
HonApiError,
|
|
36
|
+
UnsupportedControlError,
|
|
37
|
+
ApplianceNotFoundError
|
|
38
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { HonClient } = require("./client");
|
|
4
|
+
const { HonAppliance } = require("./appliance");
|
|
5
|
+
const { HonCommand } = require("./command");
|
|
6
|
+
const { HonAirConditioner } = require("./ac");
|
|
7
|
+
const presetGenerator = require("./preset-generator");
|
|
8
|
+
const {
|
|
9
|
+
HonAuthError,
|
|
10
|
+
HonApiError,
|
|
11
|
+
UnsupportedControlError,
|
|
12
|
+
ApplianceNotFoundError,
|
|
13
|
+
} = require("./errors");
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
HonClient,
|
|
17
|
+
HonAppliance,
|
|
18
|
+
HonCommand,
|
|
19
|
+
HonAirConditioner,
|
|
20
|
+
presetGenerator,
|
|
21
|
+
HonAuthError,
|
|
22
|
+
HonApiError,
|
|
23
|
+
UnsupportedControlError,
|
|
24
|
+
ApplianceNotFoundError,
|
|
25
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
|
|
3
|
+
function loadConfig(configPath = path.resolve(__dirname, "..", "config.js")) {
|
|
4
|
+
const resolved = path.resolve(configPath);
|
|
5
|
+
const config = require(resolved);
|
|
6
|
+
validateConfig(config, resolved);
|
|
7
|
+
return config;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function validateConfig(config, source = "config") {
|
|
11
|
+
if (!config || typeof config !== "object") {
|
|
12
|
+
throw new TypeError(`${source} must export a config object`);
|
|
13
|
+
}
|
|
14
|
+
for (const key of ["email", "password", "sessionFile"]) {
|
|
15
|
+
if (!config[key] || typeof config[key] !== "string") {
|
|
16
|
+
throw new TypeError(`${source} must define string ${key}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { loadConfig, validateConfig };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class CookieJar {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.cookies = new Map();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
addFromResponse(headers) {
|
|
7
|
+
const values =
|
|
8
|
+
typeof headers.getSetCookie === "function"
|
|
9
|
+
? headers.getSetCookie()
|
|
10
|
+
: splitSetCookie(headers.get("set-cookie"));
|
|
11
|
+
for (const value of values) {
|
|
12
|
+
const cookie = value.split(";")[0];
|
|
13
|
+
const index = cookie.indexOf("=");
|
|
14
|
+
if (index > 0) {
|
|
15
|
+
this.cookies.set(cookie.slice(0, index), cookie.slice(index + 1));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
header() {
|
|
21
|
+
return [...this.cookies.entries()].map(([key, value]) => `${key}=${value}`).join("; ");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
clear() {
|
|
25
|
+
this.cookies.clear();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function splitSetCookie(value) {
|
|
30
|
+
if (!value) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return value.split(/,(?=\s*[^;,=\s]+=[^;,]+)/g);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { CookieJar };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class DebugLogger {
|
|
2
|
+
/**
|
|
3
|
+
* @param {{ enabled?: boolean, sink?: (line: string) => void, now?: () => Date }} [options]
|
|
4
|
+
*/
|
|
5
|
+
constructor({ enabled = false, sink = console.log, now = () => new Date() } = {}) {
|
|
6
|
+
this.enabled = Boolean(enabled);
|
|
7
|
+
this.sink = sink;
|
|
8
|
+
this.now = now;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
log(message) {
|
|
12
|
+
this.write(message, this.now());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
write(message, date) {
|
|
16
|
+
if (!this.enabled) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.sink(`${formatTimestamp(date)}: ${message}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start(message) {
|
|
23
|
+
const startedAt = this.now();
|
|
24
|
+
this.write(message, startedAt);
|
|
25
|
+
return {
|
|
26
|
+
success: (message) => {
|
|
27
|
+
const endedAt = this.now();
|
|
28
|
+
this.write(`${message} (${formatElapsed(startedAt, endedAt)})`, endedAt);
|
|
29
|
+
},
|
|
30
|
+
failure: (message) => {
|
|
31
|
+
const endedAt = this.now();
|
|
32
|
+
this.write(`${message} (${formatElapsed(startedAt, endedAt)})`, endedAt);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatTimestamp(date) {
|
|
39
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
40
|
+
return [
|
|
41
|
+
date.getFullYear(),
|
|
42
|
+
pad(date.getMonth() + 1),
|
|
43
|
+
pad(date.getDate())
|
|
44
|
+
].join("-") + " " + [
|
|
45
|
+
pad(date.getHours()),
|
|
46
|
+
pad(date.getMinutes()),
|
|
47
|
+
pad(date.getSeconds())
|
|
48
|
+
].join(":");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatElapsed(startedAt, endedAt) {
|
|
52
|
+
const seconds = Math.max(0, Math.round((endedAt.getTime() - startedAt.getTime()) / 1000));
|
|
53
|
+
return `${seconds}secs`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { DebugLogger, formatTimestamp, formatElapsed };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function formatAc(ac) {
|
|
4
|
+
return `${ac.nickName} (${ac.macAddress})`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function formatAcIdentifiers(ac) {
|
|
8
|
+
return `macAddress=${ac.macAddress} uniqueId=${ac.uniqueId} nickName=${ac.nickName}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function printAcList(airConditioners, writer = console.log) {
|
|
12
|
+
if (!airConditioners.length) {
|
|
13
|
+
writer("No air conditioners found.");
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
writer("Available air conditioners:");
|
|
17
|
+
for (const ac of airConditioners) {
|
|
18
|
+
writer(`- ${formatAcIdentifiers(ac)}`);
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printSkipped(skipped, writer = console.log) {
|
|
24
|
+
if (!skipped.length) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
writer("Skipped fields:");
|
|
28
|
+
for (const item of skipped) {
|
|
29
|
+
writer(`- ${item.name}: ${item.reason}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { formatAc, formatAcIdentifiers, printAcList, printSkipped };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { ApplianceNotFoundError } = require("..");
|
|
2
|
+
const getClient = require("./_get-client");
|
|
3
|
+
|
|
4
|
+
async function getAcClient(options = {}) {
|
|
5
|
+
const client = await getClient(options);
|
|
6
|
+
const acId = options.acId || process.env.AC_ID;
|
|
7
|
+
if (!acId) {
|
|
8
|
+
await printAvailable(client);
|
|
9
|
+
throw new ApplianceNotFoundError(
|
|
10
|
+
"Set AC_ID to one of the listed identifiers",
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ac = await client.getAirConditionerByIdCached(acId);
|
|
15
|
+
|
|
16
|
+
return { client, ac };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function printAvailable(client) {
|
|
20
|
+
const airConditioners = await client.getAirConditioners();
|
|
21
|
+
console.log("Available air conditioners:");
|
|
22
|
+
for (const ac of airConditioners) {
|
|
23
|
+
console.log(
|
|
24
|
+
`- macAddress=${ac.macAddress} uniqueId=${ac.uniqueId} nickName=${ac.nickName}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = getAcClient;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { HonClient } = require("..");
|
|
2
|
+
const { loadConfig } = require("../lib/config");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
async function getClient(options = {}) {
|
|
6
|
+
const baseDir = options.baseDir || path.resolve(__dirname, "..", "..");
|
|
7
|
+
const configPath = options.configPath || path.resolve(baseDir, "config.js");
|
|
8
|
+
const config = loadConfig(configPath);
|
|
9
|
+
if (config.sessionFile && !path.isAbsolute(config.sessionFile)) {
|
|
10
|
+
config.sessionFile = path.resolve(baseDir, config.sessionFile);
|
|
11
|
+
}
|
|
12
|
+
if (config.applianceCacheFile && !path.isAbsolute(config.applianceCacheFile)) {
|
|
13
|
+
config.applianceCacheFile = path.resolve(baseDir, config.applianceCacheFile);
|
|
14
|
+
}
|
|
15
|
+
if (!config.applianceCacheFile) {
|
|
16
|
+
config.applianceCacheFile = path.resolve(baseDir, "cache", ".hon-appliance-cache.json");
|
|
17
|
+
}
|
|
18
|
+
const client = new HonClient(config);
|
|
19
|
+
|
|
20
|
+
await client.create();
|
|
21
|
+
|
|
22
|
+
return client;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = getClient;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const readline = require("node:readline/promises");
|
|
4
|
+
const { stdin: input, stdout: output } = require("node:process");
|
|
5
|
+
|
|
6
|
+
function createAsk() {
|
|
7
|
+
const rl = readline.createInterface({ input, output });
|
|
8
|
+
return {
|
|
9
|
+
question: (text) => rl.question(text),
|
|
10
|
+
close: () => rl.close()
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function askText(ask, label, fallback, existingSecret = "") {
|
|
15
|
+
const suffix = fallback ? ` [${fallback}]` : existingSecret ? " [keep existing]" : "";
|
|
16
|
+
const answer = (await ask.question(`${label}${suffix}: `)).trim();
|
|
17
|
+
if (answer) {
|
|
18
|
+
return answer;
|
|
19
|
+
}
|
|
20
|
+
return existingSecret || fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function askBoolean(ask, label, fallback, writer = output.write.bind(output)) {
|
|
24
|
+
const suffix = fallback ? "Y/n" : "y/N";
|
|
25
|
+
for (;;) {
|
|
26
|
+
const answer = (await ask.question(`${label} [${suffix}]: `)).trim().toLowerCase();
|
|
27
|
+
if (!answer) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
if (["y", "yes", "true", "1"].includes(answer)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (["n", "no", "false", "0"].includes(answer)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
writer("Enter yes or no.\n");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function promptChoice(ask, label, choices, fallback, writer = console.log) {
|
|
41
|
+
for (;;) {
|
|
42
|
+
writer(`${label}:`);
|
|
43
|
+
choices.forEach((choice, index) => {
|
|
44
|
+
writer(` ${index + 1}. ${choice}`);
|
|
45
|
+
});
|
|
46
|
+
const answer = (await ask.question(`${label} [${fallback}]: `)).trim();
|
|
47
|
+
if (!answer) {
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
const index = Number(answer);
|
|
51
|
+
if (Number.isInteger(index) && index >= 1 && index <= choices.length) {
|
|
52
|
+
return choices[index - 1];
|
|
53
|
+
}
|
|
54
|
+
if (choices.includes(answer)) {
|
|
55
|
+
return answer;
|
|
56
|
+
}
|
|
57
|
+
writer(`Choose a number from 1 to ${choices.length}, or enter an exact value.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { askBoolean, askText, createAsk, promptChoice };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { ApplianceNotFoundError } = require("..");
|
|
4
|
+
const { formatAcIdentifiers } = require("./_format");
|
|
5
|
+
|
|
6
|
+
function handleCliError(error) {
|
|
7
|
+
if (error instanceof ApplianceNotFoundError && error.details?.available) {
|
|
8
|
+
console.error(error.message);
|
|
9
|
+
for (const ac of error.details.available) {
|
|
10
|
+
console.error(`- ${formatAcIdentifiers(ac)}`);
|
|
11
|
+
}
|
|
12
|
+
} else {
|
|
13
|
+
console.error(error);
|
|
14
|
+
}
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { handleCliError };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { ApplianceNotFoundError } = require("..");
|
|
4
|
+
const { findApplianceIdentifierMatches } = require("../appliance-identity");
|
|
5
|
+
const { formatAc } = require("./_format");
|
|
6
|
+
const { promptChoice } = require("./_prompt");
|
|
7
|
+
|
|
8
|
+
async function selectAirConditioner(ask, airConditioners, acId = "") {
|
|
9
|
+
const selectedId = acId || process.env.AC_ID;
|
|
10
|
+
if (selectedId) {
|
|
11
|
+
const matches = findApplianceIdentifierMatches(airConditioners, selectedId);
|
|
12
|
+
if (matches.length === 1) {
|
|
13
|
+
return matches[0].item;
|
|
14
|
+
}
|
|
15
|
+
if (matches.length > 1) {
|
|
16
|
+
throw new ApplianceNotFoundError(`AC_ID matches multiple air conditioners: ${selectedId}`, {
|
|
17
|
+
id: selectedId,
|
|
18
|
+
matches: matches.map(({ item }) => item.identifiers)
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
throw new ApplianceNotFoundError(`No air conditioner found for AC_ID: ${selectedId}`, {
|
|
22
|
+
id: selectedId,
|
|
23
|
+
available: airConditioners.map((ac) => ac.identifiers)
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (airConditioners.length === 1) {
|
|
27
|
+
const ac = airConditioners[0];
|
|
28
|
+
console.log(`Selected AC: ${formatAc(ac)}`);
|
|
29
|
+
return ac;
|
|
30
|
+
}
|
|
31
|
+
const choices = airConditioners.map(formatAc);
|
|
32
|
+
const selected = await promptChoice(ask, "Air conditioner", choices, choices[0]);
|
|
33
|
+
return airConditioners[choices.indexOf(selected)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { selectAirConditioner };
|