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,116 @@
1
+ /** A single custom trip switch entry with a user-defined label. */
2
+ export interface TripModeSwitch {
3
+ label: string;
4
+ }
5
+
6
+ /** Fully-parsed, strongly-typed configuration options for the security system. */
7
+ export interface SecuritySystemOptions {
8
+ name: string;
9
+ serialNumber: string;
10
+ defaultMode: string;
11
+ armSeconds: number;
12
+ triggerSeconds: number;
13
+ resetMinutes: number;
14
+ saveState: boolean;
15
+ proxyMode: boolean;
16
+ testMode: boolean;
17
+
18
+ // Switch display names
19
+ tripSwitchName: string;
20
+ tripHomeSwitchName: string;
21
+ tripAwaySwitchName: string;
22
+ tripNightSwitchName: string;
23
+ tripOverrideSwitchName: string;
24
+ modeHomeSwitchName: string;
25
+ modeAwaySwitchName: string;
26
+ modeNightSwitchName: string;
27
+ modeOffSwitchName: string;
28
+ modeAwayExtendedSwitchName: string;
29
+ modePauseSwitchName: string;
30
+
31
+ // Behaviour toggles
32
+ overrideOff: boolean;
33
+ resetOffFlow: boolean;
34
+ disabledModes: string[];
35
+
36
+ // Per-mode arm delays (null = use global armSeconds)
37
+ homeArmSeconds: number | null;
38
+ awayArmSeconds: number | null;
39
+ nightArmSeconds: number | null;
40
+
41
+ // Per-mode trigger delays (null = use global triggerSeconds)
42
+ homeTriggerSeconds: number | null;
43
+ awayTriggerSeconds: number | null;
44
+ nightTriggerSeconds: number | null;
45
+
46
+ // Trip switches
47
+ tripSwitch: boolean;
48
+ tripOverrideSwitch: boolean;
49
+ tripModeSwitches: boolean;
50
+
51
+ // Custom trip mode switches
52
+ tripHomeSwitches: TripModeSwitch[];
53
+ tripAwaySwitches: TripModeSwitch[];
54
+ tripNightSwitches: TripModeSwitch[];
55
+
56
+ // Arming lock
57
+ armingLockSwitch: boolean;
58
+ armingLockSwitches: boolean;
59
+
60
+ // Motion sensors
61
+ armingMotionSensor: boolean;
62
+ trippedMotionSensor: boolean;
63
+ trippedMotionSensorSeconds: number;
64
+ resetSensor: boolean;
65
+
66
+ // Mode switches
67
+ modeSwitches: boolean;
68
+ modeOffSwitch: boolean;
69
+ modePauseSwitch: boolean;
70
+ pauseMinutes: number;
71
+ modeAwayExtendedSwitch: boolean;
72
+ modeAwayExtendedSwitchTriggerSeconds: number | null;
73
+
74
+ // Double-knock
75
+ doubleKnock: boolean;
76
+ doubleKnockSeconds: number;
77
+ doubleKnockModes: string[];
78
+ homeDoubleKnockSeconds: number | null;
79
+ awayDoubleKnockSeconds: number | null;
80
+ nightDoubleKnockSeconds: number | null;
81
+
82
+ // Server
83
+ serverPort: number | null;
84
+ serverApiKey: string | null;
85
+
86
+ // Shell commands
87
+ commandTargetHome: string | null;
88
+ commandTargetAway: string | null;
89
+ commandTargetNight: string | null;
90
+ commandTargetOff: string | null;
91
+ commandCurrentHome: string | null;
92
+ commandCurrentAway: string | null;
93
+ commandCurrentNight: string | null;
94
+ commandCurrentOff: string | null;
95
+ commandCurrentWarning: string | null;
96
+ commandCurrentTriggered: string | null;
97
+
98
+ // Webhooks
99
+ webhookTargetHome: string | null;
100
+ webhookTargetAway: string | null;
101
+ webhookTargetNight: string | null;
102
+ webhookTargetOff: string | null;
103
+ webhookCurrentHome: string | null;
104
+ webhookCurrentAway: string | null;
105
+ webhookCurrentNight: string | null;
106
+ webhookCurrentOff: string | null;
107
+ webhookCurrentWarning: string | null;
108
+ webhookCurrentTriggered: string | null;
109
+
110
+ // MQTT
111
+ mqttBroker: string | null;
112
+ mqttUsername: string | null;
113
+ mqttPassword: string | null;
114
+ mqttTopic: string;
115
+ mqttClientId: string | null;
116
+ }
@@ -0,0 +1,7 @@
1
+ import type { SecurityState } from '../types/security-state-type.js';
2
+
3
+ /** Subset of SystemState that is persisted across Homebridge restarts. */
4
+ export interface PersistedState {
5
+ currentState: SecurityState;
6
+ targetState: SecurityState;
7
+ }
@@ -0,0 +1,11 @@
1
+ import type { Service } from 'homebridge';
2
+
3
+ /**
4
+ * HomeKit services exposed by the security system accessory.
5
+ * Switches and motion sensors are published over Matter only, so the HomeKit
6
+ * accessory carries just the security system itself.
7
+ */
8
+ export interface ServiceRegistry {
9
+ mainService: Service;
10
+ accessoryInfoService: Service;
11
+ }
@@ -0,0 +1,25 @@
1
+ import type { SecurityState } from '../types/security-state-type.js';
2
+
3
+ /** Mutable runtime state shared across all handlers. */
4
+ export interface SystemState {
5
+ currentState: SecurityState;
6
+ targetState: SecurityState;
7
+ defaultState: SecurityState;
8
+ availableTargetStates: SecurityState[];
9
+
10
+ isArming: boolean;
11
+ isTripping: boolean;
12
+ isKnocked: boolean;
13
+ serverAuthenticationAttempts: number;
14
+ pausedCurrentState: SecurityState | null;
15
+
16
+ /** Arming-lock switch state. Switches are Matter-only, so the lock state lives here (was held by HAP characteristics). */
17
+ armingLocks: {
18
+ global: boolean;
19
+ home: boolean;
20
+ away: boolean;
21
+ night: boolean;
22
+ };
23
+ /** True while armed away via the away-extended switch; resets on any target state change. */
24
+ modeAwayExtended: boolean;
25
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from '@hono/zod-openapi';
2
+
3
+ export const ArmingLockRequestSchema = z
4
+ .object({
5
+ mode: z
6
+ .enum(['global', 'home', 'away', 'night'])
7
+ .openapi({ example: 'home', description: 'Arming lock mode to update' }),
8
+ value: z
9
+ .boolean()
10
+ .openapi({ example: true, description: 'True to enable the arming lock, false to disable it' }),
11
+ })
12
+ .openapi('ArmingLockRequest');
@@ -0,0 +1,7 @@
1
+ import { z } from '@hono/zod-openapi';
2
+
3
+ export const ErrorSchema = z
4
+ .object({
5
+ reason: z.string().openapi({ example: 'API key invalid' }),
6
+ })
7
+ .openapi('Error');
@@ -0,0 +1,18 @@
1
+ import { z } from '@hono/zod-openapi';
2
+
3
+ export const ModeRequestSchema = z
4
+ .object({
5
+ mode: z
6
+ .enum(['home', 'away', 'night', 'off'])
7
+ .openapi({ example: 'home', description: 'Target mode to activate (home, away, night, or off)' }),
8
+ delay: z
9
+ .number()
10
+ .int()
11
+ .nonnegative()
12
+ .optional()
13
+ .openapi({
14
+ example: 5,
15
+ description: 'Optional delay in seconds before applying the mode change',
16
+ }),
17
+ })
18
+ .openapi('ModeRequest');
@@ -0,0 +1,10 @@
1
+ import { z } from '@hono/zod-openapi';
2
+
3
+ export const StatusResponseSchema = z
4
+ .object({
5
+ arming: z.boolean().openapi({ example: false, description: 'Whether the system is currently arming' }),
6
+ current_mode: z.string().openapi({ example: 'off', description: 'The current active mode' }),
7
+ target_mode: z.string().openapi({ example: 'home', description: 'The target mode being transitioned to' }),
8
+ tripped: z.boolean().openapi({ example: false, description: 'Whether a trip switch is active' }),
9
+ })
10
+ .openapi('StatusResponse');
@@ -0,0 +1,24 @@
1
+ import { z } from '@hono/zod-openapi';
2
+
3
+ export const TripModeRequestSchema = z
4
+ .object({
5
+ mode: z
6
+ .enum(['home', 'away', 'night'])
7
+ .nullable()
8
+ .optional()
9
+ .openapi({
10
+ example: 'home',
11
+ description: 'Mode to trip. If omitted or null, the action applies to all modes.',
12
+ }),
13
+ delay: z
14
+ .number()
15
+ .int()
16
+ .nonnegative()
17
+ .nullable()
18
+ .optional()
19
+ .openapi({
20
+ example: 5,
21
+ description: 'Delay in seconds before activating the alarm.',
22
+ }),
23
+ })
24
+ .openapi('TripModeRequest');
@@ -0,0 +1,68 @@
1
+ import type { API, DynamicPlatformPlugin, Logging, MatterAccessory, PlatformAccessory, PlatformConfig } from 'homebridge';
2
+ import { SecuritySystem } from './security-system.js';
3
+
4
+ const PLUGIN_NAME = 'homebridge-securitysystem';
5
+ const PLATFORM_NAME = 'security-system';
6
+
7
+ /**
8
+ * Homebridge dynamic platform that exposes the security system as a
9
+ * platform accessory. The accessory UUID is stable so the same HomeKit
10
+ * accessory is restored across restarts via homebridge's accessory cache.
11
+ * Optional switch/sensor accessories are additionally published over Matter
12
+ * when Matter is enabled on the bridge.
13
+ */
14
+ export class SecuritySystemPlatform implements DynamicPlatformPlugin {
15
+ private readonly cachedAccessories = new Map<string, PlatformAccessory>();
16
+ private readonly cachedMatterAccessories = new Map<string, MatterAccessory>();
17
+
18
+ constructor(
19
+ private readonly log: Logging,
20
+ private readonly config: PlatformConfig,
21
+ private readonly api: API,
22
+ ) {
23
+ this.api.on('didFinishLaunching', () => this.setupAccessories());
24
+ }
25
+
26
+ configureAccessory(accessory: PlatformAccessory): void {
27
+ this.cachedAccessories.set(accessory.UUID, accessory);
28
+ }
29
+
30
+ configureMatterAccessory(accessory: MatterAccessory): void {
31
+ this.cachedMatterAccessories.set(accessory.UUID, accessory);
32
+ }
33
+
34
+ private setupAccessories(): void {
35
+ const uuid = this.api.hap.uuid.generate(PLATFORM_NAME);
36
+
37
+ const cached = this.cachedAccessories.get(uuid);
38
+ let system: SecuritySystem;
39
+
40
+ if (cached) {
41
+ this.cachedAccessories.delete(uuid);
42
+ this.log.info('Restoring cached accessory:', cached.displayName);
43
+ system = new SecuritySystem(this.log, this.config, this.api, cached);
44
+ this.api.updatePlatformAccessories([cached]);
45
+ } else {
46
+ const accessory = new this.api.platformAccessory(
47
+ 'Security System',
48
+ uuid,
49
+ this.api.hap.Categories.SECURITY_SYSTEM,
50
+ );
51
+ system = new SecuritySystem(this.log, this.config, this.api, accessory);
52
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
53
+
54
+ // Remove any cached accessories that are no longer configured.
55
+ for (const stale of this.cachedAccessories.values()) {
56
+ this.log.info('Removing unconfigured accessory:', stale.displayName);
57
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [stale]);
58
+ }
59
+ }
60
+
61
+ // Publish the configured Matter accessories (no-op when Matter is disabled).
62
+ system.setupMatterAccessories(this.cachedMatterAccessories).catch((error: unknown) => {
63
+ this.log.error('Failed to register Matter accessories.');
64
+ this.log.error(String(error));
65
+ });
66
+ }
67
+ }
68
+
@@ -0,0 +1,199 @@
1
+ import type { API, Logging, MatterAccessory, PlatformAccessory, Service } 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 { ConfigurationService } from './services/configuration-service.js';
6
+ import { stateToMode, modeToState } from './utils/state-util.js';
7
+ import type { SecuritySystemOptions } from './interfaces/options-interface.js';
8
+ import type { SystemState } from './interfaces/system-state-interface.js';
9
+ import type { ServiceRegistry } from './interfaces/service-registry-interface.js';
10
+ import { EventBusService } from './services/event-bus-service.js';
11
+ import { EventType } from './types/event-type.js';
12
+ import { StorageService } from './services/storage-service.js';
13
+ import { WebhookService } from './services/webhook-service.js';
14
+ import { CommandService } from './services/command-service.js';
15
+ import { MqttService } from './services/mqtt-service.js';
16
+ import { MatterService } from './services/matter-service.js';
17
+ import { ServerService } from './services/server-service.js';
18
+ import { StateHandler } from './handlers/state-handler.js';
19
+ import { TripHandler } from './handlers/trip-handler.js';
20
+ import { SwitchHandler } from './handlers/switch-handler.js';
21
+ import { SensorHandler } from './handlers/sensor-handler.js';
22
+ import { buildServiceRegistry, buildServiceList } from './homekit/service-factory.js';
23
+ import { HomeKitRegistrar } from './homekit/homekit-registrar.js';
24
+ import { TimerManager } from './timers/timer-manager.js';
25
+
26
+ export class SecuritySystem {
27
+ private readonly options: SecuritySystemOptions;
28
+ private readonly state: SystemState;
29
+ private readonly svcs: ServiceRegistry;
30
+ private readonly serviceList: Service[];
31
+
32
+ private readonly bus: EventBusService;
33
+ private readonly stateHandler: StateHandler;
34
+ private readonly tripHandler: TripHandler;
35
+ private readonly switchHandler: SwitchHandler;
36
+ private readonly sensorHandler: SensorHandler;
37
+ private readonly storageService: StorageService;
38
+ private readonly matterService: MatterService;
39
+
40
+ constructor(
41
+ private readonly log: Logging,
42
+ config: Record<string, unknown>,
43
+ private readonly api: API,
44
+ private readonly accessory: PlatformAccessory,
45
+ ) {
46
+ const Char = api.hap.Characteristic as CharacteristicConstructor;
47
+ const Svc = api.hap.Service as typeof Service;
48
+
49
+ this.options = new ConfigurationService(log, config).options;
50
+
51
+ const defaultState = modeToState(this.options.defaultMode);
52
+ this.state = this.buildState(defaultState === (-1 as SecurityState) ? SecurityState.OFF : defaultState);
53
+
54
+ this.svcs = buildServiceRegistry(Svc, Char, this.options);
55
+ this.state.availableTargetStates = this.calcAvailableTargetStates();
56
+
57
+ // Sync main service initial values.
58
+ this.svcs.mainService.getCharacteristic(Char.SecuritySystemTargetState).value = this.state.targetState;
59
+ this.svcs.mainService.getCharacteristic(Char.SecuritySystemCurrentState).value = this.state.currentState;
60
+ this.svcs.mainService
61
+ .setCharacteristic(Char.ConfiguredName, this.options.name)
62
+ .getCharacteristic(Char.SecuritySystemTargetState)
63
+ .setProps({ validValues: this.state.availableTargetStates });
64
+
65
+ // Services.
66
+ this.bus = new EventBusService();
67
+ this.storageService = new StorageService(log, this.options, api.user.storagePath());
68
+ const timerManager = new TimerManager(log);
69
+
70
+ // Handlers — construction order matters: sensorHandler first (leaf), then stateHandler,
71
+ // then switchHandler (depends on stateHandler), then tripHandler.
72
+ this.sensorHandler = new SensorHandler(log, this.bus);
73
+ this.stateHandler = new StateHandler(
74
+ this.svcs, this.state, this.options, Char, log, this.bus, this.storageService, timerManager, this.sensorHandler,
75
+ );
76
+ this.switchHandler = new SwitchHandler(this.state, this.options, log, timerManager, this.stateHandler);
77
+ this.tripHandler = new TripHandler(
78
+ this.state, this.options, log, this.bus, this.sensorHandler, timerManager,
79
+ );
80
+
81
+ // Wire bus listeners for cross-handler coordination (no more circular constructor deps).
82
+ this.bus.on(EventType.TRIGGER_FIRED, ({ origin }) => {
83
+ this.stateHandler.setCurrentState(SecurityState.TRIGGERED, origin);
84
+ });
85
+ this.bus.on(EventType.TRIP_CANCELLED, ({ stateChanged }) => {
86
+ if (this.state.currentState === SecurityState.TRIGGERED && !stateChanged) {
87
+ this.stateHandler.updateTargetState(SecurityState.OFF, OriginType.INTERNAL, 0);
88
+ } else {
89
+ this.stateHandler.resetTimers();
90
+ }
91
+ });
92
+
93
+ // Attach side-effect listeners.
94
+ const webhookSvc = new WebhookService(log, this.options, this.state);
95
+ const commandSvc = new CommandService(log, this.options, this.state);
96
+ webhookSvc.attachToBus(this.bus);
97
+ commandSvc.attachToBus(this.bus);
98
+ const mqttSvc = new MqttService(log, this.options, this.state);
99
+ mqttSvc.attachToBus(this.bus);
100
+ this.api.on('shutdown', () => mqttSvc.disconnect());
101
+
102
+ // Publish the optional switch/sensor accessories over Matter (HAP keeps the
103
+ // main SecuritySystem service and the motion sensors). Registration happens
104
+ // on the platform after didFinishLaunching, via setupMatterAccessories().
105
+ this.matterService = new MatterService(log, this.options, api, this.state, this.switchHandler, this.tripHandler, this.sensorHandler);
106
+ this.matterService.attachToBus(this.bus);
107
+
108
+ // Register HomeKit characteristic handlers.
109
+ new HomeKitRegistrar(this.svcs, this.state, this.stateHandler)
110
+ .register(Char);
111
+
112
+ // Build the exposed service list and host it on the platform accessory.
113
+ this.serviceList = buildServiceList(this.svcs);
114
+ this.populateAccessory(Svc);
115
+ this.accessory.on('identify', () => this.log.info('Identify'));
116
+
117
+ // Startup tasks.
118
+ this.logStartup();
119
+
120
+ const syncModeSwitches = (): void => {
121
+ this.bus.emit(EventType.RESET_MODE_SWITCHES, {});
122
+ this.bus.emit(EventType.UPDATE_MODE_SWITCHES, {});
123
+ };
124
+
125
+ if (this.options.saveState) {
126
+ this.storageService.init().then(() => this.storageService.load(this.state).then(() => {
127
+ this.svcs.mainService.updateCharacteristic(Char.SecuritySystemTargetState, this.state.targetState);
128
+ this.svcs.mainService.updateCharacteristic(Char.SecuritySystemCurrentState, this.state.currentState);
129
+ this.stateHandler.logMode('Current', this.state.currentState);
130
+ syncModeSwitches();
131
+ }));
132
+ } else {
133
+ syncModeSwitches();
134
+ }
135
+
136
+ if (this.options.serverPort !== null) {
137
+ new ServerService(log, this.options, this.state, this.stateHandler, this.tripHandler, this.switchHandler).start();
138
+ }
139
+ }
140
+
141
+ // ── Public API ─────────────────────────────────────────────────────────────
142
+
143
+ /**
144
+ * Registers all configured switch/sensor accessories over Matter.
145
+ * No-op when Matter is disabled on the bridge.
146
+ */
147
+ setupMatterAccessories(cachedAccessories: Map<string, MatterAccessory>): Promise<void> {
148
+ return this.matterService.registerAccessories(cachedAccessories);
149
+ }
150
+
151
+ // ── Private helpers ────────────────────────────────────────────────────────
152
+
153
+ /** Replaces the accessory's auto-created information service and adds all exposed services. */
154
+ private populateAccessory(Svc: typeof Service): void {
155
+ const existingInfoService = this.accessory.getService(Svc.AccessoryInformation);
156
+ if (existingInfoService) {
157
+ this.accessory.removeService(existingInfoService);
158
+ }
159
+ this.accessory.updateDisplayName(this.options.name);
160
+ for (const service of this.serviceList) {
161
+ this.accessory.addService(service);
162
+ }
163
+ }
164
+
165
+ private buildState(defaultState: SecurityState): SystemState {
166
+ return {
167
+ currentState: defaultState,
168
+ targetState: defaultState,
169
+ defaultState,
170
+ availableTargetStates: [],
171
+ isArming: false,
172
+ isTripping: false,
173
+ isKnocked: false,
174
+ serverAuthenticationAttempts: 0,
175
+ pausedCurrentState: null,
176
+ armingLocks: { global: false, home: false, away: false, night: false },
177
+ modeAwayExtended: false,
178
+ };
179
+ }
180
+
181
+ private calcAvailableTargetStates(): SecurityState[] {
182
+ const all = [SecurityState.HOME, SecurityState.AWAY, SecurityState.NIGHT, SecurityState.OFF];
183
+ const disabled = this.options.disabledModes.map(m => modeToState(m.toLowerCase()));
184
+ return all.filter(s => !disabled.includes(s));
185
+ }
186
+
187
+ private logStartup(): void {
188
+ if (this.options.testMode) {
189
+ this.log.warn('Test Mode');
190
+ }
191
+ stateToMode(this.state.defaultState);
192
+ this.stateHandler.logMode('Default', this.state.defaultState);
193
+ this.log.info(`Arm delay (${this.options.armSeconds}s)`);
194
+ this.log.info(`Trigger delay (${this.options.triggerSeconds}s)`);
195
+ if (this.options.proxyMode) {
196
+ this.log.info('Proxy mode (Enabled)');
197
+ }
198
+ }
199
+ }
@@ -0,0 +1,76 @@
1
+ import { spawn } from 'child_process';
2
+ import type { Logging } from 'homebridge';
3
+ import { SecurityState } from '../types/security-state-type.js';
4
+ import { OriginType } from '../types/origin-type.js';
5
+ import { stateToMode } from '../utils/state-util.js';
6
+ import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
7
+ import type { SystemState } from '../interfaces/system-state-interface.js';
8
+ import type { EventBusService } from './event-bus-service.js';
9
+ import { EventType } from '../types/event-type.js';
10
+
11
+ /** Spawns shell commands in response to state-machine events. */
12
+ export class CommandService {
13
+ constructor(
14
+ private readonly log: Logging,
15
+ private readonly options: SecuritySystemOptions,
16
+ private readonly state: SystemState,
17
+ ) {}
18
+
19
+ attachToBus(bus: EventBusService): void {
20
+ bus.on(EventType.TARGET_CHANGED, ({ state, origin }) => {
21
+ this.execute('target', state, origin);
22
+ });
23
+ bus.on(EventType.CURRENT_CHANGED, ({ state, origin }) => {
24
+ this.execute('current', state, origin);
25
+ });
26
+ bus.on(EventType.WARNING, ({ origin }) => {
27
+ this.execute('current', 'warning' as unknown as SecurityState, origin);
28
+ });
29
+ }
30
+
31
+ execute(type: string, stateOrMode: SecurityState | string, origin: OriginType): void {
32
+ if (this.options.proxyMode && origin === OriginType.EXTERNAL) {
33
+ this.log.debug('Command bypassed (proxy mode).');
34
+ return;
35
+ }
36
+
37
+ let command = this.resolveCommand(type, stateOrMode);
38
+ if (!command) {
39
+ this.log.debug(`Command for ${type}/${stateOrMode} not set.`);
40
+ return;
41
+ }
42
+
43
+ const currentMode = stateToMode(this.state.currentState);
44
+ command = command.replace('${currentMode}', currentMode);
45
+
46
+ const proc = spawn(command, { shell: true });
47
+
48
+ proc.stdout.on('data', (data: Buffer) => {
49
+ this.log.info(`Command output: ${data.toString().trim()}`);
50
+ });
51
+
52
+ proc.stderr.on('data', (data: Buffer) => {
53
+ this.log.error(`Command failed (${command})\n${data.toString().trim()}`);
54
+ });
55
+ }
56
+
57
+ private resolveCommand(type: string, stateOrMode: SecurityState | string): string | null {
58
+ const s = stateOrMode;
59
+ const o = this.options;
60
+
61
+ if (s === 'warning') {
62
+ return o.commandCurrentWarning;
63
+ }
64
+
65
+ switch (s as SecurityState) {
66
+ case SecurityState.TRIGGERED: return o.commandCurrentTriggered;
67
+ case SecurityState.HOME: return type === 'current' ? o.commandCurrentHome : o.commandTargetHome;
68
+ case SecurityState.AWAY: return type === 'current' ? o.commandCurrentAway : o.commandTargetAway;
69
+ case SecurityState.NIGHT: return type === 'current' ? o.commandCurrentNight : o.commandTargetNight;
70
+ case SecurityState.OFF: return type === 'current' ? o.commandCurrentOff : o.commandTargetOff;
71
+ default:
72
+ this.log.error(`Unknown command state (${s})`);
73
+ return null;
74
+ }
75
+ }
76
+ }