homebridge-flume 0.1.0 → 0.2.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/CHANGELOG.md +11 -0
- package/README.md +10 -14
- package/config.schema.json +15 -17
- package/lib/connection/http.js +218 -0
- package/lib/device/valve.js +38 -0
- package/lib/homebridge-ui/public/index.html +159 -0
- package/lib/homebridge-ui/server.js +14 -0
- package/lib/index.js +307 -191
- package/lib/utils/constants.js +31 -0
- package/lib/utils/functions.js +33 -0
- package/lib/utils/lang-en.js +64 -0
- package/package.json +6 -11
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -5,24 +5,20 @@
|
|
|
5
5
|
Configuration sample:
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"valveType": 3
|
|
19
|
-
}
|
|
20
|
-
]
|
|
8
|
+
{
|
|
9
|
+
"name": "Flume Water Monitor",
|
|
10
|
+
"username": "user@domain.com",
|
|
11
|
+
"password": "password",
|
|
12
|
+
"client_id": "12345678901234567890",
|
|
13
|
+
"client_secret": "1234567890",
|
|
14
|
+
"polling_minutes": 1,
|
|
15
|
+
"handicap": 0.1,
|
|
16
|
+
"platform": "Flume"
|
|
17
|
+
}
|
|
21
18
|
```
|
|
22
19
|
|
|
23
20
|
Fields:
|
|
24
21
|
|
|
25
|
-
- "accessory": Must always be "Flume" (required)
|
|
26
22
|
- "username": Your username for accessing the Flume site at https://portal.flumetech.com/ (required)
|
|
27
23
|
- "password": Your password for accessing the Flume site (required)
|
|
28
24
|
- "client_id": Your Client ID for API access, found in Settings on the Flume site (required)
|
package/config.schema.json
CHANGED
|
@@ -1,57 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pluginAlias": "Flume",
|
|
3
|
-
"pluginType": "
|
|
3
|
+
"pluginType": "platform",
|
|
4
4
|
"singular": true,
|
|
5
|
-
"
|
|
5
|
+
"customUi": true,
|
|
6
|
+
"customUiPath": "./lib/homebridge-ui",
|
|
7
|
+
"headerDisplay": "<p align=\"center\">For help and support please visit our <a href=\"https://github.com/bwp91/homebridge-flume/wiki\">GitHub Wiki</a>. We hope you find this plugin useful!</p>",
|
|
6
8
|
"schema": {
|
|
7
9
|
"type": "object",
|
|
8
10
|
"properties": {
|
|
9
11
|
"name": {
|
|
10
|
-
"title": "
|
|
12
|
+
"title": "Plugin Name",
|
|
11
13
|
"type": "string",
|
|
12
|
-
"
|
|
13
|
-
"default": "Flume Water Monitor",
|
|
14
|
-
"description": "The name used for the Flume water monitor in HomeKit"
|
|
14
|
+
"default": "Flume"
|
|
15
15
|
},
|
|
16
16
|
"username": {
|
|
17
17
|
"title": "Username",
|
|
18
18
|
"type": "string",
|
|
19
19
|
"required": true,
|
|
20
|
-
"description": "Your username for
|
|
20
|
+
"description": "Your username for accessing the Flume site at https://portal.flumetech.com"
|
|
21
21
|
},
|
|
22
22
|
"password": {
|
|
23
23
|
"title": "Password",
|
|
24
24
|
"type": "string",
|
|
25
25
|
"required": true,
|
|
26
|
-
"description": "Your password for
|
|
26
|
+
"description": "Your password for accessing the Flume site"
|
|
27
27
|
},
|
|
28
28
|
"client_id": {
|
|
29
29
|
"title": "Client ID",
|
|
30
30
|
"type": "string",
|
|
31
31
|
"placeholder": "12345678901234567890",
|
|
32
32
|
"required": true,
|
|
33
|
-
"description": "Your Client ID for API access, found in Settings on the Flume site
|
|
33
|
+
"description": "Your Client ID for API access, found in Settings on the Flume site"
|
|
34
34
|
},
|
|
35
35
|
"client_secret": {
|
|
36
36
|
"title": "Client Secret",
|
|
37
37
|
"type": "string",
|
|
38
38
|
"placeholder": "1234567890",
|
|
39
39
|
"required": true,
|
|
40
|
-
"description": "Your Client Secret for API access, found in Settings on the Flume site
|
|
40
|
+
"description": "Your Client Secret for API access, found in Settings on the Flume site"
|
|
41
41
|
},
|
|
42
42
|
"polling_minutes": {
|
|
43
43
|
"title": "Update Interval",
|
|
44
44
|
"type": "integer",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"description": "Number of minutes between updates. Defaults to 1 minute. API has a cap of 120 calls per hour. (optional)"
|
|
45
|
+
"placeholder": 1,
|
|
46
|
+
"description": "Number of minutes between updates. API has a cap of 120 calls per hour."
|
|
48
47
|
},
|
|
49
48
|
"handicap": {
|
|
50
49
|
"title": "Adjustment for False Positives",
|
|
51
|
-
"type": "
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"description": "Set to ignore a steady water draw below this value. Defaults to zero (0.0) gallons. (optional)"
|
|
50
|
+
"type": "number",
|
|
51
|
+
"placeholder": 0,
|
|
52
|
+
"description": "Set to ignore a steady water draw below this value (gallons)."
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
55
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/* jshint node: true, esversion: 10, -W014, -W033 */
|
|
2
|
+
/* eslint-disable new-cap */
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
const axios = require('axios')
|
|
6
|
+
const jwtDecode = require('jwt-decode')
|
|
7
|
+
|
|
8
|
+
module.exports = class connectionHTTP {
|
|
9
|
+
constructor (platform) {
|
|
10
|
+
// Create variables usable by the class
|
|
11
|
+
this.consts = platform.consts
|
|
12
|
+
this.debug = platform.config.debug
|
|
13
|
+
this.funcs = platform.funcs
|
|
14
|
+
this.lang = platform.lang
|
|
15
|
+
this.log = platform.log
|
|
16
|
+
this.username = platform.config.username
|
|
17
|
+
this.password = platform.config.password
|
|
18
|
+
this.client_id = platform.config.client_id
|
|
19
|
+
this.client_secret = platform.config.client_secret
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async obtainToken () {
|
|
23
|
+
try {
|
|
24
|
+
// Generate the JSON data to send
|
|
25
|
+
const body = {
|
|
26
|
+
grant_type: 'password',
|
|
27
|
+
client_id: this.client_id,
|
|
28
|
+
client_secret: this.client_secret,
|
|
29
|
+
username: this.username,
|
|
30
|
+
password: this.password
|
|
31
|
+
}
|
|
32
|
+
const now = Date.now()
|
|
33
|
+
|
|
34
|
+
// Perform the HTTP request
|
|
35
|
+
const res = await axios.post('https://api.flumetech.com/oauth/token', body, {
|
|
36
|
+
timeout: 10000
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Check to see we got a response
|
|
40
|
+
if (!res.data) {
|
|
41
|
+
throw new Error(this.lang.noToken)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
{
|
|
46
|
+
success: true,
|
|
47
|
+
code: 602,
|
|
48
|
+
message: 'Request OK',
|
|
49
|
+
http_code: 200,
|
|
50
|
+
http_message: 'OK',
|
|
51
|
+
detailed: null,
|
|
52
|
+
data: [
|
|
53
|
+
{
|
|
54
|
+
token_type: 'bearer',
|
|
55
|
+
access_token: '',
|
|
56
|
+
expires_in: 604800,
|
|
57
|
+
refresh_token: ''
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
count: 1,
|
|
61
|
+
pagination: null
|
|
62
|
+
}
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
// Make the token available in other functions
|
|
66
|
+
this.accessToken = res.data.data[0].access_token
|
|
67
|
+
this.refreshToken = res.data.data[0].refresh_token
|
|
68
|
+
this.expiresIn = now + res.data.data[0].expires_in
|
|
69
|
+
|
|
70
|
+
// Obtain the user ID
|
|
71
|
+
this.userId = jwtDecode(this.accessToken).user_id
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
{
|
|
75
|
+
user_id: 0000,
|
|
76
|
+
type: 'USER',
|
|
77
|
+
scope: [ 'read:personal', 'update:personal', 'query:personal' ],
|
|
78
|
+
iat: 0000000000,
|
|
79
|
+
exp: 0000000000,
|
|
80
|
+
iss: 'flume_oauth',
|
|
81
|
+
sub: ''
|
|
82
|
+
}
|
|
83
|
+
*/
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err.code && this.consts.httpRetryCodes.includes(err.code)) {
|
|
86
|
+
// Retry if another attempt could be successful
|
|
87
|
+
this.log.warn('[HTTP login()] %s [%s].', this.lang.httpRetry, err.code)
|
|
88
|
+
await this.funcs.sleep(30000)
|
|
89
|
+
return await this.login()
|
|
90
|
+
} else {
|
|
91
|
+
throw new Error('[HTTP obtainToken()] ' + err.message)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async renewToken () {
|
|
97
|
+
try {
|
|
98
|
+
// Generate the JSON data to send
|
|
99
|
+
const body = {
|
|
100
|
+
grant_type: 'refresh_token',
|
|
101
|
+
client_id: this.client_id,
|
|
102
|
+
client_secret: this.client_secret,
|
|
103
|
+
refresh_token: this.refreshToken
|
|
104
|
+
}
|
|
105
|
+
const now = Date.now()
|
|
106
|
+
|
|
107
|
+
// Perform the HTTP request
|
|
108
|
+
const res = await axios.post('https://api.flumetech.com/oauth/token', body, {
|
|
109
|
+
timeout: 10000
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Check to see we got a response
|
|
113
|
+
if (!res.data) {
|
|
114
|
+
throw new Error(this.lang.noToken)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
{
|
|
119
|
+
success: true,
|
|
120
|
+
code: 602,
|
|
121
|
+
message: 'Request OK',
|
|
122
|
+
http_code: 200,
|
|
123
|
+
http_message: 'OK',
|
|
124
|
+
detailed: null,
|
|
125
|
+
data: [
|
|
126
|
+
{
|
|
127
|
+
token_type: 'bearer',
|
|
128
|
+
access_token: '',
|
|
129
|
+
expires_in: 604800,
|
|
130
|
+
refresh_token: ''
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
count: 1,
|
|
134
|
+
pagination: null
|
|
135
|
+
}
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
// Make the token available in other functions
|
|
139
|
+
this.accessToken = res.data.data[0].access_token
|
|
140
|
+
this.refreshToken = res.data.data[0].refresh_token
|
|
141
|
+
this.expiresIn = now + res.data.data[0].expires_in
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (err.code && this.consts.httpRetryCodes.includes(err.code)) {
|
|
144
|
+
// Retry if another attempt could be successful
|
|
145
|
+
this.log.warn('[HTTP login()] %s [%s].', this.lang.httpRetry, err.code)
|
|
146
|
+
await this.funcs.sleep(30000)
|
|
147
|
+
return await this.login()
|
|
148
|
+
} else {
|
|
149
|
+
throw new Error('[HTTP refreshToken()] ' + err.message)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async getDevices () {
|
|
155
|
+
try {
|
|
156
|
+
// Check we have a user id
|
|
157
|
+
if (!this.userId) {
|
|
158
|
+
throw new Error('No user id has been retrieved')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Perform the HTTP request
|
|
162
|
+
const res = await axios.get('https://api.flumetech.com/users/' + this.userId + '/devices', {
|
|
163
|
+
headers: { Authorization: 'Bearer ' + this.accessToken },
|
|
164
|
+
timeout: 10000
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Check to see we got a response
|
|
168
|
+
if (!res.data) {
|
|
169
|
+
throw new Error('No data received')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return res.data.data
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (err.code && this.consts.httpRetryCodes.includes(err.code)) {
|
|
175
|
+
// Retry if another attempt could be successful
|
|
176
|
+
this.log.warn('[HTTP login()] %s [%s].', this.lang.httpRetry, err.code)
|
|
177
|
+
await this.funcs.sleep(30000)
|
|
178
|
+
return await this.login()
|
|
179
|
+
} else {
|
|
180
|
+
throw new Error('[HTTP getDevices()] ' + err.message)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async getDeviceInfo (deviceId, fromWhen) {
|
|
186
|
+
// Refresh the access token if it has expired already
|
|
187
|
+
if (Date.now() > this.expiresIn) {
|
|
188
|
+
await this.renewToken()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Generate the JSON data to send
|
|
192
|
+
const body = {
|
|
193
|
+
queries: [
|
|
194
|
+
{
|
|
195
|
+
request_id: 'currentusage',
|
|
196
|
+
bucket: 'MIN',
|
|
197
|
+
since_datetime: fromWhen,
|
|
198
|
+
operation: 'SUM',
|
|
199
|
+
units: 'GALLONS'
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Send the request
|
|
205
|
+
const res = await axios.post(
|
|
206
|
+
'https://api.flumetech.com/users/' + this.userId + '/devices/' + deviceId + '/query',
|
|
207
|
+
body,
|
|
208
|
+
{
|
|
209
|
+
headers: {
|
|
210
|
+
Authorization: 'Bearer ' + this.accessToken
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
// Parse the response
|
|
216
|
+
return res.data.data[0]
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* jshint node: true, esversion: 10, -W014, -W033 */
|
|
2
|
+
/* eslint-disable new-cap */
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
module.exports = class deviceValve {
|
|
6
|
+
constructor (platform, accessory) {
|
|
7
|
+
// Set up variables from the platform
|
|
8
|
+
this.accessory = accessory
|
|
9
|
+
this.disableDeviceLogging = platform.config.disableDeviceLogging
|
|
10
|
+
this.funcs = platform.funcs
|
|
11
|
+
this.handicap = platform.config.handicap
|
|
12
|
+
this.hapChar = platform.api.hap.Characteristic
|
|
13
|
+
this.hapErr = platform.api.hap.HapStatusError
|
|
14
|
+
this.hapServ = platform.api.hap.Service
|
|
15
|
+
this.lang = platform.lang
|
|
16
|
+
this.log = platform.log
|
|
17
|
+
this.name = accessory.displayName
|
|
18
|
+
this.platform = platform
|
|
19
|
+
this.polling_minutes = platform.config.polling_minutes
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
externalUpdate (params) {
|
|
23
|
+
// Here we deal with the incoming data
|
|
24
|
+
const usage =
|
|
25
|
+
params.currentusage && params.currentusage[0] && params.currentusage[0].value
|
|
26
|
+
? params.currentusage[0].value
|
|
27
|
+
: 0
|
|
28
|
+
if (usage > this.handicap) {
|
|
29
|
+
this.log(
|
|
30
|
+
'[%s] %s - [%s] gallons within the last [%s] minutes.',
|
|
31
|
+
this.name,
|
|
32
|
+
'usage detected',
|
|
33
|
+
usage,
|
|
34
|
+
this.polling_minutes
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<div id="pageIntro" class="text-center" style="display: none;">
|
|
2
|
+
<p class="lead">Thank you for installing <strong>homebridge-flume</strong></p>
|
|
3
|
+
<p>
|
|
4
|
+
You will need your Flume username, password, client ID and secret to continue
|
|
5
|
+
</p>
|
|
6
|
+
<button type="button" class="btn btn-primary" id="introContinue">Continue →</button>
|
|
7
|
+
</div>
|
|
8
|
+
<div
|
|
9
|
+
id="menuWrapper"
|
|
10
|
+
class="btn-group w-100 mb-0"
|
|
11
|
+
role="group"
|
|
12
|
+
aria-label="UI Menu"
|
|
13
|
+
style="display: none;"
|
|
14
|
+
>
|
|
15
|
+
<button type="button" class="btn btn-primary ml-0" id="menuSettings">Settings</button>
|
|
16
|
+
<button type="button" class="btn btn-primary mr-0" id="menuHome">Support</button>
|
|
17
|
+
</div>
|
|
18
|
+
<div
|
|
19
|
+
id="disabledBanner"
|
|
20
|
+
class="alert alert-secondary mb-0 mt-3"
|
|
21
|
+
role="alert"
|
|
22
|
+
style="display: none;"
|
|
23
|
+
>
|
|
24
|
+
Plugin is currently disabled
|
|
25
|
+
<button id="disabledEnable" type="button" class="btn btn-link p-0 m-0 float-right">Enable</button>
|
|
26
|
+
</div>
|
|
27
|
+
<div id="pageSupport" class="mt-4" style="display: none;">
|
|
28
|
+
<p class="text-center lead">Thank you for using <strong>homebridge-flume</strong></p>
|
|
29
|
+
<p class="text-center">The links below will take you to our GitHub wiki</p>
|
|
30
|
+
<h4>Setup</h4>
|
|
31
|
+
<ul>
|
|
32
|
+
<li>
|
|
33
|
+
<a href="https://github.com/bwp91/homebridge-flume/wiki/Installation" target="_blank"
|
|
34
|
+
>Installation</a
|
|
35
|
+
>
|
|
36
|
+
</li>
|
|
37
|
+
<li>
|
|
38
|
+
<a href="https://github.com/bwp91/homebridge-flume/wiki/Configuration" target="_blank"
|
|
39
|
+
>Configuration</a
|
|
40
|
+
>
|
|
41
|
+
</li>
|
|
42
|
+
<li>
|
|
43
|
+
<a href="https://github.com/bwp91/homebridge-flume/wiki/Beta-Version" target="_blank"
|
|
44
|
+
>Beta Version</a
|
|
45
|
+
>
|
|
46
|
+
</li>
|
|
47
|
+
<li>
|
|
48
|
+
<a href="https://github.com/bwp91/homebridge-flume/wiki/Node-Version" target="_blank"
|
|
49
|
+
>Node Version</a
|
|
50
|
+
>
|
|
51
|
+
</li>
|
|
52
|
+
<li>
|
|
53
|
+
<a href="https://github.com/bwp91/homebridge-flume/wiki/Uninstallation" target="_blank"
|
|
54
|
+
>Uninstallation</a
|
|
55
|
+
>
|
|
56
|
+
</li>
|
|
57
|
+
</ul>
|
|
58
|
+
<h4>Help/About</h4>
|
|
59
|
+
<ul>
|
|
60
|
+
<li>
|
|
61
|
+
<a href="https://github.com/bwp91/homebridge-flume/wiki/Common-Errors" target="_blank"
|
|
62
|
+
>Common Errors</a
|
|
63
|
+
>
|
|
64
|
+
</li>
|
|
65
|
+
<li>
|
|
66
|
+
<a href="https://github.com/bwp91/homebridge-flume/issues/new/choose" target="_blank"
|
|
67
|
+
>Support Request</a
|
|
68
|
+
>
|
|
69
|
+
</li>
|
|
70
|
+
<li>
|
|
71
|
+
<a href="https://github.com/bwp91/homebridge-flume/blob/latest/CHANGELOG.md" target="_blank"
|
|
72
|
+
>Changelog</a
|
|
73
|
+
>
|
|
74
|
+
</li>
|
|
75
|
+
<li><a href="https://github.com/sponsors/bwp91" target="_blank">About Me</a></li>
|
|
76
|
+
</ul>
|
|
77
|
+
<h4>Credits</h4>
|
|
78
|
+
<ul>
|
|
79
|
+
<li>
|
|
80
|
+
To the creators/contributors of
|
|
81
|
+
<a href="https://homebridge.io" target="_blank">Homebridge</a> who make this plugin possible.
|
|
82
|
+
</li>
|
|
83
|
+
</ul>
|
|
84
|
+
<h4>Disclaimer</h4>
|
|
85
|
+
<ul>
|
|
86
|
+
<li>
|
|
87
|
+
I am in no way affiliated with Flume and this plugin is a personal project that I maintain in
|
|
88
|
+
my free time.
|
|
89
|
+
</li>
|
|
90
|
+
<li>Use this plugin entirely at your own risk - please see licence for more information.</li>
|
|
91
|
+
</ul>
|
|
92
|
+
</div>
|
|
93
|
+
<script>
|
|
94
|
+
;(async () => {
|
|
95
|
+
try {
|
|
96
|
+
const currentConfig = await homebridge.getPluginConfig()
|
|
97
|
+
showIntro = () => {
|
|
98
|
+
const introContinue = document.getElementById('introContinue')
|
|
99
|
+
introContinue.addEventListener('click', () => {
|
|
100
|
+
homebridge.showSpinner()
|
|
101
|
+
document.getElementById('pageIntro').style.display = 'none'
|
|
102
|
+
document.getElementById('menuWrapper').style.display = 'inline-flex'
|
|
103
|
+
showSettings()
|
|
104
|
+
homebridge.hideSpinner()
|
|
105
|
+
})
|
|
106
|
+
document.getElementById('pageIntro').style.display = 'block'
|
|
107
|
+
}
|
|
108
|
+
showSupport = () => {
|
|
109
|
+
homebridge.showSpinner()
|
|
110
|
+
homebridge.hideSchemaForm()
|
|
111
|
+
document.getElementById('menuHome').classList.add('btn-elegant')
|
|
112
|
+
document.getElementById('menuHome').classList.remove('btn-primary')
|
|
113
|
+
document.getElementById('menuSettings').classList.remove('btn-elegant')
|
|
114
|
+
document.getElementById('menuSettings').classList.add('btn-primary')
|
|
115
|
+
document.getElementById('pageSupport').style.display = 'block'
|
|
116
|
+
homebridge.hideSpinner()
|
|
117
|
+
}
|
|
118
|
+
showSettings = () => {
|
|
119
|
+
homebridge.showSpinner()
|
|
120
|
+
document.getElementById('menuHome').classList.remove('btn-elegant')
|
|
121
|
+
document.getElementById('menuHome').classList.add('btn-primary')
|
|
122
|
+
document.getElementById('menuSettings').classList.add('btn-elegant')
|
|
123
|
+
document.getElementById('menuSettings').classList.remove('btn-primary')
|
|
124
|
+
document.getElementById('pageSupport').style.display = 'none'
|
|
125
|
+
homebridge.showSchemaForm()
|
|
126
|
+
homebridge.hideSpinner()
|
|
127
|
+
}
|
|
128
|
+
showDisabledBanner = () => {
|
|
129
|
+
document.getElementById('disabledBanner').style.display = 'block'
|
|
130
|
+
}
|
|
131
|
+
enablePlugin = async () => {
|
|
132
|
+
homebridge.showSpinner()
|
|
133
|
+
document.getElementById('disabledBanner').style.display = 'none'
|
|
134
|
+
currentConfig[0].disablePlugin = false
|
|
135
|
+
await homebridge.updatePluginConfig(currentConfig)
|
|
136
|
+
await homebridge.savePluginConfig()
|
|
137
|
+
homebridge.hideSpinner()
|
|
138
|
+
}
|
|
139
|
+
menuHome.addEventListener('click', () => showSupport())
|
|
140
|
+
menuSettings.addEventListener('click', () => showSettings())
|
|
141
|
+
disabledEnable.addEventListener('click', () => enablePlugin())
|
|
142
|
+
if (currentConfig.length) {
|
|
143
|
+
document.getElementById('menuWrapper').style.display = 'inline-flex'
|
|
144
|
+
showSettings()
|
|
145
|
+
if (currentConfig[0].disablePlugin) {
|
|
146
|
+
showDisabledBanner()
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
currentConfig.push({ name: 'Flume' })
|
|
150
|
+
await homebridge.updatePluginConfig(currentConfig)
|
|
151
|
+
showIntro()
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
homebridge.toast.error(err.message, 'Error')
|
|
155
|
+
} finally {
|
|
156
|
+
homebridge.hideSpinner()
|
|
157
|
+
}
|
|
158
|
+
})()
|
|
159
|
+
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* jshint node: true, esversion: 10, -W014, -W033 */
|
|
2
|
+
/* eslint-disable new-cap */
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
const { HomebridgePluginUiServer } = require('@homebridge/plugin-ui-utils')
|
|
6
|
+
|
|
7
|
+
class PluginUiServer extends HomebridgePluginUiServer {
|
|
8
|
+
constructor () {
|
|
9
|
+
super()
|
|
10
|
+
this.ready()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
;(() => new PluginUiServer())()
|
package/lib/index.js
CHANGED
|
@@ -1,219 +1,335 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
/* jshint node: true, esversion: 10, -W014, -W033 */
|
|
2
|
+
/* eslint-disable new-cap */
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
// Packages and constant variables for this class
|
|
6
|
+
const plugin = require('./../package.json')
|
|
7
|
+
|
|
8
|
+
// Create the platform class
|
|
9
|
+
class FlumePlatform {
|
|
10
|
+
constructor (log, config, api) {
|
|
11
|
+
// Don't load the plugin if these aren't accessible for any reason
|
|
12
|
+
if (!log || !api) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Begin plugin initialisation
|
|
17
|
+
try {
|
|
18
|
+
this.api = api
|
|
19
|
+
this.consts = require('./utils/constants')
|
|
20
|
+
this.funcs = require('./utils/functions')
|
|
21
|
+
this.log = log
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.config = config;
|
|
23
|
+
// Configuration objects for accessories
|
|
24
|
+
this.devicesInHB = new Map()
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
// Retrieve the user's chosen language file
|
|
27
|
+
this.lang = require('./utils/lang-en')
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
// Make sure user is running Homebridge v1.3 or above
|
|
30
|
+
if (!api.versionGreaterOrEqual || !api.versionGreaterOrEqual('1.3.0')) {
|
|
31
|
+
throw new Error(this.lang.hbVersionFail)
|
|
32
|
+
}
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
// Check the user has configured the plugin
|
|
35
|
+
if (!config) {
|
|
36
|
+
throw new Error(this.lang.pluginNotConf)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Log some environment info for debugging
|
|
40
|
+
this.log(
|
|
41
|
+
'%s v%s | Node %s | HB v%s%s...',
|
|
42
|
+
this.lang.initialising,
|
|
43
|
+
plugin.version,
|
|
44
|
+
process.version,
|
|
45
|
+
api.serverVersion,
|
|
46
|
+
config.plugin_map
|
|
47
|
+
? ' | HOOBS v3'
|
|
48
|
+
: require('os')
|
|
49
|
+
.hostname()
|
|
50
|
+
.includes('hoobs')
|
|
51
|
+
? ' | HOOBS v4'
|
|
52
|
+
: ''
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Apply the user's configuration
|
|
56
|
+
this.config = this.consts.defaultConfig
|
|
57
|
+
this.applyUserConfig(config)
|
|
58
|
+
|
|
59
|
+
// Set up the Homebridge events
|
|
60
|
+
this.api.on('didFinishLaunching', () => this.pluginSetup())
|
|
61
|
+
this.api.on('shutdown', () => this.pluginShutdown())
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Catch any errors during initialisation
|
|
64
|
+
const eText = this.funcs.parseError(err, [this.lang.hbVersionFail, this.lang.pluginNotConf])
|
|
65
|
+
log.warn('***** %s. *****', this.lang.disabling)
|
|
66
|
+
log.warn('***** %s. *****', eText)
|
|
30
67
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
applyUserConfig (config) {
|
|
71
|
+
// These shorthand functions save line space during config parsing
|
|
72
|
+
const logDefault = (k, def) => {
|
|
73
|
+
this.log.warn('%s [%s] %s %s.', this.lang.cfgItem, k, this.lang.cfgDef, def)
|
|
74
|
+
}
|
|
75
|
+
const logIgnore = k => {
|
|
76
|
+
this.log.warn('%s [%s] %s.', this.lang.cfgItem, k, this.lang.cfgIgn)
|
|
77
|
+
}
|
|
78
|
+
const logIncrease = (k, min) => {
|
|
79
|
+
this.log.warn('%s [%s] %s %s.', this.lang.cfgItem, k, this.lang.cfgLow, min)
|
|
80
|
+
}
|
|
81
|
+
const logQuotes = k => {
|
|
82
|
+
this.log.warn('%s [%s] %s.', this.lang.cfgItem, k, this.lang.cfgQts)
|
|
83
|
+
}
|
|
84
|
+
const logRemove = k => {
|
|
85
|
+
this.log.warn('%s [%s] %s.', this.lang.cfgItem, k, this.lang.cfgRmv)
|
|
35
86
|
}
|
|
36
|
-
this.debug = config["debug"] || true;
|
|
37
87
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
88
|
+
// Begin applying the user's config
|
|
89
|
+
for (const [key, val] of Object.entries(config)) {
|
|
90
|
+
switch (key) {
|
|
91
|
+
case 'client_id':
|
|
92
|
+
case 'client_secret':
|
|
93
|
+
case 'password':
|
|
94
|
+
case 'username':
|
|
95
|
+
if (typeof val !== 'string' || val === '') {
|
|
96
|
+
logIgnore(key)
|
|
97
|
+
} else {
|
|
98
|
+
this.config[key] = val
|
|
99
|
+
}
|
|
100
|
+
break
|
|
101
|
+
case 'debug':
|
|
102
|
+
case 'disableDeviceLogging':
|
|
103
|
+
case 'disablePlugin':
|
|
104
|
+
if (typeof val === 'string') {
|
|
105
|
+
logQuotes(key)
|
|
106
|
+
}
|
|
107
|
+
this.config[key] = val === 'false' ? false : !!val
|
|
108
|
+
break
|
|
109
|
+
case 'handicap': {
|
|
110
|
+
if (typeof v === 'string') {
|
|
111
|
+
logQuotes(key)
|
|
112
|
+
}
|
|
113
|
+
const numVal = Number(val)
|
|
114
|
+
if (isNaN(numVal)) {
|
|
115
|
+
logIgnore(key)
|
|
116
|
+
} else {
|
|
117
|
+
this.config[key] = numVal
|
|
118
|
+
}
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
case 'name':
|
|
122
|
+
case 'platform':
|
|
123
|
+
case 'plugin_map':
|
|
124
|
+
break
|
|
125
|
+
case 'polling_minutes': {
|
|
126
|
+
if (typeof val === 'string') {
|
|
127
|
+
logQuotes(key)
|
|
128
|
+
}
|
|
129
|
+
const intVal = parseInt(val)
|
|
130
|
+
if (isNaN(intVal)) {
|
|
131
|
+
logDefault(key, this.consts.defaultValues[key])
|
|
132
|
+
this.config[key] = this.consts.defaultValues[key]
|
|
133
|
+
} else if (intVal < this.consts.minValues[key]) {
|
|
134
|
+
logIncrease(key, this.consts.minValues[key])
|
|
135
|
+
this.config[key] = this.consts.minValues[key]
|
|
136
|
+
} else {
|
|
137
|
+
this.config[key] = intVal
|
|
138
|
+
}
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
logRemove(key)
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
42
147
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
148
|
+
async pluginSetup () {
|
|
149
|
+
// Plugin has finished initialising so now onto setup
|
|
150
|
+
try {
|
|
151
|
+
// If the user has disabled the plugin then remove all accessories
|
|
152
|
+
if (this.config.disablePlugin) {
|
|
153
|
+
this.devicesInHB.forEach(accessory => this.removeAccessory(accessory))
|
|
154
|
+
throw new Error(this.lang.disabled)
|
|
155
|
+
}
|
|
46
156
|
|
|
47
|
-
|
|
157
|
+
// Log that the plugin initialisation has been successful
|
|
158
|
+
this.log('%s.', this.lang.initialised)
|
|
48
159
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
160
|
+
// Ensure username and password have been provided
|
|
161
|
+
if (
|
|
162
|
+
!this.config.username ||
|
|
163
|
+
!this.config.password ||
|
|
164
|
+
!this.config.client_id ||
|
|
165
|
+
!this.config.client_secret
|
|
166
|
+
) {
|
|
167
|
+
throw new Error(this.lang.noCreds)
|
|
168
|
+
}
|
|
53
169
|
|
|
54
|
-
|
|
55
|
-
|
|
170
|
+
// Setup the HTTP client if Thermobit username and password have been provided
|
|
171
|
+
this.httpClient = new (require('./connection/http'))(this)
|
|
172
|
+
await this.httpClient.obtainToken()
|
|
173
|
+
const deviceList = await this.httpClient.getDevices()
|
|
56
174
|
|
|
57
|
-
|
|
175
|
+
// Check we have devices we can work with
|
|
176
|
+
if (!Array.isArray(deviceList) || deviceList.length === 0) {
|
|
177
|
+
throw new Error(this.lang.noDevices)
|
|
178
|
+
}
|
|
58
179
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
180
|
+
deviceList.forEach(device => {
|
|
181
|
+
if (!device.bridge_id) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
this.initialiseDevice(device)
|
|
185
|
+
})
|
|
62
186
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.setCharacteristic(Characteristic.Model, "001")
|
|
66
|
-
.setCharacteristic(Characteristic.SerialNumber, this.device_id)
|
|
67
|
-
.setCharacteristic(Characteristic.Name, accessory.context.device.name)
|
|
68
|
-
.setCharacteristic(Characteristic.Active, false)
|
|
69
|
-
.setCharacteristic(Characteristic.InUse, false)
|
|
70
|
-
.setCharacteristic(Characteristic.ValveType, "3")
|
|
71
|
-
.setCharacteristic(Characteristic.StatusFault, false);
|
|
187
|
+
// Set up an initial last sync time
|
|
188
|
+
this.lastSync = new Date(Date.now() - this.config.polling_minutes * 60000)
|
|
72
189
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
190
|
+
// Perform a first sync and setup the refresh interval
|
|
191
|
+
this.flumeSync()
|
|
192
|
+
this.refreshInterval = setInterval(
|
|
193
|
+
() => this.flumeSync(),
|
|
194
|
+
this.config.polling_minutes * 60000
|
|
195
|
+
)
|
|
76
196
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.then(res => {
|
|
91
|
-
if (res.ok) {
|
|
92
|
-
return res.json();
|
|
93
|
-
log("res was okay.");
|
|
94
|
-
} else {
|
|
95
|
-
log("ERROR! Unable to retrieve new token: " + res.statusText);
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
.then(json => {
|
|
99
|
-
this.access_token = json.access_token;
|
|
100
|
-
this.refresh_token = json.refresh_token;
|
|
101
|
-
this.expires_in = now + json.expires_in;
|
|
102
|
-
this.log("Token received: " + this.access_token);
|
|
103
|
-
this.log("Refresh token received: " + this.refresh_token);
|
|
104
|
-
this.log("Token expires at: " + new Date(this.expires_in));
|
|
105
|
-
return this.access_token;
|
|
106
|
-
});
|
|
107
|
-
} else if (now > this.expires_in) {
|
|
108
|
-
return fetch('https://api.flumetech.com/oauth/token', {
|
|
109
|
-
method: 'POST',
|
|
110
|
-
headers: {
|
|
111
|
-
'content-type': 'application/json'
|
|
112
|
-
},
|
|
113
|
-
body: '{"grant_type":"refresh_token","client_id":' + this.client_id + ',"client_secret":' + this.client_secret + ',"refresh_token":' + this.refresh_token + '}'
|
|
114
|
-
})
|
|
115
|
-
.then(res => {
|
|
116
|
-
if (res.ok) {
|
|
117
|
-
return res.json();
|
|
118
|
-
} else {
|
|
119
|
-
log("ERROR! Unable to retrieve new token: " + res.statusText);
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
.then(json => {
|
|
123
|
-
this.access_token = json.access_token;
|
|
124
|
-
this.expires_in = now + json.expires_in;
|
|
125
|
-
this.log("Token received: " + this.access_token);
|
|
126
|
-
this.log("Refresh token received: " + this.refresh_token);
|
|
127
|
-
this.log("Token expires at: " + new Date(this.expires_in));
|
|
128
|
-
return this.access_token;
|
|
129
|
-
});
|
|
197
|
+
// Log that the plugin setup has been successful with a welcome message
|
|
198
|
+
const randIndex = Math.floor(Math.random() * this.lang.zWelcome.length)
|
|
199
|
+
this.log('%s. %s', this.lang.complete, this.lang.zWelcome[randIndex])
|
|
200
|
+
} catch (err) {
|
|
201
|
+
// Catch any errors during setup
|
|
202
|
+
const eText = this.funcs.parseError(err, [
|
|
203
|
+
this.lang.noCreds,
|
|
204
|
+
this.lang.noDevices,
|
|
205
|
+
this.lang.disabled
|
|
206
|
+
])
|
|
207
|
+
this.log.warn('***** %s. *****', this.lang.disabling)
|
|
208
|
+
this.log.warn('***** %s. *****', eText)
|
|
209
|
+
this.pluginShutdown()
|
|
130
210
|
}
|
|
211
|
+
}
|
|
131
212
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return fetch('https://api.flumetech.com/users/' + this.user_id + '/devices', {
|
|
144
|
-
method: 'GET',
|
|
145
|
-
headers: {
|
|
146
|
-
'authorization': '' + this.access_token + ''
|
|
147
|
-
},
|
|
148
|
-
})
|
|
149
|
-
.then(res => {
|
|
150
|
-
if (res.ok) {
|
|
151
|
-
return res.json();
|
|
152
|
-
} else {
|
|
153
|
-
log("ERROR! Unable to retrieve devices: " + res.statusText);
|
|
154
|
-
}
|
|
155
|
-
})
|
|
156
|
-
.then(json => {
|
|
157
|
-
this.device_id = json.id;
|
|
158
|
-
this.log("User ID: " + this.user_id);
|
|
159
|
-
this.log("Device ID: " + this.device_id);
|
|
160
|
-
return this.device_id;
|
|
161
|
-
});
|
|
213
|
+
pluginShutdown () {
|
|
214
|
+
// A function that is called when the plugin fails to load or Homebridge restarts
|
|
215
|
+
try {
|
|
216
|
+
// Stop the refresh interval
|
|
217
|
+
if (this.refreshInterval) {
|
|
218
|
+
clearInterval(this.refreshInterval)
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// No need to show errors at this point
|
|
222
|
+
}
|
|
162
223
|
}
|
|
163
224
|
|
|
225
|
+
async flumeSync () {
|
|
226
|
+
try {
|
|
227
|
+
const fromWhen = this.lastSync
|
|
228
|
+
.toISOString()
|
|
229
|
+
.substring(0, 19)
|
|
230
|
+
.replace('T', ' ')
|
|
231
|
+
this.devicesInHB.forEach(async accessory => {
|
|
232
|
+
try {
|
|
233
|
+
const res = await this.httpClient.getDeviceInfo(accessory.context.deviceId, fromWhen)
|
|
234
|
+
res.fromWhen = fromWhen
|
|
235
|
+
accessory.control.externalUpdate(res)
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const eText = this.funcs.parseError(err)
|
|
238
|
+
this.log.warn(eText)
|
|
239
|
+
}
|
|
240
|
+
})
|
|
164
241
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.log("Token: " + this.access_token);
|
|
172
|
-
this.log("User ID: " + this.user_id);
|
|
173
|
-
this.log("Device ID: " + this.device_id);
|
|
174
|
-
|
|
175
|
-
this.getAccessToken()
|
|
176
|
-
.then(token => fetch('https://api.flumetech.com/users/' + this.user_id + '/devices/' + this.device_id + '/query', {
|
|
177
|
-
method: 'POST',
|
|
178
|
-
headers: {
|
|
179
|
-
'content-type': 'application/json',
|
|
180
|
-
'authorization': 'Bearer ' + this.access_token
|
|
181
|
-
},
|
|
182
|
-
body: '{"queries":[{"request_id":"currentusage","bucket":"MIN","since_datetime": currentminute,"operation":"SUM","units":"GALLONS"}]}'
|
|
183
|
-
// alternative approach might be payload = '{"queries":[{"request_id":"perminute","bucket":"MIN","since_datetime":"' + previousminute() + '","until_datetime":"' + currentminute() + '","group_multiplier":"1","operation":"SUM","sort_direction":"ASC","units":"GALLONS"}]}'
|
|
184
|
-
// as seen at https://github.com/ScriptBlock/flumecli/blob/master/flumecli.py
|
|
185
|
-
})
|
|
186
|
-
.then(res => {
|
|
187
|
-
if (res.ok) {
|
|
188
|
-
return res.json();
|
|
189
|
-
this.device_usage = value;
|
|
190
|
-
} else {
|
|
191
|
-
log("ERROR! Unable to retrieve devices: " + res.statusText);
|
|
192
|
-
}
|
|
193
|
-
}));
|
|
194
|
-
|
|
195
|
-
log("Updating the status");
|
|
196
|
-
this.updateState(accessory);
|
|
197
|
-
log("Updated the status");
|
|
242
|
+
this.lastSync = new Date()
|
|
243
|
+
} catch (err) {
|
|
244
|
+
// Catch any errors performing the sync
|
|
245
|
+
const eText = this.funcs.parseError(err, [])
|
|
246
|
+
this.log.warn('%s %s.', this.lang.syncFailed, eText)
|
|
198
247
|
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
initialiseDevice (device) {
|
|
251
|
+
try {
|
|
252
|
+
/*
|
|
253
|
+
{
|
|
254
|
+
id: '',
|
|
255
|
+
type: 2,
|
|
256
|
+
location_id: 0000,
|
|
257
|
+
user_id: 0000,
|
|
258
|
+
bridge_id: '',
|
|
259
|
+
oriented: true,
|
|
260
|
+
last_seen: '',
|
|
261
|
+
connected: true,
|
|
262
|
+
battery_level: 'high',
|
|
263
|
+
product: 'flume1'
|
|
264
|
+
}
|
|
265
|
+
*/
|
|
266
|
+
|
|
267
|
+
const uuid = this.api.hap.uuid.generate(device.id)
|
|
268
|
+
|
|
269
|
+
// Get the cached accessory or add to Homebridge if doesn't exist
|
|
270
|
+
const accessory = this.devicesInHB.get(uuid) || this.addAccessory(device)
|
|
271
|
+
|
|
272
|
+
// Final check the accessory now exists in Homebridge
|
|
273
|
+
if (!accessory) {
|
|
274
|
+
throw new Error(this.lang.accNotFound)
|
|
275
|
+
}
|
|
199
276
|
|
|
277
|
+
// Create the instance for this device type
|
|
278
|
+
accessory.control = new (require('./device/valve'))(this, accessory)
|
|
200
279
|
|
|
201
|
-
|
|
202
|
-
|
|
280
|
+
// Log the device initialisation
|
|
281
|
+
this.log('[%s] %s [%s].', accessory.displayName, this.lang.devInit, device.id)
|
|
282
|
+
} catch (err) {
|
|
283
|
+
// Catch any errors during device initialisation
|
|
284
|
+
const eText = this.funcs.parseError(err, [this.lang.accNotFound])
|
|
285
|
+
this.log.warn('[%s] %s %s.', device.id, this.lang.devNotInit, eText)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
addAccessory (device) {
|
|
290
|
+
// Add an accessory to Homebridge
|
|
291
|
+
try {
|
|
292
|
+
const uuid = this.api.hap.uuid.generate(device.id)
|
|
293
|
+
const accessory = new this.api.platformAccessory(this.lang.brand, uuid)
|
|
294
|
+
accessory
|
|
295
|
+
.getService(this.api.hap.Service.AccessoryInformation)
|
|
296
|
+
.setCharacteristic(this.api.hap.Characteristic.Name, this.lang.brand)
|
|
297
|
+
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, this.lang.brand)
|
|
298
|
+
.setCharacteristic(this.api.hap.Characteristic.Manufacturer, this.lang.brand)
|
|
299
|
+
.setCharacteristic(this.api.hap.Characteristic.SerialNumber, device.id)
|
|
300
|
+
.setCharacteristic(this.api.hap.Characteristic.Model, device.product)
|
|
301
|
+
.setCharacteristic(this.api.hap.Characteristic.Identify, true)
|
|
302
|
+
accessory.context.deviceId = device.id
|
|
303
|
+
this.api.registerPlatformAccessories(plugin.name, plugin.alias, [accessory])
|
|
304
|
+
this.devicesInHB.set(accessory.UUID, accessory)
|
|
305
|
+
this.log('[%s] %s.', accessory.displayName, this.lang.devAdd)
|
|
306
|
+
return accessory
|
|
307
|
+
} catch (err) {
|
|
308
|
+
// Catch any errors during add
|
|
309
|
+
const eText = this.funcs.parseError(err)
|
|
310
|
+
this.log.warn('[%s] %s %s.', this.lang.brand, this.lang.devNotAdd, eText)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
203
313
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
314
|
+
configureAccessory (accessory) {
|
|
315
|
+
// Add the configured accessory to our global map
|
|
316
|
+
this.devicesInHB.set(accessory.UUID, accessory)
|
|
317
|
+
}
|
|
207
318
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
319
|
+
removeAccessory (accessory) {
|
|
320
|
+
// Remove an accessory from Homebridge
|
|
321
|
+
try {
|
|
322
|
+
this.api.unregisterPlatformAccessories(plugin.name, plugin.alias, [accessory])
|
|
323
|
+
this.devicesInHB.delete(accessory.UUID)
|
|
324
|
+
this.log('[%s] %s.', accessory.displayName, this.lang.devRemove)
|
|
325
|
+
} catch (err) {
|
|
326
|
+
// Catch any errors during remove
|
|
327
|
+
const eText = this.funcs.parseError(err)
|
|
328
|
+
const name = accessory.displayName
|
|
329
|
+
this.log.warn('[%s] %s %s.', name, this.lang.devNotRemove, eText)
|
|
330
|
+
}
|
|
218
331
|
}
|
|
219
332
|
}
|
|
333
|
+
|
|
334
|
+
// Export the plugin to Homebridge
|
|
335
|
+
module.exports = hb => hb.registerPlatform(plugin.alias, FlumePlatform)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* jshint node: true, esversion: 10, -W014, -W033 */
|
|
2
|
+
/* eslint-disable new-cap */
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
defaultConfig: {
|
|
7
|
+
name: 'Flume',
|
|
8
|
+
username: '',
|
|
9
|
+
password: '',
|
|
10
|
+
client_id: '',
|
|
11
|
+
client_secret: '',
|
|
12
|
+
polling_minutes: 1,
|
|
13
|
+
handicap: 0,
|
|
14
|
+
disableDeviceLogging: false,
|
|
15
|
+
debug: false,
|
|
16
|
+
disablePlugin: false,
|
|
17
|
+
platform: 'Flume'
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
defaultValues: {
|
|
21
|
+
polling_minutes: 1,
|
|
22
|
+
handicap: 0
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
minValues: {
|
|
26
|
+
polling_minutes: 1,
|
|
27
|
+
handicap: 0
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
httpRetryCodes: ['ENOTFOUND', 'ETIMEDOUT', 'EAI_AGAIN', 'ECONNABORTED']
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* jshint node: true, esversion: 10, -W014, -W033 */
|
|
2
|
+
/* eslint-disable new-cap */
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
hasProperty: (obj, prop) => {
|
|
7
|
+
return Object.prototype.hasOwnProperty.call(obj, prop)
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
sleep: ms => {
|
|
11
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
parseError: (err, hideStack = []) => {
|
|
15
|
+
let toReturn = err.message
|
|
16
|
+
if (err.stack && err.stack.length > 0 && !hideStack.includes(err.message)) {
|
|
17
|
+
const stack = err.stack.split('\n')
|
|
18
|
+
if (stack[1]) {
|
|
19
|
+
toReturn += stack[1].replace(' ', '')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return toReturn
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
generateRandomString: length => {
|
|
26
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
27
|
+
let nonce = ''
|
|
28
|
+
while (nonce.length < length) {
|
|
29
|
+
nonce += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
30
|
+
}
|
|
31
|
+
return nonce
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* jshint node: true, esversion: 10, -W014, -W033 */
|
|
2
|
+
/* eslint-disable new-cap */
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
accNotFound: 'accessory not found',
|
|
7
|
+
accNotReady: 'cannot currently be controlled, see plugin startup logs for any error',
|
|
8
|
+
brand: 'Flume',
|
|
9
|
+
cfgDef: 'is not a valid number so using default of',
|
|
10
|
+
cfgDup: 'will be ignored since another entry with this ID already exists',
|
|
11
|
+
cfgIgn: 'is not configured correctly so ignoring',
|
|
12
|
+
cfgIgnItem: 'has an invalid entry which will be ignored',
|
|
13
|
+
cfgItem: 'Config entry',
|
|
14
|
+
cfgLow: 'is set too low so increasing to',
|
|
15
|
+
cfgRmv: 'is unused and can be removed',
|
|
16
|
+
cfgQts: 'should not have quotes around its entry',
|
|
17
|
+
complete: '✓ Setup complete',
|
|
18
|
+
curHeat: 'current heating',
|
|
19
|
+
curState: 'current state',
|
|
20
|
+
curTarg: 'current target',
|
|
21
|
+
curTemp: 'current temperature',
|
|
22
|
+
devAdd: 'has been added to Homebridge',
|
|
23
|
+
devInit: 'initialised with id',
|
|
24
|
+
devNotAdd: 'could not be added to Homebridge as',
|
|
25
|
+
devNotConf: 'could not be configured as',
|
|
26
|
+
devNotInit: 'could not be initialised as',
|
|
27
|
+
devNotRef: 'could not be refreshed as',
|
|
28
|
+
devNotRemove: 'could not be removed from Homebridge as',
|
|
29
|
+
devNotUpdated: 'could not be updated as',
|
|
30
|
+
devRemove: 'has been removed from Homebridge',
|
|
31
|
+
disabled: 'To change this, set disablePlugin to false',
|
|
32
|
+
disableHTTP: 'Disabling HTTP client as',
|
|
33
|
+
disabling: 'Disabling plugin',
|
|
34
|
+
hbVersionFail: 'Your version of Homebridge is too low - please update to v1.3',
|
|
35
|
+
httpRetry: 'Unable to reach Thermobit, retrying in 30 seconds',
|
|
36
|
+
initialised: 'Plugin initialised. Setting up accessories...',
|
|
37
|
+
initialising: 'Initialising plugin',
|
|
38
|
+
labelHeating: 'heating',
|
|
39
|
+
labelIdle: 'idle',
|
|
40
|
+
labelMan: 'manual',
|
|
41
|
+
labelSch: 'schedule',
|
|
42
|
+
noCreds: 'Thermobit username and/or password not configured',
|
|
43
|
+
noDevices: 'no data received from Thermobit server whilst obtaining device list',
|
|
44
|
+
noTokenExists: 'no Thermobit account token exists',
|
|
45
|
+
pluginNotConf: 'Plugin has not been configured',
|
|
46
|
+
receivingUpdate: 'receiving update',
|
|
47
|
+
scheduleFailed: 'Program schedule could not be updated as',
|
|
48
|
+
scheduleInvalid: 'Not updating program schedule as configuration is invalid or incomplete',
|
|
49
|
+
scheduleUpdated: 'Program schedule successfully updated',
|
|
50
|
+
syncFailed: 'Sync process failed as',
|
|
51
|
+
updateFail: 'could not be updated as',
|
|
52
|
+
zWelcome: [
|
|
53
|
+
"Don't forget to ☆ this plugin on GitHub if you're finding it useful!",
|
|
54
|
+
'Have a feature request? Visit https://bit.ly/hb-thermobit-issues to ask!',
|
|
55
|
+
'Interested in sponsoring this plugin? https://github.com/sponsors/bwp91',
|
|
56
|
+
"Join the plugin's Discord community! https://discord.gg/cMGhNtZ3tW",
|
|
57
|
+
'Thanks for using this plugin, I hope you find it helpful!',
|
|
58
|
+
'This plugin has been made with ♥ by bwp91 from the UK!',
|
|
59
|
+
'Check out my other Homebridge plugins! https://github.com/bwp91',
|
|
60
|
+
'Have time to give this plugin a review? https://bit.ly/hb-thermobit-review',
|
|
61
|
+
"This plugin needs it's first ☆ rating on HOOBS! https://bit.ly/hb-thermobit-review",
|
|
62
|
+
'Want to see this plugin in your own language? Let me know!'
|
|
63
|
+
]
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-flume",
|
|
3
3
|
"alias": "Flume",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"author": {
|
|
6
|
-
"name": "
|
|
6
|
+
"name": "Ben Potter",
|
|
7
7
|
"email": "bwp91@icloud.com"
|
|
8
8
|
},
|
|
9
9
|
"description": "Homebridge plugin to integrate Flume devices into HomeKit.",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"water leak detector"
|
|
24
24
|
],
|
|
25
25
|
"engines": {
|
|
26
|
-
"homebridge": "^1.3.
|
|
26
|
+
"homebridge": "^1.3.8",
|
|
27
27
|
"node": "^14.18.1 || ^16.13.0"
|
|
28
28
|
},
|
|
29
29
|
"repository": {
|
|
@@ -34,16 +34,11 @@
|
|
|
34
34
|
"url": "https://github.com/bwp91/homebridge-flume/issues"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"homebridge-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
37
|
+
"@homebridge/plugin-ui-utils": "^0.0.19",
|
|
38
|
+
"axios": "^0.24.0",
|
|
39
|
+
"jwt-decode": "^3.1.2"
|
|
40
40
|
},
|
|
41
41
|
"prettier": {
|
|
42
42
|
"printWidth": 100
|
|
43
|
-
},
|
|
44
|
-
"standard": {
|
|
45
|
-
"ignore": [
|
|
46
|
-
"/lib/index.js"
|
|
47
|
-
]
|
|
48
43
|
}
|
|
49
44
|
}
|