-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathaccessory.ts
264 lines (232 loc) · 9.47 KB
/
accessory.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
import {
AccessoryConfig,
AccessoryPlugin,
API,
Characteristic,
CharacteristicEventTypes,
CharacteristicGetCallback,
CharacteristicSetCallback,
CharacteristicValue,
Logging,
Service
} from "homebridge";
import Switchbot = require('node-switchbot');
import ping = require('net-ping');
const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec));
type Switchbot = typeof Switchbot;
type SwitchbotDeviceWoHand = typeof Switchbot.SwitchbotDeviceWoHand;
enum DiscoverState {
Discovering,
Discovered,
NotFound,
}
class WoHand {
private readonly delay: number;
private readonly retries: number;
private readonly on: { macAddress: string };
private readonly off: { macAddress: string };
private device: { [key: string]: SwitchbotDeviceWoHand } = {};
private discoverState: { [key: string]: DiscoverState } = {};
constructor(private readonly log: Logging, config: Config) {
this.delay = (typeof config.delay === 'number') ? config.delay : 0;
this.retries = (typeof config.retries === 'number') ? config.retries : 3;
if (typeof config.macAddress === 'string') {
this.on = { macAddress: config.macAddress };
this.off = { macAddress: config.macAddress };
} else if (typeof config.on.macAddress === 'string' && typeof config.off.macAddress === 'string') {
this.on = { macAddress: config.on.macAddress };
this.off = { macAddress: config.off.macAddress };
} else {
throw new Error(`Failed to initialize WoHand as it is missing the required 'macAddress' (or 'on.macAddress' and 'off.macAddress') property!`);
}
this.discover(this.on.macAddress);
this.discover(this.off.macAddress);
}
async discover(macAddress: string) {
if (this.discoverState[macAddress] === DiscoverState.Discovering || this.discoverState[macAddress] === DiscoverState.Discovered) return;
this.discoverState[macAddress] = DiscoverState.Discovering;
// Find a Bot (WoHand)
const switchbot = new Switchbot();
switchbot.ondiscover = async (bot: SwitchbotDeviceWoHand) => {
bot.onconnect = () => { this.log.debug(`${macAddress} connected.`); };
bot.ondisconnect = () => { this.log.debug(`${macAddress} disconnected.`); };
// Execute connect method because address cannot be obtained without a history of connecting.
if (bot.address === '') await bot.connect();
if (bot.connectionState === 'connected') await bot.disconnect();
if (bot.address.toLowerCase().replace(/[^a-z0-9]/g, '') === macAddress.toLowerCase().replace(/[^a-z0-9]/g, '')) {
// The `SwitchbotDeviceWoHand` object representing the found Bot.
this.device[macAddress] = bot;
this.discoverState[macAddress] = DiscoverState.Discovered;
this.log.info(`WoHand (${macAddress}) was discovered`);
}
}
try {
await switchbot.wait(this.delay);
await switchbot.discover({ duration: 60000, model: 'H' });
if (this.discoverState[macAddress] !== DiscoverState.Discovered) {
this.discoverState[macAddress] = DiscoverState.NotFound;
this.log.warn(`WoHand (${macAddress}) was not found`);
}
} catch (error) {
this.discoverState[macAddress] = DiscoverState.NotFound;
this.log.error(`Failed to discover WoHand (${macAddress}). Is Bluetooth enabled?`);
if (error instanceof Error) {
this.log.error(`${error.stack ?? error.name + ": " + error.message}`);
}
}
}
async waitDiscovered(macAddress: string) {
if (this.discoverState[macAddress] === DiscoverState.NotFound) {
this.log.info(`WoHand (${macAddress}) was not found. so retry discover.`);
this.discover(macAddress);
await sleep(1000);
}
while(this.discoverState[macAddress] !== DiscoverState.Discovered) {
switch (this.discoverState[macAddress]) {
case DiscoverState.Discovering:
await sleep(100);
continue;
case DiscoverState.NotFound:
throw new Error(`WoHand (${macAddress}) was not found.`);
}
}
}
async turn(newState: boolean, retries = this.retries) {
const humanState = newState ? 'on' : 'off';
const macAddress = newState ? this.on.macAddress : this.off.macAddress;
try {
await this.waitDiscovered(macAddress);
newState ? await this.device[macAddress].turnOn() : await this.device[macAddress].turnOff();
} catch (error) {
const message = `WoHand (${macAddress}) was failed turning ${humanState}`;
this.log.debug(message);
if (error instanceof Error) {
this.log.debug(`${error.stack ?? error.name + ": " + error.message}`);
}
if (0 < retries) {
this.log.debug(`WoHand (${macAddress}) retry turning ${humanState}: ${this.retries - (retries - 1)} times`);
await this.turn(newState, retries - 1)
} else {
throw error;
}
}
}
}
export class SwitchBotAccessory implements AccessoryPlugin {
private readonly Service: typeof Service = this.api.hap.Service;
private readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
private readonly name: string;
private readonly device: WoHand | null = null;
private readonly switchService: Service;
private readonly informationService: Service;
private state? :boolean;
constructor(private readonly log: Logging, config: AccessoryConfig, private readonly api: API) {
this.name = config.name;
try {
this.device = new WoHand(log, config as Config);
} catch (error) {
this.log.error(`Failed to initialize accessory as it is missing the required 'macAddress' (or 'on.macAddress' and 'off.macAddress') property!`);
if (error instanceof Error) {
this.log.debug(`${error.stack ?? error.name + ": " + error.message}`);
}
}
this.switchService = new this.Service.Switch(this.name);
this.switchService.getCharacteristic(this.Characteristic.On)
.on(CharacteristicEventTypes.GET, this.getOn.bind(this))
.on(CharacteristicEventTypes.SET, this.setOn.bind(this));
this.informationService = new this.Service.AccessoryInformation()
.setCharacteristic(this.Characteristic.Manufacturer, 'zizi4n5');
if (config.ping) {
const ipAddress = config.ping.ipAddress;
const interval = Math.max((typeof config.ping.interval === 'number') ? config.ping.interval : 2000, 2000);
const retries = Math.max((typeof config.ping.retries === 'number') ? config.ping.retries : 1, 1);
const timeout = Math.min((typeof config.ping.timeout === 'number') ? config.ping.timeout : interval / (retries + 1), interval / (retries + 1));
const session = ping.createSession({ retries: retries, timeout: timeout });
log.info(`ping - ipAddress:${ipAddress} interval:${interval} retries:${retries} timeout:${timeout}`);
setInterval(() => {
session.pingHost(ipAddress, (error: Error, target: string) => {
if (error && !(error instanceof ping.RequestTimedOutError)) {
log.debug(`ping ${target} is error (${error.toString()})`);
}
this.updateState(!error);
})
}, interval);
}
}
/*
* This method is called directly after creation of this instance.
* It should return all services which should be added to the accessory.
*/
getServices(): Service[] {
return [
this.informationService,
this.switchService,
];
}
private updateState(newState: boolean) {
const humanState = newState ? 'on' : 'off';
const previousState = this.state;
const hasStateChanged = (previousState !== newState);
if (hasStateChanged) {
this.log.info(`updateState: state changed, update UI (device ${humanState})`);
this.state = newState;
this.switchService.updateCharacteristic(this.Characteristic.On, newState);
} else {
this.log.debug(`updateState: state not changed, ignoring (device ${humanState})`);
}
}
/**
* Handle the "GET" requests from HomeKit
* These are sent when HomeKit wants to know the current state of the accessory.
*/
private getOn(callback: CharacteristicGetCallback) {
callback(null, this.state || false);
}
/**
* Handle "SET" requests from HomeKit
* These are sent when the user changes the state of an accessory.
*/
private async setOn(value: CharacteristicValue, callback: CharacteristicSetCallback) {
const newState = value as boolean;
const humanState = newState ? 'on' : 'off';
this.log.info(`Turning ${humanState}...`);
if (!this.device) {
this.log.error(`Failed to turning ${humanState} as it is missing the required 'macAddress' (or 'on.macAddress' and 'off.macAddress') property!`);
return;
}
if (newState === this.state) {
this.log.info(`WoHand (${this.device[humanState].macAddress}) was already ${humanState}`);
callback();
return;
}
try {
await this.device.turn(newState);
this.state = newState;
this.log.info(`WoHand (${this.device[humanState].macAddress}) was turned ${humanState}`);
callback();
} catch (error) {
const message = `WoHand (${this.device[humanState].macAddress}) was failed turning ${humanState}`;
this.log.error(message);
if (error instanceof Error) {
this.log.error(`${error.stack ?? error.name + ": " + error.message}`);
}
callback(Error(message));
}
}
}
interface Config extends AccessoryConfig {
delay: number,
macAddress: string,
on: {
macAddress: string
},
off: {
macAddress: string
},
ping: {
ipAddress: string,
interval: number,
retries: number,
timeout: number
}
}