-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
243 lines (235 loc) · 8.32 KB
/
index.js
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
const { exec } = require('child_process')
module.exports = (() => {
/**
* Scan and get the MAC addresses of the nearby sensors.
*
* @param {number} timeoutDuration=10 - Maximum time in seconds assigned to the scan.
*
* @example
* getSensors().then((res) => {
* console.log(res) // ['XX:XX:XX:XX:XX:XX', 'XX:XX:XX:XX:XX:XX', ..]
* }).catch((err) => {
* console.error(`Unable to get sensors (error: ${err})`)
* })
*
* @returns {Promise<Array<string>>} - Promise containing an array with the MAC addresses of the detected sensors.
*/
function getSensors (timeoutDuration = 10) {
return new Promise((resolve, reject) => {
const sensorsAddresses = []
const query = 'echo $(sudo ' +
'timeout -s SIGINT ' +
timeoutDuration +
' hcitool lescan)'
exec(query, (err, stdout, stderr) => {
if (err) {
reject(err.message)
} else if (stderr) {
reject(stderr.slice(0, -1))
} else {
// Remove the useless beginning of the string and the last line break
stdout = stdout.slice(0, -1).substring(12)
// Split the string into an array of substrings to browse the results
stdout = stdout.split(' ')
for (let i = 0; i < stdout.length; i++) {
if (stdout[i] === 'LYWSD03MMC') sensorsAddresses.push(stdout[i - 1])
}
resolve(sensorsAddresses)
}
})
})
}
/**
* Get the data from the specified sensor (temperature, humidity level,
* battery level) every 20 seconds during 2 minutes. Stops when data are got.
*
* @param {string} sensorAddress - MAC address of the sensor to connect.
* @param {boolean} tempInFahrenheit=false - Temperature output format (Celsius/Fahrenheit).
* @param {number} timeoutDuration=2 - Maximum time in minutes assigned to the connection.
*
* @example
* // Replace with a valid MAC address of a nearby sensor
* const sensorAddress = 'XX:XX:XX:XX:XX:XX'
* getData(sensorAddress).then((res) => {
* // {
* // address: 'XX:XX:XX:XX:XX:XX',
* // humidityLevel: XX,
* // temperature: XX,
* // batteryLevel: XX
* // }
* console.log(res)
* }).catch(() => {
* console.error(`[xiaomi-mijia-lywsd03mmc] Unable to get data (address: ${sensorAddress}, error: ${err})`)
* })
*
* @returns {Promise<Object>} - Promise containing an object with the requested data.
*/
function getData (sensorAddress, tempInFahrenheit = false, timeoutDuration = 2) {
return new Promise((resolve, reject) => {
// Try to get the data every 20 seconds during {timeoutDuration} minute(s)
for (let timeouts = [], i = 0; i <= timeoutDuration * 3; i++) {
(function (index) {
// Keep all timers to delete them later
timeouts.push(setTimeout(() => {
// Get the temperature, the humidity level and the battery level
_listenHandle(sensorAddress).then((res) => {
// Delete all existing timers
for (let j = 0; j < timeouts.length; j++) {
clearTimeout(timeouts[j])
}
resolve({
address: sensorAddress,
humidityLevel: _getHumidityLevel(res),
temperature: _getTemperature(res, tempInFahrenheit),
batteryLevel: _getBatteryLevel(res)
})
}).catch((err) => {
// No data received within the allocated time
if (index === timeoutDuration * 3) {
reject(err)
} else {
console.error(`[xiaomi-mijia-lywsd03mmc] ${err}`)
}
})
}, i * 20000, i))
})(i)
}
})
}
/**
* Listen a handle for a few seconds in order to get the desired
* data (temperature, humidity level, battery level).
*
* @param {string} sensorAddress - MAC address of the sensor to search for.
*
* @example
* // Replace with a valid MAC address of a nearby sensor
* const sensorAddress = 'XX:XX:XX:XX:XX:XX'
* // Set to true if you want the temperature in Fahrenheit
* const tempInFahrenheit = false
* _listenHandle(sensorAddress).then((res) => {
* console.log({
* 'humidityLevel': _getHumidityLevel(res),
* 'temperature': _getTemperature(res, tempInFahrenheit),
* 'batteryLevel': _getBatteryLevel(res)
* })
* }).catch((err) => {
* console.error(err)
* })
*
* @returns {Promise<string|Buffer>} - Promise with an error (string) or the requested data (Buffer).
*/
function _listenHandle (sensorAddress) {
return new Promise((resolve, reject) => {
const query = 'timeout 15 gatttool -b ' +
sensorAddress +
' --char-read' +
' -a 0x38' +
' --listen' +
' | grep --max-count=1 \'Notification handle\''
exec(query, (err, stdout, stderr) => {
if (err) {
reject(err.message.slice(0, -1))
} else if (stderr) {
reject(stderr.slice(0, -1))
} else {
// Keep only the value and remove the spaces
stdout = stdout.substr(36, 14).replace(/\s/g, '')
// Set the data in a buffer
stdout = Buffer.from(stdout, 'hex')
resolve(stdout)
}
})
})
}
/**
* Read only the useful part of a buffer and convert the
* hexadecimal value to a decimal value of the humidity level.
*
* @param {Buffer} buf - Buffer that contains the humidity level in hexadecimal value.
*
* @example
* const buf = Buffer.from('460844c00a', 'hex')
* const humidityLevel = _getHumidityLevel(buf)
* console.log(humidityLevel) // 68
*
* @returns {number} - Humidity level in decimal value.
*/
function _getHumidityLevel (buf) {
return buf.readUInt8(2)
}
/**
* Read only the useful part of a buffer and convert the
* hexadecimal value to a decimal value of the temperature.
*
* @param {Buffer} buf - Buffer that contains the temperature in hexadecimal value.
* @param {boolean} valueInFahrenheit=false - Temperature output format (Celsius/Fahrenheit).
* @param {undefined} val - Internal variable of the function.
*
* @example
* const buf = Buffer.from('460844c00a', 'hex')
* const temperature = _getTemperature(buf)
* console.log(temperature) // 21.2
*
* @returns {number} - Temperature in decimal value.
*/
function _getTemperature (buf, valueInFahrenheit = false, val) {
if (buf[1] === 255) {
// Temperature is negative
val = Number((-65536 + buf.readUInt16LE(0)) / 10)
} else {
// Temperature is positive
val = Number((buf.readUInt16LE(0) / 100).toFixed(1))
}
return valueInFahrenheit ? Number((val * 1.8 + 32).toFixed(1)) : val
}
/**
* Read only the useful part of a buffer and convert the
* hexadecimal value to a decimal value of the battery level.
*
* @param {Buffer} buf - Buffer that contains the battery level in hexadecimal value.
*
* @example
* const buf = Buffer.from('460844c00a', 'hex')
* const batteryLevel = _getBatteryLevel(buf)
* console.log(batteryLevel) // 72
*
* @returns {number} - Battery level in decimal value.
*/
function _getBatteryLevel (buf) {
return Math.round(_map(buf.readUInt16LE(3), 2100, 3000, 0, 100))
}
/**
* Re-maps a number from one range to another.
*
* @param {number} val - Number to map.
* @param {number} inMin - Lower bound of the value’s current range.
* @param {number} inMax - Upper bound of the value’s current range.
* @param {number} outMin - Lower bound of the value’s target range.
* @param {number} outMax - Upper bound of the value’s target range.
*
* @example
* const mappedValue = _map(2500, 2100, 3000, 0, 100)
* console.log(mappedValue) // 44.4
*
* @returns {number} - Mapped value.
*/
function _map (val, inMin, inMax, outMin, outMax) {
if (val > inMax) {
return outMax
} else if (val < inMin) {
return outMin
} else {
return Number(((val - inMin) * (outMax - outMin) / (inMax - inMin) + outMin).toFixed(1))
}
}
return {
getSensors: getSensors,
getData: getData,
_listenHandle: _listenHandle,
_getHumidityLevel: _getHumidityLevel,
_getTemperature: _getTemperature,
_getBatteryLevel: _getBatteryLevel,
_map: _map
}
})()