@marcel2215/homebridge-supla-plugin 2.1.24 → 2.1.25
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/.codex/environments/environment.toml +31 -0
- package/config.schema.json +25 -0
- package/dist/Accesories/FrontGateFsm.d.ts +123 -0
- package/dist/Accesories/FrontGateFsm.d.ts.map +1 -0
- package/dist/Accesories/FrontGateFsm.js +496 -0
- package/dist/Accesories/FrontGateFsm.js.map +1 -0
- package/dist/Accesories/GateAccessory.d.ts +15 -69
- package/dist/Accesories/GateAccessory.d.ts.map +1 -1
- package/dist/Accesories/GateAccessory.js +246 -621
- package/dist/Accesories/GateAccessory.js.map +1 -1
- package/dist/platform.d.ts +13 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +76 -2
- package/dist/platform.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
|
2
|
+
version = 1
|
|
3
|
+
name = "homebridge-supla-plugin"
|
|
4
|
+
|
|
5
|
+
[setup]
|
|
6
|
+
script = ""
|
|
7
|
+
|
|
8
|
+
[[actions]]
|
|
9
|
+
name = "Bump Major"
|
|
10
|
+
icon = "tool"
|
|
11
|
+
command = "npm run version:bump:major"
|
|
12
|
+
|
|
13
|
+
[[actions]]
|
|
14
|
+
name = "Bump Minor"
|
|
15
|
+
icon = "tool"
|
|
16
|
+
command = "npm run version:bump:minor"
|
|
17
|
+
|
|
18
|
+
[[actions]]
|
|
19
|
+
name = "Bump Revision"
|
|
20
|
+
icon = "tool"
|
|
21
|
+
command = "npm run version:bump:revision"
|
|
22
|
+
|
|
23
|
+
[[actions]]
|
|
24
|
+
name = "Publish"
|
|
25
|
+
icon = "tool"
|
|
26
|
+
command = "npm run publish:plugin"
|
|
27
|
+
|
|
28
|
+
[[actions]]
|
|
29
|
+
name = "Login"
|
|
30
|
+
icon = "tool"
|
|
31
|
+
command = "npm login"
|
package/config.schema.json
CHANGED
|
@@ -123,6 +123,31 @@
|
|
|
123
123
|
"type": "boolean",
|
|
124
124
|
"default": false
|
|
125
125
|
},
|
|
126
|
+
"frontGateFullTravelMs": {
|
|
127
|
+
"title": "Front Gate Full Travel (ms)",
|
|
128
|
+
"type": "number",
|
|
129
|
+
"default": 25000
|
|
130
|
+
},
|
|
131
|
+
"frontGateReversePauseMs": {
|
|
132
|
+
"title": "Front Gate Reverse Pause (ms)",
|
|
133
|
+
"type": "number",
|
|
134
|
+
"default": 900
|
|
135
|
+
},
|
|
136
|
+
"frontGateWrongDirectionRunMs": {
|
|
137
|
+
"title": "Front Gate Wrong Direction Run (ms)",
|
|
138
|
+
"type": "number",
|
|
139
|
+
"default": 700
|
|
140
|
+
},
|
|
141
|
+
"frontGateMinimumPulseGapMs": {
|
|
142
|
+
"title": "Front Gate Minimum Pulse Gap (ms)",
|
|
143
|
+
"type": "number",
|
|
144
|
+
"default": 400
|
|
145
|
+
},
|
|
146
|
+
"frontGateCloseRetryLimit": {
|
|
147
|
+
"title": "Front Gate Close Retry Limit",
|
|
148
|
+
"type": "number",
|
|
149
|
+
"default": 1
|
|
150
|
+
},
|
|
126
151
|
"gateLockControlMode": {
|
|
127
152
|
"title": "Gate Lock Control Mode",
|
|
128
153
|
"type": "string",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type GateTarget = 'open' | 'closed';
|
|
2
|
+
export type MotionDirection = 'opening' | 'closing';
|
|
3
|
+
export type MotionCertainty = 'known' | 'goalOnly';
|
|
4
|
+
export type MotionSource = 'homekit' | 'external' | 'recovery';
|
|
5
|
+
export declare const enum DoorCurrentState {
|
|
6
|
+
OPEN = 0,
|
|
7
|
+
CLOSED = 1,
|
|
8
|
+
OPENING = 2,
|
|
9
|
+
CLOSING = 3,
|
|
10
|
+
STOPPED = 4
|
|
11
|
+
}
|
|
12
|
+
export declare const enum DoorTargetState {
|
|
13
|
+
OPEN = 0,
|
|
14
|
+
CLOSED = 1
|
|
15
|
+
}
|
|
16
|
+
export interface FrontGateTimingConfig {
|
|
17
|
+
fullTravelMs: number;
|
|
18
|
+
reversePauseMs: number;
|
|
19
|
+
wrongDirectionRunMs: number;
|
|
20
|
+
minimumPulseGapMs: number;
|
|
21
|
+
closeRetryLimit: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_FRONT_GATE_TIMINGS: FrontGateTimingConfig;
|
|
24
|
+
export interface PersistedFrontGateState {
|
|
25
|
+
}
|
|
26
|
+
export interface FrontGateSnapshot {
|
|
27
|
+
available: boolean;
|
|
28
|
+
controlConnected: boolean | null;
|
|
29
|
+
sensorConnected: boolean | null;
|
|
30
|
+
closedSensor: boolean | null;
|
|
31
|
+
sensorFreshSinceOnline: boolean;
|
|
32
|
+
currentDoorState?: DoorCurrentState;
|
|
33
|
+
targetDoorState?: DoorTargetState;
|
|
34
|
+
requestedTarget: GateTarget | null;
|
|
35
|
+
motionDirection: MotionDirection | 'none';
|
|
36
|
+
motionCertainty: MotionCertainty | 'none';
|
|
37
|
+
planKind: Plan['kind'];
|
|
38
|
+
note: string;
|
|
39
|
+
}
|
|
40
|
+
export interface FrontGateLogger {
|
|
41
|
+
debug(message: string): void;
|
|
42
|
+
info(message: string): void;
|
|
43
|
+
warn(message: string): void;
|
|
44
|
+
}
|
|
45
|
+
export interface FrontGateIo {
|
|
46
|
+
pulseMotor(reason: string): Promise<void>;
|
|
47
|
+
publishSnapshot(snapshot: FrontGateSnapshot): void;
|
|
48
|
+
log: FrontGateLogger;
|
|
49
|
+
}
|
|
50
|
+
type Plan = {
|
|
51
|
+
kind: 'idle';
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'moving';
|
|
54
|
+
direction: MotionDirection;
|
|
55
|
+
certainty: MotionCertainty;
|
|
56
|
+
source: MotionSource;
|
|
57
|
+
attempt: number;
|
|
58
|
+
startedAt: number;
|
|
59
|
+
deadlineAt: number;
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'waitingSecondPulse';
|
|
62
|
+
finalDirection: MotionDirection;
|
|
63
|
+
source: MotionSource;
|
|
64
|
+
attempt: number;
|
|
65
|
+
dueAt: number;
|
|
66
|
+
deadlineAt: number;
|
|
67
|
+
reason: 'reverseToOpen' | 'reverseToClose';
|
|
68
|
+
};
|
|
69
|
+
export declare class FrontGateFsm {
|
|
70
|
+
private readonly io;
|
|
71
|
+
private readonly travelMs;
|
|
72
|
+
private readonly pulseGapMs;
|
|
73
|
+
private readonly closeRetryLimit;
|
|
74
|
+
private facts;
|
|
75
|
+
private sensorFreshSinceOnline;
|
|
76
|
+
private requestedTarget;
|
|
77
|
+
private plan;
|
|
78
|
+
private lastPulseLikeActivityAt;
|
|
79
|
+
private movementTimer?;
|
|
80
|
+
private movementTimerToken;
|
|
81
|
+
private phaseTimer?;
|
|
82
|
+
private phaseTimerToken;
|
|
83
|
+
private sequence;
|
|
84
|
+
private disposed;
|
|
85
|
+
constructor(io: FrontGateIo, timings?: FrontGateTimingConfig, _persisted?: PersistedFrontGateState);
|
|
86
|
+
dispose(): void;
|
|
87
|
+
handleConnectedChange(connected: boolean): void;
|
|
88
|
+
handleControlConnectedChange(connected: boolean): void;
|
|
89
|
+
handleSensorConnectedChange(connected: boolean): void;
|
|
90
|
+
handleClosedSensorChange(closed: boolean): void;
|
|
91
|
+
requestHomeKitTarget(target: GateTarget): Promise<void>;
|
|
92
|
+
getSnapshot(): FrontGateSnapshot;
|
|
93
|
+
private enqueue;
|
|
94
|
+
private applyControlConnectedChange;
|
|
95
|
+
private applySensorConnectedChange;
|
|
96
|
+
private applyClosedSensorChange;
|
|
97
|
+
private applyHomeKitTarget;
|
|
98
|
+
private handleOpenRequest;
|
|
99
|
+
private handleCloseRequest;
|
|
100
|
+
private startOpeningFromClosed;
|
|
101
|
+
private startCloseSeek;
|
|
102
|
+
private reverseKnownMotion;
|
|
103
|
+
private startOpeningMotion;
|
|
104
|
+
private startClosingMotion;
|
|
105
|
+
private scheduleMovementTimer;
|
|
106
|
+
private schedulePhaseTimer;
|
|
107
|
+
private clearMovementTimer;
|
|
108
|
+
private clearPhaseTimer;
|
|
109
|
+
private clearTimers;
|
|
110
|
+
private handlePhaseTimer;
|
|
111
|
+
private handleMovementTimeout;
|
|
112
|
+
private enterUnavailable;
|
|
113
|
+
private isAvailableForHomeKit;
|
|
114
|
+
private pulseMotor;
|
|
115
|
+
private emitSnapshot;
|
|
116
|
+
private buildSnapshot;
|
|
117
|
+
private getMotionDirection;
|
|
118
|
+
private getMotionCertainty;
|
|
119
|
+
private computeCurrentDoorState;
|
|
120
|
+
private computeTargetDoorState;
|
|
121
|
+
}
|
|
122
|
+
export {};
|
|
123
|
+
//# sourceMappingURL=FrontGateFsm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FrontGateFsm.d.ts","sourceRoot":"","sources":["../../src/Accesories/FrontGateFsm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FrontGateFsm = exports.DEFAULT_FRONT_GATE_TIMINGS = void 0;
|
|
4
|
+
exports.DEFAULT_FRONT_GATE_TIMINGS = {
|
|
5
|
+
fullTravelMs: 25000,
|
|
6
|
+
reversePauseMs: 3000,
|
|
7
|
+
wrongDirectionRunMs: 0,
|
|
8
|
+
minimumPulseGapMs: 3000,
|
|
9
|
+
closeRetryLimit: 1,
|
|
10
|
+
};
|
|
11
|
+
function delay(ms) {
|
|
12
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
function clampInt(value, min, max) {
|
|
15
|
+
if (!Number.isFinite(value)) {
|
|
16
|
+
return min;
|
|
17
|
+
}
|
|
18
|
+
return Math.min(max, Math.max(min, Math.round(value)));
|
|
19
|
+
}
|
|
20
|
+
class FrontGateFsm {
|
|
21
|
+
constructor(io, timings = exports.DEFAULT_FRONT_GATE_TIMINGS, _persisted = {}) {
|
|
22
|
+
this.io = io;
|
|
23
|
+
this.facts = {
|
|
24
|
+
controlConnected: null,
|
|
25
|
+
sensorConnected: null,
|
|
26
|
+
closedSensor: null,
|
|
27
|
+
};
|
|
28
|
+
this.sensorFreshSinceOnline = false;
|
|
29
|
+
this.requestedTarget = null;
|
|
30
|
+
this.plan = { kind: 'idle' };
|
|
31
|
+
this.lastPulseLikeActivityAt = 0;
|
|
32
|
+
this.movementTimerToken = 0;
|
|
33
|
+
this.phaseTimerToken = 0;
|
|
34
|
+
this.sequence = Promise.resolve(undefined);
|
|
35
|
+
this.disposed = false;
|
|
36
|
+
this.travelMs = clampInt(timings.fullTravelMs || exports.DEFAULT_FRONT_GATE_TIMINGS.fullTravelMs, 5000, 120000);
|
|
37
|
+
this.pulseGapMs = Math.max(3000, clampInt(timings.minimumPulseGapMs || exports.DEFAULT_FRONT_GATE_TIMINGS.minimumPulseGapMs, 0, 15000), clampInt(timings.reversePauseMs || exports.DEFAULT_FRONT_GATE_TIMINGS.reversePauseMs, 0, 15000));
|
|
38
|
+
this.closeRetryLimit = clampInt(timings.closeRetryLimit || exports.DEFAULT_FRONT_GATE_TIMINGS.closeRetryLimit, 0, 3);
|
|
39
|
+
this.emitSnapshot('fsm-initialized');
|
|
40
|
+
}
|
|
41
|
+
dispose() {
|
|
42
|
+
this.disposed = true;
|
|
43
|
+
this.clearTimers();
|
|
44
|
+
}
|
|
45
|
+
handleConnectedChange(connected) {
|
|
46
|
+
this.handleControlConnectedChange(connected);
|
|
47
|
+
}
|
|
48
|
+
handleControlConnectedChange(connected) {
|
|
49
|
+
void this.enqueue(`control-connected=${connected}`, () => {
|
|
50
|
+
this.applyControlConnectedChange(connected);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
handleSensorConnectedChange(connected) {
|
|
54
|
+
void this.enqueue(`sensor-connected=${connected}`, () => {
|
|
55
|
+
this.applySensorConnectedChange(connected);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
handleClosedSensorChange(closed) {
|
|
59
|
+
void this.enqueue(`closed-sensor=${closed}`, () => {
|
|
60
|
+
this.applyClosedSensorChange(closed);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async requestHomeKitTarget(target) {
|
|
64
|
+
return this.enqueue(`homekit-target=${target}`, async () => {
|
|
65
|
+
await this.applyHomeKitTarget(target);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
getSnapshot() {
|
|
69
|
+
return this.buildSnapshot('snapshot-requested');
|
|
70
|
+
}
|
|
71
|
+
enqueue(label, task) {
|
|
72
|
+
if (this.disposed) {
|
|
73
|
+
return Promise.resolve();
|
|
74
|
+
}
|
|
75
|
+
const run = this.sequence.then(async () => {
|
|
76
|
+
if (this.disposed) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await task();
|
|
80
|
+
});
|
|
81
|
+
this.sequence = run.catch(error => {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
this.io.log.warn(`front-gate sequence '${label}' failed: ${message}`);
|
|
84
|
+
});
|
|
85
|
+
return run;
|
|
86
|
+
}
|
|
87
|
+
applyControlConnectedChange(connected) {
|
|
88
|
+
if (this.facts.controlConnected === connected) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.facts.controlConnected = connected;
|
|
92
|
+
if (!connected) {
|
|
93
|
+
this.io.log.warn('front gate control channel went offline');
|
|
94
|
+
this.enterUnavailable('control-offline');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.io.log.info('front gate control channel connected');
|
|
98
|
+
this.clearTimers();
|
|
99
|
+
this.plan = { kind: 'idle' };
|
|
100
|
+
this.sensorFreshSinceOnline = false;
|
|
101
|
+
this.emitSnapshot('control-online-awaiting-fresh-sensor');
|
|
102
|
+
}
|
|
103
|
+
applySensorConnectedChange(connected) {
|
|
104
|
+
if (this.facts.sensorConnected === connected) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.facts.sensorConnected = connected;
|
|
108
|
+
if (!connected) {
|
|
109
|
+
this.io.log.warn('front gate sensor channel went offline');
|
|
110
|
+
this.enterUnavailable('sensor-offline');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.io.log.info('front gate sensor channel connected');
|
|
114
|
+
this.clearTimers();
|
|
115
|
+
this.plan = { kind: 'idle' };
|
|
116
|
+
this.sensorFreshSinceOnline = false;
|
|
117
|
+
this.emitSnapshot('sensor-online-awaiting-fresh-state');
|
|
118
|
+
}
|
|
119
|
+
applyClosedSensorChange(closed) {
|
|
120
|
+
const previous = this.facts.closedSensor;
|
|
121
|
+
const wasFresh = this.sensorFreshSinceOnline;
|
|
122
|
+
this.facts.closedSensor = closed;
|
|
123
|
+
if (this.facts.controlConnected === true && this.facts.sensorConnected === true) {
|
|
124
|
+
this.sensorFreshSinceOnline = true;
|
|
125
|
+
}
|
|
126
|
+
const becameFresh = !wasFresh && this.sensorFreshSinceOnline;
|
|
127
|
+
if (previous === closed && !becameFresh) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (closed) {
|
|
131
|
+
this.io.log.info('closed sensor is TRUE -> gate is fully closed');
|
|
132
|
+
this.clearTimers();
|
|
133
|
+
this.plan = { kind: 'idle' };
|
|
134
|
+
if (this.requestedTarget === 'open' && this.isAvailableForHomeKit()) {
|
|
135
|
+
this.emitSnapshot('closed-sensor-true-but-open-still-requested');
|
|
136
|
+
void this.enqueue('auto-open-after-closed', async () => {
|
|
137
|
+
await this.startOpeningFromClosed('auto-open-after-closed');
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.requestedTarget = null;
|
|
142
|
+
this.emitSnapshot('closed-sensor-true');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this.io.log.info('closed sensor is FALSE -> gate is not fully closed');
|
|
146
|
+
if (previous === true) {
|
|
147
|
+
// The gate just left the closed end-stop. This is the one fully reliable
|
|
148
|
+
// direction signal we have: motion is opening.
|
|
149
|
+
this.lastPulseLikeActivityAt = Date.now();
|
|
150
|
+
this.startOpeningMotion(this.requestedTarget === 'open' ? 'homekit' : 'external', 'closed-sensor-fell-from-true-to-false');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (this.requestedTarget === 'open'
|
|
154
|
+
&& !(this.plan.kind === 'moving' && this.plan.direction === 'closing')
|
|
155
|
+
&& !(this.plan.kind === 'waitingSecondPulse' && this.plan.finalDirection === 'closing')) {
|
|
156
|
+
this.requestedTarget = null;
|
|
157
|
+
}
|
|
158
|
+
this.emitSnapshot('closed-sensor-false');
|
|
159
|
+
}
|
|
160
|
+
async applyHomeKitTarget(target) {
|
|
161
|
+
if (!this.isAvailableForHomeKit()) {
|
|
162
|
+
throw new Error('front gate controller is not available');
|
|
163
|
+
}
|
|
164
|
+
this.requestedTarget = target;
|
|
165
|
+
if (target === 'open') {
|
|
166
|
+
await this.handleOpenRequest();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await this.handleCloseRequest();
|
|
170
|
+
}
|
|
171
|
+
async handleOpenRequest() {
|
|
172
|
+
if (this.facts.closedSensor === true) {
|
|
173
|
+
if (this.plan.kind === 'moving' && this.plan.direction === 'opening') {
|
|
174
|
+
this.emitSnapshot('open-request-already-opening-from-closed');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (this.plan.kind === 'waitingSecondPulse' && this.plan.finalDirection === 'opening') {
|
|
178
|
+
this.emitSnapshot('open-request-already-reversing-to-open');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await this.startOpeningFromClosed('homekit-open-from-closed');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (this.plan.kind === 'moving' && this.plan.direction === 'closing' && this.plan.certainty === 'known') {
|
|
185
|
+
await this.reverseKnownMotion('opening', 'homekit-reverse-known-closing-to-open');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (this.plan.kind === 'waitingSecondPulse' && this.plan.finalDirection === 'closing') {
|
|
189
|
+
this.io.log.info('open requested while waiting to restart towards close; cancelling close restart');
|
|
190
|
+
this.clearPhaseTimer();
|
|
191
|
+
this.plan = { kind: 'idle' };
|
|
192
|
+
this.emitSnapshot('cancelled-pending-close-restart');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this.plan.kind === 'moving' && this.plan.direction === 'closing' && this.plan.certainty === 'goalOnly') {
|
|
196
|
+
// We do not actually know whether the gate is currently closing or whether a
|
|
197
|
+
// previous close-seek pulse accidentally caused opening. In this ambiguous
|
|
198
|
+
// state, the safest action is to cancel further close retries and hold the
|
|
199
|
+
// gate in the generic open-ish state.
|
|
200
|
+
this.io.log.warn('open requested during ambiguous close-seek; cancelling close retries instead of sending more pulses');
|
|
201
|
+
this.clearTimers();
|
|
202
|
+
this.plan = { kind: 'idle' };
|
|
203
|
+
this.requestedTarget = null;
|
|
204
|
+
this.emitSnapshot('cancelled-ambiguous-close-seek');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.requestedTarget = null;
|
|
208
|
+
this.emitSnapshot('open-request-already-satisfied-openish');
|
|
209
|
+
}
|
|
210
|
+
async handleCloseRequest() {
|
|
211
|
+
if (this.facts.closedSensor === true) {
|
|
212
|
+
if (this.plan.kind === 'moving' && this.plan.direction === 'opening') {
|
|
213
|
+
// A very early cancel while we are still on the closed end-stop can be
|
|
214
|
+
// satisfied by one pulse: stop the opening attempt and remain closed.
|
|
215
|
+
await this.pulseMotor('cancel-opening-while-still-closed');
|
|
216
|
+
this.clearTimers();
|
|
217
|
+
this.plan = { kind: 'idle' };
|
|
218
|
+
this.requestedTarget = null;
|
|
219
|
+
this.emitSnapshot('opening-cancelled-before-leaving-closed');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this.requestedTarget = null;
|
|
223
|
+
this.emitSnapshot('close-request-already-satisfied');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (this.plan.kind === 'moving' && this.plan.direction === 'opening' && this.plan.certainty === 'known') {
|
|
227
|
+
await this.reverseKnownMotion('closing', 'homekit-reverse-known-opening-to-close');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (this.plan.kind === 'waitingSecondPulse' && this.plan.finalDirection === 'opening') {
|
|
231
|
+
this.io.log.info('close requested while waiting to restart towards open; cancelling open restart');
|
|
232
|
+
this.clearPhaseTimer();
|
|
233
|
+
this.plan = { kind: 'idle' };
|
|
234
|
+
await this.startCloseSeek(0, 'close-after-cancelled-open-restart');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (this.plan.kind === 'waitingSecondPulse' && this.plan.finalDirection === 'closing') {
|
|
238
|
+
this.emitSnapshot('close-request-already-reversing-to-close');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (this.plan.kind === 'moving' && this.plan.direction === 'closing') {
|
|
242
|
+
this.emitSnapshot('close-request-already-closing');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
await this.startCloseSeek(0, 'homekit-close-from-openish');
|
|
246
|
+
}
|
|
247
|
+
async startOpeningFromClosed(reason) {
|
|
248
|
+
if (this.facts.closedSensor !== true) {
|
|
249
|
+
this.requestedTarget = null;
|
|
250
|
+
this.emitSnapshot(`${reason}-already-openish`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
await this.pulseMotor(reason);
|
|
254
|
+
this.startOpeningMotion('homekit', `${reason}-pulse-sent`);
|
|
255
|
+
}
|
|
256
|
+
async startCloseSeek(attempt, reason) {
|
|
257
|
+
if (this.facts.closedSensor === true) {
|
|
258
|
+
this.requestedTarget = null;
|
|
259
|
+
this.emitSnapshot(`${reason}-already-closed`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await this.pulseMotor(`${reason}-attempt-${attempt + 1}`);
|
|
263
|
+
this.startClosingMotion('goalOnly', attempt === 0 ? 'homekit' : 'recovery', attempt, `${reason}-pulse-sent`);
|
|
264
|
+
}
|
|
265
|
+
async reverseKnownMotion(finalDirection, reason) {
|
|
266
|
+
await this.pulseMotor(`${reason}-stop-current-motion`);
|
|
267
|
+
const deadlineAt = Date.now() + this.pulseGapMs + this.travelMs;
|
|
268
|
+
this.clearMovementTimer();
|
|
269
|
+
this.plan = {
|
|
270
|
+
kind: 'waitingSecondPulse',
|
|
271
|
+
finalDirection,
|
|
272
|
+
source: 'homekit',
|
|
273
|
+
attempt: 0,
|
|
274
|
+
dueAt: Date.now() + this.pulseGapMs,
|
|
275
|
+
deadlineAt,
|
|
276
|
+
reason: finalDirection === 'opening' ? 'reverseToOpen' : 'reverseToClose',
|
|
277
|
+
};
|
|
278
|
+
this.schedulePhaseTimer(this.plan.dueAt);
|
|
279
|
+
this.emitSnapshot(`${reason}-waiting-second-pulse`);
|
|
280
|
+
}
|
|
281
|
+
startOpeningMotion(source, note) {
|
|
282
|
+
this.clearPhaseTimer();
|
|
283
|
+
this.plan = {
|
|
284
|
+
kind: 'moving',
|
|
285
|
+
direction: 'opening',
|
|
286
|
+
certainty: 'known',
|
|
287
|
+
source,
|
|
288
|
+
attempt: 0,
|
|
289
|
+
startedAt: Date.now(),
|
|
290
|
+
deadlineAt: Date.now() + this.travelMs,
|
|
291
|
+
};
|
|
292
|
+
this.scheduleMovementTimer(this.plan.deadlineAt);
|
|
293
|
+
this.emitSnapshot(note);
|
|
294
|
+
}
|
|
295
|
+
startClosingMotion(certainty, source, attempt, note) {
|
|
296
|
+
this.clearPhaseTimer();
|
|
297
|
+
this.plan = {
|
|
298
|
+
kind: 'moving',
|
|
299
|
+
direction: 'closing',
|
|
300
|
+
certainty,
|
|
301
|
+
source,
|
|
302
|
+
attempt,
|
|
303
|
+
startedAt: Date.now(),
|
|
304
|
+
deadlineAt: Date.now() + this.travelMs,
|
|
305
|
+
};
|
|
306
|
+
this.scheduleMovementTimer(this.plan.deadlineAt);
|
|
307
|
+
this.emitSnapshot(note);
|
|
308
|
+
}
|
|
309
|
+
scheduleMovementTimer(deadlineAt) {
|
|
310
|
+
this.clearMovementTimer();
|
|
311
|
+
const delayMs = Math.max(0, deadlineAt - Date.now());
|
|
312
|
+
const token = ++this.movementTimerToken;
|
|
313
|
+
this.movementTimer = setTimeout(() => {
|
|
314
|
+
void this.enqueue(`movement-timeout-${token}`, async () => {
|
|
315
|
+
if (token !== this.movementTimerToken) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
await this.handleMovementTimeout();
|
|
319
|
+
});
|
|
320
|
+
}, delayMs);
|
|
321
|
+
}
|
|
322
|
+
schedulePhaseTimer(dueAt) {
|
|
323
|
+
this.clearPhaseTimer();
|
|
324
|
+
const delayMs = Math.max(0, dueAt - Date.now());
|
|
325
|
+
const token = ++this.phaseTimerToken;
|
|
326
|
+
this.phaseTimer = setTimeout(() => {
|
|
327
|
+
void this.enqueue(`phase-timer-${token}`, async () => {
|
|
328
|
+
if (token !== this.phaseTimerToken) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
await this.handlePhaseTimer();
|
|
332
|
+
});
|
|
333
|
+
}, delayMs);
|
|
334
|
+
}
|
|
335
|
+
clearMovementTimer() {
|
|
336
|
+
if (this.movementTimer) {
|
|
337
|
+
clearTimeout(this.movementTimer);
|
|
338
|
+
this.movementTimer = undefined;
|
|
339
|
+
}
|
|
340
|
+
this.movementTimerToken += 1;
|
|
341
|
+
}
|
|
342
|
+
clearPhaseTimer() {
|
|
343
|
+
if (this.phaseTimer) {
|
|
344
|
+
clearTimeout(this.phaseTimer);
|
|
345
|
+
this.phaseTimer = undefined;
|
|
346
|
+
}
|
|
347
|
+
this.phaseTimerToken += 1;
|
|
348
|
+
}
|
|
349
|
+
clearTimers() {
|
|
350
|
+
this.clearMovementTimer();
|
|
351
|
+
this.clearPhaseTimer();
|
|
352
|
+
}
|
|
353
|
+
async handlePhaseTimer() {
|
|
354
|
+
if (this.plan.kind !== 'waitingSecondPulse') {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const finalDirection = this.plan.finalDirection;
|
|
358
|
+
const source = this.plan.source;
|
|
359
|
+
const attempt = this.plan.attempt;
|
|
360
|
+
const deadlineAt = this.plan.deadlineAt;
|
|
361
|
+
await this.pulseMotor(`second-pulse-${finalDirection}`);
|
|
362
|
+
this.clearPhaseTimer();
|
|
363
|
+
this.plan = {
|
|
364
|
+
kind: 'moving',
|
|
365
|
+
direction: finalDirection,
|
|
366
|
+
certainty: 'known',
|
|
367
|
+
source,
|
|
368
|
+
attempt,
|
|
369
|
+
startedAt: Date.now(),
|
|
370
|
+
deadlineAt,
|
|
371
|
+
};
|
|
372
|
+
this.scheduleMovementTimer(deadlineAt);
|
|
373
|
+
this.emitSnapshot(`second-pulse-fired-${finalDirection}`);
|
|
374
|
+
}
|
|
375
|
+
async handleMovementTimeout() {
|
|
376
|
+
if (this.plan.kind !== 'moving') {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (this.facts.closedSensor === true) {
|
|
380
|
+
this.clearTimers();
|
|
381
|
+
this.plan = { kind: 'idle' };
|
|
382
|
+
this.requestedTarget = null;
|
|
383
|
+
this.emitSnapshot('movement-timeout-but-already-closed');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (this.plan.direction === 'opening') {
|
|
387
|
+
// Fully-open and partially-open look the same to us. When the opening
|
|
388
|
+
// window expires we intentionally collapse to the generic open-ish state,
|
|
389
|
+
// which the Home app will still display as "Opening" per the requested UX.
|
|
390
|
+
this.clearTimers();
|
|
391
|
+
this.plan = { kind: 'idle' };
|
|
392
|
+
this.requestedTarget = null;
|
|
393
|
+
this.emitSnapshot('opening-window-elapsed-openish');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (this.requestedTarget === 'closed' && this.plan.attempt < this.closeRetryLimit) {
|
|
397
|
+
const nextAttempt = this.plan.attempt + 1;
|
|
398
|
+
this.io.log.warn('close window elapsed without a closed-sensor hit; retrying close seek from the generic open-ish state');
|
|
399
|
+
this.clearTimers();
|
|
400
|
+
this.plan = { kind: 'idle' };
|
|
401
|
+
await this.startCloseSeek(nextAttempt, 'close-timeout-retry');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.io.log.warn('close window elapsed without reaching the closed sensor; leaving gate in open-ish state');
|
|
405
|
+
this.clearTimers();
|
|
406
|
+
this.plan = { kind: 'idle' };
|
|
407
|
+
this.requestedTarget = null;
|
|
408
|
+
this.emitSnapshot('closing-window-elapsed-openish');
|
|
409
|
+
}
|
|
410
|
+
enterUnavailable(note) {
|
|
411
|
+
this.clearTimers();
|
|
412
|
+
this.plan = { kind: 'idle' };
|
|
413
|
+
this.requestedTarget = null;
|
|
414
|
+
this.sensorFreshSinceOnline = false;
|
|
415
|
+
this.emitSnapshot(note);
|
|
416
|
+
}
|
|
417
|
+
isAvailableForHomeKit() {
|
|
418
|
+
return this.facts.controlConnected === true
|
|
419
|
+
&& this.facts.sensorConnected === true
|
|
420
|
+
&& this.sensorFreshSinceOnline
|
|
421
|
+
&& this.facts.closedSensor !== null;
|
|
422
|
+
}
|
|
423
|
+
async pulseMotor(reason) {
|
|
424
|
+
const elapsed = Date.now() - this.lastPulseLikeActivityAt;
|
|
425
|
+
if (elapsed < this.pulseGapMs) {
|
|
426
|
+
await delay(this.pulseGapMs - elapsed);
|
|
427
|
+
}
|
|
428
|
+
this.io.log.info(`motor pulse -> ${reason}`);
|
|
429
|
+
await this.io.pulseMotor(reason);
|
|
430
|
+
this.lastPulseLikeActivityAt = Date.now();
|
|
431
|
+
}
|
|
432
|
+
emitSnapshot(note) {
|
|
433
|
+
if (this.disposed) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
this.io.publishSnapshot(this.buildSnapshot(note));
|
|
437
|
+
}
|
|
438
|
+
buildSnapshot(note) {
|
|
439
|
+
const available = this.isAvailableForHomeKit();
|
|
440
|
+
return {
|
|
441
|
+
available,
|
|
442
|
+
controlConnected: this.facts.controlConnected,
|
|
443
|
+
sensorConnected: this.facts.sensorConnected,
|
|
444
|
+
closedSensor: this.facts.closedSensor,
|
|
445
|
+
sensorFreshSinceOnline: this.sensorFreshSinceOnline,
|
|
446
|
+
currentDoorState: available ? this.computeCurrentDoorState() : undefined,
|
|
447
|
+
targetDoorState: available ? this.computeTargetDoorState() : undefined,
|
|
448
|
+
requestedTarget: this.requestedTarget,
|
|
449
|
+
motionDirection: this.getMotionDirection(),
|
|
450
|
+
motionCertainty: this.getMotionCertainty(),
|
|
451
|
+
planKind: this.plan.kind,
|
|
452
|
+
note,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
getMotionDirection() {
|
|
456
|
+
if (this.plan.kind === 'moving') {
|
|
457
|
+
return this.plan.direction;
|
|
458
|
+
}
|
|
459
|
+
if (this.plan.kind === 'waitingSecondPulse') {
|
|
460
|
+
return this.plan.finalDirection;
|
|
461
|
+
}
|
|
462
|
+
return 'none';
|
|
463
|
+
}
|
|
464
|
+
getMotionCertainty() {
|
|
465
|
+
if (this.plan.kind === 'moving') {
|
|
466
|
+
return this.plan.certainty;
|
|
467
|
+
}
|
|
468
|
+
if (this.plan.kind === 'waitingSecondPulse') {
|
|
469
|
+
return 'known';
|
|
470
|
+
}
|
|
471
|
+
return 'none';
|
|
472
|
+
}
|
|
473
|
+
computeCurrentDoorState() {
|
|
474
|
+
if (this.plan.kind === 'moving') {
|
|
475
|
+
return this.plan.direction === 'closing' ? 3 /* DoorCurrentState.CLOSING */ : 2 /* DoorCurrentState.OPENING */;
|
|
476
|
+
}
|
|
477
|
+
if (this.plan.kind === 'waitingSecondPulse') {
|
|
478
|
+
return this.plan.finalDirection === 'closing' ? 3 /* DoorCurrentState.CLOSING */ : 2 /* DoorCurrentState.OPENING */;
|
|
479
|
+
}
|
|
480
|
+
return this.facts.closedSensor ? 1 /* DoorCurrentState.CLOSED */ : 2 /* DoorCurrentState.OPENING */;
|
|
481
|
+
}
|
|
482
|
+
computeTargetDoorState() {
|
|
483
|
+
if (this.requestedTarget) {
|
|
484
|
+
return this.requestedTarget === 'closed' ? 1 /* DoorTargetState.CLOSED */ : 0 /* DoorTargetState.OPEN */;
|
|
485
|
+
}
|
|
486
|
+
if (this.plan.kind === 'moving') {
|
|
487
|
+
return this.plan.direction === 'closing' ? 1 /* DoorTargetState.CLOSED */ : 0 /* DoorTargetState.OPEN */;
|
|
488
|
+
}
|
|
489
|
+
if (this.plan.kind === 'waitingSecondPulse') {
|
|
490
|
+
return this.plan.finalDirection === 'closing' ? 1 /* DoorTargetState.CLOSED */ : 0 /* DoorTargetState.OPEN */;
|
|
491
|
+
}
|
|
492
|
+
return this.facts.closedSensor ? 1 /* DoorTargetState.CLOSED */ : 0 /* DoorTargetState.OPEN */;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
exports.FrontGateFsm = FrontGateFsm;
|
|
496
|
+
//# sourceMappingURL=FrontGateFsm.js.map
|