homebridge-securitysystem 9.1.2 → 10.0.0-beta.2

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 (163) hide show
  1. package/.github/workflows/publish.yml +3 -0
  2. package/AGENTS.md +214 -0
  3. package/dist/conditions/already-triggered-condition.d.ts +10 -0
  4. package/dist/conditions/already-triggered-condition.js +16 -0
  5. package/dist/conditions/already-triggered-condition.js.map +1 -0
  6. package/dist/conditions/arming-in-progress-condition.d.ts +10 -0
  7. package/dist/conditions/arming-in-progress-condition.js +15 -0
  8. package/dist/conditions/arming-in-progress-condition.js.map +1 -0
  9. package/dist/conditions/arming-lock-condition.d.ts +13 -0
  10. package/dist/conditions/arming-lock-condition.js +42 -0
  11. package/dist/conditions/arming-lock-condition.js.map +1 -0
  12. package/dist/conditions/condition.d.ts +15 -0
  13. package/dist/conditions/condition.js +9 -0
  14. package/dist/conditions/condition.js.map +1 -0
  15. package/dist/conditions/double-knock-condition.d.ts +22 -0
  16. package/dist/conditions/double-knock-condition.js +70 -0
  17. package/dist/conditions/double-knock-condition.js.map +1 -0
  18. package/dist/conditions/not-armed-condition.d.ts +10 -0
  19. package/dist/conditions/not-armed-condition.js +20 -0
  20. package/dist/conditions/not-armed-condition.js.map +1 -0
  21. package/dist/constants/default-constant.d.ts +26 -0
  22. package/dist/constants/default-constant.js +28 -0
  23. package/dist/constants/default-constant.js.map +1 -0
  24. package/dist/constants/homekit-constant.d.ts +4 -0
  25. package/dist/constants/homekit-constant.js +5 -0
  26. package/dist/constants/homekit-constant.js.map +1 -0
  27. package/dist/constants/switch-uuid-constant.d.ts +23 -0
  28. package/dist/constants/switch-uuid-constant.js +24 -0
  29. package/dist/constants/switch-uuid-constant.js.map +1 -0
  30. package/dist/handlers/sensor-handler.d.ts +17 -0
  31. package/dist/handlers/sensor-handler.js +53 -0
  32. package/dist/handlers/sensor-handler.js.map +1 -0
  33. package/dist/handlers/state-handler.d.ts +41 -0
  34. package/dist/handlers/state-handler.js +213 -0
  35. package/dist/handlers/state-handler.js.map +1 -0
  36. package/dist/handlers/switch-handler.d.ts +27 -0
  37. package/dist/handlers/switch-handler.js +139 -0
  38. package/dist/handlers/switch-handler.js.map +1 -0
  39. package/dist/handlers/trip-handler.d.ts +45 -0
  40. package/dist/handlers/trip-handler.js +179 -0
  41. package/dist/handlers/trip-handler.js.map +1 -0
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +7 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/interfaces/condition-context-interface.d.ts +13 -0
  46. package/dist/interfaces/condition-context-interface.js +2 -0
  47. package/dist/interfaces/condition-context-interface.js.map +1 -0
  48. package/dist/interfaces/hap-types-interface.d.ts +9 -0
  49. package/dist/interfaces/hap-types-interface.js +2 -0
  50. package/dist/interfaces/hap-types-interface.js.map +1 -0
  51. package/dist/interfaces/options-interface.d.ts +93 -0
  52. package/dist/interfaces/options-interface.js +2 -0
  53. package/dist/interfaces/options-interface.js.map +1 -0
  54. package/dist/interfaces/persisted-state-interface.d.ts +6 -0
  55. package/dist/interfaces/persisted-state-interface.js +2 -0
  56. package/dist/interfaces/persisted-state-interface.js.map +1 -0
  57. package/dist/interfaces/service-registry-interface.d.ts +26 -0
  58. package/dist/interfaces/service-registry-interface.js +2 -0
  59. package/dist/interfaces/service-registry-interface.js.map +1 -0
  60. package/dist/interfaces/system-state-interface.d.ts +21 -0
  61. package/dist/interfaces/system-state-interface.js +2 -0
  62. package/dist/interfaces/system-state-interface.js.map +1 -0
  63. package/dist/security-system.d.ts +26 -0
  64. package/dist/security-system.js +377 -0
  65. package/dist/security-system.js.map +1 -0
  66. package/dist/services/audio-service.d.ts +21 -0
  67. package/dist/services/audio-service.js +113 -0
  68. package/dist/services/audio-service.js.map +1 -0
  69. package/dist/services/command-service.d.ts +16 -0
  70. package/dist/services/command-service.js +65 -0
  71. package/dist/services/command-service.js.map +1 -0
  72. package/dist/services/event-bus-service.d.ts +12 -0
  73. package/dist/services/event-bus-service.js +17 -0
  74. package/dist/services/event-bus-service.js.map +1 -0
  75. package/dist/services/server-service.d.ts +25 -0
  76. package/dist/services/server-service.js +140 -0
  77. package/dist/services/server-service.js.map +1 -0
  78. package/dist/services/storage-service.d.ts +14 -0
  79. package/dist/services/storage-service.js +70 -0
  80. package/dist/services/storage-service.js.map +1 -0
  81. package/dist/services/webhook-service.d.ts +16 -0
  82. package/dist/services/webhook-service.js +73 -0
  83. package/dist/services/webhook-service.js.map +1 -0
  84. package/dist/tests/conditions.test.d.ts +1 -0
  85. package/dist/tests/conditions.test.js +151 -0
  86. package/dist/tests/conditions.test.js.map +1 -0
  87. package/dist/tests/state-handler.test.d.ts +1 -0
  88. package/dist/tests/state-handler.test.js +171 -0
  89. package/dist/tests/state-handler.test.js.map +1 -0
  90. package/dist/tests/trip-handler.test.d.ts +1 -0
  91. package/dist/tests/trip-handler.test.js +160 -0
  92. package/dist/tests/trip-handler.test.js.map +1 -0
  93. package/dist/types/event-payload-map-type.d.ts +9 -0
  94. package/dist/types/event-payload-map-type.js +2 -0
  95. package/dist/types/event-payload-map-type.js.map +1 -0
  96. package/dist/types/event-type.d.ts +24 -0
  97. package/dist/types/event-type.js +9 -0
  98. package/dist/types/event-type.js.map +1 -0
  99. package/dist/types/mode-type.d.ts +4 -0
  100. package/dist/types/mode-type.js +2 -0
  101. package/dist/types/mode-type.js.map +1 -0
  102. package/dist/types/origin-type.d.ts +9 -0
  103. package/dist/types/origin-type.js +11 -0
  104. package/dist/types/origin-type.js.map +1 -0
  105. package/dist/types/security-state-type.d.ts +11 -0
  106. package/dist/types/security-state-type.js +13 -0
  107. package/dist/types/security-state-type.js.map +1 -0
  108. package/dist/utils/log-util.d.ts +4 -0
  109. package/dist/utils/log-util.js +47 -0
  110. package/dist/utils/log-util.js.map +1 -0
  111. package/dist/utils/options-util.d.ts +6 -0
  112. package/dist/utils/options-util.js +171 -0
  113. package/dist/utils/options-util.js.map +1 -0
  114. package/dist/utils/state-util.d.ts +8 -0
  115. package/dist/utils/state-util.js +29 -0
  116. package/dist/utils/state-util.js.map +1 -0
  117. package/eslint.config.js +35 -0
  118. package/package.json +40 -34
  119. package/src/@types/homebridge-lib.d.ts +14 -0
  120. package/src/conditions/already-triggered-condition.ts +18 -0
  121. package/src/conditions/arming-in-progress-condition.ts +17 -0
  122. package/src/conditions/arming-lock-condition.ts +48 -0
  123. package/src/conditions/condition.ts +17 -0
  124. package/src/conditions/double-knock-condition.ts +82 -0
  125. package/src/conditions/not-armed-condition.ts +24 -0
  126. package/src/constants/default-constant.ts +28 -0
  127. package/src/constants/homekit-constant.ts +5 -0
  128. package/src/constants/switch-uuid-constant.ts +23 -0
  129. package/src/handlers/sensor-handler.ts +81 -0
  130. package/src/handlers/state-handler.ts +268 -0
  131. package/src/handlers/switch-handler.ts +166 -0
  132. package/src/handlers/trip-handler.ts +219 -0
  133. package/src/index.ts +9 -0
  134. package/src/interfaces/condition-context-interface.ts +14 -0
  135. package/src/interfaces/hap-types-interface.ts +11 -0
  136. package/src/interfaces/options-interface.ts +120 -0
  137. package/src/interfaces/persisted-state-interface.ts +7 -0
  138. package/src/interfaces/service-registry-interface.ts +37 -0
  139. package/src/interfaces/system-state-interface.ts +25 -0
  140. package/src/security-system.ts +433 -0
  141. package/src/services/audio-service.ts +133 -0
  142. package/src/services/command-service.ts +76 -0
  143. package/src/services/event-bus-service.ts +21 -0
  144. package/src/services/server-service.ts +176 -0
  145. package/src/services/storage-service.ts +80 -0
  146. package/src/services/webhook-service.ts +83 -0
  147. package/src/tests/conditions.test.ts +180 -0
  148. package/src/tests/state-handler.test.ts +200 -0
  149. package/src/tests/trip-handler.test.ts +182 -0
  150. package/src/types/event-payload-map-type.ts +15 -0
  151. package/src/types/event-type.ts +29 -0
  152. package/src/types/mode-type.ts +4 -0
  153. package/src/types/origin-type.ts +9 -0
  154. package/src/types/security-state-type.ts +11 -0
  155. package/src/utils/log-util.ts +54 -0
  156. package/src/utils/options-util.ts +202 -0
  157. package/src/utils/state-util.ts +32 -0
  158. package/tsconfig.json +25 -0
  159. package/vitest.config.ts +8 -0
  160. package/.eslintrc.js +0 -16
  161. package/.vscode/settings.json +0 -12
  162. package/src/index.js +0 -2525
  163. package/src/utils/options.js +0 -409
@@ -0,0 +1,81 @@
1
+ import type { Logging } from 'homebridge';
2
+ import { SENSOR_PULSE_MS } from '../constants/homekit-constant.js';
3
+ import type { ServiceRegistry } from '../interfaces/service-registry-interface.js';
4
+ import type { CharacteristicConstructor } from '../interfaces/hap-types-interface.js';
5
+
6
+ /** Manages all motion sensor characteristic updates. */
7
+ export class SensorHandler {
8
+ constructor(
9
+ private readonly services: ServiceRegistry,
10
+ private readonly Characteristic: CharacteristicConstructor,
11
+ private readonly log: Logging,
12
+ ) {}
13
+
14
+ // ── Arming sensor ──────────────────────────────────────────────────────────
15
+
16
+ updateArmingMotionSensor(value: boolean): void {
17
+ this.services.armingMotionSensorService.updateCharacteristic(
18
+ this.Characteristic.MotionDetected,
19
+ value,
20
+ );
21
+ }
22
+
23
+ resetArmingMotionSensor(): void {
24
+ const current = this.services.armingMotionSensorService
25
+ .getCharacteristic(this.Characteristic.MotionDetected).value;
26
+
27
+ if (current) {
28
+ this.services.armingMotionSensorService.updateCharacteristic(
29
+ this.Characteristic.MotionDetected,
30
+ false,
31
+ );
32
+ }
33
+ }
34
+
35
+ // ── Tripped sensor ─────────────────────────────────────────────────────────
36
+
37
+ pulseTrippedMotionSensor(): void {
38
+ this.services.trippedMotionSensorService.updateCharacteristic(
39
+ this.Characteristic.MotionDetected,
40
+ true,
41
+ );
42
+ this.scheduleReset(this.services.trippedMotionSensorService);
43
+ }
44
+
45
+ resetTrippedMotionSensor(): void {
46
+ const char = this.services.trippedMotionSensorService
47
+ .getCharacteristic(this.Characteristic.MotionDetected);
48
+ if (char.value) {
49
+ char.updateValue(false);
50
+ }
51
+ }
52
+
53
+ // ── Triggered sensor ───────────────────────────────────────────────────────
54
+
55
+ pulseTriggeredMotionSensor(): void {
56
+ this.services.triggeredMotionSensorService.updateCharacteristic(
57
+ this.Characteristic.MotionDetected,
58
+ true,
59
+ );
60
+ this.scheduleReset(this.services.triggeredMotionSensorService);
61
+ }
62
+
63
+ // ── Reset sensor ───────────────────────────────────────────────────────────
64
+
65
+ pulseResetMotionSensor(): void {
66
+ this.services.triggeredResetMotionSensorService.updateCharacteristic(
67
+ this.Characteristic.MotionDetected,
68
+ true,
69
+ );
70
+ this.scheduleReset(this.services.triggeredResetMotionSensorService);
71
+ this.log.debug('Reset sensor (Triggered)');
72
+ }
73
+
74
+ // ── Private helpers ────────────────────────────────────────────────────────
75
+
76
+ private scheduleReset(service: ServiceRegistry[keyof ServiceRegistry]): void {
77
+ setTimeout(() => {
78
+ service.updateCharacteristic(this.Characteristic.MotionDetected, false);
79
+ }, SENSOR_PULSE_MS);
80
+ }
81
+ }
@@ -0,0 +1,268 @@
1
+ import type { Logging } from 'homebridge';
2
+ import type { CharacteristicConstructor } from '../interfaces/hap-types-interface.js';
3
+ import { SecurityState } from '../types/security-state-type.js';
4
+ import { OriginType } from '../types/origin-type.js';
5
+ import { stateToMode, capitalise } from '../utils/state-util.js';
6
+ import { modeToState } from '../utils/state-util.js';
7
+ import type { ServiceRegistry } from '../interfaces/service-registry-interface.js';
8
+ import type { SystemState } from '../interfaces/system-state-interface.js';
9
+ import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
10
+ import type { EventBusService } from '../services/event-bus-service.js';
11
+ import { EventType } from '../types/event-type.js';
12
+ import type { StorageService } from '../services/storage-service.js';
13
+ import type { AudioService } from '../services/audio-service.js';
14
+ import type { TripHandler } from './trip-handler.js';
15
+ import type { SwitchHandler } from './switch-handler.js';
16
+ import type { SensorHandler } from './sensor-handler.js';
17
+
18
+ /** Manages the core security-system state machine: arming, triggering, and resetting. */
19
+ export class StateHandler {
20
+ private tripHandler!: TripHandler;
21
+ private switchHandler!: SwitchHandler;
22
+ private sensorHandler!: SensorHandler;
23
+
24
+ constructor(
25
+ private readonly services: ServiceRegistry,
26
+ private readonly state: SystemState,
27
+ private readonly options: SecuritySystemOptions,
28
+ private readonly Characteristic: CharacteristicConstructor,
29
+ private readonly log: Logging,
30
+ private readonly bus: EventBusService,
31
+ private readonly storageService: StorageService,
32
+ private readonly audio: AudioService,
33
+ ) {}
34
+
35
+ setHandlers(trip: TripHandler, sw: SwitchHandler, sensor: SensorHandler): void {
36
+ this.tripHandler = trip;
37
+ this.switchHandler = sw;
38
+ this.sensorHandler = sensor;
39
+ }
40
+
41
+ // ── Public API ─────────────────────────────────────────────────────────────
42
+
43
+ setCurrentState(state: SecurityState, origin: OriginType): void {
44
+ this.sensorHandler.resetArmingMotionSensor();
45
+ this.audio.play('current', state);
46
+
47
+ if (this.state.currentState === state) {
48
+ this.log.warn('Current mode (Already set)');
49
+ return;
50
+ }
51
+
52
+ this.state.currentState = state;
53
+ this.services.mainService.setCharacteristic(
54
+ this.Characteristic.SecuritySystemCurrentState,
55
+ state,
56
+ );
57
+ this.logMode('Current', state);
58
+
59
+ this.handleCurrentStateChange(origin);
60
+ this.storageService.save(this.state);
61
+ }
62
+
63
+ updateTargetState(state: SecurityState, origin: OriginType, delay: number): boolean {
64
+ if (this.isBadTargetState(state)) {
65
+ return false;
66
+ }
67
+
68
+ this.state.targetState = state;
69
+ this.logMode('Target', state);
70
+
71
+ if (origin === OriginType.INTERNAL || origin === OriginType.EXTERNAL) {
72
+ this.services.mainService.updateCharacteristic(
73
+ this.Characteristic.SecuritySystemTargetState,
74
+ state,
75
+ );
76
+ }
77
+
78
+ this.handleTargetStateChange(origin);
79
+
80
+ if (state === this.state.currentState) {
81
+ this.setCurrentState(state, origin);
82
+ return false;
83
+ }
84
+
85
+ const armSeconds = delay > 0 ? delay : 0;
86
+
87
+ if (armSeconds === 0) {
88
+ this.setCurrentState(state, origin);
89
+ return false;
90
+ }
91
+
92
+ this.state.isArming = true;
93
+ this.handleArmingState();
94
+ this.log.info(`Arm delay (${armSeconds}s)`);
95
+
96
+ this.state.armTimeout = setTimeout(() => {
97
+ this.state.armTimeout = null;
98
+ this.state.isArming = false;
99
+ this.setCurrentState(state, origin);
100
+ }, armSeconds * 1000);
101
+
102
+ return true;
103
+ }
104
+
105
+ getArmingSeconds(state: SecurityState): number {
106
+ const isTriggered = this.state.currentState === SecurityState.TRIGGERED;
107
+ const isOff = state === SecurityState.OFF;
108
+
109
+ if (isTriggered || isOff) {
110
+ return 0;
111
+ }
112
+
113
+ if (state === SecurityState.HOME && this.options.homeArmSeconds !== null) {
114
+ return this.options.homeArmSeconds;
115
+ }
116
+ if (state === SecurityState.AWAY && this.options.awayArmSeconds !== null) {
117
+ return this.options.awayArmSeconds;
118
+ }
119
+ if (state === SecurityState.NIGHT && this.options.nightArmSeconds !== null) {
120
+ return this.options.nightArmSeconds;
121
+ }
122
+
123
+ return this.options.armSeconds;
124
+ }
125
+
126
+ resetTimers(): void {
127
+ if (this.state.triggerTimeout) {
128
+ clearTimeout(this.state.triggerTimeout);
129
+ this.state.triggerTimeout = null;
130
+ this.log.debug('Trigger timeout (Cleared)');
131
+ }
132
+ if (this.state.armTimeout) {
133
+ clearTimeout(this.state.armTimeout);
134
+ this.state.armTimeout = null;
135
+ this.log.debug('Arming timeout (Cleared)');
136
+ }
137
+ if (this.state.triggeredMotionSensorInterval) {
138
+ clearInterval(this.state.triggeredMotionSensorInterval);
139
+ this.state.triggeredMotionSensorInterval = null;
140
+ this.log.debug('Triggered interval (Cleared)');
141
+ }
142
+ if (this.state.trippedMotionSensorInterval) {
143
+ clearInterval(this.state.trippedMotionSensorInterval);
144
+ this.state.trippedMotionSensorInterval = null;
145
+ this.log.debug('Tripped interval (Cleared)');
146
+ }
147
+ if (this.state.doubleKnockTimeout) {
148
+ clearTimeout(this.state.doubleKnockTimeout);
149
+ this.state.doubleKnockTimeout = null;
150
+ this.log.debug('Double-knock timeout (Cleared)');
151
+ }
152
+ if (this.state.pauseTimeout) {
153
+ clearTimeout(this.state.pauseTimeout);
154
+ this.state.pauseTimeout = null;
155
+ this.log.debug('Pause timeout (Cleared)');
156
+ }
157
+ if (this.state.resetTimeout) {
158
+ clearTimeout(this.state.resetTimeout);
159
+ this.state.resetTimeout = null;
160
+ this.log.debug('Reset timeout (Cleared)');
161
+ }
162
+ }
163
+
164
+ getAvailableTargetStates(): SecurityState[] {
165
+ const all = [SecurityState.HOME, SecurityState.AWAY, SecurityState.NIGHT, SecurityState.OFF];
166
+ const disabled = this.options.disabledModes.map(m => modeToState(m.toLowerCase()));
167
+ return all.filter(s => !disabled.includes(s));
168
+ }
169
+
170
+ logMode(type: string, state: SecurityState | string): void {
171
+ const mode = capitalise(stateToMode(state as SecurityState));
172
+ this.log.info(`${type} mode (${mode})`);
173
+ }
174
+
175
+ // ── Private helpers ────────────────────────────────────────────────────────
176
+
177
+ private isBadTargetState(state: SecurityState): boolean {
178
+ const isTriggered = this.state.currentState === SecurityState.TRIGGERED;
179
+ const alreadySet = this.state.targetState === state;
180
+
181
+ if (alreadySet && !isTriggered) {
182
+ this.log.warn('Target mode (Already set)');
183
+ return true;
184
+ }
185
+
186
+ if (!this.state.availableTargetStates.includes(state)) {
187
+ this.log.warn('Target mode (Disabled)');
188
+ return true;
189
+ }
190
+
191
+ const hasLock = this.options.armingLockSwitch || this.options.armingLockSwitches;
192
+ if (state !== SecurityState.OFF && hasLock && this.switchHandler.isArmingLocked(state)) {
193
+ this.log.warn('Arming lock (Not allowed)');
194
+ return true;
195
+ }
196
+
197
+ return false;
198
+ }
199
+
200
+ private handleTargetStateChange(origin: OriginType): void {
201
+ this.resetTimers();
202
+ this.tripHandler.resetTripSwitches();
203
+ this.sensorHandler.resetTrippedMotionSensor();
204
+ this.switchHandler.resetModeSwitches();
205
+ this.switchHandler.updateModeSwitches();
206
+
207
+ this.bus.emit(EventType.TARGET_CHANGED, { state: this.state.targetState, origin });
208
+
209
+ if (this.state.currentState === SecurityState.TRIGGERED) {
210
+ this.sensorHandler.pulseResetMotionSensor();
211
+ }
212
+
213
+ this.state.isKnocked = false;
214
+ }
215
+
216
+ private handleCurrentStateChange(origin: OriginType): void {
217
+ if (this.state.currentState === SecurityState.TRIGGERED) {
218
+ this.handleTriggeredState();
219
+
220
+ if (this.options.testMode) {
221
+ return;
222
+ }
223
+ }
224
+
225
+ this.tripHandler.resetTripSwitches();
226
+ this.bus.emit(EventType.CURRENT_CHANGED, { state: this.state.currentState, origin });
227
+ }
228
+
229
+ private handleTriggeredState(): void {
230
+ if (this.state.trippedMotionSensorInterval) {
231
+ clearInterval(this.state.trippedMotionSensorInterval);
232
+ this.state.trippedMotionSensorInterval = null;
233
+ }
234
+
235
+ if (this.options.triggeredMotionSensor) {
236
+ this.state.triggeredMotionSensorInterval = setInterval(
237
+ () => this.sensorHandler.pulseTriggeredMotionSensor(),
238
+ this.options.triggeredMotionSensorSeconds * 1000,
239
+ );
240
+ }
241
+
242
+ this.state.resetTimeout = setTimeout(() => {
243
+ this.state.resetTimeout = null;
244
+ this.log.info('Reset (Finished)');
245
+ this.sensorHandler.pulseResetMotionSensor();
246
+
247
+ if (this.options.resetOffFlow) {
248
+ this.resetViaOffMode();
249
+ } else {
250
+ this.setCurrentState(this.state.targetState, OriginType.EXTERNAL);
251
+ }
252
+ }, this.options.resetMinutes * 60 * 1000);
253
+ }
254
+
255
+ private handleArmingState(): void {
256
+ this.sensorHandler.updateArmingMotionSensor(true);
257
+ this.bus.emit(EventType.ARMING, { state: this.state.targetState });
258
+ }
259
+
260
+ private resetViaOffMode(): void {
261
+ const original = this.state.targetState;
262
+ this.updateTargetState(SecurityState.OFF, OriginType.INTERNAL, 0);
263
+
264
+ setTimeout(() => {
265
+ this.updateTargetState(original, OriginType.INTERNAL, this.getArmingSeconds(original));
266
+ }, 100);
267
+ }
268
+ }
@@ -0,0 +1,166 @@
1
+ import type { Logging } from 'homebridge';
2
+ import type { CharacteristicConstructor } from '../interfaces/hap-types-interface.js';
3
+ import { SecurityState } from '../types/security-state-type.js';
4
+ import { HK_NOT_ALLOWED_IN_CURRENT_STATE } from '../constants/homekit-constant.js';
5
+ import type { ServiceRegistry } from '../interfaces/service-registry-interface.js';
6
+ import type { SystemState } from '../interfaces/system-state-interface.js';
7
+ import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
8
+ import type { StateHandler } from './state-handler.js';
9
+ import { OriginType } from '../types/origin-type.js';
10
+ import { capitalise } from '../utils/state-util.js';
11
+
12
+ /** Handles all mode switches, arming-lock switches, and the pause/extended switches. */
13
+ export class SwitchHandler {
14
+ private stateHandler!: StateHandler;
15
+
16
+ constructor(
17
+ private readonly services: ServiceRegistry,
18
+ private readonly state: SystemState,
19
+ private readonly options: SecuritySystemOptions,
20
+ private readonly Characteristic: CharacteristicConstructor,
21
+ private readonly log: Logging,
22
+ ) {}
23
+
24
+ setStateHandler(handler: StateHandler): void {
25
+ this.stateHandler = handler;
26
+ }
27
+
28
+ // ── Mode switches ──────────────────────────────────────────────────────────
29
+
30
+ setModeSwitch(mode: SecurityState, value: boolean): number | null {
31
+ if (!value) {
32
+ return HK_NOT_ALLOWED_IN_CURRENT_STATE;
33
+ }
34
+ const delay = this.stateHandler.getArmingSeconds(mode);
35
+ this.stateHandler.updateTargetState(mode, OriginType.INTERNAL, delay);
36
+ return null;
37
+ }
38
+
39
+ setModeOffSwitch(value: boolean): number | null {
40
+ if (!value) {
41
+ return HK_NOT_ALLOWED_IN_CURRENT_STATE;
42
+ }
43
+ this.stateHandler.updateTargetState(SecurityState.OFF, OriginType.INTERNAL, 0);
44
+ return null;
45
+ }
46
+
47
+ setModeAwayExtendedSwitch(value: boolean): number | null {
48
+ if (!value) {
49
+ return HK_NOT_ALLOWED_IN_CURRENT_STATE;
50
+ }
51
+ const delay = this.stateHandler.getArmingSeconds(SecurityState.AWAY);
52
+ this.stateHandler.updateTargetState(SecurityState.AWAY, OriginType.INTERNAL, delay);
53
+ return null;
54
+ }
55
+
56
+ setModePauseSwitch(value: boolean): number | null {
57
+ if (this.state.currentState === SecurityState.TRIGGERED) {
58
+ this.log.warn('Mode pause (Alarm is triggered)');
59
+ return HK_NOT_ALLOWED_IN_CURRENT_STATE;
60
+ }
61
+
62
+ if (value) {
63
+ if (this.state.currentState === SecurityState.OFF) {
64
+ this.log.warn('Mode pause (Not armed)');
65
+ return HK_NOT_ALLOWED_IN_CURRENT_STATE;
66
+ }
67
+
68
+ this.log.info('Mode pause (Started)');
69
+ this.state.pausedCurrentState = this.state.currentState;
70
+ this.stateHandler.updateTargetState(SecurityState.OFF, OriginType.INTERNAL, 0);
71
+
72
+ if (this.options.pauseMinutes !== 0) {
73
+ this.state.pauseTimeout = setTimeout(() => {
74
+ this.log.info('Mode pause (Finished)');
75
+ const prev = this.state.pausedCurrentState ?? this.state.defaultState;
76
+ this.stateHandler.updateTargetState(prev, OriginType.INTERNAL, this.stateHandler.getArmingSeconds(prev));
77
+ }, this.options.pauseMinutes * 60 * 1000);
78
+ }
79
+ } else {
80
+ this.log.info('Mode pause (Cancelled)');
81
+
82
+ if (this.state.pauseTimeout !== null) {
83
+ clearTimeout(this.state.pauseTimeout);
84
+ this.state.pauseTimeout = null;
85
+ }
86
+
87
+ const prev = this.state.pausedCurrentState ?? this.state.defaultState;
88
+ this.stateHandler.updateTargetState(prev, OriginType.INTERNAL, this.stateHandler.getArmingSeconds(prev));
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ // ── Arming lock switches ───────────────────────────────────────────────────
95
+
96
+ updateArmingLock(mode: string, value: boolean): boolean {
97
+ this.logArmingLock(mode, value);
98
+
99
+ const map: Record<string, keyof ServiceRegistry> = {
100
+ global: 'armingLockSwitchService',
101
+ home: 'armingLockHomeSwitchService',
102
+ away: 'armingLockAwaySwitchService',
103
+ night: 'armingLockNightSwitchService',
104
+ };
105
+
106
+ const key = map[mode];
107
+ if (!key) {
108
+ this.log.debug(`Unknown arming lock mode (${mode})`);
109
+ return false;
110
+ }
111
+
112
+ this.services[key].getCharacteristic(this.Characteristic.On).updateValue(value);
113
+ return true;
114
+ }
115
+
116
+ isArmingLocked(targetState: SecurityState): boolean {
117
+ if (this.services.armingLockSwitchService.getCharacteristic(this.Characteristic.On).value) {
118
+ return true;
119
+ }
120
+
121
+ const modeMap: Record<number, keyof ServiceRegistry> = {
122
+ [SecurityState.HOME]: 'armingLockHomeSwitchService',
123
+ [SecurityState.AWAY]: 'armingLockAwaySwitchService',
124
+ [SecurityState.NIGHT]: 'armingLockNightSwitchService',
125
+ };
126
+
127
+ const svcKey = modeMap[targetState];
128
+ return svcKey ? Boolean(this.services[svcKey].getCharacteristic(this.Characteristic.On).value) : false;
129
+ }
130
+
131
+ // ── Mode switch display ────────────────────────────────────────────────────
132
+
133
+ resetModeSwitches(): void {
134
+ const switches: Array<keyof ServiceRegistry> = [
135
+ 'modeHomeSwitchService', 'modeAwaySwitchService', 'modeNightSwitchService',
136
+ 'modeOffSwitchService', 'modeAwayExtendedSwitchService', 'modePauseSwitchService',
137
+ ];
138
+
139
+ for (const key of switches) {
140
+ const char = this.services[key].getCharacteristic(this.Characteristic.On);
141
+ if (char.value) {
142
+ char.updateValue(false);
143
+ this.log.debug(`${key} (Off)`);
144
+ }
145
+ }
146
+ }
147
+
148
+ updateModeSwitches(): void {
149
+ const modeMap: Partial<Record<SecurityState, keyof ServiceRegistry>> = {
150
+ [SecurityState.HOME]: 'modeHomeSwitchService',
151
+ [SecurityState.AWAY]: 'modeAwaySwitchService',
152
+ [SecurityState.NIGHT]: 'modeNightSwitchService',
153
+ [SecurityState.OFF]: 'modeOffSwitchService',
154
+ };
155
+
156
+ const key = modeMap[this.state.targetState];
157
+ if (key) {
158
+ this.services[key].updateCharacteristic(this.Characteristic.On, true);
159
+ this.log.debug(`${key} (On)`);
160
+ }
161
+ }
162
+
163
+ private logArmingLock(mode: string, value: boolean): void {
164
+ this.log.info(`Arming lock [${capitalise(mode)}] (${value ? 'On' : 'Off'})`);
165
+ }
166
+ }