@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +1937 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/config.schema.json +1727 -0
  5. package/eslint.config.js +49 -0
  6. package/lib/connection/aws.js +174 -0
  7. package/lib/connection/ble.js +208 -0
  8. package/lib/connection/cert/AmazonRootCA1.pem +20 -0
  9. package/lib/connection/http.js +240 -0
  10. package/lib/connection/lan.js +284 -0
  11. package/lib/device/cooler-single.js +300 -0
  12. package/lib/device/dehumidifier-H7150.js +182 -0
  13. package/lib/device/dehumidifier-H7151.js +157 -0
  14. package/lib/device/diffuser-H7161.js +117 -0
  15. package/lib/device/diffuser-H7162.js +117 -0
  16. package/lib/device/fan-H7100.js +274 -0
  17. package/lib/device/fan-H7101.js +330 -0
  18. package/lib/device/fan-H7102.js +274 -0
  19. package/lib/device/fan-H7105.js +503 -0
  20. package/lib/device/fan-H7106.js +274 -0
  21. package/lib/device/fan-H7111.js +335 -0
  22. package/lib/device/heater-single.js +300 -0
  23. package/lib/device/heater1a.js +353 -0
  24. package/lib/device/heater1b.js +616 -0
  25. package/lib/device/heater2.js +838 -0
  26. package/lib/device/humidifier-H7140.js +224 -0
  27. package/lib/device/humidifier-H7141.js +257 -0
  28. package/lib/device/humidifier-H7142.js +522 -0
  29. package/lib/device/humidifier-H7143.js +157 -0
  30. package/lib/device/humidifier-H7148.js +157 -0
  31. package/lib/device/humidifier-H7160.js +446 -0
  32. package/lib/device/ice-maker-H7162.js +46 -0
  33. package/lib/device/index.js +105 -0
  34. package/lib/device/kettle.js +269 -0
  35. package/lib/device/light-switch.js +86 -0
  36. package/lib/device/light.js +617 -0
  37. package/lib/device/outlet-double.js +121 -0
  38. package/lib/device/outlet-single.js +172 -0
  39. package/lib/device/outlet-triple.js +160 -0
  40. package/lib/device/purifier-H7120.js +336 -0
  41. package/lib/device/purifier-H7121.js +336 -0
  42. package/lib/device/purifier-H7122.js +449 -0
  43. package/lib/device/purifier-H7123.js +411 -0
  44. package/lib/device/purifier-H7124.js +411 -0
  45. package/lib/device/purifier-H7126.js +296 -0
  46. package/lib/device/purifier-H7127.js +296 -0
  47. package/lib/device/purifier-H712C.js +296 -0
  48. package/lib/device/purifier-single.js +119 -0
  49. package/lib/device/sensor-button.js +22 -0
  50. package/lib/device/sensor-contact.js +22 -0
  51. package/lib/device/sensor-leak.js +87 -0
  52. package/lib/device/sensor-monitor.js +190 -0
  53. package/lib/device/sensor-presence.js +53 -0
  54. package/lib/device/sensor-thermo.js +144 -0
  55. package/lib/device/sensor-thermo4.js +55 -0
  56. package/lib/device/switch-double.js +121 -0
  57. package/lib/device/switch-single.js +95 -0
  58. package/lib/device/switch-triple.js +160 -0
  59. package/lib/device/tap-single.js +108 -0
  60. package/lib/device/template.js +43 -0
  61. package/lib/device/tv-single.js +84 -0
  62. package/lib/device/valve-single.js +155 -0
  63. package/lib/fakegato/LICENSE +21 -0
  64. package/lib/fakegato/fakegato-history.js +814 -0
  65. package/lib/fakegato/fakegato-storage.js +108 -0
  66. package/lib/fakegato/fakegato-timer.js +125 -0
  67. package/lib/fakegato/uuid.js +27 -0
  68. package/lib/homebridge-ui/public/index.html +433 -0
  69. package/lib/homebridge-ui/server.js +10 -0
  70. package/lib/index.js +8 -0
  71. package/lib/platform.js +1967 -0
  72. package/lib/utils/colour.js +564 -0
  73. package/lib/utils/constants.js +579 -0
  74. package/lib/utils/custom-chars.js +225 -0
  75. package/lib/utils/eve-chars.js +68 -0
  76. package/lib/utils/functions.js +117 -0
  77. package/lib/utils/lang-en.js +131 -0
  78. package/package.json +75 -0
@@ -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
+ }