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,275 @@
1
+ import { OpenAPIHono, createRoute } from '@hono/zod-openapi';
2
+ import { serve } from '@hono/node-server';
3
+ import { Scalar } from '@scalar/hono-api-reference';
4
+ import { SecurityState } from '../types/security-state-type.js';
5
+ import { OriginType } from '../types/origin-type.js';
6
+ import { stateToMode } from '../utils/state-util.js';
7
+ import { ErrorSchema } from '../schemas/error-schema.js';
8
+ import { StatusResponseSchema } from '../schemas/status-response-schema.js';
9
+ import { ModeRequestSchema } from '../schemas/mode-request-schema.js';
10
+ import { TripModeRequestSchema } from '../schemas/trip-mode-request-schema.js';
11
+ import { ArmingLockRequestSchema } from '../schemas/arming-lock-schema.js';
12
+ const MODE_TO_STATE = {
13
+ home: SecurityState.HOME,
14
+ away: SecurityState.AWAY,
15
+ night: SecurityState.NIGHT,
16
+ off: SecurityState.OFF,
17
+ };
18
+ const AUTH_RESPONSES = {
19
+ 401: {
20
+ content: { 'application/json': { schema: ErrorSchema } },
21
+ description: 'API key required',
22
+ },
23
+ 403: {
24
+ content: { 'application/json': { schema: ErrorSchema } },
25
+ description: 'API key invalid or blocked',
26
+ },
27
+ };
28
+ const statusRoute = createRoute({
29
+ method: 'get',
30
+ path: '/state',
31
+ summary: 'Get state',
32
+ description: 'Returns the current arming state, active mode, target mode, and trip status.',
33
+ security: [{ BearerAuth: [] }],
34
+ responses: {
35
+ 200: {
36
+ content: { 'application/json': { schema: StatusResponseSchema } },
37
+ description: 'Current system state',
38
+ },
39
+ ...AUTH_RESPONSES,
40
+ },
41
+ });
42
+ const modeRoute = createRoute({
43
+ method: 'put',
44
+ path: '/mode/update',
45
+ summary: 'Change mode',
46
+ description: 'Sets the target security mode. Supported modes: home, away, night, off. ' +
47
+ 'An optional delay (seconds) defers the transition. ' +
48
+ 'To trigger the alarm use the POST /mode/trip endpoint instead.',
49
+ security: [{ BearerAuth: [] }],
50
+ request: {
51
+ body: {
52
+ content: {
53
+ 'application/json': {
54
+ schema: ModeRequestSchema,
55
+ example: { mode: 'home', delay: 5 },
56
+ },
57
+ },
58
+ required: true,
59
+ },
60
+ },
61
+ responses: {
62
+ 204: { description: 'Mode change accepted' },
63
+ ...AUTH_RESPONSES,
64
+ 409: {
65
+ content: { 'application/json': { schema: ErrorSchema } },
66
+ description: 'Mode change rejected by the system',
67
+ },
68
+ },
69
+ });
70
+ const tripModeRoute = createRoute({
71
+ method: 'post',
72
+ path: '/mode/trip',
73
+ summary: 'Trip mode',
74
+ description: 'Activates the alarm ("triggered" state). ' +
75
+ 'If mode is specified the alarm only fires when the system is currently in that mode. ' +
76
+ 'An optional delay (seconds) defers the activation.',
77
+ security: [{ BearerAuth: [] }],
78
+ request: {
79
+ body: {
80
+ content: {
81
+ 'application/json': {
82
+ schema: TripModeRequestSchema,
83
+ example: { mode: 'home', delay: 30 },
84
+ },
85
+ },
86
+ required: true,
87
+ },
88
+ },
89
+ responses: {
90
+ 204: { description: 'Trip accepted' },
91
+ ...AUTH_RESPONSES,
92
+ 409: {
93
+ content: { 'application/json': { schema: ErrorSchema } },
94
+ description: 'Trip rejected by the system',
95
+ },
96
+ },
97
+ });
98
+ const armingLockRoute = createRoute({
99
+ method: 'put',
100
+ path: '/switches/arming-lock',
101
+ summary: 'Update arming lock',
102
+ description: 'Enables or disables the arming lock for a specific mode or globally.',
103
+ security: [{ BearerAuth: [] }],
104
+ request: {
105
+ body: {
106
+ content: {
107
+ 'application/json': {
108
+ schema: ArmingLockRequestSchema,
109
+ example: { mode: 'home', value: true },
110
+ },
111
+ },
112
+ required: true,
113
+ },
114
+ },
115
+ responses: {
116
+ 204: { description: 'Arming lock updated' },
117
+ ...AUTH_RESPONSES,
118
+ 409: {
119
+ content: { 'application/json': { schema: ErrorSchema } },
120
+ description: 'Arming lock update rejected by the system',
121
+ },
122
+ },
123
+ });
124
+ /** Hono HTTP server providing remote control of the security system. */
125
+ export class ServerService {
126
+ log;
127
+ options;
128
+ state;
129
+ stateHandler;
130
+ tripHandler;
131
+ switchHandler;
132
+ application = new OpenAPIHono();
133
+ constructor(log, options, state, stateHandler, tripHandler, switchHandler) {
134
+ this.log = log;
135
+ this.options = options;
136
+ this.state = state;
137
+ this.stateHandler = stateHandler;
138
+ this.tripHandler = tripHandler;
139
+ this.switchHandler = switchHandler;
140
+ this.registerRoutes();
141
+ }
142
+ /** Exposed for testing — returns the underlying Hono app. */
143
+ get app() {
144
+ return this.application;
145
+ }
146
+ start() {
147
+ const server = serve({ fetch: this.application.fetch, port: this.options.serverPort }, () => this.log.info(`Server (${this.options.serverPort})`));
148
+ server.on('error', (error) => {
149
+ this.log.error('Server error.');
150
+ this.log.error(String(error));
151
+ });
152
+ }
153
+ createAuthMiddleware() {
154
+ return async (c, next) => {
155
+ const authError = this.authenticate(c);
156
+ if (authError !== null) {
157
+ return authError;
158
+ }
159
+ await next();
160
+ };
161
+ }
162
+ registerRoutes() {
163
+ const auth = this.createAuthMiddleware();
164
+ // Scalar UI
165
+ this.application.get('/', Scalar({
166
+ url: '/openapi.json',
167
+ pageTitle: 'Homebridge Security System API',
168
+ defaultOpenAllTags: true,
169
+ }));
170
+ // Register Bearer auth security scheme for OpenAPI documentation
171
+ this.application.openAPIRegistry.registerComponent('securitySchemes', 'BearerAuth', {
172
+ type: 'http',
173
+ scheme: 'bearer',
174
+ description: 'API key configured via the server_api_key plugin option.',
175
+ });
176
+ // OpenAPI spec
177
+ this.application.doc('/openapi.json', {
178
+ openapi: '3.1.0',
179
+ info: {
180
+ title: 'Homebridge Security System API',
181
+ version: '1.0.0',
182
+ description: 'Remote control API for the Homebridge Security System plugin.',
183
+ },
184
+ });
185
+ // GET /state
186
+ this.application.use('/state', auth);
187
+ this.application.openapi(statusRoute, (c) => {
188
+ return c.json({
189
+ arming: this.state.isArming,
190
+ current_mode: stateToMode(this.state.currentState),
191
+ target_mode: stateToMode(this.state.targetState),
192
+ tripped: this.stateHandler.isTripping(),
193
+ });
194
+ });
195
+ // PUT /mode/update
196
+ this.application.use('/mode/update', auth);
197
+ this.application.openapi(modeRoute, (c) => {
198
+ const { mode, delay = 0 } = c.req.valid('json');
199
+ const result = this.stateHandler.updateTargetState(MODE_TO_STATE[mode], OriginType.EXTERNAL, delay);
200
+ if (!result.success) {
201
+ return c.json({ reason: result.reason ?? 'Mode change rejected by the system' }, 409);
202
+ }
203
+ return c.body(null, 204);
204
+ });
205
+ // POST /mode/trip
206
+ this.application.use('/mode/trip', auth);
207
+ this.application.openapi(tripModeRoute, (c) => {
208
+ const { mode, delay } = c.req.valid('json');
209
+ const effectiveDelay = delay ?? 0;
210
+ if (effectiveDelay > 0) {
211
+ const precheck = this.tripHandler.checkTripConditions(true, OriginType.EXTERNAL);
212
+ if (!precheck.success) {
213
+ return c.json({ reason: precheck.reason ?? 'Trip rejected by the system' }, 409);
214
+ }
215
+ setTimeout(() => {
216
+ if (mode != null) {
217
+ this.tripHandler.triggerIfModeSet(MODE_TO_STATE[mode], true);
218
+ }
219
+ else {
220
+ this.tripHandler.updateTripSwitch(true, OriginType.EXTERNAL, false);
221
+ }
222
+ }, effectiveDelay * 1000);
223
+ return c.body(null, 204);
224
+ }
225
+ let result;
226
+ if (mode != null) {
227
+ result = this.tripHandler.triggerIfModeSet(MODE_TO_STATE[mode], true);
228
+ }
229
+ else {
230
+ result = this.tripHandler.updateTripSwitch(true, OriginType.EXTERNAL, false);
231
+ }
232
+ if (!result.success) {
233
+ return c.json({ reason: result.reason ?? 'Trip rejected by the system' }, 409);
234
+ }
235
+ return c.body(null, 204);
236
+ });
237
+ // PUT /switches/arming-lock
238
+ this.application.use('/switches/arming-lock', auth);
239
+ this.application.openapi(armingLockRoute, (c) => {
240
+ const { mode, value } = c.req.valid('json');
241
+ const result = this.switchHandler.updateArmingLock(mode, value);
242
+ if (!result.success) {
243
+ return c.json({ reason: result.reason ?? 'Arming lock update rejected by the system' }, 409);
244
+ }
245
+ return c.body(null, 204);
246
+ });
247
+ }
248
+ /**
249
+ * Validates the Authorization: Bearer <key> header when serverApiKey is configured.
250
+ * Returns an error Response on failure, or null to allow the request through.
251
+ */
252
+ authenticate(context) {
253
+ if (this.options.serverApiKey === null) {
254
+ return null;
255
+ }
256
+ const authHeader = context.req.header('Authorization');
257
+ if (authHeader === undefined || !authHeader.startsWith('Bearer ')) {
258
+ this.log.info('API key required - Authorization header missing (Server)');
259
+ return context.json({ reason: 'API key required. Use the Authorization: Bearer <key> header.' }, 401);
260
+ }
261
+ if (this.state.serverAuthenticationAttempts >= 5) {
262
+ this.log.warn('API key blocked - too many invalid attempts (Server)');
263
+ return context.json({ reason: 'API key blocked due to too many invalid attempts' }, 403);
264
+ }
265
+ const apiKey = authHeader.slice('Bearer '.length).trim();
266
+ if (apiKey !== this.options.serverApiKey) {
267
+ this.state.serverAuthenticationAttempts++;
268
+ this.log.warn(`API key invalid - attempt ${this.state.serverAuthenticationAttempts}/5 (Server)`);
269
+ return context.json({ reason: 'API key invalid' }, 403);
270
+ }
271
+ this.state.serverAuthenticationAttempts = 0;
272
+ return null;
273
+ }
274
+ }
275
+ //# sourceMappingURL=server-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-service.js","sourceRoot":"","sources":["../../src/services/server-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Logging } from 'homebridge';
2
+ import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
3
+ import type { SystemState } from '../interfaces/system-state-interface.js';
4
+ /** Persists and restores security system state across Homebridge restarts. */
5
+ export declare class StorageService {
6
+ private readonly log;
7
+ private readonly options;
8
+ private readonly storagePath;
9
+ private initialised;
10
+ constructor(log: Logging, options: SecuritySystemOptions, storagePath: string);
11
+ init(): Promise<void>;
12
+ load(state: SystemState): Promise<void>;
13
+ save(state: SystemState): Promise<void>;
14
+ }
@@ -0,0 +1,70 @@
1
+ import path from 'path';
2
+ import storage from 'node-persist';
3
+ import { SecurityState } from '../types/security-state-type.js';
4
+ /** Persists and restores security system state across Homebridge restarts. */
5
+ export class StorageService {
6
+ log;
7
+ options;
8
+ storagePath;
9
+ initialised = false;
10
+ constructor(log, options, storagePath) {
11
+ this.log = log;
12
+ this.options = options;
13
+ this.storagePath = storagePath;
14
+ }
15
+ async init() {
16
+ const dir = path.join(this.storagePath, `homebridge-securitysystem-${this.options.serialNumber}`);
17
+ try {
18
+ await storage.init({ dir });
19
+ this.initialised = true;
20
+ }
21
+ catch (err) {
22
+ this.log.error('Unable to initialise storage.');
23
+ this.log.error(String(err));
24
+ }
25
+ }
26
+ async load(state) {
27
+ if (!this.initialised) {
28
+ return;
29
+ }
30
+ if (this.options.testMode) {
31
+ await storage.clear();
32
+ this.log.debug('Saved state cleared (test mode).');
33
+ return;
34
+ }
35
+ try {
36
+ const saved = await storage.getItem('state');
37
+ if (!saved) {
38
+ return;
39
+ }
40
+ this.log.info('Saved state (Found)');
41
+ this.log.debug('State (Loaded)', String(JSON.stringify(saved)));
42
+ const current = saved.currentState ?? state.defaultState;
43
+ const target = saved.targetState ?? state.defaultState;
44
+ state.currentState = current;
45
+ state.targetState = current === SecurityState.TRIGGERED ? target : current;
46
+ }
47
+ catch (err) {
48
+ this.log.error('Saved state (Error)');
49
+ this.log.error(String(err));
50
+ }
51
+ }
52
+ async save(state) {
53
+ if (!this.options.saveState || !this.initialised) {
54
+ return;
55
+ }
56
+ const data = {
57
+ currentState: state.currentState,
58
+ targetState: state.targetState,
59
+ };
60
+ try {
61
+ await storage.setItem('state', data);
62
+ this.log.debug('State (Saved)', String(JSON.stringify(data)));
63
+ }
64
+ catch (err) {
65
+ this.log.error('Unable to save state.');
66
+ this.log.error(String(err));
67
+ }
68
+ }
69
+ }
70
+ //# sourceMappingURL=storage-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-service.js","sourceRoot":"","sources":["../../src/services/storage-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
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 { SecuritySystemOptions } from '../interfaces/options-interface.js';
5
+ import type { SystemState } from '../interfaces/system-state-interface.js';
6
+ import type { EventBusService } from './event-bus-service.js';
7
+ /** Sends HTTP GET webhook requests in response to state-machine events. */
8
+ export declare class WebhookService {
9
+ private readonly log;
10
+ private readonly options;
11
+ private readonly state;
12
+ constructor(log: Logging, options: SecuritySystemOptions, state: SystemState);
13
+ attachToBus(bus: EventBusService): void;
14
+ send(type: string, stateOrMode: SecurityState | string, origin: OriginType): void;
15
+ private resolveUrl;
16
+ }
@@ -0,0 +1,68 @@
1
+ import { SecurityState } from '../types/security-state-type.js';
2
+ import { OriginType } from '../types/origin-type.js';
3
+ import { stateToMode } from '../utils/state-util.js';
4
+ import { EventType } from '../types/event-type.js';
5
+ /** Sends HTTP GET webhook requests in response to state-machine events. */
6
+ export class WebhookService {
7
+ log;
8
+ options;
9
+ state;
10
+ constructor(log, options, state) {
11
+ this.log = log;
12
+ this.options = options;
13
+ this.state = state;
14
+ }
15
+ attachToBus(bus) {
16
+ bus.on(EventType.TARGET_CHANGED, ({ state, origin }) => {
17
+ this.send('target', state, origin);
18
+ });
19
+ bus.on(EventType.CURRENT_CHANGED, ({ state, origin }) => {
20
+ this.send('current', state, origin);
21
+ });
22
+ bus.on(EventType.WARNING, ({ origin }) => {
23
+ this.send('current', 'warning', origin);
24
+ });
25
+ }
26
+ send(type, stateOrMode, origin) {
27
+ if (this.options.proxyMode && origin === OriginType.EXTERNAL) {
28
+ this.log.debug('Webhook bypassed (proxy mode).');
29
+ return;
30
+ }
31
+ const urlTemplate = this.resolveUrl(type, stateOrMode);
32
+ if (!urlTemplate) {
33
+ this.log.debug(`Webhook URL for ${type}/${stateOrMode} not set.`);
34
+ return;
35
+ }
36
+ const currentMode = stateToMode(this.state.currentState);
37
+ const url = urlTemplate.replace('${currentMode}', currentMode);
38
+ fetch(url)
39
+ .then(res => {
40
+ if (!res.ok) {
41
+ throw new Error(`Status ${res.status}`);
42
+ }
43
+ this.log.info('Webhook event (Sent)');
44
+ })
45
+ .catch(err => {
46
+ this.log.error(`Webhook request failed (${url})`);
47
+ this.log.error(String(err));
48
+ });
49
+ }
50
+ resolveUrl(type, stateOrMode) {
51
+ const s = stateOrMode;
52
+ const o = this.options;
53
+ if (s === 'warning') {
54
+ return o.webhookCurrentWarning;
55
+ }
56
+ switch (s) {
57
+ case SecurityState.TRIGGERED: return o.webhookCurrentTriggered;
58
+ case SecurityState.HOME: return type === 'current' ? o.webhookCurrentHome : o.webhookTargetHome;
59
+ case SecurityState.AWAY: return type === 'current' ? o.webhookCurrentAway : o.webhookTargetAway;
60
+ case SecurityState.NIGHT: return type === 'current' ? o.webhookCurrentNight : o.webhookTargetNight;
61
+ case SecurityState.OFF: return type === 'current' ? o.webhookCurrentOff : o.webhookTargetOff;
62
+ default:
63
+ this.log.error(`Unknown webhook state (${s})`);
64
+ return null;
65
+ }
66
+ }
67
+ }
68
+ //# sourceMappingURL=webhook-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-service.js","sourceRoot":"","sources":["../../src/services/webhook-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAIrD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,2EAA2E;AAC3E,MAAM,OAAO,cAAc;IAEN,GAAG;IACH,OAAO;IACP,KAAK;IAHxB,YACmB,GAAY,EACZ,OAA8B,EAC9B,KAAkB;mBAFlB,GAAG;uBACH,OAAO;qBACP,KAAK;IACrB,CAAC;IAEJ,WAAW,CAAC,GAAoB;QAC9B,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,cAAc,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;YACrD,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;QACH,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,eAAe,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;YACtD,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QACH,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;YACvC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAqC,EAAE,MAAM,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC,IAAY,EAAE,WAAmC,EAAE,MAAkB;QACxE,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,MAAM,KAAK,UAAU,CAAC,QAAQ,EAAE,CAAC;YAC7D,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;YACjD,OAAO;QACT,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACvD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,mBAAmB,IAAI,IAAI,WAAW,WAAW,CAAC,CAAC;YAClE,OAAO;QACT,CAAC;QAED,MAAM,WAAW,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QACzD,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;QAE/D,KAAK,CAAC,GAAG,CAAC;aACP,IAAI,CAAC,GAAG,CAAC,EAAE;YACV,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,UAAU,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1C,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QACxC,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,CAAC,EAAE;YACX,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,2BAA2B,GAAG,GAAG,CAAC,CAAC;YAClD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,UAAU,CAAC,IAAY,EAAE,WAAmC;QAClE,MAAM,CAAC,GAAG,WAAW,CAAC;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;QAEvB,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;YACpB,OAAO,CAAC,CAAC,qBAAqB,CAAC;QACjC,CAAC;QAED,QAAQ,CAAkB,EAAE,CAAC;YAC7B,KAAK,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,uBAAuB,CAAC;YAC/D,KAAK,aAAa,CAAC,IAAI,EAAE,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC;YAChG,KAAK,aAAa,CAAC,IAAI,EAAE,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC;YAChG,KAAK,aAAa,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC;YACnG,KAAK,aAAa,CAAC,GAAG,EAAE,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC;YAC7F;gBACE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,0BAA0B,CAAC,GAAG,CAAC,CAAC;gBAC/C,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { SecurityState } from '../types/security-state-type.js';
3
+ import { OriginType } from '../types/origin-type.js';
4
+ import { NotArmedCondition } from '../conditions/not-armed-condition.js';
5
+ import { ArmingInProgressCondition } from '../conditions/arming-in-progress-condition.js';
6
+ import { AlreadyTriggeredCondition } from '../conditions/already-triggered-condition.js';
7
+ import { DoubleKnockCondition } from '../conditions/double-knock-condition.js';
8
+ import { ArmingLockCondition } from '../conditions/arming-lock-condition.js';
9
+ // ── Helpers ──────────────────────────────────────────────────────────────────
10
+ function makeState(overrides = {}) {
11
+ return {
12
+ currentState: SecurityState.HOME,
13
+ targetState: SecurityState.HOME,
14
+ defaultState: SecurityState.OFF,
15
+ availableTargetStates: [SecurityState.HOME, SecurityState.AWAY, SecurityState.NIGHT, SecurityState.OFF],
16
+ isArming: false,
17
+ isTripping: false,
18
+ isKnocked: false,
19
+ serverAuthenticationAttempts: 0,
20
+ pausedCurrentState: null,
21
+ armingLocks: { global: false, home: false, away: false, night: false },
22
+ modeAwayExtended: false,
23
+ ...overrides,
24
+ };
25
+ }
26
+ function makeOptions(overrides = {}) {
27
+ return {
28
+ overrideOff: false,
29
+ doubleKnock: false,
30
+ doubleKnockSeconds: 90,
31
+ doubleKnockModes: [],
32
+ homeDoubleKnockSeconds: null,
33
+ awayDoubleKnockSeconds: null,
34
+ nightDoubleKnockSeconds: null,
35
+ armingLockSwitch: false,
36
+ armingLockSwitches: false,
37
+ ...overrides,
38
+ };
39
+ }
40
+ function makeLog() {
41
+ return { info: vi.fn(), warn: vi.fn(), debug: vi.fn(), error: vi.fn(), success: vi.fn() };
42
+ }
43
+ function makeCtx(state, options, value, origin = OriginType.REGULAR_SWITCH) {
44
+ return { state, options, value, origin, log: makeLog() };
45
+ }
46
+ // ── ArmingLockCondition ───────────────────────────────────────────────────────
47
+ describe('ArmingLockCondition', () => {
48
+ const cond = new ArmingLockCondition();
49
+ it('blocks when the global lock is active and arming to an armed mode', () => {
50
+ const state = makeState({ armingLocks: { global: true, home: false, away: false, night: false } });
51
+ expect(cond.evaluate(makeCtx(state, makeOptions({ armingLockSwitch: true }), true))).toBe(true);
52
+ expect(cond.failureReason).toBe('arming is blocked by the global arming lock switch');
53
+ });
54
+ it('blocks when the matching mode lock is active', () => {
55
+ const state = makeState({ targetState: SecurityState.AWAY, armingLocks: { global: false, home: false, away: true, night: false } });
56
+ expect(cond.evaluate(makeCtx(state, makeOptions({ armingLockSwitches: true }), true))).toBe(true);
57
+ });
58
+ it('does not block when the lock feature is disabled', () => {
59
+ const state = makeState({ armingLocks: { global: true, home: false, away: false, night: false } });
60
+ expect(cond.evaluate(makeCtx(state, makeOptions(), true))).toBe(false);
61
+ });
62
+ it('does not block disarming', () => {
63
+ const state = makeState({ targetState: SecurityState.OFF, armingLocks: { global: true, home: false, away: false, night: false } });
64
+ expect(cond.evaluate(makeCtx(state, makeOptions({ armingLockSwitch: true }), true))).toBe(false);
65
+ });
66
+ it('allows when no lock is active', () => {
67
+ const state = makeState();
68
+ expect(cond.evaluate(makeCtx(state, makeOptions({ armingLockSwitch: true }), true))).toBe(false);
69
+ });
70
+ });
71
+ // ── NotArmedCondition ─────────────────────────────────────────────────────────
72
+ describe('NotArmedCondition', () => {
73
+ const cond = new NotArmedCondition();
74
+ it('blocks when disarmed and override_off is false', () => {
75
+ const state = makeState({ currentState: SecurityState.OFF });
76
+ const opts = makeOptions({ overrideOff: false });
77
+ expect(cond.evaluate(makeCtx(state, opts, true))).toBe(true);
78
+ });
79
+ it('allows when disarmed but override_off is true', () => {
80
+ const state = makeState({ currentState: SecurityState.OFF });
81
+ const opts = makeOptions({ overrideOff: true });
82
+ expect(cond.evaluate(makeCtx(state, opts, true))).toBe(false);
83
+ });
84
+ it('allows when disarmed but origin is OVERRIDE_SWITCH', () => {
85
+ const state = makeState({ currentState: SecurityState.OFF });
86
+ const opts = makeOptions({ overrideOff: false });
87
+ expect(cond.evaluate(makeCtx(state, opts, true, OriginType.OVERRIDE_SWITCH))).toBe(false);
88
+ });
89
+ it('allows when system is armed', () => {
90
+ const state = makeState({ currentState: SecurityState.HOME });
91
+ const opts = makeOptions({ overrideOff: false });
92
+ expect(cond.evaluate(makeCtx(state, opts, true))).toBe(false);
93
+ });
94
+ it('does not block on value=false', () => {
95
+ const state = makeState({ currentState: SecurityState.OFF });
96
+ const opts = makeOptions({ overrideOff: false });
97
+ expect(cond.evaluate(makeCtx(state, opts, false))).toBe(false);
98
+ });
99
+ });
100
+ // ── ArmingInProgressCondition ─────────────────────────────────────────────────
101
+ describe('ArmingInProgressCondition', () => {
102
+ const cond = new ArmingInProgressCondition();
103
+ it('blocks when isArming is true and value is true', () => {
104
+ const state = makeState({ isArming: true });
105
+ expect(cond.evaluate(makeCtx(state, makeOptions(), true))).toBe(true);
106
+ });
107
+ it('allows when not arming', () => {
108
+ const state = makeState({ isArming: false });
109
+ expect(cond.evaluate(makeCtx(state, makeOptions(), true))).toBe(false);
110
+ });
111
+ it('does not block on value=false', () => {
112
+ const state = makeState({ isArming: true });
113
+ expect(cond.evaluate(makeCtx(state, makeOptions(), false))).toBe(false);
114
+ });
115
+ });
116
+ // ── AlreadyTriggeredCondition ─────────────────────────────────────────────────
117
+ describe('AlreadyTriggeredCondition', () => {
118
+ const cond = new AlreadyTriggeredCondition();
119
+ it('blocks when currently triggered and value is true', () => {
120
+ const state = makeState({ currentState: SecurityState.TRIGGERED });
121
+ expect(cond.evaluate(makeCtx(state, makeOptions(), true))).toBe(true);
122
+ });
123
+ it('allows when not triggered', () => {
124
+ const state = makeState({ currentState: SecurityState.HOME });
125
+ expect(cond.evaluate(makeCtx(state, makeOptions(), true))).toBe(false);
126
+ });
127
+ it('does not block on value=false (cancel)', () => {
128
+ const state = makeState({ currentState: SecurityState.TRIGGERED });
129
+ expect(cond.evaluate(makeCtx(state, makeOptions(), false))).toBe(false);
130
+ });
131
+ });
132
+ // ── DoubleKnockCondition ──────────────────────────────────────────────────────
133
+ describe('DoubleKnockCondition', () => {
134
+ it('allows through when doubleKnock option is disabled', () => {
135
+ const onFirstKnock = vi.fn();
136
+ const cond = new DoubleKnockCondition(onFirstKnock, vi.fn());
137
+ const state = makeState({ isKnocked: false });
138
+ const opts = makeOptions({ doubleKnock: false, doubleKnockModes: ['home'] });
139
+ expect(cond.evaluate(makeCtx(state, opts, true))).toBe(false);
140
+ expect(onFirstKnock).not.toHaveBeenCalled();
141
+ });
142
+ it('allows override switch to bypass double-knock', () => {
143
+ const onFirstKnock = vi.fn();
144
+ const cond = new DoubleKnockCondition(onFirstKnock, vi.fn());
145
+ const state = makeState({ isKnocked: false, currentState: SecurityState.HOME });
146
+ const opts = makeOptions({ doubleKnock: true, doubleKnockModes: ['home'], doubleKnockSeconds: 90 });
147
+ expect(cond.evaluate(makeCtx(state, opts, true, OriginType.OVERRIDE_SWITCH))).toBe(false);
148
+ expect(onFirstKnock).not.toHaveBeenCalled();
149
+ });
150
+ it('blocks first knock and calls onFirstKnock', () => {
151
+ const onFirstKnock = vi.fn();
152
+ const cond = new DoubleKnockCondition(onFirstKnock, vi.fn());
153
+ const state = makeState({ isKnocked: false, currentState: SecurityState.HOME });
154
+ const opts = makeOptions({ doubleKnock: true, doubleKnockModes: ['home'], doubleKnockSeconds: 90 });
155
+ expect(cond.evaluate(makeCtx(state, opts, true))).toBe(true);
156
+ expect(state.isKnocked).toBe(true);
157
+ expect(onFirstKnock).toHaveBeenCalledWith(90, expect.any(Function));
158
+ });
159
+ it('allows second knock through and clears isKnocked', () => {
160
+ const cond = new DoubleKnockCondition(vi.fn(), vi.fn());
161
+ const state = makeState({ isKnocked: true, currentState: SecurityState.HOME });
162
+ const opts = makeOptions({ doubleKnock: true, doubleKnockModes: ['home'], doubleKnockSeconds: 90 });
163
+ expect(cond.evaluate(makeCtx(state, opts, true))).toBe(false);
164
+ expect(state.isKnocked).toBe(false);
165
+ });
166
+ it('uses mode-specific knock window when configured', () => {
167
+ const onFirstKnock = vi.fn();
168
+ const cond = new DoubleKnockCondition(onFirstKnock, vi.fn());
169
+ const state = makeState({ isKnocked: false, currentState: SecurityState.AWAY });
170
+ const opts = makeOptions({ doubleKnock: true, doubleKnockModes: ['away'], doubleKnockSeconds: 90, awayDoubleKnockSeconds: 30 });
171
+ cond.evaluate(makeCtx(state, opts, true));
172
+ expect(onFirstKnock).toHaveBeenCalledWith(30, expect.any(Function));
173
+ });
174
+ });
175
+ //# sourceMappingURL=conditions.test.js.map