homebridge-securitysystem 0.0.1-matter

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 (257) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +46 -0
  3. package/.github/ISSUE_TEMPLATE/feature-request.yml +17 -0
  4. package/.github/ISSUE_TEMPLATE/questions-answers.yml +40 -0
  5. package/.github/demo.jpg +0 -0
  6. package/.github/dependabot.yml +19 -0
  7. package/.github/release.yml +36 -0
  8. package/.github/workflows/build-package.yml +49 -0
  9. package/.github/workflows/bump-version.yml +59 -0
  10. package/.github/workflows/dependency-review.yml +26 -0
  11. package/.github/workflows/publish.yml +155 -0
  12. package/.github/workflows/stale-activity.yml +27 -0
  13. package/AGENTS.md +247 -0
  14. package/CHANGELOG.md +572 -0
  15. package/CODE_OF_CONDUCT.md +128 -0
  16. package/CONTRIBUTING.md +5 -0
  17. package/LICENSE +21 -0
  18. package/README.md +47 -0
  19. package/SECURITY.md +12 -0
  20. package/config.schema.json +934 -0
  21. package/dist/conditions/already-triggered-condition.d.ts +10 -0
  22. package/dist/conditions/already-triggered-condition.js +22 -0
  23. package/dist/conditions/already-triggered-condition.js.map +1 -0
  24. package/dist/conditions/arming-in-progress-condition.d.ts +10 -0
  25. package/dist/conditions/arming-in-progress-condition.js +21 -0
  26. package/dist/conditions/arming-in-progress-condition.js.map +1 -0
  27. package/dist/conditions/arming-lock-condition.d.ts +10 -0
  28. package/dist/conditions/arming-lock-condition.js +43 -0
  29. package/dist/conditions/arming-lock-condition.js.map +1 -0
  30. package/dist/conditions/condition.d.ts +22 -0
  31. package/dist/conditions/condition.js +19 -0
  32. package/dist/conditions/condition.js.map +1 -0
  33. package/dist/conditions/double-knock-condition.d.ts +24 -0
  34. package/dist/conditions/double-knock-condition.js +73 -0
  35. package/dist/conditions/double-knock-condition.js.map +1 -0
  36. package/dist/conditions/not-armed-condition.d.ts +10 -0
  37. package/dist/conditions/not-armed-condition.js +26 -0
  38. package/dist/conditions/not-armed-condition.js.map +1 -0
  39. package/dist/conditions/trigger-already-running-condition.d.ts +10 -0
  40. package/dist/conditions/trigger-already-running-condition.js +21 -0
  41. package/dist/conditions/trigger-already-running-condition.js.map +1 -0
  42. package/dist/constants/default-constant.d.ts +24 -0
  43. package/dist/constants/default-constant.js +27 -0
  44. package/dist/constants/default-constant.js.map +1 -0
  45. package/dist/constants/homekit-constant.d.ts +2 -0
  46. package/dist/constants/homekit-constant.js +3 -0
  47. package/dist/constants/homekit-constant.js.map +1 -0
  48. package/dist/constants/matter-constant.d.ts +21 -0
  49. package/dist/constants/matter-constant.js +61 -0
  50. package/dist/constants/matter-constant.js.map +1 -0
  51. package/dist/handlers/sensor-handler.d.ts +24 -0
  52. package/dist/handlers/sensor-handler.js +63 -0
  53. package/dist/handlers/sensor-handler.js.map +1 -0
  54. package/dist/handlers/state-handler.d.ts +49 -0
  55. package/dist/handlers/state-handler.js +204 -0
  56. package/dist/handlers/state-handler.js.map +1 -0
  57. package/dist/handlers/switch-handler.d.ts +28 -0
  58. package/dist/handlers/switch-handler.js +95 -0
  59. package/dist/handlers/switch-handler.js.map +1 -0
  60. package/dist/handlers/trip-handler.d.ts +44 -0
  61. package/dist/handlers/trip-handler.js +156 -0
  62. package/dist/handlers/trip-handler.js.map +1 -0
  63. package/dist/homekit/homekit-registrar.d.ts +12 -0
  64. package/dist/homekit/homekit-registrar.js +23 -0
  65. package/dist/homekit/homekit-registrar.js.map +1 -0
  66. package/dist/homekit/service-factory.d.ts +12 -0
  67. package/dist/homekit/service-factory.js +24 -0
  68. package/dist/homekit/service-factory.js.map +1 -0
  69. package/dist/index.d.ts +3 -0
  70. package/dist/index.js +7 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/interfaces/condition-context-interface.d.ts +14 -0
  73. package/dist/interfaces/condition-context-interface.js +2 -0
  74. package/dist/interfaces/condition-context-interface.js.map +1 -0
  75. package/dist/interfaces/hap-types-interface.d.ts +9 -0
  76. package/dist/interfaces/hap-types-interface.js +2 -0
  77. package/dist/interfaces/hap-types-interface.js.map +1 -0
  78. package/dist/interfaces/mqtt-status-payload-interface.d.ts +6 -0
  79. package/dist/interfaces/mqtt-status-payload-interface.js +2 -0
  80. package/dist/interfaces/mqtt-status-payload-interface.js.map +1 -0
  81. package/dist/interfaces/options-interface.d.ts +87 -0
  82. package/dist/interfaces/options-interface.js +2 -0
  83. package/dist/interfaces/options-interface.js.map +1 -0
  84. package/dist/interfaces/persisted-state-interface.d.ts +6 -0
  85. package/dist/interfaces/persisted-state-interface.js +2 -0
  86. package/dist/interfaces/persisted-state-interface.js.map +1 -0
  87. package/dist/interfaces/service-registry-interface.d.ts +10 -0
  88. package/dist/interfaces/service-registry-interface.js +2 -0
  89. package/dist/interfaces/service-registry-interface.js.map +1 -0
  90. package/dist/interfaces/system-state-interface.d.ts +22 -0
  91. package/dist/interfaces/system-state-interface.js +2 -0
  92. package/dist/interfaces/system-state-interface.js.map +1 -0
  93. package/dist/schemas/arming-lock-schema.d.ts +10 -0
  94. package/dist/schemas/arming-lock-schema.js +12 -0
  95. package/dist/schemas/arming-lock-schema.js.map +1 -0
  96. package/dist/schemas/error-schema.d.ts +4 -0
  97. package/dist/schemas/error-schema.js +7 -0
  98. package/dist/schemas/error-schema.js.map +1 -0
  99. package/dist/schemas/mode-request-schema.d.ts +10 -0
  100. package/dist/schemas/mode-request-schema.js +18 -0
  101. package/dist/schemas/mode-request-schema.js.map +1 -0
  102. package/dist/schemas/status-response-schema.d.ts +7 -0
  103. package/dist/schemas/status-response-schema.js +10 -0
  104. package/dist/schemas/status-response-schema.js.map +1 -0
  105. package/dist/schemas/trip-mode-request-schema.d.ts +9 -0
  106. package/dist/schemas/trip-mode-request-schema.js +24 -0
  107. package/dist/schemas/trip-mode-request-schema.js.map +1 -0
  108. package/dist/security-system-platform.d.ts +19 -0
  109. package/dist/security-system-platform.js +56 -0
  110. package/dist/security-system-platform.js.map +1 -0
  111. package/dist/security-system.d.ts +28 -0
  112. package/dist/security-system.js +169 -0
  113. package/dist/security-system.js.map +1 -0
  114. package/dist/services/command-service.d.ts +16 -0
  115. package/dist/services/command-service.js +65 -0
  116. package/dist/services/command-service.js.map +1 -0
  117. package/dist/services/configuration-service.d.ts +24 -0
  118. package/dist/services/configuration-service.js +189 -0
  119. package/dist/services/configuration-service.js.map +1 -0
  120. package/dist/services/event-bus-service.d.ts +12 -0
  121. package/dist/services/event-bus-service.js +17 -0
  122. package/dist/services/event-bus-service.js.map +1 -0
  123. package/dist/services/matter-service.d.ts +69 -0
  124. package/dist/services/matter-service.js +363 -0
  125. package/dist/services/matter-service.js.map +1 -0
  126. package/dist/services/mqtt-service.d.ts +16 -0
  127. package/dist/services/mqtt-service.js +97 -0
  128. package/dist/services/mqtt-service.js.map +1 -0
  129. package/dist/services/server-service.d.ts +28 -0
  130. package/dist/services/server-service.js +275 -0
  131. package/dist/services/server-service.js.map +1 -0
  132. package/dist/services/storage-service.d.ts +14 -0
  133. package/dist/services/storage-service.js +70 -0
  134. package/dist/services/storage-service.js.map +1 -0
  135. package/dist/services/webhook-service.d.ts +16 -0
  136. package/dist/services/webhook-service.js +68 -0
  137. package/dist/services/webhook-service.js.map +1 -0
  138. package/dist/tests/conditions.test.d.ts +1 -0
  139. package/dist/tests/conditions.test.js +175 -0
  140. package/dist/tests/conditions.test.js.map +1 -0
  141. package/dist/tests/matter-service.test.d.ts +1 -0
  142. package/dist/tests/matter-service.test.js +475 -0
  143. package/dist/tests/matter-service.test.js.map +1 -0
  144. package/dist/tests/mqtt-service.test.d.ts +1 -0
  145. package/dist/tests/mqtt-service.test.js +168 -0
  146. package/dist/tests/mqtt-service.test.js.map +1 -0
  147. package/dist/tests/sensor-handler.test.d.ts +1 -0
  148. package/dist/tests/sensor-handler.test.js +87 -0
  149. package/dist/tests/sensor-handler.test.js.map +1 -0
  150. package/dist/tests/server-service.test.d.ts +1 -0
  151. package/dist/tests/server-service.test.js +188 -0
  152. package/dist/tests/server-service.test.js.map +1 -0
  153. package/dist/tests/state-handler.test.d.ts +1 -0
  154. package/dist/tests/state-handler.test.js +276 -0
  155. package/dist/tests/state-handler.test.js.map +1 -0
  156. package/dist/tests/switch-handler.test.d.ts +1 -0
  157. package/dist/tests/switch-handler.test.js +186 -0
  158. package/dist/tests/switch-handler.test.js.map +1 -0
  159. package/dist/tests/trip-handler.test.d.ts +1 -0
  160. package/dist/tests/trip-handler.test.js +179 -0
  161. package/dist/tests/trip-handler.test.js.map +1 -0
  162. package/dist/timers/timer-manager.d.ts +31 -0
  163. package/dist/timers/timer-manager.js +118 -0
  164. package/dist/timers/timer-manager.js.map +1 -0
  165. package/dist/types/event-payload-map-type.d.ts +15 -0
  166. package/dist/types/event-payload-map-type.js +2 -0
  167. package/dist/types/event-payload-map-type.js.map +1 -0
  168. package/dist/types/event-type.d.ts +44 -0
  169. package/dist/types/event-type.js +20 -0
  170. package/dist/types/event-type.js.map +1 -0
  171. package/dist/types/matter-accessory-key-type.d.ts +7 -0
  172. package/dist/types/matter-accessory-key-type.js +2 -0
  173. package/dist/types/matter-accessory-key-type.js.map +1 -0
  174. package/dist/types/mode-type.d.ts +4 -0
  175. package/dist/types/mode-type.js +2 -0
  176. package/dist/types/mode-type.js.map +1 -0
  177. package/dist/types/origin-type.d.ts +9 -0
  178. package/dist/types/origin-type.js +11 -0
  179. package/dist/types/origin-type.js.map +1 -0
  180. package/dist/types/security-state-type.d.ts +11 -0
  181. package/dist/types/security-state-type.js +13 -0
  182. package/dist/types/security-state-type.js.map +1 -0
  183. package/dist/types/sensor-kind-type.d.ts +2 -0
  184. package/dist/types/sensor-kind-type.js +2 -0
  185. package/dist/types/sensor-kind-type.js.map +1 -0
  186. package/dist/types/service-result-type.d.ts +4 -0
  187. package/dist/types/service-result-type.js +2 -0
  188. package/dist/types/service-result-type.js.map +1 -0
  189. package/dist/utils/arming-util.d.ts +19 -0
  190. package/dist/utils/arming-util.js +24 -0
  191. package/dist/utils/arming-util.js.map +1 -0
  192. package/dist/utils/state-util.d.ts +8 -0
  193. package/dist/utils/state-util.js +29 -0
  194. package/dist/utils/state-util.js.map +1 -0
  195. package/docs/MQTT.md +50 -0
  196. package/package.json +64 -0
  197. package/src/@types/homebridge-lib.d.ts +11 -0
  198. package/src/conditions/already-triggered-condition.ts +24 -0
  199. package/src/conditions/arming-in-progress-condition.ts +23 -0
  200. package/src/conditions/arming-lock-condition.ts +50 -0
  201. package/src/conditions/condition.ts +31 -0
  202. package/src/conditions/double-knock-condition.ts +84 -0
  203. package/src/conditions/not-armed-condition.ts +30 -0
  204. package/src/conditions/trigger-already-running-condition.ts +23 -0
  205. package/src/constants/default-constant.ts +28 -0
  206. package/src/constants/homekit-constant.ts +2 -0
  207. package/src/constants/matter-constant.ts +69 -0
  208. package/src/handlers/sensor-handler.ts +78 -0
  209. package/src/handlers/state-handler.ts +257 -0
  210. package/src/handlers/switch-handler.ts +111 -0
  211. package/src/handlers/trip-handler.ts +193 -0
  212. package/src/homekit/homekit-registrar.ts +28 -0
  213. package/src/homekit/service-factory.ts +35 -0
  214. package/src/index.ts +9 -0
  215. package/src/interfaces/condition-context-interface.ts +15 -0
  216. package/src/interfaces/hap-types-interface.ts +11 -0
  217. package/src/interfaces/mqtt-status-payload-interface.ts +6 -0
  218. package/src/interfaces/options-interface.ts +116 -0
  219. package/src/interfaces/persisted-state-interface.ts +7 -0
  220. package/src/interfaces/service-registry-interface.ts +11 -0
  221. package/src/interfaces/system-state-interface.ts +25 -0
  222. package/src/schemas/arming-lock-schema.ts +12 -0
  223. package/src/schemas/error-schema.ts +7 -0
  224. package/src/schemas/mode-request-schema.ts +18 -0
  225. package/src/schemas/status-response-schema.ts +10 -0
  226. package/src/schemas/trip-mode-request-schema.ts +24 -0
  227. package/src/security-system-platform.ts +68 -0
  228. package/src/security-system.ts +199 -0
  229. package/src/services/command-service.ts +76 -0
  230. package/src/services/configuration-service.ts +224 -0
  231. package/src/services/event-bus-service.ts +21 -0
  232. package/src/services/matter-service.ts +446 -0
  233. package/src/services/mqtt-service.ts +123 -0
  234. package/src/services/server-service.ts +327 -0
  235. package/src/services/storage-service.ts +80 -0
  236. package/src/services/webhook-service.ts +77 -0
  237. package/src/tests/conditions.test.ts +212 -0
  238. package/src/tests/matter-service.test.ts +610 -0
  239. package/src/tests/mqtt-service.test.ts +231 -0
  240. package/src/tests/sensor-handler.test.ts +116 -0
  241. package/src/tests/server-service.test.ts +238 -0
  242. package/src/tests/state-handler.test.ts +371 -0
  243. package/src/tests/switch-handler.test.ts +234 -0
  244. package/src/tests/trip-handler.test.ts +244 -0
  245. package/src/timers/timer-manager.ts +141 -0
  246. package/src/types/event-payload-map-type.ts +25 -0
  247. package/src/types/event-type.ts +61 -0
  248. package/src/types/matter-accessory-key-type.ts +33 -0
  249. package/src/types/mode-type.ts +4 -0
  250. package/src/types/origin-type.ts +9 -0
  251. package/src/types/security-state-type.ts +11 -0
  252. package/src/types/sensor-kind-type.ts +2 -0
  253. package/src/types/service-result-type.ts +4 -0
  254. package/src/utils/arming-util.ts +44 -0
  255. package/src/utils/state-util.ts +32 -0
  256. package/tsconfig.json +26 -0
  257. package/vitest.config.ts +8 -0
@@ -0,0 +1,257 @@
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 { SensorHandler } from './sensor-handler.js';
14
+ import type { TimerManager } from '../timers/timer-manager.js';
15
+ import { getArmingSeconds } from '../utils/arming-util.js';
16
+ import type { ServiceResult } from '../types/service-result-type.js';
17
+
18
+ /**
19
+ * Manages the core security-system state machine: arming, triggering, and resetting.
20
+ * Cross-handler side effects are signalled via the event bus so that this class has
21
+ * no direct dependencies on TripHandler or SwitchHandler.
22
+ */
23
+ export class StateHandler {
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 timers: TimerManager,
33
+ private readonly sensorHandler: SensorHandler,
34
+ ) {}
35
+
36
+ // ── Public API ─────────────────────────────────────────────────────────────
37
+
38
+ setCurrentState(state: SecurityState, origin: OriginType): void {
39
+ this.sensorHandler.resetArmingMotionSensor();
40
+
41
+ if (this.state.currentState === state) {
42
+ this.log.warn('Current mode (Already set)');
43
+ return;
44
+ }
45
+
46
+ this.state.currentState = state;
47
+ this.services.mainService.setCharacteristic(
48
+ this.Characteristic.SecuritySystemCurrentState,
49
+ state,
50
+ );
51
+ this.logMode('Current', state);
52
+
53
+ this.handleCurrentStateChange(origin);
54
+ this.storageService.save(this.state);
55
+ }
56
+
57
+ updateTargetState(state: SecurityState, origin: OriginType, delay: number): ServiceResult {
58
+ // Same-target re-call: "confirm this mode externally; apply it now"
59
+ if (state === this.state.targetState && this.state.currentState !== SecurityState.TRIGGERED) {
60
+ if (state === this.state.currentState) {
61
+ return { success: true };
62
+ }
63
+
64
+ if (this.state.isArming) {
65
+ this.timers.clearArmTimer();
66
+ this.state.isArming = false;
67
+ }
68
+
69
+ this.setCurrentState(state, origin);
70
+ return { success: true };
71
+ }
72
+
73
+ const reason = this.getBadTargetStateReason(state);
74
+ if (reason !== null) {
75
+ return { success: false, reason };
76
+ }
77
+
78
+ this.state.targetState = state;
79
+ this.logMode('Target', state);
80
+
81
+ if (origin === OriginType.INTERNAL || origin === OriginType.EXTERNAL) {
82
+ this.services.mainService.updateCharacteristic(
83
+ this.Characteristic.SecuritySystemTargetState,
84
+ state,
85
+ );
86
+ }
87
+
88
+ this.handleTargetStateChange(origin);
89
+
90
+ if (state === this.state.currentState) {
91
+ this.setCurrentState(state, origin);
92
+ return { success: true };
93
+ }
94
+
95
+ const armSeconds = delay > 0 ? delay : 0;
96
+
97
+ if (armSeconds === 0) {
98
+ this.setCurrentState(state, origin);
99
+ return { success: true };
100
+ }
101
+
102
+ this.state.isArming = true;
103
+ this.handleArmingState();
104
+ this.log.info(`Arm delay (${armSeconds}s)`);
105
+
106
+ this.timers.setArmTimer(armSeconds * 1000, () => {
107
+ this.state.isArming = false;
108
+ this.setCurrentState(state, origin);
109
+ });
110
+
111
+ return { success: true };
112
+ }
113
+
114
+ getArmingSeconds(targetState: SecurityState): number {
115
+ return getArmingSeconds(this.state, this.options, targetState);
116
+ }
117
+
118
+ resetTimers(): void {
119
+ this.timers.clearAll();
120
+ }
121
+
122
+ /** Returns true while the trigger delay is counting down (trip switch is active). */
123
+ isTripping(): boolean {
124
+ return this.state.isTripping;
125
+ }
126
+
127
+ /** Checks whether arming is currently blocked by an arming-lock switch. */
128
+ isArmingLocked(targetState: SecurityState): boolean {
129
+ // Locks never block disarming.
130
+ if (targetState === SecurityState.OFF) {
131
+ return false;
132
+ }
133
+
134
+ if (this.state.armingLocks.global) {
135
+ return true;
136
+ }
137
+
138
+ const modeMap: Partial<Record<SecurityState, keyof SystemState['armingLocks']>> = {
139
+ [SecurityState.HOME]: 'home',
140
+ [SecurityState.AWAY]: 'away',
141
+ [SecurityState.NIGHT]: 'night',
142
+ };
143
+
144
+ const lock = modeMap[targetState];
145
+ return lock ? this.state.armingLocks[lock] : false;
146
+ }
147
+
148
+ getAvailableTargetStates(): SecurityState[] {
149
+ const all = [SecurityState.HOME, SecurityState.AWAY, SecurityState.NIGHT, SecurityState.OFF];
150
+ const disabled = this.options.disabledModes.map(m => modeToState(m.toLowerCase()));
151
+ return all.filter(s => !disabled.includes(s));
152
+ }
153
+
154
+ logMode(type: string, state: SecurityState | string): void {
155
+ const mode = capitalise(stateToMode(state as SecurityState));
156
+ this.log.info(`${type} mode (${mode})`);
157
+ }
158
+
159
+ // ── Private helpers ────────────────────────────────────────────────────────
160
+
161
+ /**
162
+ * Returns a human-readable reason string if `state` is an invalid target,
163
+ * or `null` if the transition is permitted.
164
+ */
165
+ private getBadTargetStateReason(state: SecurityState): string | null {
166
+ const isTriggered = this.state.currentState === SecurityState.TRIGGERED;
167
+ const alreadySet = this.state.targetState === state;
168
+
169
+ if (alreadySet && !isTriggered) {
170
+ this.log.warn('Target mode (Already set)');
171
+ return 'target mode is already set';
172
+ }
173
+
174
+ if (!this.state.availableTargetStates.includes(state)) {
175
+ this.log.warn('Target mode (Disabled)');
176
+ return 'target mode is disabled';
177
+ }
178
+
179
+ const hasLock = this.options.armingLockSwitch || this.options.armingLockSwitches;
180
+ if (state !== SecurityState.OFF && hasLock && this.isArmingLocked(state)) {
181
+ this.log.warn('Arming lock (Not allowed)');
182
+ return 'arming is blocked by an arming lock switch';
183
+ }
184
+
185
+ return null;
186
+ }
187
+
188
+ private handleTargetStateChange(origin: OriginType): void {
189
+ this.resetTimers();
190
+
191
+ // The away-extended flag only applies to the arm cycle it started; any
192
+ // subsequent target change clears it (mirrors the HAP mode-switch reset).
193
+ this.state.modeAwayExtended = false;
194
+
195
+ // Notify handlers to reset their displayed state (bus is synchronous).
196
+ this.bus.emit(EventType.RESET_TRIP_SWITCHES, {});
197
+ this.sensorHandler.resetTrippedMotionSensor();
198
+ this.bus.emit(EventType.RESET_MODE_SWITCHES, {});
199
+ this.bus.emit(EventType.UPDATE_MODE_SWITCHES, {});
200
+
201
+ this.bus.emit(EventType.TARGET_CHANGED, { state: this.state.targetState, origin });
202
+
203
+ if (this.state.currentState === SecurityState.TRIGGERED) {
204
+ this.sensorHandler.pulseResetMotionSensor();
205
+ }
206
+
207
+ this.state.isTripping = false;
208
+ this.state.isKnocked = false;
209
+ }
210
+
211
+ private handleCurrentStateChange(origin: OriginType): void {
212
+ if (this.state.currentState === SecurityState.TRIGGERED) {
213
+ this.handleTriggeredState();
214
+
215
+ if (this.options.testMode) {
216
+ return;
217
+ }
218
+ } else {
219
+ // Notify TripHandler to reset trip switches on any non-triggered state change.
220
+ // (Do not reset when entering TRIGGERED — the trip switch should remain ON
221
+ // to reflect the active sensor / cause of the alarm.)
222
+ this.bus.emit(EventType.RESET_TRIP_SWITCHES, {});
223
+ }
224
+
225
+ this.bus.emit(EventType.CURRENT_CHANGED, { state: this.state.currentState, origin });
226
+ }
227
+
228
+ private handleTriggeredState(): void {
229
+ this.timers.clearTrippedInterval();
230
+ this.sensorHandler.resetTrippedMotionSensor();
231
+
232
+ this.timers.setResetTimer(this.options.resetMinutes * 60 * 1000, () => {
233
+ this.log.info('Reset (Finished)');
234
+ this.sensorHandler.pulseResetMotionSensor();
235
+
236
+ if (this.options.resetOffFlow) {
237
+ this.resetViaOffMode();
238
+ } else {
239
+ this.setCurrentState(this.state.targetState, OriginType.EXTERNAL);
240
+ }
241
+ });
242
+ }
243
+
244
+ private handleArmingState(): void {
245
+ this.sensorHandler.updateArmingMotionSensor(true);
246
+ this.bus.emit(EventType.ARMING, { state: this.state.targetState });
247
+ }
248
+
249
+ private resetViaOffMode(): void {
250
+ const original = this.state.targetState;
251
+ this.updateTargetState(SecurityState.OFF, OriginType.INTERNAL, 0);
252
+
253
+ setTimeout(() => {
254
+ this.updateTargetState(original, OriginType.INTERNAL, this.getArmingSeconds(original));
255
+ }, 100);
256
+ }
257
+ }
@@ -0,0 +1,111 @@
1
+ import type { Logging } from 'homebridge';
2
+ import { SecurityState } from '../types/security-state-type.js';
3
+ import { OriginType } from '../types/origin-type.js';
4
+ import type { SystemState } from '../interfaces/system-state-interface.js';
5
+ import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
6
+ import type { StateHandler } from './state-handler.js';
7
+ import { capitalise } from '../utils/state-util.js';
8
+ import type { TimerManager } from '../timers/timer-manager.js';
9
+ import type { ServiceResult } from '../types/service-result-type.js';
10
+
11
+ /**
12
+ * Handles all mode switches and the pause/extended switches.
13
+ * Switches are published over Matter only; the HAP layer no longer carries them,
14
+ * so every method reports outcomes as ServiceResult which MatterService maps to
15
+ * Matter status errors. The switch display state is pushed to Matter via bus
16
+ * events handled by MatterService.
17
+ */
18
+ export class SwitchHandler {
19
+ constructor(
20
+ private readonly state: SystemState,
21
+ private readonly options: SecuritySystemOptions,
22
+ private readonly log: Logging,
23
+ private readonly timers: TimerManager,
24
+ private readonly stateHandler: StateHandler,
25
+ ) {}
26
+
27
+ // ── Mode switches ──────────────────────────────────────────────────────────
28
+
29
+ setModeSwitch(mode: SecurityState, value: boolean): ServiceResult {
30
+ if (!value) {
31
+ return { success: false, reason: 'a mode switch can only be turned on' };
32
+ }
33
+ const delay = this.stateHandler.getArmingSeconds(mode);
34
+ return this.stateHandler.updateTargetState(mode, OriginType.INTERNAL, delay);
35
+ }
36
+
37
+ setModeOffSwitch(value: boolean): ServiceResult {
38
+ if (!value) {
39
+ return { success: false, reason: 'a mode switch can only be turned on' };
40
+ }
41
+ return this.stateHandler.updateTargetState(SecurityState.OFF, OriginType.INTERNAL, 0);
42
+ }
43
+
44
+ setModeAwayExtendedSwitch(value: boolean): ServiceResult {
45
+ if (!value) {
46
+ return { success: false, reason: 'the away-extended switch can only be turned on' };
47
+ }
48
+ const delay = this.stateHandler.getArmingSeconds(SecurityState.AWAY);
49
+ const result = this.stateHandler.updateTargetState(SecurityState.AWAY, OriginType.INTERNAL, delay);
50
+ if (result.success) {
51
+ this.state.modeAwayExtended = true;
52
+ }
53
+ return result;
54
+ }
55
+
56
+ setModePauseSwitch(value: boolean): ServiceResult {
57
+ if (this.state.currentState === SecurityState.TRIGGERED) {
58
+ this.log.warn('Mode pause (Alarm is triggered)');
59
+ return { success: false, reason: 'mode pause is not allowed while the alarm is triggered' };
60
+ }
61
+
62
+ if (value) {
63
+ if (this.state.currentState === SecurityState.OFF) {
64
+ this.log.warn('Mode pause (Not armed)');
65
+ return { success: false, reason: 'mode pause is not allowed while disarmed' };
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.timers.setPauseTimer(this.options.pauseMinutes * 60 * 1000, () => {
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
+ });
78
+ }
79
+ } else {
80
+ this.log.info('Mode pause (Cancelled)');
81
+ this.timers.clearPauseTimer();
82
+
83
+ const prev = this.state.pausedCurrentState ?? this.state.defaultState;
84
+ this.stateHandler.updateTargetState(prev, OriginType.INTERNAL, this.stateHandler.getArmingSeconds(prev));
85
+ }
86
+
87
+ return { success: true };
88
+ }
89
+
90
+ // ── Arming lock switches ───────────────────────────────────────────────────
91
+
92
+ updateArmingLock(mode: string, value: boolean): ServiceResult {
93
+ this.logArmingLock(mode, value);
94
+
95
+ const locks = this.state.armingLocks;
96
+ const key: keyof typeof locks | undefined = ['global', 'home', 'away', 'night']
97
+ .find(candidate => candidate === mode) as keyof typeof locks | undefined;
98
+
99
+ if (!key) {
100
+ this.log.debug(`Unknown arming lock mode (${mode})`);
101
+ return { success: false, reason: `unknown arming lock mode: ${mode}` };
102
+ }
103
+
104
+ locks[key] = value;
105
+ return { success: true };
106
+ }
107
+
108
+ private logArmingLock(mode: string, value: boolean): void {
109
+ this.log.info(`Arming lock [${capitalise(mode)}] (${value ? 'On' : 'Off'})`);
110
+ }
111
+ }
@@ -0,0 +1,193 @@
1
+ import type { Logging } from 'homebridge';
2
+ import { SecurityState } from '../types/security-state-type.js';
3
+ import { OriginType } from '../types/origin-type.js';
4
+ import type { SystemState } from '../interfaces/system-state-interface.js';
5
+ import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
6
+ import type { EventBusService } from '../services/event-bus-service.js';
7
+ import { EventType } from '../types/event-type.js';
8
+ import type { SensorHandler } from './sensor-handler.js';
9
+ import type { Condition } from '../conditions/condition.js';
10
+ import type { ConditionContext } from '../interfaces/condition-context-interface.js';
11
+ import { NotArmedCondition } from '../conditions/not-armed-condition.js';
12
+ import { ArmingInProgressCondition } from '../conditions/arming-in-progress-condition.js';
13
+ import { AlreadyTriggeredCondition } from '../conditions/already-triggered-condition.js';
14
+ import { DoubleKnockCondition } from '../conditions/double-knock-condition.js';
15
+ import { TriggerAlreadyRunningCondition } from '../conditions/trigger-already-running-condition.js';
16
+ import type { TimerManager } from '../timers/timer-manager.js';
17
+ import type { ServiceResult } from '../types/service-result-type.js';
18
+
19
+ /**
20
+ * Handles the trip switch and trigger-delay logic, including all blocking conditions.
21
+ * Communicates state transitions back to the state machine via the event bus so that
22
+ * no circular import is needed with StateHandler.
23
+ */
24
+ export class TripHandler {
25
+ private readonly conditions: readonly Condition[];
26
+
27
+ constructor(
28
+ private readonly state: SystemState,
29
+ private readonly options: SecuritySystemOptions,
30
+ private readonly log: Logging,
31
+ private readonly bus: EventBusService,
32
+ private readonly sensorHandler: SensorHandler,
33
+ private readonly timers: TimerManager,
34
+ ) {
35
+ const doubleKnock = new DoubleKnockCondition(
36
+ (seconds, onExpire) => {
37
+ this.timers.setDoubleKnockTimer(seconds * 1000, onExpire);
38
+ },
39
+ () => {
40
+ this.timers.clearDoubleKnockTimer();
41
+ },
42
+ );
43
+
44
+ this.conditions = [
45
+ new NotArmedCondition(),
46
+ new ArmingInProgressCondition(),
47
+ doubleKnock,
48
+ new AlreadyTriggeredCondition(),
49
+ new TriggerAlreadyRunningCondition(),
50
+ ];
51
+ }
52
+
53
+ /**
54
+ * Evaluates all blocking conditions for a trip action without side effects.
55
+ * Returns a failed result with the condition's reason if any condition blocks the action.
56
+ */
57
+ checkTripConditions(value: boolean, origin: OriginType): ServiceResult {
58
+ if (!value) {
59
+ return { success: true };
60
+ }
61
+
62
+ const ctx = this.makeContext(value, origin);
63
+ for (const condition of this.conditions) {
64
+ if (condition.evaluate(ctx)) {
65
+ return { success: false, reason: condition.failureReason };
66
+ }
67
+ }
68
+
69
+ return { success: true };
70
+ }
71
+
72
+ /**
73
+ * Core trip-switch logic shared by all trip/trigger paths.
74
+ * Returns a result object indicating success or the reason for failure.
75
+ */
76
+ updateTripSwitch(value: boolean, origin: OriginType, stateChanged: boolean): ServiceResult {
77
+ if (value) {
78
+ const conditionResult = this.checkTripConditions(value, origin);
79
+ if (!conditionResult.success) {
80
+ return conditionResult;
81
+ }
82
+
83
+ this.activateTrip(origin);
84
+ } else {
85
+ this.cancelTrip(origin, stateChanged);
86
+ }
87
+
88
+ // Sync trip switch characteristic when origin is not a direct switch press.
89
+ return { success: true };
90
+ }
91
+
92
+ /**
93
+ * Trip a mode-specific switch. Only triggers if the system is currently in
94
+ * the required mode (or the alarm is triggered and target matches).
95
+ */
96
+ triggerIfModeSet(requiredState: SecurityState, value: boolean): ServiceResult {
97
+ const isTriggered = this.state.currentState === SecurityState.TRIGGERED;
98
+
99
+ if (value) {
100
+ const modeMatches = this.state.currentState === requiredState
101
+ || (isTriggered && this.state.targetState === requiredState);
102
+
103
+ if (!modeMatches) {
104
+ this.log.debug('Security System (Trip mode not set)');
105
+ return { success: false, reason: 'mode not set' };
106
+ }
107
+ }
108
+
109
+ return this.updateTripSwitch(value, OriginType.REGULAR_SWITCH, false);
110
+ }
111
+
112
+ private makeContext(value: boolean, origin: OriginType): ConditionContext {
113
+ return {
114
+ state: this.state,
115
+ options: this.options,
116
+ value,
117
+ origin,
118
+ log: this.log,
119
+ };
120
+ }
121
+
122
+ private activateTrip(origin: OriginType): void {
123
+ this.log.info('Security System (Tripped)');
124
+
125
+ if (this.options.trippedMotionSensor) {
126
+ this.startTrippedMotionSensor();
127
+ }
128
+
129
+ const triggerSeconds = this.resolveTriggerSeconds();
130
+ this.log.debug(`Trigger delay (${triggerSeconds}s)`);
131
+
132
+ this.state.isTripping = true;
133
+ this.timers.setTriggerTimer(triggerSeconds * 1000, () => {
134
+ this.state.isTripping = false;
135
+ this.bus.emit(EventType.TRIGGER_FIRED, { origin });
136
+ });
137
+
138
+ if (triggerSeconds > 0) {
139
+ this.bus.emit(EventType.WARNING, { origin, triggerSeconds });
140
+ }
141
+ }
142
+
143
+ private startTrippedMotionSensor(): void {
144
+ const seconds = this.options.trippedMotionSensorSeconds;
145
+ if (seconds === 0) {
146
+ this.sensorHandler.setTrippedMotionSensor(true);
147
+ } else {
148
+ this.sensorHandler.pulseTrippedMotionSensor();
149
+ this.timers.setTrippedInterval(
150
+ seconds * 1000,
151
+ () => this.sensorHandler.pulseTrippedMotionSensor(),
152
+ );
153
+ }
154
+ }
155
+
156
+ private cancelTrip(origin: OriginType, stateChanged: boolean): void {
157
+ this.log.info('Security System (Cancelled)');
158
+ this.state.isTripping = false;
159
+
160
+ this.bus.emit(EventType.TRIP_CANCELLED, { origin, stateChanged });
161
+
162
+ if (this.options.trippedMotionSensor) {
163
+ this.sensorHandler.resetTrippedMotionSensor();
164
+ }
165
+
166
+ this.state.isKnocked = false;
167
+ }
168
+
169
+ private resolveTriggerSeconds(): number {
170
+ const seconds = this.options.triggerSeconds;
171
+ const cur = this.state.currentState;
172
+
173
+ if (cur === SecurityState.HOME && this.options.homeTriggerSeconds !== null) {
174
+ return this.options.homeTriggerSeconds;
175
+ }
176
+
177
+ if (cur === SecurityState.AWAY) {
178
+ if (this.options.modeAwayExtendedSwitchTriggerSeconds !== null && this.state.modeAwayExtended) {
179
+ return this.options.modeAwayExtendedSwitchTriggerSeconds;
180
+ }
181
+
182
+ if (this.options.awayTriggerSeconds !== null) {
183
+ return this.options.awayTriggerSeconds;
184
+ }
185
+ }
186
+
187
+ if (cur === SecurityState.NIGHT && this.options.nightTriggerSeconds !== null) {
188
+ return this.options.nightTriggerSeconds;
189
+ }
190
+
191
+ return seconds;
192
+ }
193
+ }
@@ -0,0 +1,28 @@
1
+ import type { CharacteristicValue } from 'homebridge';
2
+ import type { CharacteristicConstructor } from '../interfaces/hap-types-interface.js';
3
+ import type { ServiceRegistry } from '../interfaces/service-registry-interface.js';
4
+ import type { SystemState } from '../interfaces/system-state-interface.js';
5
+ import { SecurityState } from '../types/security-state-type.js';
6
+ import { OriginType } from '../types/origin-type.js';
7
+ import type { StateHandler } from '../handlers/state-handler.js';
8
+
9
+ /** Attaches the HomeKit characteristic handlers (onGet / onSet) to the security system service. */
10
+ export class HomeKitRegistrar {
11
+ constructor(
12
+ private readonly svcs: ServiceRegistry,
13
+ private readonly state: SystemState,
14
+ private readonly stateHandler: StateHandler,
15
+ ) {}
16
+
17
+ register(Char: CharacteristicConstructor): void {
18
+ const s = this.svcs;
19
+
20
+ s.mainService.getCharacteristic(Char.SecuritySystemCurrentState)
21
+ .onGet(async (): Promise<CharacteristicValue> => this.state.currentState);
22
+ s.mainService.getCharacteristic(Char.SecuritySystemTargetState)
23
+ .onGet(async (): Promise<CharacteristicValue> => this.state.targetState)
24
+ .onSet(async (v: CharacteristicValue) => {
25
+ this.stateHandler.updateTargetState(v as SecurityState, OriginType.REGULAR_SWITCH, this.stateHandler.getArmingSeconds(v as SecurityState));
26
+ });
27
+ }
28
+ }
@@ -0,0 +1,35 @@
1
+ import type { Service } from 'homebridge';
2
+ import type { CharacteristicConstructor } from '../interfaces/hap-types-interface.js';
3
+ import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
4
+ import type { ServiceRegistry } from '../interfaces/service-registry-interface.js';
5
+
6
+ /**
7
+ * Creates the HomeKit services for the accessory. Switches and motion sensors
8
+ * are published over Matter only, so the HomeKit accessory only carries the
9
+ * security system service and its accessory information service.
10
+ */
11
+ export function buildServiceRegistry(
12
+ Svc: typeof Service,
13
+ Char: CharacteristicConstructor,
14
+ options: SecuritySystemOptions,
15
+ ): ServiceRegistry {
16
+ const mainSvc = new Svc.SecuritySystem(options.name);
17
+ mainSvc.addCharacteristic(Char.ConfiguredName);
18
+
19
+ const infoSvc = new Svc.AccessoryInformation();
20
+ infoSvc.setCharacteristic(Char.Name, options.name);
21
+ infoSvc.setCharacteristic(Char.Identify, true);
22
+ infoSvc.setCharacteristic(Char.Manufacturer, 'MiguelRipoll23');
23
+ infoSvc.setCharacteristic(Char.Model, 'DIY');
24
+ infoSvc.setCharacteristic(Char.SerialNumber, options.serialNumber);
25
+
26
+ return {
27
+ mainService: mainSvc,
28
+ accessoryInfoService: infoSvc,
29
+ };
30
+ }
31
+
32
+ /** Builds the list of services to expose to HomeKit. */
33
+ export function buildServiceList(svcs: ServiceRegistry): Service[] {
34
+ return [svcs.mainService, svcs.accessoryInfoService];
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { API } from 'homebridge';
2
+ import { SecuritySystemPlatform } from './security-system-platform.js';
3
+
4
+ const PLUGIN_NAME = 'homebridge-securitysystem';
5
+ const PLATFORM_NAME = 'security-system';
6
+
7
+ export default (api: API): void => {
8
+ api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, SecuritySystemPlatform);
9
+ };
@@ -0,0 +1,15 @@
1
+ import type { Logging } from 'homebridge';
2
+ import type { OriginType } from '../types/origin-type.js';
3
+ import type { SecuritySystemOptions } from './options-interface.js';
4
+ import type { SystemState } from './system-state-interface.js';
5
+
6
+ /** Context passed to Condition.evaluate() to determine if an action is blocked. */
7
+ export interface ConditionContext {
8
+ state: SystemState;
9
+ options: SecuritySystemOptions;
10
+ /** The value being set (true = activating, false = deactivating). */
11
+ value: boolean;
12
+ origin: OriginType;
13
+ /** Logger used by conditions to emit their own warning messages when blocking. */
14
+ log: Logging;
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { API } from 'homebridge';
2
+
3
+ /**
4
+ * Runtime type of `api.hap.Characteristic` (the class with all static characteristic
5
+ * definitions). Using `API['hap']['Characteristic']` avoids the self-referential
6
+ * `typeof Characteristic` issue that arises with `import type { Characteristic }`.
7
+ */
8
+ export type CharacteristicConstructor = API['hap']['Characteristic'];
9
+
10
+ /** Runtime type of `api.hap.Service`. */
11
+ export type ServiceConstructor = API['hap']['Service'];
@@ -0,0 +1,6 @@
1
+ export interface MqttStatusPayload {
2
+ arming: boolean;
3
+ current_mode: string;
4
+ target_mode: string;
5
+ tripped: boolean;
6
+ }