homebridge-flume 0.1.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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Darrin Thomas & Ben Potter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # homebridge-flume
2
+
3
+ # Configuration
4
+
5
+ Configuration sample:
6
+
7
+ ```
8
+ "accessories": [
9
+ {
10
+ "accessory": "Flume",
11
+ "name": "Flume Water Monitor",
12
+ "username": "user@domain.com",
13
+ "password": "password",
14
+ "client_id": "12345678901234567890",
15
+ "client_secret": "1234567890",
16
+ "polling_minutes": 1,
17
+ "handicap": 0.1,
18
+ "valveType": 3
19
+ }
20
+ ]
21
+ ```
22
+
23
+ Fields:
24
+
25
+ - "accessory": Must always be "Flume" (required)
26
+ - "username": Your username for accessing the Flume site at https://portal.flumetech.com/ (required)
27
+ - "password": Your password for accessing the Flume site (required)
28
+ - "client_id": Your Client ID for API access, found in Settings on the Flume site (required)
29
+ - "client_secret": Your Client Secret for API access, found in Settings on the Flume site (required)
30
+ - "polling_minutes": Number of minutes between updates. Defaults to 1 minute. API has a cap of 120 calls per hour. (optional)
31
+ - "handicap": Set to ignore a steady water draw below this value. Defaults to zero (0.0) gallons. (optional)
32
+
33
+ # Retrieving API Login Credentials
34
+
35
+ You'll need to get your API Access Client ID and Client Secret from Flume's site at https://portal.flumetech.com/
36
+ This guide offers the steps to take: https://flumetech.readme.io/reference#accessing-the-api
@@ -0,0 +1,58 @@
1
+ {
2
+ "pluginAlias": "Flume",
3
+ "pluginType": "accessory",
4
+ "singular": true,
5
+ "headerDisplay": "[Flume Water Monitor](https://flumetech.com) plugin for Homebridge.",
6
+ "schema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "name": {
10
+ "title": "Accessory Name",
11
+ "type": "string",
12
+ "required": true,
13
+ "default": "Flume Water Monitor",
14
+ "description": "The name used for the Flume water monitor in HomeKit"
15
+ },
16
+ "username": {
17
+ "title": "Username",
18
+ "type": "string",
19
+ "required": true,
20
+ "description": "Your username for acessing the Flume site at https://portal.flumetech.com/ (required)"
21
+ },
22
+ "password": {
23
+ "title": "Password",
24
+ "type": "string",
25
+ "required": true,
26
+ "description": "Your password for acessing the Flume site (required)"
27
+ },
28
+ "client_id": {
29
+ "title": "Client ID",
30
+ "type": "string",
31
+ "placeholder": "12345678901234567890",
32
+ "required": true,
33
+ "description": "Your Client ID for API access, found in Settings on the Flume site (required)"
34
+ },
35
+ "client_secret": {
36
+ "title": "Client Secret",
37
+ "type": "string",
38
+ "placeholder": "1234567890",
39
+ "required": true,
40
+ "description": "Your Client Secret for API access, found in Settings on the Flume site (required)"
41
+ },
42
+ "polling_minutes": {
43
+ "title": "Update Interval",
44
+ "type": "integer",
45
+ "default": "1",
46
+ "required": false,
47
+ "description": "Number of minutes between updates. Defaults to 1 minute. API has a cap of 120 calls per hour. (optional)"
48
+ },
49
+ "handicap": {
50
+ "title": "Adjustment for False Positives",
51
+ "type": "float",
52
+ "default": "0.0",
53
+ "required": false,
54
+ "description": "Set to ignore a steady water draw below this value. Defaults to zero (0.0) gallons. (optional)"
55
+ }
56
+ }
57
+ }
58
+ }
package/lib/index.js ADDED
@@ -0,0 +1,219 @@
1
+ const fetch = require('node-fetch');
2
+ const packageJson = require('./package.json');
3
+ var jwtDecode = require('jwt-decode');
4
+ var Service, Characteristic;
5
+
6
+ module.exports = function (homebridge) {
7
+ Service = homebridge.hap.Service;
8
+ Characteristic = homebridge.hap.Characteristic;
9
+ homebridge.registerAccessory('homebridge-flume', 'Flume', Flume);
10
+ }
11
+
12
+ function Flume (log, config, api) {
13
+ this.log = log;
14
+ this.config = config;
15
+
16
+ log("Starting homebridge-flume.");
17
+
18
+ // extract name from config
19
+ this.name = config.name;
20
+
21
+ // pull in initial variables
22
+ this.username = config["username"];
23
+ this.password = config["password"];
24
+ this.client_id = config["client_id"];
25
+ this.client_secret = config["client_secret"];
26
+ if (config["handicap"] != null) {
27
+ this.handicap = config["handicap"];
28
+ } else {
29
+ this.handicap = 0;
30
+ }
31
+ if (config["polling_minutes"] != null) {
32
+ this.interval = parseInt(config["polling_minutes"]) * 60 * 1000;
33
+ } else {
34
+ this.interval = 1 * 60 * 1000;
35
+ }
36
+ this.debug = config["debug"] || true;
37
+
38
+ if (!this.username) throw new Error("You must provide a value for username.");
39
+ if (!this.password) throw new Error("You must provide a value for password.");
40
+ if (!this.client_id) throw new Error("You must provide a value for client_id.");
41
+ if (!this.client_secret) throw new Error("You must provide a value for client_secret.");
42
+
43
+ this.expires_in = Date.now();
44
+ this.device_id = "";
45
+ this.device_usage = 0.00;
46
+
47
+ log("User variables loaded.");
48
+
49
+ if (api) {
50
+ this.api = api;
51
+ this.api.on('didFinishLaunching', this.fetchDevices.bind(this));
52
+ this.timer = setInterval(this.fetchDevices.bind(this), this.interval);
53
+
54
+ log("Update interval set.");
55
+ }
56
+
57
+ log("Initial load of the accessory started.");
58
+
59
+ // get the Valve service if it exists, otherwise create a new Valve service
60
+ this.informationService = this.accessory.getService(Service.Valve) ||
61
+ this.accessory.addService(Service.Valve);
62
+
63
+ this.informationService
64
+ .setCharacteristic(Characteristic.Manufacturer, "Flume Water Monitor")
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);
72
+
73
+ log("Starting position is water is Off");
74
+ log("Load of the accessory complete.");
75
+ }
76
+
77
+ Flume.prototype.getAccessToken = function() {
78
+ var now = Date.now();
79
+
80
+ log("Getting the access token.");
81
+
82
+ if (!this.access_token) {
83
+ return fetch('https://api.flumetech.com/oauth/token', {
84
+ method: 'POST',
85
+ headers: {
86
+ 'content-type': 'application/json'
87
+ },
88
+ body: '{"grant_type":"password","client_id":' + this.client_id + ',"client_secret":' + this.client_secret + ',"username":' + this.username + ',"password":' + this.password + '}'
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
+ });
130
+ }
131
+
132
+ this.log("Getting User ID from JWT.")
133
+
134
+ var token = this.access_token;
135
+ var decoded = jwt_decode(token);
136
+ // or perhaps: decoded = jwt.decode(config["access_token"], verify=False)
137
+
138
+ this.log(decoded);
139
+ this.user_id = decoded.user_id; // or perhaps: decoded["user_id"]
140
+ this.log("User ID: " + this.user_id);
141
+ this.log("Device ID: " + this.device_id);
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
+ });
162
+ }
163
+
164
+
165
+ Flume.prototype.fetchDevices = function() {
166
+ this.log("Fetching current devices and statuses.");
167
+
168
+ // alternative approach: var previousminute = (datetime.datetime.now() - datetime.timedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S');
169
+ var currentminute = (datetime.datetime.now() - datetime.timedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S');
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");
198
+ }
199
+
200
+
201
+ Flume.prototype.updateState = function(accessory) {
202
+ var fresh = Date.now() - Date.parse(accessory.context.time + ".000Z") < 60 * 60 * 1000;
203
+
204
+ log("Device Usage & Handicap: ");
205
+ log(this.device_usage);
206
+ log(this.handicap);
207
+
208
+ if (this.device_usage > this.handicap) {
209
+ log("Water is On");
210
+ accessory.getService(Service.Valve)
211
+ .setCharacteristic(Characteristic.Active, true)
212
+ .setCharacteristic(Characteristic.InUse, true);
213
+ } else {
214
+ log("Water is Off");
215
+ accessory.getService(Service.Valve)
216
+ .setCharacteristic(Characteristic.Active, false)
217
+ .setCharacteristic(Characteristic.InUse, false);
218
+ }
219
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "homebridge-flume",
3
+ "alias": "Flume",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Darrin Thomas",
7
+ "email": "bwp91@icloud.com"
8
+ },
9
+ "description": "Homebridge plugin to integrate Flume devices into HomeKit.",
10
+ "main": "lib/index.js",
11
+ "keywords": [
12
+ "homebridge",
13
+ "homebridge-plugin",
14
+ "homebridge-flume",
15
+ "hoobs",
16
+ "hoobs-plugin",
17
+ "homekit",
18
+ "siri",
19
+ "flume",
20
+ "water sensor",
21
+ "water monitor",
22
+ "leak detector",
23
+ "water leak detector"
24
+ ],
25
+ "engines": {
26
+ "homebridge": "^1.3.6",
27
+ "node": "^14.18.1 || ^16.13.0"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/bwp91/homebridge-flume.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/bwp91/homebridge-flume/issues"
35
+ },
36
+ "dependencies": {
37
+ "homebridge-http-base": "^2.1.12",
38
+ "jwt-decode": "^3.1.2",
39
+ "node-fetch": "^2.6.0"
40
+ },
41
+ "prettier": {
42
+ "printWidth": 100
43
+ },
44
+ "standard": {
45
+ "ignore": [
46
+ "/lib/index.js"
47
+ ]
48
+ }
49
+ }