scrypted-kasa 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/.github/dependabot.yml +14 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +52 -0
- package/.nvmrc +1 -0
- package/.prettierignore +6 -0
- package/.prettierrc.json +11 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/eslint.config.mjs +33 -0
- package/package.json +75 -0
- package/src/camera/api.ts +329 -0
- package/src/camera/intercom.ts +163 -0
- package/src/camera/linkie.ts +185 -0
- package/src/camera/rtsp.ts +204 -0
- package/src/camera/stream.ts +428 -0
- package/src/iot/bulb.ts +178 -0
- package/src/iot/device.ts +103 -0
- package/src/iot/dimmer.ts +19 -0
- package/src/iot/plug.ts +5 -0
- package/src/iot/protocol.ts +143 -0
- package/src/iot/switch.ts +5 -0
- package/src/main.ts +1055 -0
- package/src/shared/cipher.ts +49 -0
- package/src/shared/discovery.ts +232 -0
- package/tsconfig.json +14 -0
package/src/iot/bulb.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Brightness,
|
|
3
|
+
ColorHsv,
|
|
4
|
+
ColorSettingHsv,
|
|
5
|
+
ColorSettingTemperature,
|
|
6
|
+
OnOff,
|
|
7
|
+
ScryptedDeviceBase,
|
|
8
|
+
Setting,
|
|
9
|
+
Settings,
|
|
10
|
+
SettingValue,
|
|
11
|
+
} from '@scrypted/sdk';
|
|
12
|
+
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
|
13
|
+
import { KASA_IOT_PORT, getSysInfo, kasaIotCall } from './protocol';
|
|
14
|
+
|
|
15
|
+
const STATE_POLL_INTERVAL_MS = 30000;
|
|
16
|
+
|
|
17
|
+
// Smart bulbs (LB100/110/120/130, KL110/120/130/135/...). All bulb state — on/off,
|
|
18
|
+
// brightness, hue, saturation, color temperature — is controlled by a single command:
|
|
19
|
+
// {"smartlife.iot.smartbulb.lightingservice":{"transition_light_state":{...}}}
|
|
20
|
+
//
|
|
21
|
+
// Capability flags from sysinfo:
|
|
22
|
+
// is_dimmable 1 = supports brightness
|
|
23
|
+
// is_color 1 = supports hue/saturation
|
|
24
|
+
// is_variable_color_temp 1 = supports color temperature
|
|
25
|
+
// Color temperature range is determined per-model; we expose the values the bulb returns
|
|
26
|
+
// in `getTemperatureMinK`/`getTemperatureMaxK` and fall back to a safe pair otherwise.
|
|
27
|
+
export class KasaBulb
|
|
28
|
+
extends ScryptedDeviceBase
|
|
29
|
+
implements OnOff, Brightness, ColorSettingHsv, ColorSettingTemperature, Settings
|
|
30
|
+
{
|
|
31
|
+
storageSettings = new StorageSettings(this, {
|
|
32
|
+
ip: {
|
|
33
|
+
title: 'IP Address',
|
|
34
|
+
placeholder: '192.168.1.100',
|
|
35
|
+
},
|
|
36
|
+
port: {
|
|
37
|
+
title: 'Port',
|
|
38
|
+
type: 'number',
|
|
39
|
+
defaultValue: KASA_IOT_PORT,
|
|
40
|
+
},
|
|
41
|
+
// Capability flags captured from sysinfo at adoption — internal only, drive which
|
|
42
|
+
// interfaces the device advertises. Hidden because users don't typically need to
|
|
43
|
+
// toggle them (auto-detected from the bulb's own metadata).
|
|
44
|
+
isColor: {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
hide: true,
|
|
47
|
+
},
|
|
48
|
+
isVariableColorTemp: {
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
hide: true,
|
|
51
|
+
},
|
|
52
|
+
colorTemperatureMinK: {
|
|
53
|
+
type: 'number',
|
|
54
|
+
defaultValue: 2700,
|
|
55
|
+
hide: true,
|
|
56
|
+
},
|
|
57
|
+
colorTemperatureMaxK: {
|
|
58
|
+
type: 'number',
|
|
59
|
+
defaultValue: 6500,
|
|
60
|
+
hide: true,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
private pollTimer?: NodeJS.Timeout;
|
|
65
|
+
private pollStartTimer?: NodeJS.Timeout;
|
|
66
|
+
// See KasaIotDevice for rationale: shared in-flight promise prevents overlapping
|
|
67
|
+
// refreshState calls from racing if a poll takes longer than the interval.
|
|
68
|
+
private refreshInFlight?: Promise<void>;
|
|
69
|
+
|
|
70
|
+
constructor(nativeId: string) {
|
|
71
|
+
super(nativeId);
|
|
72
|
+
process.nextTick(() => this.refreshState().catch(e => this.console.warn('refresh state failed', e)));
|
|
73
|
+
// Random first-fire offset to spread load when many bulbs are constructed together.
|
|
74
|
+
const jitter = Math.floor(Math.random() * STATE_POLL_INTERVAL_MS);
|
|
75
|
+
this.pollStartTimer = setTimeout(() => {
|
|
76
|
+
this.pollStartTimer = undefined;
|
|
77
|
+
this.pollTimer = setInterval(() => this.refreshState().catch(() => {}), STATE_POLL_INTERVAL_MS);
|
|
78
|
+
}, jitter);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getSettings(): Promise<Setting[]> {
|
|
82
|
+
return this.storageSettings.getSettings();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
putSetting(key: string, value: SettingValue): Promise<void> {
|
|
86
|
+
return this.storageSettings.putSetting(key, value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async turnOn(): Promise<void> {
|
|
90
|
+
await this.lightState({ on_off: 1 });
|
|
91
|
+
this.on = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async turnOff(): Promise<void> {
|
|
95
|
+
await this.lightState({ on_off: 0 });
|
|
96
|
+
this.on = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async setBrightness(brightness: number): Promise<void> {
|
|
100
|
+
await this.lightState({ brightness });
|
|
101
|
+
this.brightness = brightness;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async setHsv(hue: number, saturation: number, value: number): Promise<void> {
|
|
105
|
+
// Switch the bulb to color mode (mode 1) when HSV is set; without this some bulbs
|
|
106
|
+
// ignore the hue/saturation while in color-temperature mode.
|
|
107
|
+
await this.lightState({
|
|
108
|
+
color_temp: 0,
|
|
109
|
+
hue,
|
|
110
|
+
saturation,
|
|
111
|
+
brightness: value,
|
|
112
|
+
});
|
|
113
|
+
this.hsv = { h: hue, s: saturation, v: value };
|
|
114
|
+
this.brightness = value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async setColorTemperature(kelvin: number): Promise<void> {
|
|
118
|
+
await this.lightState({ color_temp: kelvin });
|
|
119
|
+
this.colorTemperature = kelvin;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getTemperatureMinK(): Promise<number> {
|
|
123
|
+
return this.storageSettings.values.colorTemperatureMinK;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getTemperatureMaxK(): Promise<number> {
|
|
127
|
+
return this.storageSettings.values.colorTemperatureMaxK;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async refreshState(): Promise<void> {
|
|
131
|
+
if (this.refreshInFlight) return this.refreshInFlight;
|
|
132
|
+
this.refreshInFlight = this.refreshStateInternal().finally(() => {
|
|
133
|
+
this.refreshInFlight = undefined;
|
|
134
|
+
});
|
|
135
|
+
return this.refreshInFlight;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async refreshStateInternal(): Promise<void> {
|
|
139
|
+
if (!this.storageSettings.values.ip) return;
|
|
140
|
+
const sys = await getSysInfo(this.iotOptions());
|
|
141
|
+
const ls = sys?.light_state;
|
|
142
|
+
if (!ls) return;
|
|
143
|
+
// light_state during off-state nests the actual values under `dft_on_state`.
|
|
144
|
+
const active = ls.on_off ? ls : ls.dft_on_state || {};
|
|
145
|
+
this.on = ls.on_off === 1;
|
|
146
|
+
if (typeof active.brightness === 'number') this.brightness = active.brightness;
|
|
147
|
+
if (typeof active.color_temp === 'number') this.colorTemperature = active.color_temp;
|
|
148
|
+
if (typeof active.hue === 'number' && typeof active.saturation === 'number') {
|
|
149
|
+
const hsv: ColorHsv = {
|
|
150
|
+
h: active.hue,
|
|
151
|
+
s: active.saturation,
|
|
152
|
+
v: typeof active.brightness === 'number' ? active.brightness : 100,
|
|
153
|
+
};
|
|
154
|
+
this.hsv = hsv;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private lightState(state: Record<string, any>): Promise<any> {
|
|
159
|
+
// ignore_default tells the bulb to merge with current state instead of resetting.
|
|
160
|
+
return kasaIotCall(this.iotOptions(), {
|
|
161
|
+
'smartlife.iot.smartbulb.lightingservice': {
|
|
162
|
+
transition_light_state: { ignore_default: 1, ...state },
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private iotOptions() {
|
|
168
|
+
return {
|
|
169
|
+
host: this.storageSettings.values.ip,
|
|
170
|
+
port: this.storageSettings.values.port,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
release(): void {
|
|
175
|
+
clearInterval(this.pollTimer);
|
|
176
|
+
clearTimeout(this.pollStartTimer);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { OnOff, ScryptedDeviceBase, Setting, Settings, SettingValue } from '@scrypted/sdk';
|
|
2
|
+
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
|
3
|
+
import { KASA_IOT_PORT, getSysInfo, kasaIotCall } from './protocol';
|
|
4
|
+
|
|
5
|
+
const STATE_POLL_INTERVAL_MS = 30000;
|
|
6
|
+
|
|
7
|
+
// Shared base for every Kasa "smarthome" single-relay device — outlets, switches, and
|
|
8
|
+
// dimmers. Only the on/off relay protocol lives here; dimmable devices add Brightness on
|
|
9
|
+
// top in KasaDimmer.
|
|
10
|
+
//
|
|
11
|
+
// Wire protocol (TCP/9999):
|
|
12
|
+
// on/off: {"system":{"set_relay_state":{"state":1|0}}}
|
|
13
|
+
// query: {"system":{"get_sysinfo":{}}}
|
|
14
|
+
//
|
|
15
|
+
// Multi-outlet strips (HS300, KP303) aren't modeled — they need per-child relay handling.
|
|
16
|
+
export abstract class KasaIotDevice extends ScryptedDeviceBase implements OnOff, Settings {
|
|
17
|
+
storageSettings = new StorageSettings(this, {
|
|
18
|
+
ip: {
|
|
19
|
+
title: 'IP Address',
|
|
20
|
+
placeholder: '192.168.1.100',
|
|
21
|
+
},
|
|
22
|
+
port: {
|
|
23
|
+
title: 'Port',
|
|
24
|
+
type: 'number',
|
|
25
|
+
defaultValue: KASA_IOT_PORT,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
private pollTimer?: NodeJS.Timeout;
|
|
30
|
+
private pollStartTimer?: NodeJS.Timeout;
|
|
31
|
+
// Guard against overlapping polls: if a refresh is in-flight when the interval fires
|
|
32
|
+
// (e.g., the camera is slow), the second call would open a parallel TCP connection
|
|
33
|
+
// and could race the first response. Sharing the in-flight promise avoids that.
|
|
34
|
+
private refreshInFlight?: Promise<void>;
|
|
35
|
+
|
|
36
|
+
constructor(nativeId: string) {
|
|
37
|
+
super(nativeId);
|
|
38
|
+
// Drive an initial state refresh on next tick (so storageSettings is ready) and a
|
|
39
|
+
// periodic poll thereafter so external state changes (other apps, physical button
|
|
40
|
+
// presses) eventually surface in Scrypted/HomeKit. A small per-instance jitter on
|
|
41
|
+
// the first poll spreads the load when many devices were started together.
|
|
42
|
+
process.nextTick(() => this.refreshState().catch(e => this.console.warn('refresh state failed', e)));
|
|
43
|
+
const jitter = Math.floor(Math.random() * STATE_POLL_INTERVAL_MS);
|
|
44
|
+
this.pollStartTimer = setTimeout(() => {
|
|
45
|
+
this.pollStartTimer = undefined;
|
|
46
|
+
this.pollTimer = setInterval(() => this.refreshState().catch(() => {}), STATE_POLL_INTERVAL_MS);
|
|
47
|
+
}, jitter);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getSettings(): Promise<Setting[]> {
|
|
51
|
+
return this.storageSettings.getSettings();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
putSetting(key: string, value: SettingValue): Promise<void> {
|
|
55
|
+
return this.storageSettings.putSetting(key, value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async turnOn(): Promise<void> {
|
|
59
|
+
await this.callIot({ system: { set_relay_state: { state: 1 } } });
|
|
60
|
+
this.on = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async turnOff(): Promise<void> {
|
|
64
|
+
await this.callIot({ system: { set_relay_state: { state: 0 } } });
|
|
65
|
+
this.on = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async refreshState(): Promise<void> {
|
|
69
|
+
if (this.refreshInFlight) return this.refreshInFlight;
|
|
70
|
+
this.refreshInFlight = this.refreshStateInternal().finally(() => {
|
|
71
|
+
this.refreshInFlight = undefined;
|
|
72
|
+
});
|
|
73
|
+
return this.refreshInFlight;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async refreshStateInternal(): Promise<void> {
|
|
77
|
+
if (!this.storageSettings.values.ip) return;
|
|
78
|
+
const sys = await getSysInfo(this.iotOptions());
|
|
79
|
+
if (!sys) return;
|
|
80
|
+
if (typeof sys.relay_state === 'number') this.on = sys.relay_state === 1;
|
|
81
|
+
// Subclasses with extra state extend via onSysInfo hook.
|
|
82
|
+
this.onSysInfo(sys);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Hook for subclasses to consume additional sysinfo fields (e.g. brightness for dimmers).
|
|
86
|
+
protected onSysInfo(_sys: Record<string, any>): void {}
|
|
87
|
+
|
|
88
|
+
protected callIot(command: Record<string, any>): Promise<any> {
|
|
89
|
+
return kasaIotCall(this.iotOptions(), command);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected iotOptions() {
|
|
93
|
+
return {
|
|
94
|
+
host: this.storageSettings.values.ip,
|
|
95
|
+
port: this.storageSettings.values.port,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
release(): void {
|
|
100
|
+
clearInterval(this.pollTimer);
|
|
101
|
+
clearTimeout(this.pollStartTimer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Brightness } from '@scrypted/sdk';
|
|
2
|
+
import { KasaIotDevice } from './device';
|
|
3
|
+
|
|
4
|
+
// Dimmable Kasa devices (HS220 dimmer plug, KS230 3-way dimmer switch, etc.). Same
|
|
5
|
+
// relay protocol as KasaPlug/KasaSwitch plus a brightness command:
|
|
6
|
+
// {"smartlife.iot.dimmer":{"set_brightness":{"brightness":N}}}
|
|
7
|
+
//
|
|
8
|
+
// Always exposed as `Light` since these are essentially always wired to a light fixture
|
|
9
|
+
// (the Kasa app does the same). On/off uses the relay; brightness uses the dimmer module.
|
|
10
|
+
export class KasaDimmer extends KasaIotDevice implements Brightness {
|
|
11
|
+
async setBrightness(brightness: number): Promise<void> {
|
|
12
|
+
await this.callIot({ 'smartlife.iot.dimmer': { set_brightness: { brightness } } });
|
|
13
|
+
this.brightness = brightness;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protected onSysInfo(sys: Record<string, any>): void {
|
|
17
|
+
if (typeof sys.brightness === 'number') this.brightness = sys.brightness;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/iot/plug.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
import { xorDecrypt, xorEncryptInPlace } from '../shared/cipher';
|
|
3
|
+
|
|
4
|
+
// Legacy Kasa "IOT" protocol used by plugs, switches, and bulbs (everything that isn't a
|
|
5
|
+
// camera). Wire format on TCP/9999:
|
|
6
|
+
// 4-byte big-endian length prefix + XOR-AB(plaintext-json) bytes
|
|
7
|
+
// Same XOR-AB autokey cipher as UDP discovery; the only difference is the framing.
|
|
8
|
+
//
|
|
9
|
+
// Same-port UDP/9999 also accepts the encrypted JSON without the 4-byte prefix, but TCP
|
|
10
|
+
// is more reliable for control commands so we use it exclusively here.
|
|
11
|
+
|
|
12
|
+
export const KASA_IOT_PORT = 9999;
|
|
13
|
+
const REQUEST_TIMEOUT_MS = 5000;
|
|
14
|
+
|
|
15
|
+
export interface KasaIotOptions {
|
|
16
|
+
host: string;
|
|
17
|
+
port?: number;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function kasaIotCall(options: KasaIotOptions, command: Record<string, any>): Promise<any> {
|
|
22
|
+
const port = options.port || KASA_IOT_PORT;
|
|
23
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
24
|
+
const json = JSON.stringify(command);
|
|
25
|
+
// Encode the 4-byte length prefix and JSON in one allocation, encrypt the JSON portion
|
|
26
|
+
// in-place (we own this buffer and don't read the plaintext again).
|
|
27
|
+
const jsonBytes = Buffer.byteLength(json, 'utf8');
|
|
28
|
+
const payload = Buffer.allocUnsafe(4 + jsonBytes);
|
|
29
|
+
payload.writeUInt32BE(jsonBytes, 0);
|
|
30
|
+
payload.write(json, 4, 'utf8');
|
|
31
|
+
xorEncryptInPlace(payload.subarray(4));
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const socket = new net.Socket();
|
|
35
|
+
// Queue of received TCP chunks plus byte total, so we don't repeatedly Buffer.concat
|
|
36
|
+
// the accumulator on every 'data' event (O(n²) over multi-chunk responses; bulb
|
|
37
|
+
// light_state replies span 2-3 chunks).
|
|
38
|
+
const chunks: Buffer[] = [];
|
|
39
|
+
let queuedBytes = 0;
|
|
40
|
+
let expected = -1;
|
|
41
|
+
let settled = false;
|
|
42
|
+
|
|
43
|
+
const finish = (err?: Error, value?: any) => {
|
|
44
|
+
if (settled) return;
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
try {
|
|
48
|
+
socket.destroy();
|
|
49
|
+
} catch {}
|
|
50
|
+
// Drop references so any retained chunks can be GC'd promptly.
|
|
51
|
+
chunks.length = 0;
|
|
52
|
+
if (err) reject(err);
|
|
53
|
+
else resolve(value);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const timer = setTimeout(() => finish(new Error(`kasa iot call timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
57
|
+
|
|
58
|
+
const peek4 = (): number => {
|
|
59
|
+
// We've verified queuedBytes >= 4 before calling. Read big-endian uint32 either
|
|
60
|
+
// straight from the head buffer (common case) or after walking the queue.
|
|
61
|
+
if (chunks[0].length >= 4) return chunks[0].readUInt32BE(0);
|
|
62
|
+
// Rare: 4-byte length straddles two chunks. Concat just the first two.
|
|
63
|
+
return Buffer.concat(chunks.slice(0, 2), 4).readUInt32BE(0);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const consume = (n: number): Buffer => {
|
|
67
|
+
// Pop and return exactly n bytes from the head of the queue.
|
|
68
|
+
queuedBytes -= n;
|
|
69
|
+
if (chunks[0].length === n) return chunks.shift()!;
|
|
70
|
+
if (chunks[0].length > n) {
|
|
71
|
+
const out = chunks[0].subarray(0, n);
|
|
72
|
+
chunks[0] = chunks[0].subarray(n);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
const out = Buffer.allocUnsafe(n);
|
|
76
|
+
let written = 0;
|
|
77
|
+
while (written < n) {
|
|
78
|
+
const head = chunks[0];
|
|
79
|
+
const need = n - written;
|
|
80
|
+
if (head.length <= need) {
|
|
81
|
+
head.copy(out, written);
|
|
82
|
+
written += head.length;
|
|
83
|
+
chunks.shift();
|
|
84
|
+
} else {
|
|
85
|
+
head.copy(out, written, 0, need);
|
|
86
|
+
chunks[0] = head.subarray(need);
|
|
87
|
+
written = n;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
socket.on('data', chunk => {
|
|
94
|
+
chunks.push(chunk);
|
|
95
|
+
queuedBytes += chunk.length;
|
|
96
|
+
if (expected < 0) {
|
|
97
|
+
if (queuedBytes < 4) return;
|
|
98
|
+
expected = peek4();
|
|
99
|
+
consume(4);
|
|
100
|
+
}
|
|
101
|
+
if (queuedBytes >= expected) {
|
|
102
|
+
const cipher = consume(expected);
|
|
103
|
+
const plain = xorDecrypt(cipher).toString('utf8');
|
|
104
|
+
try {
|
|
105
|
+
finish(undefined, JSON.parse(plain));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
finish(e as Error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
socket.on('error', e => finish(e));
|
|
113
|
+
socket.on('close', () => {
|
|
114
|
+
if (!settled) finish(new Error('kasa iot socket closed before response'));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
socket.connect(port, options.host, () => {
|
|
118
|
+
socket.write(payload);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Common helpers used by both plugs/switches and bulbs.
|
|
124
|
+
|
|
125
|
+
export interface KasaSysInfoCommon {
|
|
126
|
+
alias?: string;
|
|
127
|
+
model?: string;
|
|
128
|
+
mac?: string;
|
|
129
|
+
mic_mac?: string;
|
|
130
|
+
deviceId?: string;
|
|
131
|
+
sw_ver?: string;
|
|
132
|
+
hw_ver?: string;
|
|
133
|
+
type?: string;
|
|
134
|
+
mic_type?: string;
|
|
135
|
+
dev_name?: string;
|
|
136
|
+
feature?: string;
|
|
137
|
+
[k: string]: any;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function getSysInfo(options: KasaIotOptions): Promise<KasaSysInfoCommon | undefined> {
|
|
141
|
+
const r = await kasaIotCall(options, { system: { get_sysinfo: {} } });
|
|
142
|
+
return r?.system?.get_sysinfo;
|
|
143
|
+
}
|