@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,226 @@
|
|
|
1
|
+
// Static regexes for version parsing (for lint rule e18e/prefer-static-regex)
|
|
2
|
+
/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* device.ts: homebridge-firstalert.
|
|
5
|
+
*/
|
|
6
|
+
import type { API, HAP, Logging, PlatformAccessory } from 'homebridge'
|
|
7
|
+
|
|
8
|
+
import type { ResideoDevice } from '../api/resideoClient.js'
|
|
9
|
+
import type { ResideoPlatform } from '../platform.js'
|
|
10
|
+
import type { ResideoPlatformConfig } from '../settings.js'
|
|
11
|
+
|
|
12
|
+
export abstract class deviceBase {
|
|
13
|
+
public readonly api: API
|
|
14
|
+
public readonly log: Logging
|
|
15
|
+
public readonly config!: ResideoPlatformConfig
|
|
16
|
+
protected readonly hap: HAP
|
|
17
|
+
|
|
18
|
+
// Config
|
|
19
|
+
protected deviceLogging!: string
|
|
20
|
+
protected deviceRefreshRate!: number
|
|
21
|
+
protected deviceUpdateRate!: number
|
|
22
|
+
protected devicePushRate!: number
|
|
23
|
+
protected deviceFirmwareVersion!: string
|
|
24
|
+
protected deviceMaxRetries!: number
|
|
25
|
+
protected deviceDelayBetweenRetries!: number
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
protected readonly platform: ResideoPlatform,
|
|
29
|
+
protected accessory: PlatformAccessory,
|
|
30
|
+
protected device: ResideoDevice,
|
|
31
|
+
) {
|
|
32
|
+
this.api = (platform as any).api
|
|
33
|
+
this.log = (platform as any).log
|
|
34
|
+
this.config = (platform as any).config as ResideoPlatformConfig
|
|
35
|
+
this.hap = (platform as any).api.hap
|
|
36
|
+
|
|
37
|
+
this.getDeviceLogSettings()
|
|
38
|
+
this.getDeviceRateSettings()
|
|
39
|
+
this.getDeviceConfigSettings(device)
|
|
40
|
+
this.getDeviceContext(accessory, device)
|
|
41
|
+
|
|
42
|
+
// Set accessory information
|
|
43
|
+
accessory
|
|
44
|
+
.getService(this.hap.Service.AccessoryInformation)!
|
|
45
|
+
.setCharacteristic(this.hap.Characteristic.Manufacturer, 'FirstAlert')
|
|
46
|
+
.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName)
|
|
47
|
+
.setCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName)
|
|
48
|
+
.setCharacteristic(this.hap.Characteristic.Model, accessory.context.model)
|
|
49
|
+
.setCharacteristic(this.hap.Characteristic.SerialNumber, accessory.context.deviceID)
|
|
50
|
+
.setCharacteristic(this.hap.Characteristic.FirmwareRevision, this.deviceFirmwareVersion)
|
|
51
|
+
.getCharacteristic(this.hap.Characteristic.FirmwareRevision)
|
|
52
|
+
.updateValue(this.deviceFirmwareVersion)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getDeviceLogSettings(): Promise<void> {
|
|
56
|
+
this.deviceLogging = 'standard'
|
|
57
|
+
await this.debugLog(`Using Device Logging: ${this.deviceLogging}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getDeviceRateSettings(): Promise<void> {
|
|
61
|
+
// Use default values; ResideoDevice does not have these properties
|
|
62
|
+
this.deviceRefreshRate = 120
|
|
63
|
+
this.deviceUpdateRate = 5
|
|
64
|
+
this.devicePushRate = 0.1
|
|
65
|
+
await this.debugLog(`Using refreshRate: ${this.deviceRefreshRate}, updateRate: ${this.deviceUpdateRate}, pushRate: ${this.devicePushRate}`)
|
|
66
|
+
this.deviceMaxRetries = 5
|
|
67
|
+
await this.debugLog(`Using maxRetries: ${this.deviceMaxRetries}`)
|
|
68
|
+
this.deviceDelayBetweenRetries = 3 * 1000
|
|
69
|
+
await this.debugLog(`Using delayBetweenRetries: ${this.deviceDelayBetweenRetries}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getDeviceConfigSettings(device: ResideoDevice): Promise<void> {
|
|
73
|
+
// No config merging; ResideoDevice does not have these properties
|
|
74
|
+
this.debugSuccessLog(`Config: ${JSON.stringify(device)}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getDeviceContext(accessory: PlatformAccessory, device: ResideoDevice): Promise<void> {
|
|
78
|
+
// Context Information
|
|
79
|
+
accessory.context.model = device.globalDeviceType
|
|
80
|
+
accessory.context.deviceID = device.deviceId
|
|
81
|
+
accessory.context.deviceType = device.globalDeviceType
|
|
82
|
+
// FirmwareRevision
|
|
83
|
+
const version = '0.0.0'
|
|
84
|
+
this.deviceFirmwareVersion = version
|
|
85
|
+
accessory
|
|
86
|
+
.getService(this.hap.Service.AccessoryInformation)!
|
|
87
|
+
.setCharacteristic(this.hap.Characteristic.HardwareRevision, this.deviceFirmwareVersion)
|
|
88
|
+
.setCharacteristic(this.hap.Characteristic.SoftwareRevision, this.deviceFirmwareVersion)
|
|
89
|
+
.setCharacteristic(this.hap.Characteristic.FirmwareRevision, this.deviceFirmwareVersion)
|
|
90
|
+
.getCharacteristic(this.hap.Characteristic.FirmwareRevision)
|
|
91
|
+
.updateValue(this.deviceFirmwareVersion)
|
|
92
|
+
this.debugSuccessLog(`deviceFirmwareVersion: ${this.deviceFirmwareVersion}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async statusCode(statusCode: number, action: string): Promise<void> {
|
|
96
|
+
switch (statusCode) {
|
|
97
|
+
case 200:
|
|
98
|
+
this.debugLog(`${this.device.name}: ${this.accessory.displayName} Standard Response, statusCode: ${statusCode}, Action: ${action}`)
|
|
99
|
+
break
|
|
100
|
+
case 400:
|
|
101
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} Bad Request, statusCode: ${statusCode}, Action: ${action}`)
|
|
102
|
+
break
|
|
103
|
+
case 401:
|
|
104
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} Unauthorized, statusCode: ${statusCode}, Action: ${action}`)
|
|
105
|
+
break
|
|
106
|
+
case 403:
|
|
107
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`)
|
|
108
|
+
break
|
|
109
|
+
case 404:
|
|
110
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} Not Found, statusCode: ${statusCode}, Action: ${action}`)
|
|
111
|
+
break
|
|
112
|
+
case 429:
|
|
113
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} Too Many Requests, statusCode: ${statusCode}, Action: ${action}`)
|
|
114
|
+
break
|
|
115
|
+
case 500:
|
|
116
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} Internal Server Error (Meater Server), statusCode: ${statusCode}, Action: ${action}`)
|
|
117
|
+
break
|
|
118
|
+
default:
|
|
119
|
+
this.infoLog(`${this.device.name}: ${this.accessory.displayName} Unknown statusCode: ${statusCode}, Action: ${action}, Report Bugs Here: https://bit.ly/homebridge-firstalert-bug-report`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async resideoAPIError(e: any, action: string): Promise<void> {
|
|
124
|
+
if (e.message.includes('400')) {
|
|
125
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action}, Bad Request`)
|
|
126
|
+
this.debugLog('The client has issued an invalid request. This is commonly used to specify validation errors in a request payload.')
|
|
127
|
+
} else if (e.message.includes('401')) {
|
|
128
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action}, Unauthorized Request`)
|
|
129
|
+
this.debugLog('Authorization for the API is required, but the request has not been authenticated.')
|
|
130
|
+
} else if (e.message.includes('403')) {
|
|
131
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action}, Forbidden Request`)
|
|
132
|
+
this.debugLog('The request has been authenticated but does not have appropriate permissions, or a requested resource is not found.')
|
|
133
|
+
} else if (e.message.includes('404')) {
|
|
134
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action}, Requst Not Found`)
|
|
135
|
+
this.debugLog('Specifies the requested path does not exist.')
|
|
136
|
+
} else if (e.message.includes('406')) {
|
|
137
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action}, Request Not Acceptable`)
|
|
138
|
+
this.debugLog('The client has requested a MIME type via the Accept header for a value not supported by the server.')
|
|
139
|
+
} else if (e.message.includes('415')) {
|
|
140
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action}, Unsupported Requst Header`)
|
|
141
|
+
this.debugLog('The client has defined a contentType header that is not supported by the server.')
|
|
142
|
+
} else if (e.message.includes('422')) {
|
|
143
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action}, Unprocessable Entity`)
|
|
144
|
+
this.debugLog(
|
|
145
|
+
'The client has made a valid request, but the server cannot process it.'
|
|
146
|
+
+ ' This is often used for APIs for which certain limits have been exceeded.',
|
|
147
|
+
)
|
|
148
|
+
} else if (e.message.includes('429')) {
|
|
149
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action}, Too Many Requests`)
|
|
150
|
+
this.debugLog('The client has exceeded the number of requests allowed for a given time window.')
|
|
151
|
+
} else if (e.message.includes('500')) {
|
|
152
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action}, Internal Server Error`)
|
|
153
|
+
this.debugLog('An unexpected error on the SmartThings servers has occurred. These errors should be rare.')
|
|
154
|
+
} else {
|
|
155
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to ${action},`)
|
|
156
|
+
}
|
|
157
|
+
if (this.deviceLogging.includes('debug')) {
|
|
158
|
+
this.errorLog(`${this.device.name}: ${this.accessory.displayName} failed to pushChanges, Error Message: ${JSON.stringify(e.message)}`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Logging for Device
|
|
164
|
+
*/
|
|
165
|
+
async infoLog(...log: any[]): Promise<void> {
|
|
166
|
+
if (await this.enablingDeviceLogging()) {
|
|
167
|
+
this.log.info(`${this.device.name}: ${this.accessory.displayName}`, String(...log))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async successLog(...log: any[]): Promise<void> {
|
|
172
|
+
if (await this.enablingDeviceLogging()) {
|
|
173
|
+
this.log.success(`${this.device.name}: ${this.accessory.displayName}`, String(...log))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async debugSuccessLog(...log: any[]): Promise<void> {
|
|
178
|
+
if (await this.enablingDeviceLogging()) {
|
|
179
|
+
if (this.deviceLogging?.includes('debug')) {
|
|
180
|
+
this.log.success(`[DEBUG] ${this.device.name}: ${this.accessory.displayName}`, String(...log))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async warnLog(...log: any[]): Promise<void> {
|
|
186
|
+
if (await this.enablingDeviceLogging()) {
|
|
187
|
+
this.log.warn(`${this.device.name}: ${this.accessory.displayName}`, String(...log))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async debugWarnLog(...log: any[]): Promise<void> {
|
|
192
|
+
if (await this.enablingDeviceLogging()) {
|
|
193
|
+
if (this.deviceLogging?.includes('debug')) {
|
|
194
|
+
this.log.warn(`[DEBUG] ${this.device.name}: ${this.accessory.displayName}`, String(...log))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async errorLog(...log: any[]): Promise<void> {
|
|
200
|
+
if (await this.enablingDeviceLogging()) {
|
|
201
|
+
this.log.error(`${this.device.name}: ${this.accessory.displayName}`, String(...log))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async debugErrorLog(...log: any[]): Promise<void> {
|
|
206
|
+
if (await this.enablingDeviceLogging()) {
|
|
207
|
+
if (this.deviceLogging?.includes('debug')) {
|
|
208
|
+
this.log.error(`[DEBUG] ${this.device.name}: ${this.accessory.displayName}`, String(...log))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async debugLog(...log: any[]): Promise<void> {
|
|
214
|
+
if (await this.enablingDeviceLogging()) {
|
|
215
|
+
if (this.deviceLogging === 'debug') {
|
|
216
|
+
this.log.info(`[DEBUG] ${this.device.name}: ${this.accessory.displayName}`, String(...log))
|
|
217
|
+
} else {
|
|
218
|
+
this.log.debug(`${this.device.name}: ${this.accessory.displayName}`, String(...log))
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async enablingDeviceLogging(): Promise<boolean> {
|
|
224
|
+
return this.deviceLogging.includes('debug') ?? this.deviceLogging === 'standard'
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* leaksensors.ts: homebridge-firstalert.
|
|
4
|
+
*/
|
|
5
|
+
import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'
|
|
6
|
+
import { interval, Subject } from 'rxjs'
|
|
7
|
+
import { skipWhile } from 'rxjs/operators'
|
|
8
|
+
|
|
9
|
+
import type { ResideoDevice } from '../api/resideoClient.js'
|
|
10
|
+
import type { ResideoPlatform } from '../platform.js'
|
|
11
|
+
import { deviceBase } from './device.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Platform Accessory
|
|
15
|
+
* An instance of this class is created for each accessory your platform registers
|
|
16
|
+
* Each accessory may expose multiple services of different service types.
|
|
17
|
+
*/
|
|
18
|
+
export class LeakSensor extends deviceBase {
|
|
19
|
+
// Services
|
|
20
|
+
private Battery: {
|
|
21
|
+
Name: CharacteristicValue
|
|
22
|
+
Service: Service
|
|
23
|
+
BatteryLevel: CharacteristicValue
|
|
24
|
+
ChargingState: CharacteristicValue
|
|
25
|
+
StatusLowBattery: CharacteristicValue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private LeakSensor?: {
|
|
29
|
+
Name: CharacteristicValue
|
|
30
|
+
Service: Service
|
|
31
|
+
StatusActive: CharacteristicValue
|
|
32
|
+
LeakDetected: CharacteristicValue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private HumiditySensor?: {
|
|
36
|
+
Name: CharacteristicValue
|
|
37
|
+
Service: Service
|
|
38
|
+
CurrentRelativeHumidity: CharacteristicValue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private TemperatureSensor?: {
|
|
42
|
+
Name: CharacteristicValue
|
|
43
|
+
Service: Service
|
|
44
|
+
CurrentTemperature: CharacteristicValue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Sensor Update
|
|
48
|
+
SensorUpdateInProgress!: boolean
|
|
49
|
+
doSensorUpdate!: Subject<void>
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
readonly platform: ResideoPlatform,
|
|
53
|
+
accessory: PlatformAccessory,
|
|
54
|
+
device: ResideoDevice,
|
|
55
|
+
) {
|
|
56
|
+
super(platform, accessory, device)
|
|
57
|
+
|
|
58
|
+
this.doSensorUpdate = new Subject()
|
|
59
|
+
this.SensorUpdateInProgress = false
|
|
60
|
+
|
|
61
|
+
// Initialize Battery Service
|
|
62
|
+
accessory.context.Battery = accessory.context.Battery ?? {}
|
|
63
|
+
this.Battery = {
|
|
64
|
+
Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`,
|
|
65
|
+
Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service,
|
|
66
|
+
BatteryLevel: accessory.context.BatteryLevel ?? 100,
|
|
67
|
+
ChargingState: accessory.context.ChargingState ?? this.hap.Characteristic.ChargingState.NOT_CHARGEABLE,
|
|
68
|
+
StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL,
|
|
69
|
+
}
|
|
70
|
+
accessory.context.Battery = this.Battery as object
|
|
71
|
+
this.Battery.Service
|
|
72
|
+
.setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name)
|
|
73
|
+
.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE)
|
|
74
|
+
.getCharacteristic(this.hap.Characteristic.BatteryLevel)
|
|
75
|
+
.onGet(() => this.Battery.BatteryLevel)
|
|
76
|
+
|
|
77
|
+
// Leak Sensor Service
|
|
78
|
+
accessory.context.LeakSensor = accessory.context.LeakSensor ?? {}
|
|
79
|
+
this.LeakSensor = {
|
|
80
|
+
Name: accessory.context.LeakSensor.Name ?? `${accessory.displayName} Leak Sensor`,
|
|
81
|
+
Service: accessory.getService(this.hap.Service.LeakSensor) ?? accessory.addService(this.hap.Service.LeakSensor) as Service,
|
|
82
|
+
StatusActive: accessory.context.StatusActive ?? false,
|
|
83
|
+
LeakDetected: accessory.context.LeakDetected ?? this.hap.Characteristic.LeakDetected.LEAK_NOT_DETECTED,
|
|
84
|
+
}
|
|
85
|
+
accessory.context.LeakSensor = this.LeakSensor as object
|
|
86
|
+
this.LeakSensor.Service
|
|
87
|
+
.setCharacteristic(this.hap.Characteristic.Name, this.LeakSensor.Name)
|
|
88
|
+
.getCharacteristic(this.hap.Characteristic.StatusActive)
|
|
89
|
+
.onGet(() => this.LeakSensor!.StatusActive)
|
|
90
|
+
this.LeakSensor.Service
|
|
91
|
+
.getCharacteristic(this.hap.Characteristic.LeakDetected)
|
|
92
|
+
.onGet(() => this.LeakSensor!.LeakDetected)
|
|
93
|
+
|
|
94
|
+
// Temperature Sensor Service
|
|
95
|
+
accessory.context.TemperatureSensor = accessory.context.TemperatureSensor ?? {}
|
|
96
|
+
this.TemperatureSensor = {
|
|
97
|
+
Name: accessory.context.TemperatureSensor.Name ?? `${accessory.displayName} Temperature Sensor`,
|
|
98
|
+
Service: accessory.getService(this.hap.Service.TemperatureSensor) ?? accessory.addService(this.hap.Service.TemperatureSensor) as Service,
|
|
99
|
+
CurrentTemperature: accessory.context.CurrentTemperature ?? 20,
|
|
100
|
+
}
|
|
101
|
+
accessory.context.TemperatureSensor = this.TemperatureSensor as object
|
|
102
|
+
this.TemperatureSensor.Service
|
|
103
|
+
.setCharacteristic(this.hap.Characteristic.Name, this.TemperatureSensor.Name)
|
|
104
|
+
.getCharacteristic(this.hap.Characteristic.CurrentTemperature)
|
|
105
|
+
.setProps({
|
|
106
|
+
minValue: -273.15,
|
|
107
|
+
maxValue: 100,
|
|
108
|
+
minStep: 0.1,
|
|
109
|
+
})
|
|
110
|
+
.onGet(() => this.TemperatureSensor!.CurrentTemperature)
|
|
111
|
+
|
|
112
|
+
// Humidity Sensor Service
|
|
113
|
+
accessory.context.HumiditySensor = accessory.context.HumiditySensor ?? {}
|
|
114
|
+
this.HumiditySensor = {
|
|
115
|
+
Name: accessory.context.HumiditySensor.Name ?? `${accessory.displayName} Humidity Sensor`,
|
|
116
|
+
Service: accessory.getService(this.hap.Service.HumiditySensor) ?? accessory.addService(this.hap.Service.HumiditySensor) as Service,
|
|
117
|
+
CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity ?? 50,
|
|
118
|
+
}
|
|
119
|
+
accessory.context.HumiditySensor = this.HumiditySensor as object
|
|
120
|
+
this.HumiditySensor.Service
|
|
121
|
+
.setCharacteristic(this.hap.Characteristic.Name, this.HumiditySensor.Name)
|
|
122
|
+
this.HumiditySensor.Service
|
|
123
|
+
.getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity)
|
|
124
|
+
.setProps({ minStep: 0.1 })
|
|
125
|
+
.onGet(() => this.HumiditySensor!.CurrentRelativeHumidity)
|
|
126
|
+
|
|
127
|
+
// Initial refresh
|
|
128
|
+
this.refreshStatus()
|
|
129
|
+
this.updateHomeKitCharacteristics()
|
|
130
|
+
interval(120 * 1000)
|
|
131
|
+
.pipe(skipWhile(() => this.SensorUpdateInProgress))
|
|
132
|
+
.subscribe(async () => {
|
|
133
|
+
await this.refreshStatus()
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parse the device status from the FirstAlert api
|
|
139
|
+
*/
|
|
140
|
+
async parseStatus(): Promise<void> {
|
|
141
|
+
// Example: parse state from API response (simulate for now)
|
|
142
|
+
// In a real implementation, fetch and parse state from the API
|
|
143
|
+
// For now, just set some dummy values
|
|
144
|
+
this.Battery.BatteryLevel = 100
|
|
145
|
+
this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
|
|
146
|
+
if (this.LeakSensor) {
|
|
147
|
+
this.LeakSensor.StatusActive = true
|
|
148
|
+
this.LeakSensor.LeakDetected = this.hap.Characteristic.LeakDetected.LEAK_NOT_DETECTED
|
|
149
|
+
}
|
|
150
|
+
if (this.TemperatureSensor) {
|
|
151
|
+
this.TemperatureSensor.CurrentTemperature = 20
|
|
152
|
+
}
|
|
153
|
+
if (this.HumiditySensor) {
|
|
154
|
+
this.HumiditySensor.CurrentRelativeHumidity = 50
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Asks the FirstAlert Home API for the latest device information
|
|
160
|
+
*/
|
|
161
|
+
async refreshStatus(): Promise<void> {
|
|
162
|
+
try {
|
|
163
|
+
const deviceId = String(this.device.deviceId)
|
|
164
|
+
await this.platform.client.getDeviceState(deviceId)
|
|
165
|
+
// In a real implementation, parse deviceState and update class state
|
|
166
|
+
await this.parseStatus()
|
|
167
|
+
await this.updateHomeKitCharacteristics()
|
|
168
|
+
} catch (e: any) {
|
|
169
|
+
const action = 'refreshStatus'
|
|
170
|
+
this.resideoAPIError(e, action)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Updates the status for each of the HomeKit Characteristics
|
|
176
|
+
*/
|
|
177
|
+
async updateHomeKitCharacteristics(): Promise<void> {
|
|
178
|
+
this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel)
|
|
179
|
+
this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery)
|
|
180
|
+
if (this.LeakSensor) {
|
|
181
|
+
this.LeakSensor.Service.updateCharacteristic(this.hap.Characteristic.LeakDetected, this.LeakSensor.LeakDetected)
|
|
182
|
+
this.LeakSensor.Service.updateCharacteristic(this.hap.Characteristic.StatusActive, this.LeakSensor.StatusActive)
|
|
183
|
+
}
|
|
184
|
+
if (this.TemperatureSensor) {
|
|
185
|
+
this.TemperatureSensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor.CurrentTemperature)
|
|
186
|
+
}
|
|
187
|
+
if (this.HumiditySensor) {
|
|
188
|
+
this.HumiditySensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.HumiditySensor.CurrentRelativeHumidity)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async apiError(e: any): Promise<void> {
|
|
193
|
+
this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e)
|
|
194
|
+
this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e)
|
|
195
|
+
if (this.LeakSensor) {
|
|
196
|
+
this.LeakSensor.Service.updateCharacteristic(this.hap.Characteristic.LeakDetected, e)
|
|
197
|
+
this.LeakSensor.Service.updateCharacteristic(this.hap.Characteristic.StatusActive, e)
|
|
198
|
+
}
|
|
199
|
+
if (this.TemperatureSensor) {
|
|
200
|
+
this.TemperatureSensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e)
|
|
201
|
+
}
|
|
202
|
+
if (this.HumiditySensor) {
|
|
203
|
+
this.HumiditySensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* smoke.ts: homebridge-firstalert.
|
|
4
|
+
*/
|
|
5
|
+
import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'
|
|
6
|
+
import { interval, Subject } from 'rxjs'
|
|
7
|
+
import { skipWhile } from 'rxjs/operators'
|
|
8
|
+
|
|
9
|
+
import type { ResideoDevice } from '../api/resideoClient.js'
|
|
10
|
+
import type { ResideoPlatform } from '../platform.js'
|
|
11
|
+
import { deviceBase } from './device.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HomeKit Smoke/CO/CO2 Sensor Accessory
|
|
15
|
+
*/
|
|
16
|
+
export class SmokeSensor extends deviceBase {
|
|
17
|
+
// Services
|
|
18
|
+
private SmokeSensor: { Name: CharacteristicValue, Service: Service, SmokeDetected: CharacteristicValue }
|
|
19
|
+
private CarbonMonoxideSensor?: { Name: CharacteristicValue, Service: Service, CarbonMonoxideDetected: CharacteristicValue }
|
|
20
|
+
private CarbonDioxideSensor?: { Name: CharacteristicValue, Service: Service, CarbonDioxideDetected: CharacteristicValue }
|
|
21
|
+
private Battery: { Name: CharacteristicValue, Service: Service, BatteryLevel: CharacteristicValue, ChargingState: CharacteristicValue, StatusLowBattery: CharacteristicValue }
|
|
22
|
+
|
|
23
|
+
// Update
|
|
24
|
+
SensorUpdateInProgress!: boolean
|
|
25
|
+
doSensorUpdate!: Subject<void>
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
readonly platform: ResideoPlatform,
|
|
29
|
+
accessory: PlatformAccessory,
|
|
30
|
+
device: ResideoDevice,
|
|
31
|
+
) {
|
|
32
|
+
super(platform, accessory, device)
|
|
33
|
+
this.doSensorUpdate = new Subject()
|
|
34
|
+
this.SensorUpdateInProgress = false
|
|
35
|
+
|
|
36
|
+
// Battery Service
|
|
37
|
+
accessory.context.Battery = accessory.context.Battery ?? {}
|
|
38
|
+
this.Battery = {
|
|
39
|
+
Name: accessory.context.Battery.Name ?? `${accessory.displayName} Battery`,
|
|
40
|
+
Service: accessory.getService(this.hap.Service.Battery) ?? accessory.addService(this.hap.Service.Battery) as Service,
|
|
41
|
+
BatteryLevel: accessory.context.BatteryLevel ?? 100,
|
|
42
|
+
ChargingState: accessory.context.ChargingState ?? this.hap.Characteristic.ChargingState.NOT_CHARGEABLE,
|
|
43
|
+
StatusLowBattery: accessory.context.StatusLowBattery ?? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL,
|
|
44
|
+
}
|
|
45
|
+
accessory.context.Battery = this.Battery as object
|
|
46
|
+
this.Battery.Service
|
|
47
|
+
.setCharacteristic(this.hap.Characteristic.Name, this.Battery.Name)
|
|
48
|
+
.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE)
|
|
49
|
+
.getCharacteristic(this.hap.Characteristic.BatteryLevel)
|
|
50
|
+
.onGet(() => this.Battery.BatteryLevel)
|
|
51
|
+
|
|
52
|
+
// Smoke Sensor Service
|
|
53
|
+
accessory.context.SmokeSensor = accessory.context.SmokeSensor ?? {}
|
|
54
|
+
this.SmokeSensor = {
|
|
55
|
+
Name: accessory.context.SmokeSensor.Name ?? `${accessory.displayName} Smoke Sensor`,
|
|
56
|
+
Service: accessory.getService(this.hap.Service.SmokeSensor) ?? accessory.addService(this.hap.Service.SmokeSensor) as Service,
|
|
57
|
+
SmokeDetected: accessory.context.SmokeDetected ?? this.hap.Characteristic.SmokeDetected.SMOKE_NOT_DETECTED,
|
|
58
|
+
}
|
|
59
|
+
accessory.context.SmokeSensor = this.SmokeSensor as object
|
|
60
|
+
this.SmokeSensor.Service
|
|
61
|
+
.setCharacteristic(this.hap.Characteristic.Name, this.SmokeSensor.Name)
|
|
62
|
+
.getCharacteristic(this.hap.Characteristic.SmokeDetected)
|
|
63
|
+
.onGet(() => this.SmokeSensor.SmokeDetected)
|
|
64
|
+
|
|
65
|
+
// Carbon Monoxide Sensor Service (optional)
|
|
66
|
+
if (device.globalDeviceType?.toLowerCase().includes('co')) {
|
|
67
|
+
accessory.context.CarbonMonoxideSensor = accessory.context.CarbonMonoxideSensor ?? {}
|
|
68
|
+
this.CarbonMonoxideSensor = {
|
|
69
|
+
Name: accessory.context.CarbonMonoxideSensor.Name ?? `${accessory.displayName} CO Sensor`,
|
|
70
|
+
Service: accessory.getService(this.hap.Service.CarbonMonoxideSensor) ?? accessory.addService(this.hap.Service.CarbonMonoxideSensor) as Service,
|
|
71
|
+
CarbonMonoxideDetected: accessory.context.CarbonMonoxideDetected ?? this.hap.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL,
|
|
72
|
+
}
|
|
73
|
+
accessory.context.CarbonMonoxideSensor = this.CarbonMonoxideSensor as object
|
|
74
|
+
this.CarbonMonoxideSensor.Service
|
|
75
|
+
.setCharacteristic(this.hap.Characteristic.Name, this.CarbonMonoxideSensor.Name)
|
|
76
|
+
.getCharacteristic(this.hap.Characteristic.CarbonMonoxideDetected)
|
|
77
|
+
.onGet(() => this.CarbonMonoxideSensor!.CarbonMonoxideDetected)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Carbon Dioxide Sensor Service (optional)
|
|
81
|
+
if (device.globalDeviceType?.toLowerCase().includes('co2')) {
|
|
82
|
+
accessory.context.CarbonDioxideSensor = accessory.context.CarbonDioxideSensor ?? {}
|
|
83
|
+
this.CarbonDioxideSensor = {
|
|
84
|
+
Name: accessory.context.CarbonDioxideSensor.Name ?? `${accessory.displayName} CO2 Sensor`,
|
|
85
|
+
Service: accessory.getService(this.hap.Service.CarbonDioxideSensor) ?? accessory.addService(this.hap.Service.CarbonDioxideSensor) as Service,
|
|
86
|
+
CarbonDioxideDetected: accessory.context.CarbonDioxideDetected ?? false,
|
|
87
|
+
}
|
|
88
|
+
accessory.context.CarbonDioxideSensor = this.CarbonDioxideSensor as object
|
|
89
|
+
this.CarbonDioxideSensor.Service
|
|
90
|
+
.setCharacteristic(this.hap.Characteristic.Name, this.CarbonDioxideSensor.Name)
|
|
91
|
+
.getCharacteristic(this.hap.Characteristic.CarbonDioxideDetected)
|
|
92
|
+
.onGet(() => this.CarbonDioxideSensor!.CarbonDioxideDetected)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Initial refresh
|
|
96
|
+
this.refreshStatus()
|
|
97
|
+
this.updateHomeKitCharacteristics()
|
|
98
|
+
|
|
99
|
+
// Start update interval
|
|
100
|
+
interval(this.deviceRefreshRate * 1000)
|
|
101
|
+
.pipe(skipWhile(() => this.SensorUpdateInProgress))
|
|
102
|
+
.subscribe(async () => {
|
|
103
|
+
await this.refreshStatus()
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse the device status from the FirstAlert api
|
|
109
|
+
*/
|
|
110
|
+
async parseStatus(): Promise<void> {
|
|
111
|
+
// Use a single variable for dynamic property access
|
|
112
|
+
const d: any = this.device
|
|
113
|
+
// Battery
|
|
114
|
+
this.Battery.BatteryLevel = Number(d.batteryLevel ?? 100)
|
|
115
|
+
this.Battery.Service.getCharacteristic(this.hap.Characteristic.BatteryLevel).updateValue(this.Battery.BatteryLevel)
|
|
116
|
+
// Consider battery low if < 15%
|
|
117
|
+
this.Battery.StatusLowBattery = this.Battery.BatteryLevel < 15 ? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
|
|
118
|
+
|
|
119
|
+
// Smoke
|
|
120
|
+
this.SmokeSensor.SmokeDetected = (d.smokePresent ?? d.smoke_detected ?? d.smoke ?? false)
|
|
121
|
+
? this.hap.Characteristic.SmokeDetected.SMOKE_DETECTED
|
|
122
|
+
: this.hap.Characteristic.SmokeDetected.SMOKE_NOT_DETECTED
|
|
123
|
+
|
|
124
|
+
// CO
|
|
125
|
+
if (this.CarbonMonoxideSensor) {
|
|
126
|
+
this.CarbonMonoxideSensor.CarbonMonoxideDetected = (d.coPresent ?? d.co_detected ?? d.co ?? false)
|
|
127
|
+
? this.hap.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL
|
|
128
|
+
: this.hap.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL
|
|
129
|
+
}
|
|
130
|
+
// CO2
|
|
131
|
+
if (this.CarbonDioxideSensor) {
|
|
132
|
+
this.CarbonDioxideSensor.CarbonDioxideDetected = !!((d.co2Present ?? d.co2_detected ?? d.co2 ?? false))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Ask the FirstAlert API for the latest device information
|
|
138
|
+
*/
|
|
139
|
+
async refreshStatus(): Promise<void> {
|
|
140
|
+
try {
|
|
141
|
+
const deviceId = String(this.device.deviceId)
|
|
142
|
+
const deviceState = await this.platform.client.getDeviceState(deviceId)
|
|
143
|
+
this.device = { ...this.device, ...deviceState }
|
|
144
|
+
await this.parseStatus()
|
|
145
|
+
await this.updateHomeKitCharacteristics()
|
|
146
|
+
} catch (e: any) {
|
|
147
|
+
const action = 'refreshStatus'
|
|
148
|
+
this.resideoAPIError(e, action)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Update HomeKit characteristics
|
|
154
|
+
*/
|
|
155
|
+
async updateHomeKitCharacteristics(): Promise<void> {
|
|
156
|
+
this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel)
|
|
157
|
+
this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery)
|
|
158
|
+
this.SmokeSensor.Service.updateCharacteristic(this.hap.Characteristic.SmokeDetected, this.SmokeSensor.SmokeDetected)
|
|
159
|
+
if (this.CarbonMonoxideSensor) {
|
|
160
|
+
this.CarbonMonoxideSensor.Service.updateCharacteristic(this.hap.Characteristic.CarbonMonoxideDetected, this.CarbonMonoxideSensor.CarbonMonoxideDetected)
|
|
161
|
+
}
|
|
162
|
+
if (this.CarbonDioxideSensor) {
|
|
163
|
+
this.CarbonDioxideSensor.Service.updateCharacteristic(this.hap.Characteristic.CarbonDioxideDetected, this.CarbonDioxideSensor.CarbonDioxideDetected)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Called by platform polling
|
|
169
|
+
*/
|
|
170
|
+
async updateState(): Promise<void> {
|
|
171
|
+
await this.refreshStatus()
|
|
172
|
+
}
|
|
173
|
+
}
|