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.
- package/.gitattributes +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +46 -0
- package/.github/ISSUE_TEMPLATE/feature-request.yml +17 -0
- package/.github/ISSUE_TEMPLATE/questions-answers.yml +40 -0
- package/.github/demo.jpg +0 -0
- package/.github/dependabot.yml +19 -0
- package/.github/release.yml +36 -0
- package/.github/workflows/build-package.yml +49 -0
- package/.github/workflows/bump-version.yml +59 -0
- package/.github/workflows/dependency-review.yml +26 -0
- package/.github/workflows/publish.yml +155 -0
- package/.github/workflows/stale-activity.yml +27 -0
- package/AGENTS.md +247 -0
- package/CHANGELOG.md +572 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +5 -0
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/SECURITY.md +12 -0
- package/config.schema.json +934 -0
- package/dist/conditions/already-triggered-condition.d.ts +10 -0
- package/dist/conditions/already-triggered-condition.js +22 -0
- package/dist/conditions/already-triggered-condition.js.map +1 -0
- package/dist/conditions/arming-in-progress-condition.d.ts +10 -0
- package/dist/conditions/arming-in-progress-condition.js +21 -0
- package/dist/conditions/arming-in-progress-condition.js.map +1 -0
- package/dist/conditions/arming-lock-condition.d.ts +10 -0
- package/dist/conditions/arming-lock-condition.js +43 -0
- package/dist/conditions/arming-lock-condition.js.map +1 -0
- package/dist/conditions/condition.d.ts +22 -0
- package/dist/conditions/condition.js +19 -0
- package/dist/conditions/condition.js.map +1 -0
- package/dist/conditions/double-knock-condition.d.ts +24 -0
- package/dist/conditions/double-knock-condition.js +73 -0
- package/dist/conditions/double-knock-condition.js.map +1 -0
- package/dist/conditions/not-armed-condition.d.ts +10 -0
- package/dist/conditions/not-armed-condition.js +26 -0
- package/dist/conditions/not-armed-condition.js.map +1 -0
- package/dist/conditions/trigger-already-running-condition.d.ts +10 -0
- package/dist/conditions/trigger-already-running-condition.js +21 -0
- package/dist/conditions/trigger-already-running-condition.js.map +1 -0
- package/dist/constants/default-constant.d.ts +24 -0
- package/dist/constants/default-constant.js +27 -0
- package/dist/constants/default-constant.js.map +1 -0
- package/dist/constants/homekit-constant.d.ts +2 -0
- package/dist/constants/homekit-constant.js +3 -0
- package/dist/constants/homekit-constant.js.map +1 -0
- package/dist/constants/matter-constant.d.ts +21 -0
- package/dist/constants/matter-constant.js +61 -0
- package/dist/constants/matter-constant.js.map +1 -0
- package/dist/handlers/sensor-handler.d.ts +24 -0
- package/dist/handlers/sensor-handler.js +63 -0
- package/dist/handlers/sensor-handler.js.map +1 -0
- package/dist/handlers/state-handler.d.ts +49 -0
- package/dist/handlers/state-handler.js +204 -0
- package/dist/handlers/state-handler.js.map +1 -0
- package/dist/handlers/switch-handler.d.ts +28 -0
- package/dist/handlers/switch-handler.js +95 -0
- package/dist/handlers/switch-handler.js.map +1 -0
- package/dist/handlers/trip-handler.d.ts +44 -0
- package/dist/handlers/trip-handler.js +156 -0
- package/dist/handlers/trip-handler.js.map +1 -0
- package/dist/homekit/homekit-registrar.d.ts +12 -0
- package/dist/homekit/homekit-registrar.js +23 -0
- package/dist/homekit/homekit-registrar.js.map +1 -0
- package/dist/homekit/service-factory.d.ts +12 -0
- package/dist/homekit/service-factory.js +24 -0
- package/dist/homekit/service-factory.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/condition-context-interface.d.ts +14 -0
- package/dist/interfaces/condition-context-interface.js +2 -0
- package/dist/interfaces/condition-context-interface.js.map +1 -0
- package/dist/interfaces/hap-types-interface.d.ts +9 -0
- package/dist/interfaces/hap-types-interface.js +2 -0
- package/dist/interfaces/hap-types-interface.js.map +1 -0
- package/dist/interfaces/mqtt-status-payload-interface.d.ts +6 -0
- package/dist/interfaces/mqtt-status-payload-interface.js +2 -0
- package/dist/interfaces/mqtt-status-payload-interface.js.map +1 -0
- package/dist/interfaces/options-interface.d.ts +87 -0
- package/dist/interfaces/options-interface.js +2 -0
- package/dist/interfaces/options-interface.js.map +1 -0
- package/dist/interfaces/persisted-state-interface.d.ts +6 -0
- package/dist/interfaces/persisted-state-interface.js +2 -0
- package/dist/interfaces/persisted-state-interface.js.map +1 -0
- package/dist/interfaces/service-registry-interface.d.ts +10 -0
- package/dist/interfaces/service-registry-interface.js +2 -0
- package/dist/interfaces/service-registry-interface.js.map +1 -0
- package/dist/interfaces/system-state-interface.d.ts +22 -0
- package/dist/interfaces/system-state-interface.js +2 -0
- package/dist/interfaces/system-state-interface.js.map +1 -0
- package/dist/schemas/arming-lock-schema.d.ts +10 -0
- package/dist/schemas/arming-lock-schema.js +12 -0
- package/dist/schemas/arming-lock-schema.js.map +1 -0
- package/dist/schemas/error-schema.d.ts +4 -0
- package/dist/schemas/error-schema.js +7 -0
- package/dist/schemas/error-schema.js.map +1 -0
- package/dist/schemas/mode-request-schema.d.ts +10 -0
- package/dist/schemas/mode-request-schema.js +18 -0
- package/dist/schemas/mode-request-schema.js.map +1 -0
- package/dist/schemas/status-response-schema.d.ts +7 -0
- package/dist/schemas/status-response-schema.js +10 -0
- package/dist/schemas/status-response-schema.js.map +1 -0
- package/dist/schemas/trip-mode-request-schema.d.ts +9 -0
- package/dist/schemas/trip-mode-request-schema.js +24 -0
- package/dist/schemas/trip-mode-request-schema.js.map +1 -0
- package/dist/security-system-platform.d.ts +19 -0
- package/dist/security-system-platform.js +56 -0
- package/dist/security-system-platform.js.map +1 -0
- package/dist/security-system.d.ts +28 -0
- package/dist/security-system.js +169 -0
- package/dist/security-system.js.map +1 -0
- package/dist/services/command-service.d.ts +16 -0
- package/dist/services/command-service.js +65 -0
- package/dist/services/command-service.js.map +1 -0
- package/dist/services/configuration-service.d.ts +24 -0
- package/dist/services/configuration-service.js +189 -0
- package/dist/services/configuration-service.js.map +1 -0
- package/dist/services/event-bus-service.d.ts +12 -0
- package/dist/services/event-bus-service.js +17 -0
- package/dist/services/event-bus-service.js.map +1 -0
- package/dist/services/matter-service.d.ts +69 -0
- package/dist/services/matter-service.js +363 -0
- package/dist/services/matter-service.js.map +1 -0
- package/dist/services/mqtt-service.d.ts +16 -0
- package/dist/services/mqtt-service.js +97 -0
- package/dist/services/mqtt-service.js.map +1 -0
- package/dist/services/server-service.d.ts +28 -0
- package/dist/services/server-service.js +275 -0
- package/dist/services/server-service.js.map +1 -0
- package/dist/services/storage-service.d.ts +14 -0
- package/dist/services/storage-service.js +70 -0
- package/dist/services/storage-service.js.map +1 -0
- package/dist/services/webhook-service.d.ts +16 -0
- package/dist/services/webhook-service.js +68 -0
- package/dist/services/webhook-service.js.map +1 -0
- package/dist/tests/conditions.test.d.ts +1 -0
- package/dist/tests/conditions.test.js +175 -0
- package/dist/tests/conditions.test.js.map +1 -0
- package/dist/tests/matter-service.test.d.ts +1 -0
- package/dist/tests/matter-service.test.js +475 -0
- package/dist/tests/matter-service.test.js.map +1 -0
- package/dist/tests/mqtt-service.test.d.ts +1 -0
- package/dist/tests/mqtt-service.test.js +168 -0
- package/dist/tests/mqtt-service.test.js.map +1 -0
- package/dist/tests/sensor-handler.test.d.ts +1 -0
- package/dist/tests/sensor-handler.test.js +87 -0
- package/dist/tests/sensor-handler.test.js.map +1 -0
- package/dist/tests/server-service.test.d.ts +1 -0
- package/dist/tests/server-service.test.js +188 -0
- package/dist/tests/server-service.test.js.map +1 -0
- package/dist/tests/state-handler.test.d.ts +1 -0
- package/dist/tests/state-handler.test.js +276 -0
- package/dist/tests/state-handler.test.js.map +1 -0
- package/dist/tests/switch-handler.test.d.ts +1 -0
- package/dist/tests/switch-handler.test.js +186 -0
- package/dist/tests/switch-handler.test.js.map +1 -0
- package/dist/tests/trip-handler.test.d.ts +1 -0
- package/dist/tests/trip-handler.test.js +179 -0
- package/dist/tests/trip-handler.test.js.map +1 -0
- package/dist/timers/timer-manager.d.ts +31 -0
- package/dist/timers/timer-manager.js +118 -0
- package/dist/timers/timer-manager.js.map +1 -0
- package/dist/types/event-payload-map-type.d.ts +15 -0
- package/dist/types/event-payload-map-type.js +2 -0
- package/dist/types/event-payload-map-type.js.map +1 -0
- package/dist/types/event-type.d.ts +44 -0
- package/dist/types/event-type.js +20 -0
- package/dist/types/event-type.js.map +1 -0
- package/dist/types/matter-accessory-key-type.d.ts +7 -0
- package/dist/types/matter-accessory-key-type.js +2 -0
- package/dist/types/matter-accessory-key-type.js.map +1 -0
- package/dist/types/mode-type.d.ts +4 -0
- package/dist/types/mode-type.js +2 -0
- package/dist/types/mode-type.js.map +1 -0
- package/dist/types/origin-type.d.ts +9 -0
- package/dist/types/origin-type.js +11 -0
- package/dist/types/origin-type.js.map +1 -0
- package/dist/types/security-state-type.d.ts +11 -0
- package/dist/types/security-state-type.js +13 -0
- package/dist/types/security-state-type.js.map +1 -0
- package/dist/types/sensor-kind-type.d.ts +2 -0
- package/dist/types/sensor-kind-type.js +2 -0
- package/dist/types/sensor-kind-type.js.map +1 -0
- package/dist/types/service-result-type.d.ts +4 -0
- package/dist/types/service-result-type.js +2 -0
- package/dist/types/service-result-type.js.map +1 -0
- package/dist/utils/arming-util.d.ts +19 -0
- package/dist/utils/arming-util.js +24 -0
- package/dist/utils/arming-util.js.map +1 -0
- package/dist/utils/state-util.d.ts +8 -0
- package/dist/utils/state-util.js +29 -0
- package/dist/utils/state-util.js.map +1 -0
- package/docs/MQTT.md +50 -0
- package/package.json +64 -0
- package/src/@types/homebridge-lib.d.ts +11 -0
- package/src/conditions/already-triggered-condition.ts +24 -0
- package/src/conditions/arming-in-progress-condition.ts +23 -0
- package/src/conditions/arming-lock-condition.ts +50 -0
- package/src/conditions/condition.ts +31 -0
- package/src/conditions/double-knock-condition.ts +84 -0
- package/src/conditions/not-armed-condition.ts +30 -0
- package/src/conditions/trigger-already-running-condition.ts +23 -0
- package/src/constants/default-constant.ts +28 -0
- package/src/constants/homekit-constant.ts +2 -0
- package/src/constants/matter-constant.ts +69 -0
- package/src/handlers/sensor-handler.ts +78 -0
- package/src/handlers/state-handler.ts +257 -0
- package/src/handlers/switch-handler.ts +111 -0
- package/src/handlers/trip-handler.ts +193 -0
- package/src/homekit/homekit-registrar.ts +28 -0
- package/src/homekit/service-factory.ts +35 -0
- package/src/index.ts +9 -0
- package/src/interfaces/condition-context-interface.ts +15 -0
- package/src/interfaces/hap-types-interface.ts +11 -0
- package/src/interfaces/mqtt-status-payload-interface.ts +6 -0
- package/src/interfaces/options-interface.ts +116 -0
- package/src/interfaces/persisted-state-interface.ts +7 -0
- package/src/interfaces/service-registry-interface.ts +11 -0
- package/src/interfaces/system-state-interface.ts +25 -0
- package/src/schemas/arming-lock-schema.ts +12 -0
- package/src/schemas/error-schema.ts +7 -0
- package/src/schemas/mode-request-schema.ts +18 -0
- package/src/schemas/status-response-schema.ts +10 -0
- package/src/schemas/trip-mode-request-schema.ts +24 -0
- package/src/security-system-platform.ts +68 -0
- package/src/security-system.ts +199 -0
- package/src/services/command-service.ts +76 -0
- package/src/services/configuration-service.ts +224 -0
- package/src/services/event-bus-service.ts +21 -0
- package/src/services/matter-service.ts +446 -0
- package/src/services/mqtt-service.ts +123 -0
- package/src/services/server-service.ts +327 -0
- package/src/services/storage-service.ts +80 -0
- package/src/services/webhook-service.ts +77 -0
- package/src/tests/conditions.test.ts +212 -0
- package/src/tests/matter-service.test.ts +610 -0
- package/src/tests/mqtt-service.test.ts +231 -0
- package/src/tests/sensor-handler.test.ts +116 -0
- package/src/tests/server-service.test.ts +238 -0
- package/src/tests/state-handler.test.ts +371 -0
- package/src/tests/switch-handler.test.ts +234 -0
- package/src/tests/trip-handler.test.ts +244 -0
- package/src/timers/timer-manager.ts +141 -0
- package/src/types/event-payload-map-type.ts +25 -0
- package/src/types/event-type.ts +61 -0
- package/src/types/matter-accessory-key-type.ts +33 -0
- package/src/types/mode-type.ts +4 -0
- package/src/types/origin-type.ts +9 -0
- package/src/types/security-state-type.ts +11 -0
- package/src/types/sensor-kind-type.ts +2 -0
- package/src/types/service-result-type.ts +4 -0
- package/src/utils/arming-util.ts +44 -0
- package/src/utils/state-util.ts +32 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SecurityState } from '../types/security-state-type.js';
|
|
2
|
+
/** Converts a SecurityState value to its human-readable mode string. */
|
|
3
|
+
export function stateToMode(state) {
|
|
4
|
+
switch (state) {
|
|
5
|
+
case SecurityState.TRIGGERED: return 'triggered';
|
|
6
|
+
case SecurityState.HOME: return 'home';
|
|
7
|
+
case SecurityState.AWAY: return 'away';
|
|
8
|
+
case SecurityState.NIGHT: return 'night';
|
|
9
|
+
case SecurityState.OFF: return 'off';
|
|
10
|
+
case 'lock': return 'lock';
|
|
11
|
+
case 'warning': return 'warning';
|
|
12
|
+
default: return 'unknown';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Converts a mode string to its corresponding SecurityState value (-1 if unknown). */
|
|
16
|
+
export function modeToState(mode) {
|
|
17
|
+
switch (mode) {
|
|
18
|
+
case 'home': return SecurityState.HOME;
|
|
19
|
+
case 'away': return SecurityState.AWAY;
|
|
20
|
+
case 'night': return SecurityState.NIGHT;
|
|
21
|
+
case 'off': return SecurityState.OFF;
|
|
22
|
+
default: return -1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Capitalises the first letter of a string. */
|
|
26
|
+
export function capitalise(s) {
|
|
27
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=state-util.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-util.js","sourceRoot":"","sources":["../../src/utils/state-util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGhE,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAC,KAA6B;IACvD,QAAQ,KAAK,EAAE,CAAC;QAChB,KAAK,aAAa,CAAC,SAAS,EAAE,OAAO,WAAW,CAAC;QACjD,KAAK,aAAa,CAAC,IAAI,EAAE,OAAO,MAAM,CAAC;QACvC,KAAK,aAAa,CAAC,IAAI,EAAE,OAAO,MAAM,CAAC;QACvC,KAAK,aAAa,CAAC,KAAK,EAAE,OAAO,OAAO,CAAC;QACzC,KAAK,aAAa,CAAC,GAAG,EAAE,OAAO,KAAK,CAAC;QACrC,KAAK,MAAM,EAAE,OAAO,MAAM,CAAC;QAC3B,KAAK,SAAS,EAAE,OAAO,SAAS,CAAC;QACjC,SAAS,OAAO,SAAS,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,QAAQ,IAAI,EAAE,CAAC;QACf,KAAK,MAAM,EAAE,OAAO,aAAa,CAAC,IAAI,CAAC;QACvC,KAAK,MAAM,EAAE,OAAO,aAAa,CAAC,IAAI,CAAC;QACvC,KAAK,OAAO,EAAE,OAAO,aAAa,CAAC,KAAK,CAAC;QACzC,KAAK,KAAK,EAAE,OAAO,aAAa,CAAC,GAAG,CAAC;QACrC,SAAS,OAAO,CAAC,CAAkB,CAAC;IACpC,CAAC;AACH,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC"}
|
package/docs/MQTT.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# MQTT
|
|
2
|
+
|
|
3
|
+
Publish real-time security system state updates to an MQTT broker.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Add these fields to your plugin config (or configure via the Homebridge UI):
|
|
8
|
+
|
|
9
|
+
| Field | Type | Default | Description |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `mqtt_broker` | `string` | — | Broker URL, e.g. `mqtt://localhost:1883` or `mqtts://broker.example.com:8883` |
|
|
12
|
+
| `mqtt_username` | `string` | — | Optional username for authentication |
|
|
13
|
+
| `mqtt_password` | `string` | — | Optional password for authentication |
|
|
14
|
+
| `mqtt_topic` | `string` | `security-system/state` | Topic to publish status payloads to |
|
|
15
|
+
| `mqtt_client_id` | `string` | — | Optional client ID (auto-generated if omitted) |
|
|
16
|
+
|
|
17
|
+
MQTT is **optional** — omit `mqtt_broker` to disable it.
|
|
18
|
+
|
|
19
|
+
## Payload
|
|
20
|
+
|
|
21
|
+
Published as a retained JSON message on every state change:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"arming": false,
|
|
26
|
+
"current_mode": "home",
|
|
27
|
+
"target_mode": "home",
|
|
28
|
+
"tripped": true
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Field | Type | Description |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `arming` | `boolean` | Whether the system is counting down an arming delay |
|
|
35
|
+
| `current_mode` | `string` | Current active mode: `off`, `home`, `away`, `night`, `triggered` |
|
|
36
|
+
| `target_mode` | `string` | Target mode being transitioned to |
|
|
37
|
+
| `tripped` | `boolean` | Whether a trip switch is active (trigger delay counting down) |
|
|
38
|
+
|
|
39
|
+
## Events
|
|
40
|
+
|
|
41
|
+
Publishes are triggered by these state machine changes:
|
|
42
|
+
|
|
43
|
+
- Current mode changes (`CURRENT_CHANGED`)
|
|
44
|
+
- Target mode changes (`TARGET_CHANGED`)
|
|
45
|
+
- Arming delay start (`ARMING`)
|
|
46
|
+
- Warning state (`WARNING`)
|
|
47
|
+
|
|
48
|
+
## Proxy Mode
|
|
49
|
+
|
|
50
|
+
If `proxy_mode` is enabled, publishes originating from external sources (e.g. HTTP API) are suppressed.
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homebridge-securitysystem",
|
|
3
|
+
"displayName": "Homebridge Security System",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.0.1-matter",
|
|
6
|
+
"description": "Homebridge plugin that creates a security system accessory that can be triggered by HomeKit sensors.",
|
|
7
|
+
"author": "MiguelRipoll23",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"homepage": "https://github.com/MiguelRipoll23/homebridge-securitysystem#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/MiguelRipoll23/homebridge-securitysystem.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/MiguelRipoll23/homebridge-securitysystem/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"homebridge-plugin",
|
|
19
|
+
"homebridge",
|
|
20
|
+
"homekit",
|
|
21
|
+
"security system",
|
|
22
|
+
"alarm",
|
|
23
|
+
"category-security",
|
|
24
|
+
"supports-hap",
|
|
25
|
+
"supports-matter"
|
|
26
|
+
],
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": "^22 || ^24",
|
|
30
|
+
"homebridge": "^1.8.0 || ^2.0.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@hono/node-server": "^2.1.1",
|
|
34
|
+
"@hono/zod-openapi": "^1.6.0",
|
|
35
|
+
"@scalar/hono-api-reference": "^0.11.14",
|
|
36
|
+
"hono": "^4.13.2",
|
|
37
|
+
"mqtt": "^5.15.2",
|
|
38
|
+
"node-persist": "^4.0.4",
|
|
39
|
+
"zod": "^4.4.3"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^26.2.0",
|
|
43
|
+
"@types/node-persist": "^3.1.8",
|
|
44
|
+
"homebridge": "^2.3.1",
|
|
45
|
+
"nodemon": "^3.1.14",
|
|
46
|
+
"oxlint": "^1.78.0",
|
|
47
|
+
"rimraf": "^6.1.3",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"vitest": "^4.1.10"
|
|
50
|
+
},
|
|
51
|
+
"funding": [
|
|
52
|
+
{
|
|
53
|
+
"type": "github",
|
|
54
|
+
"url": "https://github.com/sponsors/MiguelRipoll23"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "rimraf ./dist && tsc",
|
|
59
|
+
"lint": "oxlint",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"watch": "pnpm run build && pnpm link && nodemon"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SecurityState } from '../types/security-state-type.js';
|
|
2
|
+
import type { ConditionContext } from '../interfaces/condition-context-interface.js';
|
|
3
|
+
import { Condition } from './condition.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Blocks a new trip activation when the alarm has already been triggered.
|
|
7
|
+
* Prevents stacking multiple trigger events on an active alarm.
|
|
8
|
+
*/
|
|
9
|
+
export class AlreadyTriggeredCondition extends Condition {
|
|
10
|
+
readonly name = 'already-triggered';
|
|
11
|
+
|
|
12
|
+
evaluate({ state, value, log }: ConditionContext): boolean {
|
|
13
|
+
this.clearFailureReason();
|
|
14
|
+
if (!value) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const blocked = state.currentState === SecurityState.TRIGGERED;
|
|
18
|
+
if (blocked) {
|
|
19
|
+
this._failureReason = 'Security System (Already triggered): alarm is already active';
|
|
20
|
+
log.warn(this._failureReason);
|
|
21
|
+
}
|
|
22
|
+
return blocked;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ConditionContext } from '../interfaces/condition-context-interface.js';
|
|
2
|
+
import { Condition } from './condition.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Blocks a trip action while the arm delay countdown is still running.
|
|
6
|
+
* Prevents an intruder from tripping the system before it has fully armed.
|
|
7
|
+
*/
|
|
8
|
+
export class ArmingInProgressCondition extends Condition {
|
|
9
|
+
readonly name = 'arming-in-progress';
|
|
10
|
+
|
|
11
|
+
evaluate({ state, value, log }: ConditionContext): boolean {
|
|
12
|
+
this.clearFailureReason();
|
|
13
|
+
if (!value) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (state.isArming) {
|
|
17
|
+
this._failureReason = 'Trip Switch (Still arming): arm delay countdown is still in progress';
|
|
18
|
+
log.warn(this._failureReason);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { SecurityState } from '../types/security-state-type.js';
|
|
2
|
+
import type { ConditionContext } from '../interfaces/condition-context-interface.js';
|
|
3
|
+
import { Condition } from './condition.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Blocks arming when an arming-lock switch (global or mode-specific) is active.
|
|
7
|
+
* Does NOT block disarming.
|
|
8
|
+
*/
|
|
9
|
+
export class ArmingLockCondition extends Condition {
|
|
10
|
+
readonly name = 'arming-lock';
|
|
11
|
+
|
|
12
|
+
evaluate({ state, options }: ConditionContext): boolean {
|
|
13
|
+
this.clearFailureReason();
|
|
14
|
+
const hasLockFeature = options.armingLockSwitch || options.armingLockSwitches;
|
|
15
|
+
if (!hasLockFeature) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const targetState = state.targetState;
|
|
20
|
+
if (targetState === SecurityState.OFF) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check global arming-lock switch.
|
|
25
|
+
if (state.armingLocks.global) {
|
|
26
|
+
this._failureReason = 'arming is blocked by the global arming lock switch';
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check mode-specific arming-lock switch.
|
|
31
|
+
let blocked = false;
|
|
32
|
+
switch (targetState) {
|
|
33
|
+
case SecurityState.HOME:
|
|
34
|
+
blocked = state.armingLocks.home;
|
|
35
|
+
break;
|
|
36
|
+
case SecurityState.AWAY:
|
|
37
|
+
blocked = state.armingLocks.away;
|
|
38
|
+
break;
|
|
39
|
+
case SecurityState.NIGHT:
|
|
40
|
+
blocked = state.armingLocks.night;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (blocked) {
|
|
45
|
+
this._failureReason = 'arming is blocked by a mode-specific arming lock switch';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return blocked;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ConditionContext } from '../interfaces/condition-context-interface.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for all conditions that can block a trip/trigger action.
|
|
5
|
+
*
|
|
6
|
+
* Extend this class and implement `evaluate()` to return `true` when the
|
|
7
|
+
* condition should prevent the action from proceeding. Set `_failureReason`
|
|
8
|
+
* before returning `true` so callers can retrieve it via `failureReason`.
|
|
9
|
+
*/
|
|
10
|
+
export abstract class Condition {
|
|
11
|
+
abstract readonly name: string;
|
|
12
|
+
|
|
13
|
+
protected _failureReason: string | undefined;
|
|
14
|
+
|
|
15
|
+
/** Returns the reason the condition blocked the last evaluated action, if any. */
|
|
16
|
+
get failureReason(): string | undefined {
|
|
17
|
+
return this._failureReason;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Must be called at the top of every evaluate() implementation to clear any stale reason. */
|
|
21
|
+
protected clearFailureReason(): void {
|
|
22
|
+
this._failureReason = undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns `true` if this condition blocks the action described by `context`.
|
|
27
|
+
* When `true` is returned the caller must reject the HomeKit request.
|
|
28
|
+
* Implementations must call `this.clearFailureReason()` before any allow/deny logic.
|
|
29
|
+
*/
|
|
30
|
+
abstract evaluate(context: ConditionContext): boolean;
|
|
31
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { SecurityState } from '../types/security-state-type.js';
|
|
2
|
+
import { OriginType } from '../types/origin-type.js';
|
|
3
|
+
import type { ConditionContext } from '../interfaces/condition-context-interface.js';
|
|
4
|
+
import { modeToState } from '../utils/state-util.js';
|
|
5
|
+
import { Condition } from './condition.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Implements the double-knock pattern: the first activation within a mode that
|
|
9
|
+
* has double-knock enabled sets a "knocked" flag and rejects the trip. Only the
|
|
10
|
+
* second activation within the configured window proceeds.
|
|
11
|
+
*
|
|
12
|
+
* The caller is responsible for managing `state.isKnocked` and the timeout
|
|
13
|
+
* via the `onFirstKnock` callback injected at construction.
|
|
14
|
+
*/
|
|
15
|
+
export class DoubleKnockCondition extends Condition {
|
|
16
|
+
readonly name = 'double-knock';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param onFirstKnock Called when the first knock is detected.
|
|
20
|
+
* The callback receives the window duration in seconds and a reset function
|
|
21
|
+
* to invoke when the window expires.
|
|
22
|
+
* @param onCancelTimer Called on the second knock to cancel the running window timer.
|
|
23
|
+
*/
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly onFirstKnock: (seconds: number, onExpire: () => void) => void,
|
|
26
|
+
private readonly onCancelTimer: () => void,
|
|
27
|
+
) {
|
|
28
|
+
super();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
evaluate({ state, options, value, origin, log }: ConditionContext): boolean {
|
|
32
|
+
this.clearFailureReason();
|
|
33
|
+
if (!value || !options.doubleKnock) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (origin === OriginType.OVERRIDE_SWITCH) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (state.isArming) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const knockableStates = options.doubleKnockModes.map(m => modeToState(m.toLowerCase()));
|
|
44
|
+
if (!knockableStates.includes(state.currentState)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (state.isKnocked) {
|
|
49
|
+
// Second knock — clear the flag and cancel the window timer, then allow through.
|
|
50
|
+
state.isKnocked = false;
|
|
51
|
+
this.onCancelTimer();
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// First knock — block and schedule expiry.
|
|
56
|
+
state.isKnocked = true;
|
|
57
|
+
const seconds = this.resolveWindow(state.currentState, options);
|
|
58
|
+
|
|
59
|
+
this.onFirstKnock(seconds, () => {
|
|
60
|
+
state.isKnocked = false;
|
|
61
|
+
log.info('Trip Switch (Reset): double-knock window expired without second activation');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this._failureReason = 'Trip Switch (Knock): double-knock is required, waiting for second activation';
|
|
65
|
+
log.warn(this._failureReason);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private resolveWindow(
|
|
70
|
+
currentState: SecurityState,
|
|
71
|
+
options: ConditionContext['options'],
|
|
72
|
+
): number {
|
|
73
|
+
if (currentState === SecurityState.HOME && options.homeDoubleKnockSeconds !== null) {
|
|
74
|
+
return options.homeDoubleKnockSeconds;
|
|
75
|
+
}
|
|
76
|
+
if (currentState === SecurityState.AWAY && options.awayDoubleKnockSeconds !== null) {
|
|
77
|
+
return options.awayDoubleKnockSeconds;
|
|
78
|
+
}
|
|
79
|
+
if (currentState === SecurityState.NIGHT && options.nightDoubleKnockSeconds !== null) {
|
|
80
|
+
return options.nightDoubleKnockSeconds;
|
|
81
|
+
}
|
|
82
|
+
return options.doubleKnockSeconds;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SecurityState } from '../types/security-state-type.js';
|
|
2
|
+
import { OriginType } from '../types/origin-type.js';
|
|
3
|
+
import type { ConditionContext } from '../interfaces/condition-context-interface.js';
|
|
4
|
+
import { Condition } from './condition.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Blocks a trip action when the system is disarmed and neither
|
|
8
|
+
* `override_off` nor the override switch is in use.
|
|
9
|
+
*/
|
|
10
|
+
export class NotArmedCondition extends Condition {
|
|
11
|
+
readonly name = 'not-armed';
|
|
12
|
+
|
|
13
|
+
evaluate({ state, options, value, origin, log }: ConditionContext): boolean {
|
|
14
|
+
this.clearFailureReason();
|
|
15
|
+
if (!value) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isDisarmed = state.currentState === SecurityState.OFF;
|
|
20
|
+
const isNotOverridingOff = !options.overrideOff;
|
|
21
|
+
const isNotOverrideSwitch = origin !== OriginType.OVERRIDE_SWITCH;
|
|
22
|
+
|
|
23
|
+
const blocked = isDisarmed && isNotOverridingOff && isNotOverrideSwitch;
|
|
24
|
+
if (blocked) {
|
|
25
|
+
this._failureReason = 'Trip Switch (Not armed): system is disarmed and override is not enabled';
|
|
26
|
+
log.warn(this._failureReason);
|
|
27
|
+
}
|
|
28
|
+
return blocked;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ConditionContext } from '../interfaces/condition-context-interface.js';
|
|
2
|
+
import { Condition } from './condition.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Blocks a trip activation when the trigger-delay countdown is already running.
|
|
6
|
+
* Prevents stacking multiple trigger events during the pre-alarm window.
|
|
7
|
+
*/
|
|
8
|
+
export class TriggerAlreadyRunningCondition extends Condition {
|
|
9
|
+
readonly name = 'trigger-already-running';
|
|
10
|
+
|
|
11
|
+
evaluate({ state, value, log }: ConditionContext): boolean {
|
|
12
|
+
this.clearFailureReason();
|
|
13
|
+
if (!value) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (state.isTripping) {
|
|
17
|
+
this._failureReason = 'Security System (Already tripped): trigger delay countdown is already running';
|
|
18
|
+
log.warn(this._failureReason);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Default configuration values applied when a config property is not set. */
|
|
2
|
+
export const DEFAULTS = {
|
|
3
|
+
NAME: 'Security System',
|
|
4
|
+
SERIAL_NUMBER: 'S3CUR1TYSYST3M',
|
|
5
|
+
DEFAULT_MODE: 'off',
|
|
6
|
+
ARM_SECONDS: 0,
|
|
7
|
+
TRIGGER_SECONDS: 0,
|
|
8
|
+
RESET_MINUTES: 10,
|
|
9
|
+
DOUBLE_KNOCK_SECONDS: 90,
|
|
10
|
+
TRIPPED_SENSOR_SECONDS: 5,
|
|
11
|
+
PAUSE_MINUTES: 0,
|
|
12
|
+
|
|
13
|
+
// Switch display names
|
|
14
|
+
TRIP_SWITCH_NAME: 'Trip',
|
|
15
|
+
TRIP_HOME_SWITCH_NAME: 'Trip Home',
|
|
16
|
+
TRIP_AWAY_SWITCH_NAME: 'Trip Away',
|
|
17
|
+
TRIP_NIGHT_SWITCH_NAME: 'Trip Night',
|
|
18
|
+
TRIP_OVERRIDE_SWITCH_NAME: 'Trip Override',
|
|
19
|
+
MODE_HOME_SWITCH_NAME: 'Mode Home',
|
|
20
|
+
MODE_AWAY_SWITCH_NAME: 'Mode Away',
|
|
21
|
+
MODE_NIGHT_SWITCH_NAME: 'Mode Night',
|
|
22
|
+
MODE_OFF_SWITCH_NAME: 'Mode Off',
|
|
23
|
+
MODE_AWAY_EXTENDED_SWITCH_NAME: 'Mode Away Extended',
|
|
24
|
+
MODE_PAUSE_SWITCH_NAME: 'Mode Pause',
|
|
25
|
+
|
|
26
|
+
// MQTT
|
|
27
|
+
MQTT_TOPIC: 'security-system/state',
|
|
28
|
+
} as const;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { FixedMatterSwitchKey, MatterAccessoryKey } from '../types/matter-accessory-key-type.js';
|
|
2
|
+
import type { SensorKind } from '../types/sensor-kind-type.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stable device-type-indexed identity for every fixed switch, following the
|
|
6
|
+
* Matter convention of `<device-type>-<index>` (e.g. `motion-sensor-1`).
|
|
7
|
+
* The index comes from the fixed slot, so disabling an accessory never
|
|
8
|
+
* renumbers the others.
|
|
9
|
+
*/
|
|
10
|
+
export const MATTER_SWITCH_IDENTITIES: Record<FixedMatterSwitchKey, string> = {
|
|
11
|
+
// Trip switches
|
|
12
|
+
trip: 'switch-1',
|
|
13
|
+
'trip-home': 'switch-2',
|
|
14
|
+
'trip-away': 'switch-3',
|
|
15
|
+
'trip-night': 'switch-4',
|
|
16
|
+
'trip-override': 'switch-5',
|
|
17
|
+
// Mode switches
|
|
18
|
+
'mode-home': 'switch-6',
|
|
19
|
+
'mode-away': 'switch-7',
|
|
20
|
+
'mode-night': 'switch-8',
|
|
21
|
+
'mode-off': 'switch-9',
|
|
22
|
+
'mode-away-extended': 'switch-10',
|
|
23
|
+
'mode-pause': 'switch-11',
|
|
24
|
+
// Arming lock switches
|
|
25
|
+
'arming-lock': 'switch-12',
|
|
26
|
+
'arming-lock-home': 'switch-13',
|
|
27
|
+
'arming-lock-away': 'switch-14',
|
|
28
|
+
'arming-lock-night': 'switch-15',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** The first index available for custom trip switches. */
|
|
32
|
+
export const MATTER_CUSTOM_TRIP_START_INDEX = 16;
|
|
33
|
+
|
|
34
|
+
/** Stable device-type-indexed identity for each motion sensor. */
|
|
35
|
+
export const MATTER_SENSOR_IDENTITIES: Record<SensorKind, string> = {
|
|
36
|
+
arming: 'motion-sensor-1',
|
|
37
|
+
tripped: 'motion-sensor-2',
|
|
38
|
+
reset: 'motion-sensor-3',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** All mode-switch keys, used to push their displayed state to Matter. */
|
|
42
|
+
export const MATTER_MODE_SWITCH_KEYS = [
|
|
43
|
+
'mode-home',
|
|
44
|
+
'mode-away',
|
|
45
|
+
'mode-night',
|
|
46
|
+
'mode-off',
|
|
47
|
+
'mode-away-extended',
|
|
48
|
+
'mode-pause',
|
|
49
|
+
] as const satisfies readonly MatterAccessoryKey[];
|
|
50
|
+
|
|
51
|
+
/** All trip-switch keys, used to push their displayed state to Matter. */
|
|
52
|
+
export const MATTER_TRIP_SWITCH_KEYS = [
|
|
53
|
+
'trip',
|
|
54
|
+
'trip-home',
|
|
55
|
+
'trip-away',
|
|
56
|
+
'trip-night',
|
|
57
|
+
'trip-override',
|
|
58
|
+
] as const satisfies readonly MatterAccessoryKey[];
|
|
59
|
+
|
|
60
|
+
/** All arming-lock switch keys, used to push their displayed state to Matter. */
|
|
61
|
+
export const MATTER_ARMING_LOCK_SWITCH_KEYS = [
|
|
62
|
+
'arming-lock',
|
|
63
|
+
'arming-lock-home',
|
|
64
|
+
'arming-lock-away',
|
|
65
|
+
'arming-lock-night',
|
|
66
|
+
] as const satisfies readonly MatterAccessoryKey[];
|
|
67
|
+
|
|
68
|
+
export const MATTER_MANUFACTURER = 'MiguelRipoll23';
|
|
69
|
+
export const MATTER_MODEL = 'Security System (Matter)';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Logging } from 'homebridge';
|
|
2
|
+
import { SENSOR_PULSE_MS } from '../constants/homekit-constant.js';
|
|
3
|
+
import type { EventBusService } from '../services/event-bus-service.js';
|
|
4
|
+
import { EventType } from '../types/event-type.js';
|
|
5
|
+
import type { SensorKind } from '../types/sensor-kind-type.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Owns the current state of every motion sensor and publishes changes over the
|
|
9
|
+
* bus (SENSOR_STATE_CHANGED), which MatterService pushes to Matter. Sensors are
|
|
10
|
+
* Matter-only, so no HomeKit characteristics are touched here.
|
|
11
|
+
*/
|
|
12
|
+
export class SensorHandler {
|
|
13
|
+
private readonly currentValueByKind: Record<SensorKind, boolean> = {
|
|
14
|
+
arming: false,
|
|
15
|
+
tripped: false,
|
|
16
|
+
reset: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly log: Logging,
|
|
21
|
+
private readonly bus: EventBusService,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
// ── Arming sensor ──────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
updateArmingMotionSensor(value: boolean): void {
|
|
27
|
+
this.setSensorValue('arming', value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
resetArmingMotionSensor(): void {
|
|
31
|
+
if (this.currentValueByKind.arming) {
|
|
32
|
+
this.setSensorValue('arming', false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Tripped sensor ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
pulseTrippedMotionSensor(): void {
|
|
39
|
+
this.setTrippedMotionSensor(true);
|
|
40
|
+
this.scheduleReset('tripped');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setTrippedMotionSensor(value: boolean): void {
|
|
44
|
+
this.setSensorValue('tripped', value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
resetTrippedMotionSensor(): void {
|
|
48
|
+
if (this.currentValueByKind.tripped) {
|
|
49
|
+
this.setSensorValue('tripped', false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Reset sensor ───────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
pulseResetMotionSensor(): void {
|
|
56
|
+
this.setSensorValue('reset', true);
|
|
57
|
+
this.scheduleReset('reset');
|
|
58
|
+
this.log.debug('Reset sensor (Triggered)');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Returns the current value of the given sensor, for Matter startup sync. */
|
|
62
|
+
getMotionState(kind: SensorKind): boolean {
|
|
63
|
+
return this.currentValueByKind[kind];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
private setSensorValue(kind: SensorKind, value: boolean): void {
|
|
69
|
+
this.currentValueByKind[kind] = value;
|
|
70
|
+
this.bus.emit(EventType.SENSOR_STATE_CHANGED, { sensor: kind, value });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private scheduleReset(kind: SensorKind): void {
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
this.setSensorValue(kind, false);
|
|
76
|
+
}, SENSOR_PULSE_MS);
|
|
77
|
+
}
|
|
78
|
+
}
|