@homebridge-plugins/homebridge-firstalert 0.0.1-beta.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 +2 -0
- package/.github/FUNDING.yml +4 -0
- package/.github/ISSUE_TEMPLATE/bug-report.yml +97 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature-request.yml +38 -0
- package/.github/ISSUE_TEMPLATE/support-request.yml +85 -0
- package/.github/ISSUE_TEMPLATE.md +52 -0
- package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +27 -0
- package/.github/dependabot.yml +17 -0
- package/.github/labeler.yml +38 -0
- package/.github/release-drafter.yml +33 -0
- package/.github/workflows/beta-release.yml +55 -0
- package/.github/workflows/build.yml +18 -0
- package/.github/workflows/changerelease.yml +11 -0
- package/.github/workflows/labeler.yml +9 -0
- package/.github/workflows/release-drafter.yml +14 -0
- package/.github/workflows/release.yml +35 -0
- package/.github/workflows/stale.yml +12 -0
- package/CHANGELOG.md +10 -0
- package/LICENSE +14 -0
- package/README.md +67 -0
- package/SECURITY.md +19 -0
- package/branding/Homebridge_x_FirstAlert.svg +48 -0
- package/branding/icon.png +0 -0
- package/config.schema.json +58 -0
- package/dist/homebridge-ui/public/index.html +548 -0
- package/dist/src/api/resideoClient.d.ts +32 -0
- package/dist/src/api/resideoClient.d.ts.map +1 -0
- package/dist/src/api/resideoClient.js +76 -0
- package/dist/src/api/resideoClient.js.map +1 -0
- package/dist/src/devices/device.d.ts +40 -0
- package/dist/src/devices/device.d.ts.map +1 -0
- package/dist/src/devices/device.js +207 -0
- package/dist/src/devices/device.js.map +1 -0
- package/dist/src/devices/leaksensors.d.ts +34 -0
- package/dist/src/devices/leaksensors.d.ts.map +1 -0
- package/dist/src/devices/leaksensors.js +163 -0
- package/dist/src/devices/leaksensors.js.map +1 -0
- package/dist/src/devices/smoke.d.ts +35 -0
- package/dist/src/devices/smoke.d.ts.map +1 -0
- package/dist/src/devices/smoke.js +150 -0
- package/dist/src/devices/smoke.js.map +1 -0
- package/dist/src/devices/thermostats.d.ts +42 -0
- package/dist/src/devices/thermostats.d.ts.map +1 -0
- package/dist/src/devices/thermostats.js +192 -0
- package/dist/src/devices/thermostats.js.map +1 -0
- package/dist/src/devices/valve.d.ts +21 -0
- package/dist/src/devices/valve.d.ts.map +1 -0
- package/dist/src/devices/valve.js +131 -0
- package/dist/src/devices/valve.js.map +1 -0
- package/dist/src/homebridge-ui/server.d.ts +5 -0
- package/dist/src/homebridge-ui/server.d.ts.map +1 -0
- package/dist/src/homebridge-ui/server.js +95 -0
- package/dist/src/homebridge-ui/server.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/platform.d.ts +18 -0
- package/dist/src/platform.d.ts.map +1 -0
- package/dist/src/platform.js +108 -0
- package/dist/src/platform.js.map +1 -0
- package/dist/src/settings.d.ts +341 -0
- package/dist/src/settings.d.ts.map +1 -0
- package/dist/src/settings.js +25 -0
- package/dist/src/settings.js.map +1 -0
- package/dist/src/utils.d.ts +21 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +58 -0
- package/dist/src/utils.js.map +1 -0
- package/dist/test/index.test.d.ts +2 -0
- package/dist/test/index.test.d.ts.map +1 -0
- package/dist/test/index.test.js +14 -0
- package/dist/test/index.test.js.map +1 -0
- package/dist/test/platform.test.d.ts +2 -0
- package/dist/test/platform.test.d.ts.map +1 -0
- package/dist/test/platform.test.js +56 -0
- package/dist/test/platform.test.js.map +1 -0
- package/dist/test/settings.test.d.ts +2 -0
- package/dist/test/settings.test.d.ts.map +1 -0
- package/dist/test/settings.test.js +48 -0
- package/dist/test/settings.test.js.map +1 -0
- package/dist/test/utils.test.d.ts +2 -0
- package/dist/test/utils.test.d.ts.map +1 -0
- package/dist/test/utils.test.js +17 -0
- package/dist/test/utils.test.js.map +1 -0
- package/docs/.nojekyll +1 -0
- package/docs/assets/hierarchy.js +1 -0
- package/docs/assets/highlight.css +22 -0
- package/docs/assets/icons.js +18 -0
- package/docs/assets/icons.svg +1 -0
- package/docs/assets/main.js +60 -0
- package/docs/assets/navigation.js +1 -0
- package/docs/assets/search.js +1 -0
- package/docs/assets/style.css +1633 -0
- package/docs/hierarchy.html +1 -0
- package/docs/index.html +77 -0
- package/docs/modules.html +1 -0
- package/docs/variables/default.html +1 -0
- package/eslint.config.js +44 -0
- package/nodemon.json +10 -0
- package/package.json +106 -0
- package/scripts/free-dev-ports.mjs +105 -0
- package/src/api/resideoClient.ts +106 -0
- package/src/devices/device.ts +226 -0
- package/src/devices/leaksensors.ts +206 -0
- package/src/devices/smoke.ts +173 -0
- package/src/devices/thermostats.ts +243 -0
- package/src/devices/valve.ts +162 -0
- package/src/homebridge-ui/public/index.html +548 -0
- package/src/homebridge-ui/server.ts +102 -0
- package/src/index.ts +13 -0
- package/src/platform.ts +112 -0
- package/src/settings.ts +402 -0
- package/src/utils.ts +61 -0
- package/test/index.test.ts +18 -0
- package/test/platform.test.ts +65 -0
- package/test/settings.test.ts +56 -0
- package/test/utils.test.ts +20 -0
- package/tsconfig.json +27 -0
- package/typedoc.json +22 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
<p class="text-center">
|
|
2
|
+
<img
|
|
3
|
+
src="https://raw.githubusercontent.com/homebridge-plugins/homebridge-firstalert/latest/branding/Homebridge_x_Resideo.svg"
|
|
4
|
+
alt="homebridge-firstalert logo" style="width: 40%;" />
|
|
5
|
+
</p>
|
|
6
|
+
<div id="pageIntro" style="display: none;">
|
|
7
|
+
<p class="lead text-center">Thank you for installing <strong>homebridge-firstalert</strong></p>
|
|
8
|
+
<p class="lead text-center">Before continuing:</p>
|
|
9
|
+
<ol>
|
|
10
|
+
<li class="mb-3">Login / create an account at <a href="https://developer.honeywellhome.com/user" target="_blank"
|
|
11
|
+
rel="noreferrer noopener">https://developer.honeywellhome.com/user</a>.</li>
|
|
12
|
+
<li class="mb-3">Click <strong>Create New App</strong>.</li>
|
|
13
|
+
<li class="mb-3">
|
|
14
|
+
Give your application a name, and enter the 'Callback URL' exactly as it is displayed below.
|
|
15
|
+
<br>
|
|
16
|
+
<div class="input-group mt-3">
|
|
17
|
+
<input type="text" class="form-control" disabled id="copyMe">
|
|
18
|
+
<div class="input-group-append">
|
|
19
|
+
<button class="btn btn-primary m-0 py-0 px-3" onclick="copyMyText()" type="button">
|
|
20
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor"
|
|
21
|
+
class="mb-1 bi bi-clipboard" viewBox="0 0 16 16">
|
|
22
|
+
<path
|
|
23
|
+
d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
|
|
24
|
+
<path
|
|
25
|
+
d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" />
|
|
26
|
+
</svg>
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</li>
|
|
31
|
+
<li>Enter the generated 'Consumer Key' and 'Consumer Secret' on the next page.</li>
|
|
32
|
+
</ol>
|
|
33
|
+
<div class="text-center">
|
|
34
|
+
<button type="button" class="btn btn-primary" id="introLink">Continue →</button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div id="menuWrapper" class="btn-group w-100 mb-0" role="group" aria-label="UI Menu" style="display: none;">
|
|
38
|
+
<button type="button" class="btn btn-primary ml-0" id="menuAccount">
|
|
39
|
+
Account
|
|
40
|
+
</button>
|
|
41
|
+
<button type="button" class="btn btn-primary" id="menuSettings">
|
|
42
|
+
Settings
|
|
43
|
+
</button>
|
|
44
|
+
<button type="button" class="btn btn-primary" id="menuDevices">
|
|
45
|
+
Devices
|
|
46
|
+
</button>
|
|
47
|
+
<button type="button" class="btn btn-primary mr-0" id="menuHome">
|
|
48
|
+
Support
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
<div id="pageAccount" class="mt-4" style="display: none;">
|
|
52
|
+
<div class="alert alert-warning" style="display: none;" id="validateForm">
|
|
53
|
+
Please enter both values
|
|
54
|
+
</div>
|
|
55
|
+
<div class="alert alert-success" style="display: none;" id="linkSuccess">
|
|
56
|
+
Config saved, please close this window and restart Homebridge
|
|
57
|
+
</div>
|
|
58
|
+
<div class="form-group">
|
|
59
|
+
<label for="inputConsumerKey">Consumer Key <span class="text-danger">*</span></label>
|
|
60
|
+
<input type="text" class="form-control" id="inputConsumerKey">
|
|
61
|
+
</div>
|
|
62
|
+
<div class="form-group">
|
|
63
|
+
<label for="inputConsumerSecret">Consumer Secret <span class="text-danger">*</span></label>
|
|
64
|
+
<input type="text" class="form-control" id="inputConsumerSecret">
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form-group text-center">
|
|
67
|
+
<button type="button" class="btn btn-primary" id="backToIntro">←</button>
|
|
68
|
+
<button type="button" class="btn btn-primary" id="linkButton">Continue →</button>
|
|
69
|
+
<button type="button" class="btn btn-danger" id="unLinkButton">Unlink Account →</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div id="pageDevices" class="mt-4" style="display: none;">
|
|
73
|
+
<div id="deviceInfo">
|
|
74
|
+
<form>
|
|
75
|
+
<div class="form-group">
|
|
76
|
+
<select class="form-control" id="deviceSelect"></select>
|
|
77
|
+
</div>
|
|
78
|
+
</form>
|
|
79
|
+
<table class="table w-100" id="deviceTable" style="display: none;">
|
|
80
|
+
<thead>
|
|
81
|
+
<tr class="table-active">
|
|
82
|
+
<th scope="col" style="width: 40%;">Device Name</th>
|
|
83
|
+
<th scope="col" style="width: 60%;" id="displayName"></th>
|
|
84
|
+
</tr>
|
|
85
|
+
</thead>
|
|
86
|
+
<tbody>
|
|
87
|
+
<tr>
|
|
88
|
+
<th scope="row">Device ID</th>
|
|
89
|
+
<td id="deviceID"></td>
|
|
90
|
+
</tr>
|
|
91
|
+
<tr>
|
|
92
|
+
<th scope="row">Model</th>
|
|
93
|
+
<td id="model"></td>
|
|
94
|
+
</tr>
|
|
95
|
+
<tr>
|
|
96
|
+
<th scope="row">Firmware Version</th>
|
|
97
|
+
<td id="firmwareRevision"></td>
|
|
98
|
+
</tr>
|
|
99
|
+
</tbody>
|
|
100
|
+
</table>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div id="pageSupport" class="mt-4" style="display: none;">
|
|
104
|
+
<p class="text-center lead">Thank you for using <strong>homebridge-firstalert</strong></p>
|
|
105
|
+
<p class="text-center">The links below will take you to our GitHub wiki</p>
|
|
106
|
+
<h5>Setup</h5>
|
|
107
|
+
<ul>
|
|
108
|
+
<li>
|
|
109
|
+
<a href="https://github.com/homebridge-plugins/homebridge-firstalert/wiki" target="_blank">Wiki Home</a>
|
|
110
|
+
</li>
|
|
111
|
+
<li>
|
|
112
|
+
<a href="https://github.com/homebridge-plugins/homebridge-firstalert/wiki/Configuration"
|
|
113
|
+
target="_blank">Configuration</a>
|
|
114
|
+
</li>
|
|
115
|
+
<li>
|
|
116
|
+
<a href="https://github.com/homebridge-plugins/homebridge-firstalert/wiki/Beta-Version" target="_blank">Beta
|
|
117
|
+
Version</a>
|
|
118
|
+
</li>
|
|
119
|
+
<li>
|
|
120
|
+
<a href="https://github.com/homebridge-plugins/homebridge-firstalert/wiki/Node-Version" target="_blank">Node
|
|
121
|
+
Version</a>
|
|
122
|
+
</li>
|
|
123
|
+
<li>
|
|
124
|
+
<a href="https://github.com/homebridge-plugins/homebridge-firstalert/wiki/Uninstallation"
|
|
125
|
+
target="_blank">Uninstallation</a>
|
|
126
|
+
</li>
|
|
127
|
+
</ul>
|
|
128
|
+
<h5>Features</h5>
|
|
129
|
+
<ul>
|
|
130
|
+
<li>
|
|
131
|
+
<a href="https://github.com/homebridge-plugins/homebridge-firstalert/wiki/Supported-Devices"
|
|
132
|
+
target="_blank">Supported
|
|
133
|
+
Devices</a>
|
|
134
|
+
</li>
|
|
135
|
+
<li>
|
|
136
|
+
<a href="https://github.com/homebridge-plugins/homebridge-firstalert/wiki/Fan-Modes" target="_blank">Fan
|
|
137
|
+
Modes</a>
|
|
138
|
+
</li>
|
|
139
|
+
</ul>
|
|
140
|
+
<h5>Help/About</h5>
|
|
141
|
+
<ul>
|
|
142
|
+
<li>
|
|
143
|
+
<a href="https://github.com/homebridge-plugins/homebridge-firstalert/issues/new/choose" target="_blank">Support
|
|
144
|
+
Request</a>
|
|
145
|
+
</li>
|
|
146
|
+
<li>
|
|
147
|
+
<a href="https://github.com/homebridge-plugins/homebridge-firstalert/blob/latest/CHANGELOG.md"
|
|
148
|
+
target="_blank">Changelog</a>
|
|
149
|
+
</li>
|
|
150
|
+
<li>
|
|
151
|
+
<a href="https://github.com/sponsors/donavanbecker" target="_blank">About Me</a>
|
|
152
|
+
</li>
|
|
153
|
+
</ul>
|
|
154
|
+
<h5>Disclaimer</h5>
|
|
155
|
+
<ul>
|
|
156
|
+
<li>
|
|
157
|
+
I am in no way affiliated with FirstAlert and this plugin is a personal project that I maintain in
|
|
158
|
+
my free time.
|
|
159
|
+
</li>
|
|
160
|
+
<li>
|
|
161
|
+
Use this plugin entirely at your own risk - please see licence for more information.
|
|
162
|
+
</li>
|
|
163
|
+
</ul>
|
|
164
|
+
</div>
|
|
165
|
+
<script>
|
|
166
|
+
; (async () => {
|
|
167
|
+
try {
|
|
168
|
+
const currentConfig = await homebridge.getPluginConfig()
|
|
169
|
+
const hostname = window.location.hostname
|
|
170
|
+
try {
|
|
171
|
+
await homebridge.request('Start FirstAlert Login Server')
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const msg = err.message
|
|
174
|
+
homebridge.toast.error(msg, 'HTTP Server Not Started')
|
|
175
|
+
}
|
|
176
|
+
homebridge.addEventListener('creds-received', async event => {
|
|
177
|
+
try {
|
|
178
|
+
if (event.data.access && event.data.refresh) {
|
|
179
|
+
this.popup.close()
|
|
180
|
+
currentConfig[0].credentials = {
|
|
181
|
+
consumerKey: event.data.key,
|
|
182
|
+
consumerSecret: event.data.secret,
|
|
183
|
+
accessToken: event.data.access,
|
|
184
|
+
refreshToken: event.data.refresh
|
|
185
|
+
}
|
|
186
|
+
await homebridge.updatePluginConfig(currentConfig)
|
|
187
|
+
await homebridge.savePluginConfig()
|
|
188
|
+
homebridge.toast.success("Successfully Linked FirstAlert Account", "homebridge-firstalert")
|
|
189
|
+
document.getElementById('backToIntro').style.display = 'none'
|
|
190
|
+
document.getElementById('linkButton').style.display = 'none'
|
|
191
|
+
document.getElementById('unLinkButton').style.display = 'none'
|
|
192
|
+
document.getElementById('linkSuccess').style.display = 'block'
|
|
193
|
+
// Show discover devices button after successful login
|
|
194
|
+
if (!document.getElementById('discoverDevicesBtn')) {
|
|
195
|
+
const discoverBtn = document.createElement('button')
|
|
196
|
+
discoverBtn.id = 'discoverDevicesBtn'
|
|
197
|
+
discoverBtn.className = 'btn btn-success mt-3'
|
|
198
|
+
discoverBtn.innerText = 'Discover Devices'
|
|
199
|
+
discoverBtn.onclick = async () => {
|
|
200
|
+
homebridge.showSpinner()
|
|
201
|
+
try {
|
|
202
|
+
const result = await homebridge.request('discoverDevices', { accessToken: result.data.accessToken })
|
|
203
|
+
if (result.status === 'ok' && result.data) {
|
|
204
|
+
// Show device list and allow adding to config
|
|
205
|
+
let deviceList = result.data.devices || result.data;
|
|
206
|
+
if (!Array.isArray(deviceList)) deviceList = Object.values(deviceList).flat();
|
|
207
|
+
const deviceDiv = document.createElement('div')
|
|
208
|
+
deviceDiv.id = 'discoveredDevices'
|
|
209
|
+
deviceDiv.innerHTML = '<h5>Discovered Devices</h5>'
|
|
210
|
+
deviceList.forEach(device => {
|
|
211
|
+
const d = document.createElement('div')
|
|
212
|
+
d.className = 'alert alert-info mb-2'
|
|
213
|
+
d.innerHTML = `<b>${device.name || device.deviceID}</b> (${device.type || device.deviceType || ''}) <button class='btn btn-sm btn-primary ml-2 addDeviceBtn'>Add</button>`
|
|
214
|
+
d.querySelector('.addDeviceBtn').onclick = async () => {
|
|
215
|
+
// Add device to config
|
|
216
|
+
if (!currentConfig[0].devices) currentConfig[0].devices = []
|
|
217
|
+
currentConfig[0].devices.push({ id: device.deviceID, name: device.name, type: device.type || device.deviceType })
|
|
218
|
+
await homebridge.updatePluginConfig(currentConfig)
|
|
219
|
+
await homebridge.savePluginConfig()
|
|
220
|
+
homebridge.toast.success('Device added to config', 'homebridge-firstalert')
|
|
221
|
+
}
|
|
222
|
+
deviceDiv.appendChild(d)
|
|
223
|
+
})
|
|
224
|
+
document.getElementById('pageAccount').appendChild(deviceDiv)
|
|
225
|
+
} else {
|
|
226
|
+
homebridge.toast.error('Device discovery failed: ' + (result.data || 'Unknown error'), 'Error')
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
homebridge.toast.error('Device discovery failed: ' + (err?.message || err), 'Error')
|
|
230
|
+
} finally {
|
|
231
|
+
homebridge.hideSpinner()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
document.getElementById('pageAccount').appendChild(discoverBtn)
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
throw new Error('no access/token received from server')
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.log(err)
|
|
241
|
+
homebridge.toast.error('Try again, or see the console message for info.', 'Error')
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
copyMyText = () => {
|
|
245
|
+
var textToCopy = document.getElementById("copyMe");
|
|
246
|
+
textToCopy.select();
|
|
247
|
+
document.execCommand("copy");
|
|
248
|
+
}
|
|
249
|
+
linkAccount = async () => {
|
|
250
|
+
document.getElementById('validateForm').style.display = 'none'
|
|
251
|
+
const consumerKey = document.getElementById('inputConsumerKey').value
|
|
252
|
+
const consumerSecret = document.getElementById('inputConsumerSecret').value
|
|
253
|
+
if (!consumerKey || !consumerSecret) {
|
|
254
|
+
document.getElementById('validateForm').style.display = 'block'
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
homebridge.showSpinner()
|
|
258
|
+
try {
|
|
259
|
+
// Step 1: Start OAuth flow, get authUrl and codeVerifier
|
|
260
|
+
const startResult = await homebridge.request('startOAuthFlow', { consumerKey, consumerSecret })
|
|
261
|
+
if (startResult.status === 'need_code' && startResult.data && startResult.data.authUrl && startResult.data.codeVerifier) {
|
|
262
|
+
// Show modal for code entry
|
|
263
|
+
let modal = document.getElementById('oauthModal')
|
|
264
|
+
if (!modal) {
|
|
265
|
+
modal = document.createElement('div')
|
|
266
|
+
modal.id = 'oauthModal'
|
|
267
|
+
modal.style.position = 'fixed'
|
|
268
|
+
modal.style.top = '0'
|
|
269
|
+
modal.style.left = '0'
|
|
270
|
+
modal.style.width = '100vw'
|
|
271
|
+
modal.style.height = '100vh'
|
|
272
|
+
modal.style.background = 'rgba(0,0,0,0.5)'
|
|
273
|
+
modal.style.display = 'flex'
|
|
274
|
+
modal.style.alignItems = 'center'
|
|
275
|
+
modal.style.justifyContent = 'center'
|
|
276
|
+
modal.innerHTML = `<div style="background:#fff;padding:2em;border-radius:8px;max-width:400px;width:100%;text-align:center;">
|
|
277
|
+
<h5>FirstAlert Login</h5>
|
|
278
|
+
<p>Click <a href='${startResult.data.authUrl}' target='_blank'>here</a> to log in and authorize, then paste the code below:</p>
|
|
279
|
+
<input id='oauthCodeInput' class='form-control mb-2' placeholder='Paste code here'>
|
|
280
|
+
<div class='text-danger mb-2' id='oauthCodeError' style='display:none'></div>
|
|
281
|
+
<button class='btn btn-primary' id='oauthCodeSubmit'>Submit</button>
|
|
282
|
+
<button class='btn btn-link' id='oauthCodeCancel'>Cancel</button>
|
|
283
|
+
</div>`
|
|
284
|
+
document.body.appendChild(modal)
|
|
285
|
+
} else {
|
|
286
|
+
modal.style.display = 'flex'
|
|
287
|
+
}
|
|
288
|
+
document.getElementById('oauthCodeInput').value = ''
|
|
289
|
+
document.getElementById('oauthCodeError').style.display = 'none'
|
|
290
|
+
document.getElementById('oauthCodeSubmit').onclick = async () => {
|
|
291
|
+
const code = document.getElementById('oauthCodeInput').value
|
|
292
|
+
if (!code) {
|
|
293
|
+
document.getElementById('oauthCodeError').innerText = 'Please enter the code.'
|
|
294
|
+
document.getElementById('oauthCodeError').style.display = 'block'
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
modal.style.display = 'none'
|
|
298
|
+
homebridge.showSpinner()
|
|
299
|
+
try {
|
|
300
|
+
const finishResult = await homebridge.request('startOAuthFlow', { consumerKey, consumerSecret, code, codeVerifier: startResult.data.codeVerifier })
|
|
301
|
+
if (finishResult.status === 'ok' && finishResult.data && finishResult.data.refreshToken) {
|
|
302
|
+
currentConfig[0].credentials = {
|
|
303
|
+
consumerKey,
|
|
304
|
+
consumerSecret,
|
|
305
|
+
accessToken: finishResult.data.accessToken,
|
|
306
|
+
refreshToken: finishResult.data.refreshToken
|
|
307
|
+
}
|
|
308
|
+
await homebridge.updatePluginConfig(currentConfig)
|
|
309
|
+
await homebridge.savePluginConfig()
|
|
310
|
+
homebridge.toast.success('Successfully Linked FirstAlert Account', 'homebridge-firstalert')
|
|
311
|
+
document.getElementById('backToIntro').style.display = 'none'
|
|
312
|
+
document.getElementById('linkButton').style.display = 'none'
|
|
313
|
+
document.getElementById('unLinkButton').style.display = 'none'
|
|
314
|
+
document.getElementById('linkSuccess').style.display = 'block'
|
|
315
|
+
} else {
|
|
316
|
+
homebridge.toast.error('Login failed: ' + (finishResult.data?.error_description || finishResult.data || 'Unknown error'), 'Error')
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
homebridge.toast.error('Login failed: ' + (err?.message || err), 'Error')
|
|
320
|
+
} finally {
|
|
321
|
+
homebridge.hideSpinner()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
document.getElementById('oauthCodeCancel').onclick = () => {
|
|
325
|
+
modal.style.display = 'none'
|
|
326
|
+
}
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
// If we get here, try the legacy direct token flow
|
|
330
|
+
if (startResult.status === 'ok' && startResult.data && startResult.data.refreshToken) {
|
|
331
|
+
currentConfig[0].credentials = {
|
|
332
|
+
consumerKey,
|
|
333
|
+
consumerSecret,
|
|
334
|
+
accessToken: startResult.data.accessToken,
|
|
335
|
+
refreshToken: startResult.data.refreshToken
|
|
336
|
+
}
|
|
337
|
+
await homebridge.updatePluginConfig(currentConfig)
|
|
338
|
+
await homebridge.savePluginConfig()
|
|
339
|
+
homebridge.toast.success('Successfully Linked FirstAlert Account', 'homebridge-firstalert')
|
|
340
|
+
document.getElementById('backToIntro').style.display = 'none'
|
|
341
|
+
document.getElementById('linkButton').style.display = 'none'
|
|
342
|
+
document.getElementById('unLinkButton').style.display = 'none'
|
|
343
|
+
document.getElementById('linkSuccess').style.display = 'block'
|
|
344
|
+
} else {
|
|
345
|
+
homebridge.toast.error('Login failed: ' + (startResult.data?.error_description || startResult.data || 'Unknown error'), 'Error')
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
homebridge.toast.error('Login failed: ' + (err?.message || err), 'Error')
|
|
349
|
+
} finally {
|
|
350
|
+
homebridge.hideSpinner()
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
unLinkAccount = async () => {
|
|
354
|
+
try {
|
|
355
|
+
document.getElementById('validateForm').style.display = 'none'
|
|
356
|
+
if (
|
|
357
|
+
!document.getElementById('inputConsumerKey').value ||
|
|
358
|
+
!document.getElementById('inputConsumerSecret').value
|
|
359
|
+
) {
|
|
360
|
+
document.getElementById('validateForm').style.display = 'block'
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
delete currentConfig[0].credentials
|
|
364
|
+
await homebridge.updatePluginConfig(currentConfig)
|
|
365
|
+
await homebridge.savePluginConfig()
|
|
366
|
+
document.getElementById('inputConsumerKey').value = ''
|
|
367
|
+
document.getElementById('inputConsumerSecret').value = ''
|
|
368
|
+
document.getElementById('unLinkButton').style.display = 'none'
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.error(err)
|
|
371
|
+
homebridge.toast.error('Try again, or see the console message for info.', 'Error')
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
showIntro = () => {
|
|
375
|
+
const introLink = document.getElementById('introLink')
|
|
376
|
+
document.getElementById('copyMe').value = 'http://' + hostname + ':8585/auth'
|
|
377
|
+
introLink.addEventListener('click', () => {
|
|
378
|
+
homebridge.showSpinner()
|
|
379
|
+
document.getElementById('pageIntro').style.display = 'none'
|
|
380
|
+
document.getElementById('menuWrapper').style.display = 'inline-flex'
|
|
381
|
+
showAccount()
|
|
382
|
+
homebridge.hideSpinner()
|
|
383
|
+
})
|
|
384
|
+
document.getElementById('pageAccount').style.display = 'none'
|
|
385
|
+
document.getElementById('menuWrapper').style.display = 'none'
|
|
386
|
+
document.getElementById('pageIntro').style.display = 'block'
|
|
387
|
+
}
|
|
388
|
+
showDevices = async () => {
|
|
389
|
+
homebridge.showSpinner()
|
|
390
|
+
homebridge.hideSchemaForm()
|
|
391
|
+
document.getElementById('menuHome').classList.remove('btn-elegant')
|
|
392
|
+
document.getElementById('menuHome').classList.add('btn-primary')
|
|
393
|
+
document.getElementById('menuDevices').classList.add('btn-elegant')
|
|
394
|
+
document.getElementById('menuDevices').classList.remove('btn-primary')
|
|
395
|
+
document.getElementById('menuAccount').classList.remove('btn-elegant')
|
|
396
|
+
document.getElementById('menuAccount').classList.add('btn-primary')
|
|
397
|
+
document.getElementById('menuSettings').classList.remove('btn-elegant')
|
|
398
|
+
document.getElementById('menuSettings').classList.add('btn-primary')
|
|
399
|
+
document.getElementById('pageSupport').style.display = 'none'
|
|
400
|
+
document.getElementById('pageAccount').style.display = 'none'
|
|
401
|
+
document.getElementById('pageDevices').style.display = 'block'
|
|
402
|
+
const cachedAccessories =
|
|
403
|
+
typeof homebridge.getCachedAccessories === 'function'
|
|
404
|
+
? await homebridge.getCachedAccessories()
|
|
405
|
+
: await homebridge.request('/getCachedAccessories')
|
|
406
|
+
if (cachedAccessories.length > 0) {
|
|
407
|
+
cachedAccessories.sort((a, b) => {
|
|
408
|
+
return a.displayName.toLowerCase() > b.displayName.toLowerCase()
|
|
409
|
+
? 1
|
|
410
|
+
: b.displayName.toLowerCase() > a.displayName.toLowerCase()
|
|
411
|
+
? -1
|
|
412
|
+
: 0
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
const deviceSelect = document.getElementById('deviceSelect')
|
|
416
|
+
deviceSelect.innerHTML = ''
|
|
417
|
+
cachedAccessories.forEach(a => {
|
|
418
|
+
const option = document.createElement('option')
|
|
419
|
+
option.text = a.displayName
|
|
420
|
+
option.value = a.UUID
|
|
421
|
+
deviceSelect.add(option)
|
|
422
|
+
})
|
|
423
|
+
showDeviceInfo = async UUID => {
|
|
424
|
+
homebridge.showSpinner()
|
|
425
|
+
const thisAcc = cachedAccessories.find(x => x.UUID === UUID)
|
|
426
|
+
const context = thisAcc.context
|
|
427
|
+
document.getElementById('displayName').innerHTML = thisAcc.displayName
|
|
428
|
+
document.getElementById('deviceID').innerHTML = context.deviceID
|
|
429
|
+
document.getElementById('model').innerHTML = context.model
|
|
430
|
+
document.getElementById('firmwareRevision').innerHTML = context.firmwareRevision || 'N/A'
|
|
431
|
+
document.getElementById('deviceTable').style.display = 'inline-table'
|
|
432
|
+
homebridge.hideSpinner()
|
|
433
|
+
}
|
|
434
|
+
deviceSelect.addEventListener('change', event => showDeviceInfo(event.target.value))
|
|
435
|
+
if (cachedAccessories.length > 0) {
|
|
436
|
+
showDeviceInfo(cachedAccessories[0].UUID)
|
|
437
|
+
} else {
|
|
438
|
+
const option = document.createElement('option')
|
|
439
|
+
option.text = 'No Devices'
|
|
440
|
+
deviceSelect.add(option)
|
|
441
|
+
deviceSelect.disabled = true
|
|
442
|
+
}
|
|
443
|
+
homebridge.hideSpinner()
|
|
444
|
+
}
|
|
445
|
+
showSupport = () => {
|
|
446
|
+
homebridge.showSpinner()
|
|
447
|
+
homebridge.hideSchemaForm()
|
|
448
|
+
document.getElementById('menuHome').classList.add('btn-elegant')
|
|
449
|
+
document.getElementById('menuHome').classList.remove('btn-primary')
|
|
450
|
+
document.getElementById('menuDevices').classList.remove('btn-elegant')
|
|
451
|
+
document.getElementById('menuDevices').classList.add('btn-primary')
|
|
452
|
+
document.getElementById('menuSettings').classList.remove('btn-elegant')
|
|
453
|
+
document.getElementById('menuSettings').classList.add('btn-primary')
|
|
454
|
+
document.getElementById('menuAccount').classList.remove('btn-elegant')
|
|
455
|
+
document.getElementById('menuAccount').classList.add('btn-primary')
|
|
456
|
+
document.getElementById('pageSupport').style.display = 'block'
|
|
457
|
+
document.getElementById('pageDevices').style.display = 'none'
|
|
458
|
+
document.getElementById('pageAccount').style.display = 'none'
|
|
459
|
+
homebridge.hideSpinner()
|
|
460
|
+
}
|
|
461
|
+
showSettings = () => {
|
|
462
|
+
homebridge.showSpinner()
|
|
463
|
+
document.getElementById('menuHome').classList.remove('btn-elegant')
|
|
464
|
+
document.getElementById('menuHome').classList.add('btn-primary')
|
|
465
|
+
document.getElementById('menuDevices').classList.remove('btn-elegant')
|
|
466
|
+
document.getElementById('menuDevices').classList.add('btn-primary')
|
|
467
|
+
document.getElementById('menuSettings').classList.add('btn-elegant')
|
|
468
|
+
document.getElementById('menuSettings').classList.remove('btn-primary')
|
|
469
|
+
document.getElementById('menuAccount').classList.remove('btn-elegant')
|
|
470
|
+
document.getElementById('menuAccount').classList.add('btn-primary')
|
|
471
|
+
document.getElementById('pageSupport').style.display = 'none'
|
|
472
|
+
document.getElementById('pageDevices').style.display = 'none'
|
|
473
|
+
document.getElementById('pageAccount').style.display = 'none'
|
|
474
|
+
homebridge.showSchemaForm()
|
|
475
|
+
homebridge.hideSpinner()
|
|
476
|
+
}
|
|
477
|
+
showAccount = () => {
|
|
478
|
+
homebridge.showSpinner()
|
|
479
|
+
const backToIntro = document.getElementById('backToIntro')
|
|
480
|
+
backToIntro.addEventListener('click', () => {
|
|
481
|
+
showIntro()
|
|
482
|
+
})
|
|
483
|
+
backToIntro.style.display = 'none'
|
|
484
|
+
const linkButton = document.getElementById('linkButton')
|
|
485
|
+
linkButton.addEventListener('click', () => {
|
|
486
|
+
linkAccount()
|
|
487
|
+
})
|
|
488
|
+
const unLinkButton = document.getElementById('unLinkButton')
|
|
489
|
+
unLinkButton.style.display = 'none'
|
|
490
|
+
unLinkButton.addEventListener('click', () => {
|
|
491
|
+
unLinkAccount()
|
|
492
|
+
})
|
|
493
|
+
document.getElementById('linkSuccess').style.display = 'none'
|
|
494
|
+
homebridge.hideSchemaForm()
|
|
495
|
+
document.getElementById('menuHome').classList.remove('btn-elegant')
|
|
496
|
+
document.getElementById('menuHome').classList.add('btn-primary')
|
|
497
|
+
document.getElementById('menuDevices').classList.remove('btn-elegant')
|
|
498
|
+
document.getElementById('menuDevices').classList.add('btn-primary')
|
|
499
|
+
document.getElementById('menuAccount').classList.add('btn-elegant')
|
|
500
|
+
document.getElementById('menuAccount').classList.remove('btn-primary')
|
|
501
|
+
document.getElementById('menuSettings').classList.remove('btn-elegant')
|
|
502
|
+
document.getElementById('menuSettings').classList.add('btn-primary')
|
|
503
|
+
document.getElementById('pageSupport').style.display = 'none'
|
|
504
|
+
document.getElementById('pageDevices').style.display = 'none'
|
|
505
|
+
document.getElementById('pageAccount').style.display = 'block'
|
|
506
|
+
document.getElementById('linkButton').style.display = 'inline-block'
|
|
507
|
+
let isRelink = false
|
|
508
|
+
if (currentConfig[0] && currentConfig[0].credentials) {
|
|
509
|
+
const key = currentConfig[0].credentials.consumerKey || ''
|
|
510
|
+
const secret = currentConfig[0].credentials.consumerSecret || ''
|
|
511
|
+
document.getElementById('inputConsumerKey').value = key
|
|
512
|
+
document.getElementById('inputConsumerSecret').value = secret
|
|
513
|
+
if (key && secret) {
|
|
514
|
+
isRelink = true
|
|
515
|
+
document.getElementById('unLinkButton').style.display = 'inline-block'
|
|
516
|
+
} else {
|
|
517
|
+
document.getElementById('unLinkButton').style.display = 'none'
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
document.getElementById('linkButton').innerHTML = isRelink
|
|
521
|
+
? 'Relink Account →'
|
|
522
|
+
: 'Link Account →'
|
|
523
|
+
if (!isRelink) {
|
|
524
|
+
document.getElementById('backToIntro').style.display = 'inline-block'
|
|
525
|
+
} else {
|
|
526
|
+
document.getElementById('backToIntro').style.display = 'none'
|
|
527
|
+
}
|
|
528
|
+
homebridge.hideSpinner()
|
|
529
|
+
}
|
|
530
|
+
menuHome.addEventListener('click', () => showSupport())
|
|
531
|
+
menuAccount.addEventListener('click', () => showAccount())
|
|
532
|
+
menuDevices.addEventListener('click', () => showDevices())
|
|
533
|
+
menuSettings.addEventListener('click', () => showSettings())
|
|
534
|
+
if (currentConfig.length) {
|
|
535
|
+
document.getElementById('menuWrapper').style.display = 'inline-flex'
|
|
536
|
+
showAccount()
|
|
537
|
+
} else {
|
|
538
|
+
currentConfig.push({ name: 'FirstAlert' })
|
|
539
|
+
await homebridge.updatePluginConfig(currentConfig)
|
|
540
|
+
showIntro()
|
|
541
|
+
}
|
|
542
|
+
} catch (err) {
|
|
543
|
+
homebridge.toast.error(err.message, 'Error')
|
|
544
|
+
} finally {
|
|
545
|
+
homebridge.hideSpinner()
|
|
546
|
+
}
|
|
547
|
+
})()
|
|
548
|
+
</script>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ResideoDevice {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
deviceId: string;
|
|
5
|
+
globalDeviceType: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ResideoAccount {
|
|
8
|
+
id: string;
|
|
9
|
+
firstName: string;
|
|
10
|
+
lastName: string;
|
|
11
|
+
contactEmail: string;
|
|
12
|
+
countryCode: string;
|
|
13
|
+
locale: string;
|
|
14
|
+
devices: ResideoDevice[];
|
|
15
|
+
}
|
|
16
|
+
export interface ResideoDeviceState {
|
|
17
|
+
name: string;
|
|
18
|
+
deviceType: string;
|
|
19
|
+
isOnline: boolean;
|
|
20
|
+
deviceState: any;
|
|
21
|
+
}
|
|
22
|
+
export declare class ResideoClient {
|
|
23
|
+
private clientId;
|
|
24
|
+
private refreshToken;
|
|
25
|
+
private accessToken;
|
|
26
|
+
constructor(refreshToken: string);
|
|
27
|
+
refreshAccessToken(): Promise<string>;
|
|
28
|
+
getAccessToken(): Promise<string>;
|
|
29
|
+
getAccount(): Promise<ResideoAccount>;
|
|
30
|
+
getDeviceState(deviceId: string): Promise<ResideoDeviceState>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=resideoClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resideoClient.d.ts","sourceRoot":"","sources":["../../../src/api/resideoClient.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,aAAa,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,GAAG,CAAA;CACjB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,WAAW,CAAsB;gBAE7B,YAAY,EAAE,MAAM;IAI1B,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC;IAmBrC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAOjC,UAAU,IAAI,OAAO,CAAC,cAAc,CAAC;IAiCrC,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAQpE"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// FirstAlert API client for OAuth and device polling
|
|
2
|
+
// Implements token refresh, account/device fetch, and strong typing
|
|
3
|
+
import { request } from 'undici';
|
|
4
|
+
export class ResideoClient {
|
|
5
|
+
clientId = 'SRmiA7CaYi1JgivDZdzzoZu4X5VBogGt';
|
|
6
|
+
refreshToken;
|
|
7
|
+
accessToken = null;
|
|
8
|
+
constructor(refreshToken) {
|
|
9
|
+
this.refreshToken = refreshToken;
|
|
10
|
+
}
|
|
11
|
+
async refreshAccessToken() {
|
|
12
|
+
const body = JSON.stringify({
|
|
13
|
+
grant_type: 'refresh_token',
|
|
14
|
+
refresh_token: this.refreshToken,
|
|
15
|
+
client_id: this.clientId,
|
|
16
|
+
});
|
|
17
|
+
const { body: resBody } = await request('https://login.firstalert.com/oauth/token', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body,
|
|
21
|
+
});
|
|
22
|
+
const data = await resBody.json();
|
|
23
|
+
this.accessToken = data.access_token ?? '';
|
|
24
|
+
if (!this.accessToken) {
|
|
25
|
+
throw new Error('Failed to obtain access token from FirstAlert API');
|
|
26
|
+
}
|
|
27
|
+
return this.accessToken;
|
|
28
|
+
}
|
|
29
|
+
async getAccessToken() {
|
|
30
|
+
if (!this.accessToken) {
|
|
31
|
+
await this.refreshAccessToken();
|
|
32
|
+
}
|
|
33
|
+
return this.accessToken;
|
|
34
|
+
}
|
|
35
|
+
async getAccount() {
|
|
36
|
+
const token = await this.getAccessToken();
|
|
37
|
+
const { body: resBody } = await request('https://api.firstalert.com/ris-public-api/api/v1/accounts', {
|
|
38
|
+
method: 'GET',
|
|
39
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
40
|
+
});
|
|
41
|
+
const resp = await resBody.json();
|
|
42
|
+
// Parse and flatten devices
|
|
43
|
+
const data = resp.data;
|
|
44
|
+
const devices = [];
|
|
45
|
+
for (const user of data.consumerUsers) {
|
|
46
|
+
for (const loc of user.consumerAccount.locations) {
|
|
47
|
+
for (const dev of loc.consumerDevices) {
|
|
48
|
+
devices.push({
|
|
49
|
+
id: dev.id,
|
|
50
|
+
name: dev.name,
|
|
51
|
+
deviceId: dev.device.deviceId,
|
|
52
|
+
globalDeviceType: dev.device.globalDeviceType,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
id: data.id,
|
|
59
|
+
firstName: data.firstName,
|
|
60
|
+
lastName: data.lastName,
|
|
61
|
+
contactEmail: data.contactEmail,
|
|
62
|
+
countryCode: data.countryCode,
|
|
63
|
+
locale: data.locale,
|
|
64
|
+
devices,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async getDeviceState(deviceId) {
|
|
68
|
+
const token = await this.getAccessToken();
|
|
69
|
+
const { body: resBody } = await request(`https://api.firstalert.com/ris-public-api/api/v2/devices/smokeDetectors/${deviceId}/state`, {
|
|
70
|
+
method: 'GET',
|
|
71
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
72
|
+
});
|
|
73
|
+
return await resBody.json();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=resideoClient.js.map
|