@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.
Files changed (121) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/FUNDING.yml +4 -0
  3. package/.github/ISSUE_TEMPLATE/bug-report.yml +97 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  5. package/.github/ISSUE_TEMPLATE/feature-request.yml +38 -0
  6. package/.github/ISSUE_TEMPLATE/support-request.yml +85 -0
  7. package/.github/ISSUE_TEMPLATE.md +52 -0
  8. package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +27 -0
  9. package/.github/dependabot.yml +17 -0
  10. package/.github/labeler.yml +38 -0
  11. package/.github/release-drafter.yml +33 -0
  12. package/.github/workflows/beta-release.yml +55 -0
  13. package/.github/workflows/build.yml +18 -0
  14. package/.github/workflows/changerelease.yml +11 -0
  15. package/.github/workflows/labeler.yml +9 -0
  16. package/.github/workflows/release-drafter.yml +14 -0
  17. package/.github/workflows/release.yml +35 -0
  18. package/.github/workflows/stale.yml +12 -0
  19. package/CHANGELOG.md +10 -0
  20. package/LICENSE +14 -0
  21. package/README.md +67 -0
  22. package/SECURITY.md +19 -0
  23. package/branding/Homebridge_x_FirstAlert.svg +48 -0
  24. package/branding/icon.png +0 -0
  25. package/config.schema.json +58 -0
  26. package/dist/homebridge-ui/public/index.html +548 -0
  27. package/dist/src/api/resideoClient.d.ts +32 -0
  28. package/dist/src/api/resideoClient.d.ts.map +1 -0
  29. package/dist/src/api/resideoClient.js +76 -0
  30. package/dist/src/api/resideoClient.js.map +1 -0
  31. package/dist/src/devices/device.d.ts +40 -0
  32. package/dist/src/devices/device.d.ts.map +1 -0
  33. package/dist/src/devices/device.js +207 -0
  34. package/dist/src/devices/device.js.map +1 -0
  35. package/dist/src/devices/leaksensors.d.ts +34 -0
  36. package/dist/src/devices/leaksensors.d.ts.map +1 -0
  37. package/dist/src/devices/leaksensors.js +163 -0
  38. package/dist/src/devices/leaksensors.js.map +1 -0
  39. package/dist/src/devices/smoke.d.ts +35 -0
  40. package/dist/src/devices/smoke.d.ts.map +1 -0
  41. package/dist/src/devices/smoke.js +150 -0
  42. package/dist/src/devices/smoke.js.map +1 -0
  43. package/dist/src/devices/thermostats.d.ts +42 -0
  44. package/dist/src/devices/thermostats.d.ts.map +1 -0
  45. package/dist/src/devices/thermostats.js +192 -0
  46. package/dist/src/devices/thermostats.js.map +1 -0
  47. package/dist/src/devices/valve.d.ts +21 -0
  48. package/dist/src/devices/valve.d.ts.map +1 -0
  49. package/dist/src/devices/valve.js +131 -0
  50. package/dist/src/devices/valve.js.map +1 -0
  51. package/dist/src/homebridge-ui/server.d.ts +5 -0
  52. package/dist/src/homebridge-ui/server.d.ts.map +1 -0
  53. package/dist/src/homebridge-ui/server.js +95 -0
  54. package/dist/src/homebridge-ui/server.js.map +1 -0
  55. package/dist/src/index.d.ts +4 -0
  56. package/dist/src/index.d.ts.map +1 -0
  57. package/dist/src/index.js +7 -0
  58. package/dist/src/index.js.map +1 -0
  59. package/dist/src/platform.d.ts +18 -0
  60. package/dist/src/platform.d.ts.map +1 -0
  61. package/dist/src/platform.js +108 -0
  62. package/dist/src/platform.js.map +1 -0
  63. package/dist/src/settings.d.ts +341 -0
  64. package/dist/src/settings.d.ts.map +1 -0
  65. package/dist/src/settings.js +25 -0
  66. package/dist/src/settings.js.map +1 -0
  67. package/dist/src/utils.d.ts +21 -0
  68. package/dist/src/utils.d.ts.map +1 -0
  69. package/dist/src/utils.js +58 -0
  70. package/dist/src/utils.js.map +1 -0
  71. package/dist/test/index.test.d.ts +2 -0
  72. package/dist/test/index.test.d.ts.map +1 -0
  73. package/dist/test/index.test.js +14 -0
  74. package/dist/test/index.test.js.map +1 -0
  75. package/dist/test/platform.test.d.ts +2 -0
  76. package/dist/test/platform.test.d.ts.map +1 -0
  77. package/dist/test/platform.test.js +56 -0
  78. package/dist/test/platform.test.js.map +1 -0
  79. package/dist/test/settings.test.d.ts +2 -0
  80. package/dist/test/settings.test.d.ts.map +1 -0
  81. package/dist/test/settings.test.js +48 -0
  82. package/dist/test/settings.test.js.map +1 -0
  83. package/dist/test/utils.test.d.ts +2 -0
  84. package/dist/test/utils.test.d.ts.map +1 -0
  85. package/dist/test/utils.test.js +17 -0
  86. package/dist/test/utils.test.js.map +1 -0
  87. package/docs/.nojekyll +1 -0
  88. package/docs/assets/hierarchy.js +1 -0
  89. package/docs/assets/highlight.css +22 -0
  90. package/docs/assets/icons.js +18 -0
  91. package/docs/assets/icons.svg +1 -0
  92. package/docs/assets/main.js +60 -0
  93. package/docs/assets/navigation.js +1 -0
  94. package/docs/assets/search.js +1 -0
  95. package/docs/assets/style.css +1633 -0
  96. package/docs/hierarchy.html +1 -0
  97. package/docs/index.html +77 -0
  98. package/docs/modules.html +1 -0
  99. package/docs/variables/default.html +1 -0
  100. package/eslint.config.js +44 -0
  101. package/nodemon.json +10 -0
  102. package/package.json +106 -0
  103. package/scripts/free-dev-ports.mjs +105 -0
  104. package/src/api/resideoClient.ts +106 -0
  105. package/src/devices/device.ts +226 -0
  106. package/src/devices/leaksensors.ts +206 -0
  107. package/src/devices/smoke.ts +173 -0
  108. package/src/devices/thermostats.ts +243 -0
  109. package/src/devices/valve.ts +162 -0
  110. package/src/homebridge-ui/public/index.html +548 -0
  111. package/src/homebridge-ui/server.ts +102 -0
  112. package/src/index.ts +13 -0
  113. package/src/platform.ts +112 -0
  114. package/src/settings.ts +402 -0
  115. package/src/utils.ts +61 -0
  116. package/test/index.test.ts +18 -0
  117. package/test/platform.test.ts +65 -0
  118. package/test/settings.test.ts +56 -0
  119. package/test/utils.test.ts +20 -0
  120. package/tsconfig.json +27 -0
  121. 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
+ }