@homebridge-plugins/homebridge-govee 10.12.1
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/CHANGELOG.md +1937 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/config.schema.json +1727 -0
- package/eslint.config.js +49 -0
- package/lib/connection/aws.js +174 -0
- package/lib/connection/ble.js +208 -0
- package/lib/connection/cert/AmazonRootCA1.pem +20 -0
- package/lib/connection/http.js +240 -0
- package/lib/connection/lan.js +284 -0
- package/lib/device/cooler-single.js +300 -0
- package/lib/device/dehumidifier-H7150.js +182 -0
- package/lib/device/dehumidifier-H7151.js +157 -0
- package/lib/device/diffuser-H7161.js +117 -0
- package/lib/device/diffuser-H7162.js +117 -0
- package/lib/device/fan-H7100.js +274 -0
- package/lib/device/fan-H7101.js +330 -0
- package/lib/device/fan-H7102.js +274 -0
- package/lib/device/fan-H7105.js +503 -0
- package/lib/device/fan-H7106.js +274 -0
- package/lib/device/fan-H7111.js +335 -0
- package/lib/device/heater-single.js +300 -0
- package/lib/device/heater1a.js +353 -0
- package/lib/device/heater1b.js +616 -0
- package/lib/device/heater2.js +838 -0
- package/lib/device/humidifier-H7140.js +224 -0
- package/lib/device/humidifier-H7141.js +257 -0
- package/lib/device/humidifier-H7142.js +522 -0
- package/lib/device/humidifier-H7143.js +157 -0
- package/lib/device/humidifier-H7148.js +157 -0
- package/lib/device/humidifier-H7160.js +446 -0
- package/lib/device/ice-maker-H7162.js +46 -0
- package/lib/device/index.js +105 -0
- package/lib/device/kettle.js +269 -0
- package/lib/device/light-switch.js +86 -0
- package/lib/device/light.js +617 -0
- package/lib/device/outlet-double.js +121 -0
- package/lib/device/outlet-single.js +172 -0
- package/lib/device/outlet-triple.js +160 -0
- package/lib/device/purifier-H7120.js +336 -0
- package/lib/device/purifier-H7121.js +336 -0
- package/lib/device/purifier-H7122.js +449 -0
- package/lib/device/purifier-H7123.js +411 -0
- package/lib/device/purifier-H7124.js +411 -0
- package/lib/device/purifier-H7126.js +296 -0
- package/lib/device/purifier-H7127.js +296 -0
- package/lib/device/purifier-H712C.js +296 -0
- package/lib/device/purifier-single.js +119 -0
- package/lib/device/sensor-button.js +22 -0
- package/lib/device/sensor-contact.js +22 -0
- package/lib/device/sensor-leak.js +87 -0
- package/lib/device/sensor-monitor.js +190 -0
- package/lib/device/sensor-presence.js +53 -0
- package/lib/device/sensor-thermo.js +144 -0
- package/lib/device/sensor-thermo4.js +55 -0
- package/lib/device/switch-double.js +121 -0
- package/lib/device/switch-single.js +95 -0
- package/lib/device/switch-triple.js +160 -0
- package/lib/device/tap-single.js +108 -0
- package/lib/device/template.js +43 -0
- package/lib/device/tv-single.js +84 -0
- package/lib/device/valve-single.js +155 -0
- package/lib/fakegato/LICENSE +21 -0
- package/lib/fakegato/fakegato-history.js +814 -0
- package/lib/fakegato/fakegato-storage.js +108 -0
- package/lib/fakegato/fakegato-timer.js +125 -0
- package/lib/fakegato/uuid.js +27 -0
- package/lib/homebridge-ui/public/index.html +433 -0
- package/lib/homebridge-ui/server.js +10 -0
- package/lib/index.js +8 -0
- package/lib/platform.js +1967 -0
- package/lib/utils/colour.js +564 -0
- package/lib/utils/constants.js +579 -0
- package/lib/utils/custom-chars.js +225 -0
- package/lib/utils/eve-chars.js +68 -0
- package/lib/utils/functions.js +117 -0
- package/lib/utils/lang-en.js +131 -0
- package/package.json +75 -0
package/eslint.config.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { antfu } from '@antfu/eslint-config'
|
|
2
|
+
|
|
3
|
+
/** @type {typeof antfu} */
|
|
4
|
+
export default antfu(
|
|
5
|
+
{
|
|
6
|
+
ignores: [],
|
|
7
|
+
jsx: false,
|
|
8
|
+
rules: {
|
|
9
|
+
'curly': ['error', 'multi-line'],
|
|
10
|
+
'new-cap': 'off',
|
|
11
|
+
'import/extensions': ['error', 'ignorePackages'],
|
|
12
|
+
'import/order': 0,
|
|
13
|
+
'jsdoc/check-alignment': 'warn',
|
|
14
|
+
'jsdoc/check-line-alignment': 'warn',
|
|
15
|
+
'jsdoc/require-returns-check': 0,
|
|
16
|
+
'jsdoc/require-returns-description': 0,
|
|
17
|
+
'no-undef': 'error',
|
|
18
|
+
'perfectionist/sort-exports': 'error',
|
|
19
|
+
'perfectionist/sort-imports': [
|
|
20
|
+
'error',
|
|
21
|
+
{
|
|
22
|
+
groups: [
|
|
23
|
+
'type',
|
|
24
|
+
'internal-type',
|
|
25
|
+
'builtin',
|
|
26
|
+
'external',
|
|
27
|
+
'internal',
|
|
28
|
+
['parent-type', 'sibling-type', 'index-type'],
|
|
29
|
+
['parent', 'sibling', 'index'],
|
|
30
|
+
'object',
|
|
31
|
+
'unknown',
|
|
32
|
+
],
|
|
33
|
+
order: 'asc',
|
|
34
|
+
type: 'natural',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
'perfectionist/sort-named-exports': 'error',
|
|
38
|
+
'perfectionist/sort-named-imports': 'error',
|
|
39
|
+
'quotes': ['error', 'single'],
|
|
40
|
+
'sort-imports': 0,
|
|
41
|
+
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
|
42
|
+
'style/quote-props': ['error', 'consistent-as-needed'],
|
|
43
|
+
'test/no-only-tests': 'error',
|
|
44
|
+
'unicorn/no-useless-spread': 'error',
|
|
45
|
+
'unused-imports/no-unused-vars': ['error', { caughtErrors: 'none' }],
|
|
46
|
+
},
|
|
47
|
+
typescript: false,
|
|
48
|
+
},
|
|
49
|
+
)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import url from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { device as iotDevice } from 'aws-iot-device-sdk'
|
|
6
|
+
|
|
7
|
+
import { parseError } from '../utils/functions.js'
|
|
8
|
+
import platformLang from '../utils/lang-en.js'
|
|
9
|
+
|
|
10
|
+
const dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
|
11
|
+
|
|
12
|
+
export default class {
|
|
13
|
+
constructor(platform, iotFile) {
|
|
14
|
+
this.accountTopic = platform.accountTopic
|
|
15
|
+
|
|
16
|
+
this.device = iotDevice({
|
|
17
|
+
privateKey: Buffer.from(iotFile.key, 'utf8'),
|
|
18
|
+
clientCert: Buffer.from(iotFile.cert, 'utf8'),
|
|
19
|
+
caPath: resolve(dirname, './cert/AmazonRootCA1.pem'),
|
|
20
|
+
clientId: `AP/${platform.accountId}/${platform.clientId}`,
|
|
21
|
+
host: platform.iotEndpoint,
|
|
22
|
+
enableMetrics: false,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// A listener event for if the connection closes
|
|
26
|
+
this.device.on('close', () => {
|
|
27
|
+
platform.log.debugWarn('[AWS] %s.', platformLang.awsEventClose)
|
|
28
|
+
this.connected = false
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// A listener event for if the connection reconnects
|
|
32
|
+
this.device.on('reconnect', () => {
|
|
33
|
+
platform.log.debug('[AWS] %s.', platformLang.awsEventReconnect)
|
|
34
|
+
this.connected = true
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// A listener event for if the connection goes offline
|
|
38
|
+
this.device.on('offline', () => {
|
|
39
|
+
platform.log.debugWarn('[AWS] %s.', platformLang.awsEventOffline)
|
|
40
|
+
this.connected = false
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// A listener event for if the connection creates an error
|
|
44
|
+
this.device.on('error', (error) => {
|
|
45
|
+
platform.log.debugWarn('[AWS] %s [%s].', platformLang.awsEventError, parseError(error))
|
|
46
|
+
this.connected = false
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// A listener event for receiving a message
|
|
50
|
+
this.device.on('message', (topic, payload) => {
|
|
51
|
+
const payloadString = Buffer.from(payload).toString()
|
|
52
|
+
|
|
53
|
+
// Parse the message to JSON
|
|
54
|
+
try {
|
|
55
|
+
payload = JSON.parse(payloadString)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
platform.log.debugWarn('[AWS] %s [%s].', platformLang.invalidJson, payloadString)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Older models may have the message in a msg property
|
|
62
|
+
if (payload.msg) {
|
|
63
|
+
try {
|
|
64
|
+
payload = JSON.parse(payload.msg)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
platform.log.debugWarn('[AWS] %s [%s].', platformLang.invalidJson, payloadString)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Also JSON parse any data property that may exist, do not throw on error
|
|
72
|
+
if (payload.data && typeof payload.data === 'string') {
|
|
73
|
+
try {
|
|
74
|
+
payload.data = JSON.parse(payload.data)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Do nothing, leave as string
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Log the received message if debug is enabled
|
|
81
|
+
platform.log.debug('[AWS] %s [%s].', platformLang.awsEventMessage, payloadString)
|
|
82
|
+
|
|
83
|
+
// Send the update to the receiver function
|
|
84
|
+
platform.receiveUpdateAWS(payload)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// A listener event for when the connection is created
|
|
88
|
+
this.device.on('connect', () => {
|
|
89
|
+
platform.log.debug('[AWS] %s.', platformLang.awsEventConnect)
|
|
90
|
+
this.connected = true
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async connect() {
|
|
95
|
+
return new Promise((res, rej) => {
|
|
96
|
+
this.device.subscribe(this.accountTopic, {}, (err) => {
|
|
97
|
+
if (err) {
|
|
98
|
+
rej(err)
|
|
99
|
+
} else {
|
|
100
|
+
res()
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async requestUpdate(accessory) {
|
|
107
|
+
// Check if we are connected before attempting an update
|
|
108
|
+
if (!this.connected) {
|
|
109
|
+
throw new Error(platformLang.notAWSConn)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Generate the AWS payload
|
|
113
|
+
const payload = {
|
|
114
|
+
msg: {
|
|
115
|
+
cmd: 'status',
|
|
116
|
+
cmdVersion: 2,
|
|
117
|
+
transaction: `v_${Date.now()}000`,
|
|
118
|
+
type: 0,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Log the update if accessory debug is enabled
|
|
123
|
+
accessory.logDebug(`[AWS] ${platformLang.sendingUpdate} ${JSON.stringify(payload)}`)
|
|
124
|
+
|
|
125
|
+
// Add the account topic after logging
|
|
126
|
+
payload.msg.accountTopic = this.accountTopic
|
|
127
|
+
|
|
128
|
+
// Send the update over AWS
|
|
129
|
+
return new Promise((res, rej) => {
|
|
130
|
+
this.device.publish(accessory.context.awsTopic, JSON.stringify(payload), {}, (err) => {
|
|
131
|
+
if (err) {
|
|
132
|
+
rej(err)
|
|
133
|
+
} else {
|
|
134
|
+
res()
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async updateDevice(accessory, params) {
|
|
141
|
+
// Check if we are connected before attempting an update
|
|
142
|
+
if (!this.connected) {
|
|
143
|
+
throw new Error(platformLang.notAWSConn)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Generate the AWS payload
|
|
147
|
+
const payload = {
|
|
148
|
+
msg: {
|
|
149
|
+
cmd: params.cmd,
|
|
150
|
+
cmdVersion: 0,
|
|
151
|
+
data: params.data,
|
|
152
|
+
transaction: `v_${Date.now()}000`,
|
|
153
|
+
type: 1,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Log the update if accessory debug is enabled
|
|
158
|
+
accessory.logDebug(`[AWS] ${platformLang.sendingUpdate} ${JSON.stringify(payload)}`)
|
|
159
|
+
|
|
160
|
+
// Add the account topic after logging
|
|
161
|
+
payload.msg.accountTopic = this.accountTopic
|
|
162
|
+
|
|
163
|
+
// Send the update over AWS
|
|
164
|
+
return new Promise((res, rej) => {
|
|
165
|
+
this.device.publish(accessory.context.awsTopic, JSON.stringify(payload), {}, (err) => {
|
|
166
|
+
if (err) {
|
|
167
|
+
rej(err)
|
|
168
|
+
} else {
|
|
169
|
+
res()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
import btClient from '@abandonware/noble'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
base64ToHex,
|
|
7
|
+
generateCodeFromHexValues,
|
|
8
|
+
hexToTwoItems,
|
|
9
|
+
sleep,
|
|
10
|
+
} from '../utils/functions.js'
|
|
11
|
+
import platformLang from '../utils/lang-en.js'
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
The necessary commands to send and functions are taken from and credit to:
|
|
15
|
+
https://www.npmjs.com/package/govee-led-client
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export default class {
|
|
19
|
+
constructor(platform) {
|
|
20
|
+
this.connectedTo = false
|
|
21
|
+
this.log = platform.log
|
|
22
|
+
this.platform = platform
|
|
23
|
+
this.stateChange = false
|
|
24
|
+
|
|
25
|
+
// Can only scan/connect/send if the noble stateChange is 'poweredOn'
|
|
26
|
+
btClient.on('stateChange', (state) => {
|
|
27
|
+
this.stateChange = state
|
|
28
|
+
this.log.debug('[BLE] stateChange: %s.', state)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Event listener for noble scanning start
|
|
32
|
+
btClient.on('scanStart', () => {
|
|
33
|
+
this.log.debug('[BLE] %s.', platformLang.bleStart)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Event listener for noble scanning stop
|
|
37
|
+
btClient.on('scanStop', () => {
|
|
38
|
+
this.log.debug('[BLE] %s.', platformLang.bleStop)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Event and log noble warnings
|
|
42
|
+
btClient.on('warning', (message) => {
|
|
43
|
+
this.log.debugWarn('[BLE] %s.', message)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Event handler for discovering bluetooth devices
|
|
47
|
+
// This should only be each and every time a device update is sent
|
|
48
|
+
btClient.on('discover', (device) => {
|
|
49
|
+
// Log the address found can be useful for debugging what's working
|
|
50
|
+
this.log.debug('[BLE] found device [%s] [%s].', device.address, device.advertisement.localName)
|
|
51
|
+
|
|
52
|
+
// Look for the device to update at the time
|
|
53
|
+
if (!this.accessory || this.accessory.context.bleAddress !== device.address) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Found the device so stop scanning
|
|
58
|
+
btClient.stopScanning()
|
|
59
|
+
|
|
60
|
+
// Make the device global as needed in other functions
|
|
61
|
+
this.device = device
|
|
62
|
+
|
|
63
|
+
// Log that the device has been discovered
|
|
64
|
+
this.accessory.logDebug(platformLang.onlineBT)
|
|
65
|
+
|
|
66
|
+
// Remove previous listeners that may still be intact
|
|
67
|
+
this.device.removeAllListeners()
|
|
68
|
+
|
|
69
|
+
// Add a listener for device disconnect
|
|
70
|
+
this.device.on('disconnect', (reason) => {
|
|
71
|
+
// Log the disconnection
|
|
72
|
+
if (this.accessory) {
|
|
73
|
+
this.accessory.logDebug(`${platformLang.offlineBTConn} [${reason || 'unknown'}]`)
|
|
74
|
+
} else {
|
|
75
|
+
this.log.debug(
|
|
76
|
+
'[BLE] [%s] %s [%s].',
|
|
77
|
+
this.device ? this.device.address : 'unknown',
|
|
78
|
+
platformLang.offlineBTConn,
|
|
79
|
+
reason || 'unknown',
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Un-define the variables used within the class
|
|
84
|
+
this.device = undefined
|
|
85
|
+
this.connectedTo = false
|
|
86
|
+
this.controlChar = undefined
|
|
87
|
+
this.accessory = undefined
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Reset adapter
|
|
91
|
+
btClient.reset()
|
|
92
|
+
|
|
93
|
+
// Connect to the device
|
|
94
|
+
this.accessory.logDebug('attempting to connect')
|
|
95
|
+
this.device.connect((error) => {
|
|
96
|
+
if (error) {
|
|
97
|
+
this.accessory.logWarn(`could not connect as ${error}`)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
// Update the currently-connect-to variable
|
|
101
|
+
this.connectedTo = this.accessory.context.bleAddress
|
|
102
|
+
|
|
103
|
+
// Log the connection
|
|
104
|
+
this.accessory.logDebug(platformLang.onlineBTConn)
|
|
105
|
+
|
|
106
|
+
// Find the noble characteristic we need for controlling the device
|
|
107
|
+
this.accessory.logDebug('finding device characteristics')
|
|
108
|
+
device.discoverAllServicesAndCharacteristics((error2, services, characteristics) => {
|
|
109
|
+
if (error2) {
|
|
110
|
+
this.accessory.logWarn(`could not find device characteristics as ${error2}`)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
this.accessory.logDebug('found some device characteristics')
|
|
114
|
+
Object.values(characteristics).forEach((char) => {
|
|
115
|
+
// Make sure we found the characteristic and make it global for the sendUpdate function
|
|
116
|
+
const formattedChar = char.uuid.replace(/-/g, '')
|
|
117
|
+
if (formattedChar === '000102030405060708090a0b0c0d2b11') {
|
|
118
|
+
this.controlChar = char
|
|
119
|
+
this.accessory.logDebug(`found correct characteristic [${formattedChar}]`)
|
|
120
|
+
} else {
|
|
121
|
+
this.accessory.logDebug(`found different characteristic [${formattedChar}]`)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
if (!this.controlChar) {
|
|
125
|
+
this.accessory.logWarn('could not find control characteristic')
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async updateDevice(accessory, params) {
|
|
133
|
+
// This is called by the platform on sending a device update via bluetooth
|
|
134
|
+
accessory.logDebug(`starting update with params [${JSON.stringify(params)}]`)
|
|
135
|
+
|
|
136
|
+
// Check the noble state is ready for bluetooth action
|
|
137
|
+
if (this.stateChange !== 'poweredOn') {
|
|
138
|
+
throw new Error(`${platformLang.bleWrongState} [${this.stateChange}]`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// This is used to time out the request later on if it's taking too much time
|
|
142
|
+
// 7 seconds, to take into account AWS control failing as well as the ~10 second HK limit
|
|
143
|
+
let doIt = true
|
|
144
|
+
accessory.logDebug('starting timer')
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
doIt = false
|
|
147
|
+
}, 7000)
|
|
148
|
+
|
|
149
|
+
// Check if we are already connected to a device - and disconnect
|
|
150
|
+
if (this.device) {
|
|
151
|
+
if (this.connectedTo && this.connectedTo !== accessory.context.bleAddress) {
|
|
152
|
+
accessory.logDebug(`disconnecting from [${this.connectedTo}] to connect to [${accessory.context.bleAddress}]`)
|
|
153
|
+
await this.device.disconnectAsync()
|
|
154
|
+
accessory.logDebug('disconnect successful')
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Make global the accessory in question which we are sending an update to
|
|
159
|
+
this.accessory = accessory
|
|
160
|
+
|
|
161
|
+
// Start the bluetooth scan to discover this accessory
|
|
162
|
+
// Service UUID for future reference 000102030405060708090a0b0c0d1910
|
|
163
|
+
accessory.logDebug('starting scan')
|
|
164
|
+
await btClient.startScanningAsync()
|
|
165
|
+
accessory.logDebug('scanning started')
|
|
166
|
+
|
|
167
|
+
// We want to wait for the .on('discover') function to find the accessory and the characteristic
|
|
168
|
+
accessory.logDebug('starting loop')
|
|
169
|
+
|
|
170
|
+
while (true) {
|
|
171
|
+
if (!doIt) {
|
|
172
|
+
accessory.logWarn(`could not find device [${accessory.context.bleAddress}]`)
|
|
173
|
+
throw new Error(platformLang.bleTimeout)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Once the characteristic (this.controlChar) has been found then break the loop
|
|
177
|
+
if (this.connectedTo === accessory.context.bleAddress && this.controlChar) {
|
|
178
|
+
accessory.logDebug('found correct characteristic so breaking loop')
|
|
179
|
+
break
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Repeat this process every 200ms until the characteristic is available
|
|
183
|
+
await sleep(200)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// We can be sent either:
|
|
187
|
+
// - a base64 action code (with params.cmd === 'ptReal')
|
|
188
|
+
// - an array containing a varied amount of already-hex values
|
|
189
|
+
const finalBuffer = params.cmd === 'ptReal'
|
|
190
|
+
? Buffer.from(hexToTwoItems(base64ToHex(params.data)).map(byte => `0x${byte}`))
|
|
191
|
+
: generateCodeFromHexValues([0x33, params.cmd, params.data], true)
|
|
192
|
+
|
|
193
|
+
// Log the request if in debug mode
|
|
194
|
+
accessory.logDebug(`[BLE] ${platformLang.sendingUpdate} [${finalBuffer.toString('hex')}]`)
|
|
195
|
+
|
|
196
|
+
// Send the data to the device
|
|
197
|
+
await this.controlChar.writeAsync(finalBuffer, true)
|
|
198
|
+
|
|
199
|
+
// Maybe a slight await here helps (for an unknown reason)
|
|
200
|
+
await sleep(100)
|
|
201
|
+
|
|
202
|
+
// Disconnect from device
|
|
203
|
+
await this.device.disconnectAsync()
|
|
204
|
+
|
|
205
|
+
// Maybe a slight await here helps (for an unknown reason)
|
|
206
|
+
await sleep(100)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
|
|
3
|
+
ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
|
|
4
|
+
b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL
|
|
5
|
+
MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
|
|
6
|
+
b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj
|
|
7
|
+
ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM
|
|
8
|
+
9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw
|
|
9
|
+
IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6
|
|
10
|
+
VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L
|
|
11
|
+
93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm
|
|
12
|
+
jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
|
|
13
|
+
AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA
|
|
14
|
+
A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI
|
|
15
|
+
U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs
|
|
16
|
+
N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv
|
|
17
|
+
o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU
|
|
18
|
+
5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy
|
|
19
|
+
rqXRfboQnoZsG4q5WTP468SQvvG5
|
|
20
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
|
|
5
|
+
import platformConsts from '../utils/constants.js'
|
|
6
|
+
import { parseError, sleep } from '../utils/functions.js'
|
|
7
|
+
import platformLang from '../utils/lang-en.js'
|
|
8
|
+
|
|
9
|
+
export default class {
|
|
10
|
+
constructor(platform) {
|
|
11
|
+
// Create variables usable by the class
|
|
12
|
+
this.log = platform.log
|
|
13
|
+
this.password = platform.config.password
|
|
14
|
+
this.token = platform.accountToken
|
|
15
|
+
this.tokenTTR = platform.accountTokenTTR
|
|
16
|
+
this.username = platform.config.username
|
|
17
|
+
|
|
18
|
+
// May need changing from time to time
|
|
19
|
+
this.appVersion = '5.6.01'
|
|
20
|
+
this.userAgent = `GoveeHome/${this.appVersion} (com.ihoment.GoVeeSensor; build:2; iOS 16.5.0) Alamofire/5.6.4`
|
|
21
|
+
|
|
22
|
+
// Create a client id generated from Govee username which should remain constant
|
|
23
|
+
let clientSuffix = platform.api.hap.uuid.generate(this.username).replace(/-/g, '') // 32 chars
|
|
24
|
+
clientSuffix = clientSuffix.substring(0, clientSuffix.length - 2) // 30 chars
|
|
25
|
+
this.clientId = `hb${clientSuffix}` // 32 chars
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async login() {
|
|
29
|
+
try {
|
|
30
|
+
// Perform the HTTP request
|
|
31
|
+
const res = await axios({
|
|
32
|
+
url: 'https://app2.govee.com/account/rest/account/v1/login',
|
|
33
|
+
method: 'post',
|
|
34
|
+
data: {
|
|
35
|
+
email: this.username,
|
|
36
|
+
password: this.password,
|
|
37
|
+
client: this.clientId,
|
|
38
|
+
},
|
|
39
|
+
timeout: 30000,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Check to see we got a response
|
|
43
|
+
if (!res.data) {
|
|
44
|
+
throw new Error(platformLang.noToken)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check to see we got a needed response
|
|
48
|
+
if (!res.data.client || !res.data.client.token) {
|
|
49
|
+
if (res.data.message && res.data.message.replace(/\s+/g, '') === 'Incorrectpassword') {
|
|
50
|
+
if (this.base64Tried) {
|
|
51
|
+
throw new Error(res.data.message || platformLang.noToken)
|
|
52
|
+
} else {
|
|
53
|
+
this.base64Tried = true
|
|
54
|
+
this.password = Buffer.from(this.password, 'base64')
|
|
55
|
+
.toString('utf8')
|
|
56
|
+
.replace(/\r\n|\n|\r/g, '')
|
|
57
|
+
.trim()
|
|
58
|
+
return await this.login()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw new Error(res.data.message || platformLang.noToken)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Also grab an access token specifically for the get tap to run endpoint
|
|
65
|
+
const ttrRes = await axios({
|
|
66
|
+
url: 'https://community-api.govee.com/os/v1/login',
|
|
67
|
+
method: 'post',
|
|
68
|
+
data: {
|
|
69
|
+
email: this.username,
|
|
70
|
+
password: this.password,
|
|
71
|
+
},
|
|
72
|
+
timeout: 30000,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Make the token available in other functions
|
|
76
|
+
this.token = res.data.client.token
|
|
77
|
+
this.tokenTTR = ttrRes.data.data.token
|
|
78
|
+
|
|
79
|
+
// Mark this request complete if in debug mode
|
|
80
|
+
this.log.debug('[HTTP] %s.', platformLang.loginSuccess)
|
|
81
|
+
|
|
82
|
+
// Also grab the iot data
|
|
83
|
+
const iotRes = await axios({
|
|
84
|
+
url: 'https://app2.govee.com/app/v1/account/iot/key',
|
|
85
|
+
method: 'get',
|
|
86
|
+
headers: {
|
|
87
|
+
'Authorization': `Bearer ${this.token}`,
|
|
88
|
+
'appVersion': this.appVersion,
|
|
89
|
+
'clientId': this.clientId,
|
|
90
|
+
'clientType': 1,
|
|
91
|
+
'iotVersion': 0,
|
|
92
|
+
'timestamp': Date.now(),
|
|
93
|
+
'User-Agent': this.userAgent,
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Return the account token and topic for AWS
|
|
98
|
+
return {
|
|
99
|
+
accountId: res.data.client.accountId,
|
|
100
|
+
client: this.clientId,
|
|
101
|
+
endpoint: iotRes.data.data.endpoint,
|
|
102
|
+
iot: iotRes.data.data.p12,
|
|
103
|
+
iotPass: iotRes.data.data.p12Pass,
|
|
104
|
+
token: res.data.client.token,
|
|
105
|
+
tokenTTR: this.tokenTTR,
|
|
106
|
+
topic: res.data.client.topic,
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err.code && platformConsts.httpRetryCodes.includes(err.code)) {
|
|
110
|
+
// Retry if another attempt could be successful
|
|
111
|
+
this.log.warn('[HTTP] %s [login() - %s].', platformLang.httpRetry, err.code)
|
|
112
|
+
await sleep(30000)
|
|
113
|
+
return this.login()
|
|
114
|
+
}
|
|
115
|
+
throw err
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async logout() {
|
|
120
|
+
try {
|
|
121
|
+
await axios({
|
|
122
|
+
url: 'https://app2.govee.com/account/rest/account/v1/logout',
|
|
123
|
+
method: 'post',
|
|
124
|
+
headers: {
|
|
125
|
+
'Authorization': `Bearer ${this.token}`,
|
|
126
|
+
'appVersion': this.appVersion,
|
|
127
|
+
'clientId': this.clientId,
|
|
128
|
+
'clientType': 1,
|
|
129
|
+
'iotVersion': 0,
|
|
130
|
+
'timestamp': Date.now(),
|
|
131
|
+
'User-Agent': this.userAgent,
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// Logout is only called on homebridge shutdown, so we can just log the error
|
|
136
|
+
this.log.warn('[HTTP] %s %s.', platformLang.logoutFail, parseError(err))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async getDevices(isSync = true) {
|
|
141
|
+
try {
|
|
142
|
+
// Make sure we do have the account token
|
|
143
|
+
if (!this.token) {
|
|
144
|
+
throw new Error(platformLang.noTokenExists)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Use the token received to get a device list
|
|
148
|
+
const res = await axios({
|
|
149
|
+
url: 'https://app2.govee.com/device/rest/devices/v1/list',
|
|
150
|
+
method: 'post',
|
|
151
|
+
headers: {
|
|
152
|
+
'Authorization': `Bearer ${this.token}`,
|
|
153
|
+
'appVersion': this.appVersion,
|
|
154
|
+
'clientId': this.clientId,
|
|
155
|
+
'clientType': 1,
|
|
156
|
+
'iotVersion': 0,
|
|
157
|
+
'timestamp': Date.now(),
|
|
158
|
+
'User-Agent': this.userAgent,
|
|
159
|
+
},
|
|
160
|
+
timeout: 30000,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Check to see we got a response
|
|
164
|
+
if (!res.data || !res.data.devices) {
|
|
165
|
+
throw new Error(platformLang.noDevices)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Return the device list
|
|
169
|
+
return res.data.devices || []
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (!isSync && err.code && platformConsts.httpRetryCodes.includes(err.code)) {
|
|
172
|
+
// Retry if another attempt could be successful (only on init, not sync)
|
|
173
|
+
this.log.warn('[HTTP] %s [getDevices() - %s].', platformLang.httpRetry, err.code)
|
|
174
|
+
await sleep(30000)
|
|
175
|
+
return this.getDevices()
|
|
176
|
+
}
|
|
177
|
+
throw err
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async getTapToRuns() {
|
|
182
|
+
// Build and send the request
|
|
183
|
+
const res = await axios({
|
|
184
|
+
url: 'https://app2.govee.com/bff-app/v1/exec-plat/home',
|
|
185
|
+
method: 'get',
|
|
186
|
+
headers: {
|
|
187
|
+
'Authorization': `Bearer ${this.tokenTTR}`,
|
|
188
|
+
'appVersion': this.appVersion,
|
|
189
|
+
'clientId': this.clientId,
|
|
190
|
+
'clientType': 1,
|
|
191
|
+
'iotVersion': 0,
|
|
192
|
+
'timestamp': Date.now(),
|
|
193
|
+
'User-Agent': this.userAgent,
|
|
194
|
+
},
|
|
195
|
+
timeout: 10000,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// Check to see we got a response
|
|
199
|
+
if (!res?.data?.data?.components) {
|
|
200
|
+
throw new Error('not a valid response')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return res.data.data.components
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async getLeakDeviceWarning(deviceId, deviceSku) {
|
|
207
|
+
// Make sure we do have the account token
|
|
208
|
+
if (!this.token) {
|
|
209
|
+
throw new Error(platformLang.noTokenExists)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Build and send the request
|
|
213
|
+
const res = await axios({
|
|
214
|
+
url: 'https://app2.govee.com/leak/rest/device/v1/warnMessage',
|
|
215
|
+
method: 'post',
|
|
216
|
+
headers: {
|
|
217
|
+
'Authorization': `Bearer ${this.token}`,
|
|
218
|
+
'appVersion': this.appVersion,
|
|
219
|
+
'clientId': this.clientId,
|
|
220
|
+
'clientType': 1,
|
|
221
|
+
'iotVersion': 0,
|
|
222
|
+
'timestamp': Date.now(),
|
|
223
|
+
'User-Agent': this.userAgent,
|
|
224
|
+
},
|
|
225
|
+
data: {
|
|
226
|
+
device: deviceId.replaceAll(':', ''),
|
|
227
|
+
limit: 50,
|
|
228
|
+
sku: deviceSku,
|
|
229
|
+
},
|
|
230
|
+
timeout: 10000,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Check to see we got a response
|
|
234
|
+
if (!res?.data?.data) {
|
|
235
|
+
throw new Error(platformLang.noDevices)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return res.data.data
|
|
239
|
+
}
|
|
240
|
+
}
|