@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
package/src/platform.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
import type { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig } from 'homebridge'
|
|
3
|
+
import { ResideoClient } from './api/resideoClient.js'
|
|
4
|
+
import { Thermostats } from './devices/thermostats.js'
|
|
5
|
+
import { LeakSensor } from './devices/leaksensors.js'
|
|
6
|
+
import { Valve } from './devices/valve.js'
|
|
7
|
+
import { deviceBase } from './devices/device.js'
|
|
8
|
+
import { SmokeSensor } from './devices/smoke.js'
|
|
9
|
+
|
|
10
|
+
export class ResideoPlatform implements DynamicPlatformPlugin {
|
|
11
|
+
private readonly log: Logger
|
|
12
|
+
private readonly config: PlatformConfig
|
|
13
|
+
private readonly api: API
|
|
14
|
+
private readonly pollInterval: number
|
|
15
|
+
public readonly client!: ResideoClient
|
|
16
|
+
private pollTimer?: any
|
|
17
|
+
private accessories: PlatformAccessory[] = []
|
|
18
|
+
private deviceInstances: Record<string, deviceBase> = {}
|
|
19
|
+
|
|
20
|
+
constructor(log: Logger, config: PlatformConfig, api: API) {
|
|
21
|
+
this.log = log
|
|
22
|
+
this.config = config
|
|
23
|
+
this.api = api
|
|
24
|
+
this.pollInterval = config.pollInterval || 60
|
|
25
|
+
if (!config.refreshToken) {
|
|
26
|
+
this.log.error('No refresh token configured. Please log in via the Homebridge UI.')
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
this.client = new ResideoClient(config.refreshToken)
|
|
30
|
+
api.on('didFinishLaunching', () => this.discoverDevices())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async discoverDevices() {
|
|
34
|
+
try {
|
|
35
|
+
const account = await this.client.getAccount()
|
|
36
|
+
for (const device of account.devices) {
|
|
37
|
+
// Find or create accessory
|
|
38
|
+
let accessory = this.accessories.find(a => a.context.deviceID === device.deviceId)
|
|
39
|
+
if (!accessory) {
|
|
40
|
+
accessory = new this.api.platformAccessory(device.name, this.api.hap.uuid.generate(device.deviceId))
|
|
41
|
+
accessory.context.deviceID = device.deviceId
|
|
42
|
+
accessory.context.deviceType = device.globalDeviceType
|
|
43
|
+
this.api.registerPlatformAccessories('@homebridge-plugins/homebridge-firstalert', 'FirstAlert', [accessory])
|
|
44
|
+
this.accessories.push(accessory)
|
|
45
|
+
this.log.info(`Registered new accessory: ${device.name} (${device.deviceId})`)
|
|
46
|
+
}
|
|
47
|
+
// Instantiate correct device class
|
|
48
|
+
let instance: deviceBase | undefined
|
|
49
|
+
switch (device.globalDeviceType) {
|
|
50
|
+
case 'thermostat':
|
|
51
|
+
instance = new Thermostats(this, accessory, device as any)
|
|
52
|
+
break
|
|
53
|
+
case 'leakSensor':
|
|
54
|
+
instance = new LeakSensor(this, accessory, device as any)
|
|
55
|
+
break
|
|
56
|
+
case 'valve':
|
|
57
|
+
instance = new Valve(this, accessory, device as any)
|
|
58
|
+
break
|
|
59
|
+
case 'smoke':
|
|
60
|
+
case 'smokeSensor':
|
|
61
|
+
case 'smokeDetector':
|
|
62
|
+
case 'carbonMonoxideSensor':
|
|
63
|
+
case 'carbonDioxideSensor':
|
|
64
|
+
instance = new SmokeSensor(this, accessory, device as any)
|
|
65
|
+
break
|
|
66
|
+
default:
|
|
67
|
+
this.log.warn(`Unknown device type: ${device.globalDeviceType} (${device.deviceId})`)
|
|
68
|
+
}
|
|
69
|
+
if (instance) {
|
|
70
|
+
this.deviceInstances[device.deviceId] = instance
|
|
71
|
+
}
|
|
72
|
+
this.log.info(`Discovered device: ${device.name} (${device.deviceId})`)
|
|
73
|
+
}
|
|
74
|
+
this.startPolling(account.devices.map(d => d.deviceId))
|
|
75
|
+
} catch (err) {
|
|
76
|
+
this.log.error('Failed to discover devices:', err)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
startPolling(deviceIds: string[]) {
|
|
81
|
+
if (this.pollTimer) {
|
|
82
|
+
clearInterval(this.pollTimer)
|
|
83
|
+
}
|
|
84
|
+
this.pollTimer = setInterval(() => this.pollDevices(deviceIds), this.pollInterval * 1000)
|
|
85
|
+
this.pollDevices(deviceIds) // Initial poll
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async pollDevices(deviceIds: string[]) {
|
|
89
|
+
for (const deviceId of deviceIds) {
|
|
90
|
+
const instance = this.deviceInstances[deviceId]
|
|
91
|
+
if (instance && typeof (instance as any).updateState === 'function') {
|
|
92
|
+
try {
|
|
93
|
+
await (instance as any).updateState()
|
|
94
|
+
} catch (err) {
|
|
95
|
+
this.log.warn(`Failed to update state for device ${deviceId}:`, err)
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
try {
|
|
99
|
+
const state = await this.client.getDeviceState(deviceId)
|
|
100
|
+
this.log.debug(`Polled device ${deviceId}:`, JSON.stringify(state))
|
|
101
|
+
} catch (err) {
|
|
102
|
+
this.log.warn(`Failed to poll device ${deviceId}:`, err)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
configureAccessory(accessory: PlatformAccessory) {
|
|
109
|
+
// Required for restoring cached accessories
|
|
110
|
+
this.accessories.push(accessory)
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* settings.ts: homebridge-firstalert.
|
|
4
|
+
*/
|
|
5
|
+
import type { PlatformConfig } from 'homebridge'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
|
|
9
|
+
*/
|
|
10
|
+
export const PLATFORM_NAME = 'FirstAlert'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This must match the name of your plugin as defined the package.json
|
|
14
|
+
*/
|
|
15
|
+
export const PLUGIN_NAME = '@homebridge-plugins/homebridge-firstalert'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This is the main url used to access FirstAlert API
|
|
19
|
+
*/
|
|
20
|
+
export const AuthorizeURL = 'https://api.honeywell.com/oauth2/authorize?'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* This is the main url used to access FirstAlert API
|
|
24
|
+
*/
|
|
25
|
+
export const TokenURL = 'https://api.honeywell.com/oauth2/token'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This is the main url used to access FirstAlert API
|
|
29
|
+
*/
|
|
30
|
+
export const LocationURL = 'https://api.honeywell.com/v2/locations'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* This is the main url used to access FirstAlert API
|
|
34
|
+
*/
|
|
35
|
+
export const DeviceURL = 'https://api.honeywell.com/v2/devices'
|
|
36
|
+
|
|
37
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD'
|
|
38
|
+
|
|
39
|
+
// Config
|
|
40
|
+
export interface ResideoPlatformConfig extends PlatformConfig {
|
|
41
|
+
credentials?: credentials
|
|
42
|
+
callbackUrl?: string
|
|
43
|
+
port?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface credentials {
|
|
47
|
+
accessToken?: string
|
|
48
|
+
refreshToken?: string
|
|
49
|
+
consumerKey?: string
|
|
50
|
+
consumerSecret?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface modes {
|
|
54
|
+
Off: number
|
|
55
|
+
Heat: number
|
|
56
|
+
Cool: number
|
|
57
|
+
Auto: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface holdModes {
|
|
61
|
+
NoHold: number
|
|
62
|
+
TemporaryHold: number
|
|
63
|
+
PermanentHold: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface payload {
|
|
67
|
+
mode?: string
|
|
68
|
+
heatSetpoint?: number
|
|
69
|
+
coolSetpoint?: number
|
|
70
|
+
thermostatSetpointStatus?: string
|
|
71
|
+
nextPeriodTime?: string
|
|
72
|
+
autoChangeoverActive?: boolean
|
|
73
|
+
thermostatSetpoint?: number
|
|
74
|
+
unit?: string
|
|
75
|
+
state?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type locations = location[]
|
|
79
|
+
|
|
80
|
+
// Location
|
|
81
|
+
export interface location {
|
|
82
|
+
locationID: string | number // Updated to handle both string and number
|
|
83
|
+
name: string
|
|
84
|
+
devices: resideoDevice[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface resideoDevice {
|
|
88
|
+
deviceID: string | number // Updated to handle both string and number
|
|
89
|
+
deviceClass: string
|
|
90
|
+
deviceModel: string
|
|
91
|
+
userDefinedDeviceName: string
|
|
92
|
+
firmware?: string
|
|
93
|
+
thermostatVersion?: string
|
|
94
|
+
external?: boolean
|
|
95
|
+
groups?: T9groups[]
|
|
96
|
+
inBuiltSensorState?: inBuiltSensorState
|
|
97
|
+
settings?: Settings
|
|
98
|
+
deviceType: string
|
|
99
|
+
name?: string
|
|
100
|
+
isAlive: boolean
|
|
101
|
+
priorityType?: string
|
|
102
|
+
units?: string
|
|
103
|
+
indoorTemperature?: number
|
|
104
|
+
allowedModes?: string[]
|
|
105
|
+
minHeatSetpoint?: number
|
|
106
|
+
maxHeatSetpoint?: number
|
|
107
|
+
minCoolSetpoint?: number
|
|
108
|
+
maxCoolSetpoint?: number
|
|
109
|
+
changeableValues?: ChangeableValues
|
|
110
|
+
operationStatus?: OperationStatus
|
|
111
|
+
indoorHumidity?: number
|
|
112
|
+
displayedOutdoorHumidity?: number
|
|
113
|
+
scheduleStatus?: string
|
|
114
|
+
allowedTimeIncrements?: number
|
|
115
|
+
isUpgrading?: boolean
|
|
116
|
+
isProvisioned?: boolean
|
|
117
|
+
macID?: string
|
|
118
|
+
dataSyncStatus?: string
|
|
119
|
+
outdoorTemperature?: number
|
|
120
|
+
deadband?: number
|
|
121
|
+
hasDualSetpointStatus?: boolean
|
|
122
|
+
parentDeviceId?: number
|
|
123
|
+
service: Service
|
|
124
|
+
deviceSettings: Record<string, unknown> // DeviceSettings
|
|
125
|
+
firmwareVersion?: string
|
|
126
|
+
vacationHold?: VacationHold
|
|
127
|
+
currentSchedulePeriod?: CurrentSchedulePeriod
|
|
128
|
+
scheduleCapabilities?: ScheduleCapabilities
|
|
129
|
+
scheduleType?: ScheduleType
|
|
130
|
+
changeSource?: ChangeSource
|
|
131
|
+
partnerInfo?: PartnerInfo
|
|
132
|
+
deviceRegistrationDate?: Date
|
|
133
|
+
indoorHumidityStatus?: string
|
|
134
|
+
waterPresent: boolean
|
|
135
|
+
currentSensorReadings: CurrentSensorReadings
|
|
136
|
+
batteryRemaining: number
|
|
137
|
+
isRegistered: boolean
|
|
138
|
+
hasDeviceCheckedIn: boolean
|
|
139
|
+
isDeviceOffline: boolean
|
|
140
|
+
deviceMac: string // Device MAC address
|
|
141
|
+
dataSyncInfo: dataSyncInfo
|
|
142
|
+
lastCheckin: Date // Last time data received from device
|
|
143
|
+
actuatorValve: actuatorValve // Values specific to the valve operation
|
|
144
|
+
daylightSavingsInfo: daylightSavingsInfo // Daylight savings time config info
|
|
145
|
+
maintenance: maintenance // Maintenance settings
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface T9groups {
|
|
149
|
+
id: number
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface inBuiltSensorState {
|
|
153
|
+
roomId: number
|
|
154
|
+
roomName: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fan Settings
|
|
158
|
+
export interface Settings {
|
|
159
|
+
homeSetPoints?: HomeSetPoints
|
|
160
|
+
awaySetPoints?: AwaySetPoints
|
|
161
|
+
fan: Fan
|
|
162
|
+
temperatureMode?: TemperatureMode
|
|
163
|
+
specialMode?: SpecialMode
|
|
164
|
+
hardwareSettings?: HardwareSettings
|
|
165
|
+
devicePairingEnabled?: boolean
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface ChangeableValues {
|
|
169
|
+
mode: string
|
|
170
|
+
autoChangeoverActive?: boolean
|
|
171
|
+
heatSetpoint: number
|
|
172
|
+
coolSetpoint: number
|
|
173
|
+
thermostatSetpointStatus?: string
|
|
174
|
+
nextPeriodTime?: string
|
|
175
|
+
endHeatSetpoint?: number
|
|
176
|
+
endCoolSetpoint?: number
|
|
177
|
+
heatCoolMode: string
|
|
178
|
+
emergencyHeatActive?: boolean
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface OperationStatus {
|
|
182
|
+
mode: string
|
|
183
|
+
fanRequest?: boolean
|
|
184
|
+
circulationFanRequest?: boolean
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface Service {
|
|
188
|
+
mode: string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface AwaySetPoints {
|
|
192
|
+
awayHeatSP: number
|
|
193
|
+
awayCoolSP: number
|
|
194
|
+
smartCoolSP: number
|
|
195
|
+
smartHeatSP: number
|
|
196
|
+
useAutoSmart: boolean
|
|
197
|
+
units: string
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface HomeSetPoints {
|
|
201
|
+
homeHeatSP: number
|
|
202
|
+
homeCoolSP: number
|
|
203
|
+
units: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface TemperatureMode {
|
|
207
|
+
feelsLike?: boolean
|
|
208
|
+
air: boolean
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface SpecialMode {
|
|
212
|
+
autoChangeoverActive: boolean
|
|
213
|
+
emergencyHeatActive?: boolean
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface Fan {
|
|
217
|
+
allowedModes: string[]
|
|
218
|
+
changeableValues: FanChangeableValues
|
|
219
|
+
fanRunning: boolean
|
|
220
|
+
allowedSpeeds?: AllowedSpeed[]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface FanChangeableValues {
|
|
224
|
+
mode: string
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface AllowedSpeed {
|
|
228
|
+
item: string
|
|
229
|
+
value: Value
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface Value {
|
|
233
|
+
speed?: number
|
|
234
|
+
mode: string
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface VacationHold {
|
|
238
|
+
enabled: boolean
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface CurrentSchedulePeriod {
|
|
242
|
+
day: string
|
|
243
|
+
period: string
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface ScheduleCapabilities {
|
|
247
|
+
availableScheduleTypes: string[]
|
|
248
|
+
schedulableFan: boolean
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface ScheduleType {
|
|
252
|
+
scheduleType: string
|
|
253
|
+
scheduleSubType: string
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface ChangeSource {
|
|
257
|
+
by: string
|
|
258
|
+
name: string
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface HardwareSettings {
|
|
262
|
+
brightness: number
|
|
263
|
+
maxBrightness: number
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface PartnerInfo {
|
|
267
|
+
singleOrMultiODUConfiguration: number
|
|
268
|
+
parentDeviceModelId: number
|
|
269
|
+
parentDeviceBrandId: number
|
|
270
|
+
oduName: string
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface DeviceSettings {
|
|
274
|
+
temp: Temp
|
|
275
|
+
humidity: Humidity
|
|
276
|
+
userDefinedName: string
|
|
277
|
+
buzzerMuted: boolean
|
|
278
|
+
checkinPeriod: number
|
|
279
|
+
currentSensorReadPeriod: number
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface Humidity {
|
|
283
|
+
high: Record<string, unknown>
|
|
284
|
+
low: Record<string, unknown>
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface CurrentSensorReadings {
|
|
288
|
+
temperature: number
|
|
289
|
+
humidity: number
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface Temp {
|
|
293
|
+
high: Record<string, unknown>
|
|
294
|
+
low: Record<string, unknown>
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// T9 Room Sensors
|
|
298
|
+
export interface sensorAccessory {
|
|
299
|
+
accessoryId: number
|
|
300
|
+
accessoryAttribute: accessoryAttribute
|
|
301
|
+
accessoryValue: accessoryValue
|
|
302
|
+
roomId: number
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface accessoryAttribute {
|
|
306
|
+
type: string
|
|
307
|
+
connectionMethod: string
|
|
308
|
+
name: string
|
|
309
|
+
model: string
|
|
310
|
+
serialNumber: string
|
|
311
|
+
softwareRevision: string
|
|
312
|
+
hardwareRevision: string
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface accessoryValue {
|
|
316
|
+
coolSetpoint: number
|
|
317
|
+
heatSetpoint: number
|
|
318
|
+
indoorHumidity: number
|
|
319
|
+
indoorTemperature: number
|
|
320
|
+
motionDet: boolean
|
|
321
|
+
occupancyDet: boolean
|
|
322
|
+
excludeTemp: boolean
|
|
323
|
+
excludeMotion: boolean
|
|
324
|
+
pressure: number
|
|
325
|
+
occupancyTimeout: number
|
|
326
|
+
status: string
|
|
327
|
+
batteryStatus: string
|
|
328
|
+
rssiAverage: number
|
|
329
|
+
occupancySensitivity: string
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// T9 Room Priority
|
|
333
|
+
export interface roomPriorityStatus {
|
|
334
|
+
deviceId: string
|
|
335
|
+
status: string
|
|
336
|
+
currentPriority: CurrentPriority
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export interface CurrentPriority {
|
|
340
|
+
priorityType: string
|
|
341
|
+
selectedRooms: Record<string, unknown>
|
|
342
|
+
rooms: PriorityRooms[]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface PriorityRooms {
|
|
346
|
+
rooms: PriorityRoom
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface PriorityRoom {
|
|
350
|
+
id: number
|
|
351
|
+
roomName: string
|
|
352
|
+
roomAvgTemp: number
|
|
353
|
+
roomAvgHumidity: number
|
|
354
|
+
overallMotion: boolean
|
|
355
|
+
accessories: Accessory[]
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export interface Accessory {
|
|
359
|
+
id: number
|
|
360
|
+
type: string
|
|
361
|
+
excludeTemp: boolean
|
|
362
|
+
excludeMotion: boolean
|
|
363
|
+
temperature: number
|
|
364
|
+
status: string
|
|
365
|
+
detectMotion: boolean
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface dataSyncInfo {
|
|
369
|
+
state: string // 'NotStarted' | 'Initiated' | 'Completed' | 'Failed'
|
|
370
|
+
transactionId: string // Internal reference ID for the DataSync operation
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface actuatorValve {
|
|
374
|
+
commandSource: string// 'app' | 'wldFreeze' | 'wldLeak' | 'manual' | 'buildInLeak' | 'maintenance';
|
|
375
|
+
runningTime: number // Operation time
|
|
376
|
+
valveStatus: string // 'unknown' | 'open' | 'close' | 'notOpen' | 'notClose' | 'opening' | 'closing' | 'antiScaleOpening' | 'antiScaleClosing';
|
|
377
|
+
motorCycles: number // Count of motor operations
|
|
378
|
+
motorCurrentAverage: number
|
|
379
|
+
motorCurrentMax: number
|
|
380
|
+
deviceTemperature: number // Current temperature of device in Fahrenheit units
|
|
381
|
+
lastAntiScaleTime: Date // Last time of anti - scale operation
|
|
382
|
+
leakStatus: string // 'ok' | 'leak' | 'na' | 'err'
|
|
383
|
+
timeValveChanged: Date // Time of last valve change
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export interface daylightSavingsInfo {
|
|
387
|
+
isDaylightSaving: boolean // If device is currently using DST or not
|
|
388
|
+
nextOffsetChange: Date // Next scheduled DST changeover
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export interface maintenance {
|
|
392
|
+
antiScaleSettings: string // Current anti - scale cycle: 'OncePerWeek' | 'OncePerTwoWeeks' | 'OncePerMonth' | 'OncePerTwoMonths' | 'Disabled'
|
|
393
|
+
antiScaleDOWSettings: string // 'Sunday' | 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday'
|
|
394
|
+
antiScaleDOMSettings: number // If monthly anti - scale is used, day of the month.
|
|
395
|
+
antiScaleTimeSettings: string // Time for anti - scale in 24 hrs format
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface fanStatus {
|
|
399
|
+
changeableValues: {
|
|
400
|
+
mode: string
|
|
401
|
+
}
|
|
402
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* util.ts: homebridge-firstalert platform class.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Converts the value to celsius if the temperature units are in Fahrenheit
|
|
8
|
+
*/
|
|
9
|
+
export function toCelsius(value: number, unit: number): number {
|
|
10
|
+
if (unit === 0) {
|
|
11
|
+
return value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// celsius should be to the nearest 0.5 degree
|
|
15
|
+
return Math.round((5 / 9) * (value - 32) * 2) / 2
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Converts the value to fahrenheit if the temperature units are in Fahrenheit
|
|
20
|
+
*/
|
|
21
|
+
export function toFahrenheit(value: number, unit: number): number {
|
|
22
|
+
if (unit === 0) {
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Math.round((value * 9) / 5 + 32)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Map HomeKit Modes to FirstAlert Modes
|
|
30
|
+
export enum HomeKitModes {
|
|
31
|
+
Off = 0, // this.hap.Characteristic.TargetHeatingCoolingState.OFF
|
|
32
|
+
Heat = 1, // this.hap.Characteristic.TargetHeatingCoolingState.HEAT
|
|
33
|
+
Cool = 2, // this.hap.Characteristic.TargetHeatingCoolingState.COOL
|
|
34
|
+
Auto = 3, // this.hap.Characteristic.TargetHeatingCoolingState.AUTO
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Don't change the order of these!
|
|
38
|
+
export enum ResideoModes {
|
|
39
|
+
Off = 'Off',
|
|
40
|
+
Heat = 'Heat',
|
|
41
|
+
Cool = 'Cool',
|
|
42
|
+
Auto = 'Auto',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
export enum holdModes {
|
|
47
|
+
NoHold = 0, //this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS
|
|
48
|
+
TemporaryHold = 1, //this.hap.Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS
|
|
49
|
+
PermanentHold = 2, //this.hap.Characteristic.ProgrammableSwitchEvent.LONG_PRESS
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export enum fanModes {
|
|
53
|
+
Auto = 0, //this.hap.Characteristic.TargetFanState.AUTO
|
|
54
|
+
On = 1, //this.hap.Characteristic.TargetFanState.ON
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type resideoHold = {
|
|
58
|
+
NoHold: 'NoHold',
|
|
59
|
+
TemporaryHold: 'TemporaryHold',
|
|
60
|
+
PermanentHold: 'PermanentHold'
|
|
61
|
+
}; */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { API } from 'homebridge'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import registerPlatform from '../src/index.js'
|
|
5
|
+
import { ResideoPlatform } from '../src/platform.js'
|
|
6
|
+
import { PLATFORM_NAME, PLUGIN_NAME } from '../src/settings.js'
|
|
7
|
+
|
|
8
|
+
describe('index.ts', () => {
|
|
9
|
+
it('should register the platform with homebridge', () => {
|
|
10
|
+
const apiMock: API = {
|
|
11
|
+
registerPlatform: vi.fn(),
|
|
12
|
+
} as unknown as API
|
|
13
|
+
|
|
14
|
+
registerPlatform(apiMock)
|
|
15
|
+
|
|
16
|
+
expect(apiMock.registerPlatform).toHaveBeenCalledWith(PLUGIN_NAME, PLATFORM_NAME, ResideoPlatform)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { API, Logging } from 'homebridge'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { ResideoPlatform } from '../src/platform.js'
|
|
5
|
+
import type { ResideoPlatformConfig } from '../src/settings.js'
|
|
6
|
+
|
|
7
|
+
vi.mock('axios')
|
|
8
|
+
|
|
9
|
+
describe('resideoPlatform', () => {
|
|
10
|
+
let platform: ResideoPlatform
|
|
11
|
+
let log: Logging
|
|
12
|
+
let config: ResideoPlatformConfig
|
|
13
|
+
let api: API
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
log = {
|
|
17
|
+
info: vi.fn(),
|
|
18
|
+
warn: vi.fn(),
|
|
19
|
+
error: vi.fn(),
|
|
20
|
+
debug: vi.fn(),
|
|
21
|
+
} as unknown as Logging
|
|
22
|
+
|
|
23
|
+
config = {
|
|
24
|
+
platform: 'FirstAlert',
|
|
25
|
+
name: 'Test Platform',
|
|
26
|
+
credentials: {
|
|
27
|
+
accessToken: 'testAccessToken',
|
|
28
|
+
consumerKey: 'testConsumerKey',
|
|
29
|
+
consumerSecret: 'testConsumerSecret',
|
|
30
|
+
refreshToken: 'testRefreshToken',
|
|
31
|
+
},
|
|
32
|
+
options: {
|
|
33
|
+
refreshRate: 120,
|
|
34
|
+
pushRate: 0.1,
|
|
35
|
+
devices: [],
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
api = {
|
|
40
|
+
hap: {
|
|
41
|
+
uuid: {
|
|
42
|
+
generate: vi.fn().mockReturnValue('test-uuid'),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
platformAccessory: vi.fn().mockImplementation((name, uuid) => ({
|
|
46
|
+
displayName: name,
|
|
47
|
+
UUID: uuid,
|
|
48
|
+
context: {},
|
|
49
|
+
})),
|
|
50
|
+
user: {
|
|
51
|
+
configPath: vi.fn().mockReturnValue('/path/to/config.json'),
|
|
52
|
+
},
|
|
53
|
+
on: vi.fn(),
|
|
54
|
+
updatePlatformAccessories: vi.fn(),
|
|
55
|
+
} as unknown as API
|
|
56
|
+
|
|
57
|
+
platform = new ResideoPlatform(log, config, api)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should initialize platform and call discoverDevices', async () => {
|
|
61
|
+
// The constructor should call discoverDevices (which logs an error if no refreshToken)
|
|
62
|
+
expect(platform).toBeDefined()
|
|
63
|
+
// Optionally, you can spy on log methods or client methods here
|
|
64
|
+
})
|
|
65
|
+
})
|