homebridge-pitpat-treadmill 0.1.1 → 0.1.2
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. The project follows Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## [0.1.2] - 2026-10-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Always use the hardware-validated 1.5 km/h minimum for remote starts.
|
|
10
|
+
- Prevent `RotationSpeed` changes from starting a stopped treadmill.
|
|
11
|
+
- Debounce HomeKit speed-slider updates to avoid BLE command floods.
|
|
12
|
+
- Handle disconnects during command confirmation without unhandled HomeKit write errors.
|
|
13
|
+
- Remove the unsupported `StatusFault` characteristic from the `Fanv2` service.
|
|
14
|
+
|
|
5
15
|
## [0.1.1] - 2026-10-09
|
|
6
16
|
|
|
7
17
|
### Changed
|
|
@@ -11,14 +11,20 @@ export declare class TreadmillAccessory {
|
|
|
11
11
|
private readonly fanService;
|
|
12
12
|
private readonly telemetryService;
|
|
13
13
|
private targetSpeedKph;
|
|
14
|
+
private activeCommand;
|
|
15
|
+
private pendingSpeedKph;
|
|
16
|
+
private speedUpdateTimer;
|
|
14
17
|
constructor(accessory: PlatformAccessory, bleManager: BleManager, config: ValidatedConfig, api: API, log: Logger);
|
|
15
18
|
private isActive;
|
|
16
19
|
private currentSpeedPercentage;
|
|
17
20
|
private currentFanState;
|
|
18
|
-
private statusFault;
|
|
19
21
|
private setActive;
|
|
20
22
|
private setSpeed;
|
|
23
|
+
private applyActive;
|
|
24
|
+
private scheduleSpeedUpdate;
|
|
25
|
+
private cancelPendingSpeedUpdate;
|
|
21
26
|
private updateStatus;
|
|
22
27
|
private updateConnectionState;
|
|
28
|
+
private refreshControlCharacteristics;
|
|
23
29
|
private effectiveMaximumSpeed;
|
|
24
30
|
}
|
|
@@ -10,6 +10,9 @@ export class TreadmillAccessory {
|
|
|
10
10
|
fanService;
|
|
11
11
|
telemetryService;
|
|
12
12
|
targetSpeedKph = MINIMUM_COMMAND_SPEED_KPH;
|
|
13
|
+
activeCommand;
|
|
14
|
+
pendingSpeedKph;
|
|
15
|
+
speedUpdateTimer;
|
|
13
16
|
constructor(accessory, bleManager, config, api, log) {
|
|
14
17
|
this.accessory = accessory;
|
|
15
18
|
this.bleManager = bleManager;
|
|
@@ -34,7 +37,6 @@ export class TreadmillAccessory {
|
|
|
34
37
|
.onGet(() => this.currentSpeedPercentage())
|
|
35
38
|
.onSet(async (value) => this.setSpeed(value));
|
|
36
39
|
this.fanService.getCharacteristic(Characteristic.CurrentFanState).onGet(() => this.currentFanState());
|
|
37
|
-
this.fanService.getCharacteristic(Characteristic.StatusFault).onGet(() => this.statusFault());
|
|
38
40
|
accessory
|
|
39
41
|
.getService(Service.AccessoryInformation)
|
|
40
42
|
?.setCharacteristic(Characteristic.Manufacturer, 'PitPat (unofficial integration)')
|
|
@@ -63,40 +65,81 @@ export class TreadmillAccessory {
|
|
|
63
65
|
}
|
|
64
66
|
return this.api.hap.Characteristic.CurrentFanState.INACTIVE;
|
|
65
67
|
}
|
|
66
|
-
statusFault() {
|
|
67
|
-
return this.bleManager.state === 'ready'
|
|
68
|
-
? this.api.hap.Characteristic.StatusFault.NO_FAULT
|
|
69
|
-
: this.api.hap.Characteristic.StatusFault.GENERAL_FAULT;
|
|
70
|
-
}
|
|
71
68
|
async setActive(value) {
|
|
72
69
|
const active = Number(value) === this.api.hap.Characteristic.Active.ACTIVE;
|
|
73
|
-
if (
|
|
74
|
-
await this.
|
|
70
|
+
if (this.activeCommand) {
|
|
71
|
+
await this.activeCommand;
|
|
75
72
|
return;
|
|
76
73
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
this.activeCommand = this.applyActive(active)
|
|
75
|
+
.catch((error) => {
|
|
76
|
+
this.log.warn(`[PiTPAT] HomeKit ${active ? 'start' : 'stop'} request was not applied: ${errorMessage(error)}`);
|
|
77
|
+
this.refreshControlCharacteristics();
|
|
78
|
+
})
|
|
79
|
+
.finally(() => {
|
|
80
|
+
this.activeCommand = undefined;
|
|
81
|
+
});
|
|
82
|
+
await this.activeCommand;
|
|
81
83
|
}
|
|
82
84
|
async setSpeed(value) {
|
|
83
85
|
const percentage = Number(value);
|
|
84
86
|
if (percentage === 0) {
|
|
85
|
-
await this.
|
|
87
|
+
await this.setActive(this.api.hap.Characteristic.Active.INACTIVE);
|
|
86
88
|
return;
|
|
87
89
|
}
|
|
88
90
|
const deviceMaximum = this.bleManager.status?.maximumSpeedKph ?? this.config.maximumSpeedKph;
|
|
89
91
|
const requested = percentageToSpeed(percentage, this.effectiveMaximumSpeed(deviceMaximum));
|
|
90
92
|
this.targetSpeedKph = Math.max(MINIMUM_COMMAND_SPEED_KPH, requested);
|
|
91
93
|
if (this.bleManager.status?.runningState === RunningState.Running) {
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
else if (this.config.allowRemoteStart) {
|
|
95
|
-
await this.bleManager.startTreadmill(this.targetSpeedKph);
|
|
94
|
+
this.scheduleSpeedUpdate(this.targetSpeedKph);
|
|
96
95
|
}
|
|
97
96
|
else {
|
|
98
|
-
|
|
97
|
+
// RotationSpeed must never start a stopped treadmill. Active is the only
|
|
98
|
+
// HomeKit control allowed to request a remote start.
|
|
99
|
+
this.refreshControlCharacteristics();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async applyActive(active) {
|
|
103
|
+
if (!active) {
|
|
104
|
+
this.cancelPendingSpeedUpdate();
|
|
105
|
+
if (this.bleManager.status?.runningState === RunningState.Stopped) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await this.bleManager.stopTreadmill();
|
|
109
|
+
return;
|
|
99
110
|
}
|
|
111
|
+
if (!this.config.allowRemoteStart) {
|
|
112
|
+
throw new Error('remote start is disabled by configuration');
|
|
113
|
+
}
|
|
114
|
+
if (this.isActive() === this.api.hap.Characteristic.Active.ACTIVE) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// A remote start always uses the hardware-validated minimum. A separate,
|
|
118
|
+
// confirmed speed request may increase it only after Running is reported.
|
|
119
|
+
this.targetSpeedKph = MINIMUM_COMMAND_SPEED_KPH;
|
|
120
|
+
await this.bleManager.startTreadmill(MINIMUM_COMMAND_SPEED_KPH);
|
|
121
|
+
}
|
|
122
|
+
scheduleSpeedUpdate(speedKph) {
|
|
123
|
+
this.pendingSpeedKph = speedKph;
|
|
124
|
+
clearTimeout(this.speedUpdateTimer);
|
|
125
|
+
this.speedUpdateTimer = setTimeout(() => {
|
|
126
|
+
this.speedUpdateTimer = undefined;
|
|
127
|
+
const pendingSpeedKph = this.pendingSpeedKph;
|
|
128
|
+
this.pendingSpeedKph = undefined;
|
|
129
|
+
if (pendingSpeedKph === undefined || this.bleManager.status?.runningState !== RunningState.Running) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
void this.bleManager.setSpeed(pendingSpeedKph).catch((error) => {
|
|
133
|
+
this.log.warn(`[PiTPAT] HomeKit speed request was not applied: ${errorMessage(error)}`);
|
|
134
|
+
this.refreshControlCharacteristics();
|
|
135
|
+
});
|
|
136
|
+
}, 750);
|
|
137
|
+
this.speedUpdateTimer.unref();
|
|
138
|
+
}
|
|
139
|
+
cancelPendingSpeedUpdate() {
|
|
140
|
+
clearTimeout(this.speedUpdateTimer);
|
|
141
|
+
this.speedUpdateTimer = undefined;
|
|
142
|
+
this.pendingSpeedKph = undefined;
|
|
100
143
|
}
|
|
101
144
|
updateStatus({ status }) {
|
|
102
145
|
const { Characteristic } = this.api.hap;
|
|
@@ -105,19 +148,28 @@ export class TreadmillAccessory {
|
|
|
105
148
|
this.fanService.updateCharacteristic(Characteristic.Active, this.isActive());
|
|
106
149
|
this.fanService.updateCharacteristic(Characteristic.RotationSpeed, this.currentSpeedPercentage());
|
|
107
150
|
this.fanService.updateCharacteristic(Characteristic.CurrentFanState, this.currentFanState());
|
|
108
|
-
this.fanService.updateCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.NO_FAULT);
|
|
109
151
|
this.telemetryService.updateStatus(status);
|
|
110
152
|
if (this.config.debug) {
|
|
111
153
|
this.log.debug(`[PiTPAT] Speed updated: ${status.currentSpeedKph.toFixed(1)} km/h, state ${status.runningState}`);
|
|
112
154
|
}
|
|
113
155
|
}
|
|
114
156
|
updateConnectionState(state) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
157
|
+
if (state !== 'ready') {
|
|
158
|
+
this.cancelPendingSpeedUpdate();
|
|
159
|
+
this.refreshControlCharacteristics();
|
|
160
|
+
}
|
|
118
161
|
this.telemetryService.updateConnection(state);
|
|
119
162
|
}
|
|
163
|
+
refreshControlCharacteristics() {
|
|
164
|
+
const { Characteristic } = this.api.hap;
|
|
165
|
+
this.fanService.updateCharacteristic(Characteristic.Active, this.isActive());
|
|
166
|
+
this.fanService.updateCharacteristic(Characteristic.RotationSpeed, this.currentSpeedPercentage());
|
|
167
|
+
this.fanService.updateCharacteristic(Characteristic.CurrentFanState, this.currentFanState());
|
|
168
|
+
}
|
|
120
169
|
effectiveMaximumSpeed(deviceMaximumSpeedKph) {
|
|
121
170
|
return Math.min(this.config.maximumSpeedKph, deviceMaximumSpeedKph);
|
|
122
171
|
}
|
|
123
172
|
}
|
|
173
|
+
function errorMessage(error) {
|
|
174
|
+
return error instanceof Error ? error.message : String(error);
|
|
175
|
+
}
|
|
@@ -172,15 +172,23 @@ export class BleManager extends EventEmitter {
|
|
|
172
172
|
resolve(event.status);
|
|
173
173
|
}
|
|
174
174
|
};
|
|
175
|
+
const onConnectionState = (state) => {
|
|
176
|
+
if (state === 'disconnected' || state === 'disconnecting') {
|
|
177
|
+
cleanup();
|
|
178
|
+
reject(new Error('BLE disconnected while waiting for treadmill confirmation'));
|
|
179
|
+
}
|
|
180
|
+
};
|
|
175
181
|
const cleanup = () => {
|
|
176
182
|
clearTimeout(timer);
|
|
177
183
|
this.removeListener('status', onStatus);
|
|
184
|
+
this.removeListener('connectionState', onConnectionState);
|
|
178
185
|
};
|
|
179
186
|
const timer = setTimeout(() => {
|
|
180
187
|
cleanup();
|
|
181
188
|
reject(new Error('timed out waiting for treadmill confirmation'));
|
|
182
189
|
}, timeoutMs);
|
|
183
190
|
this.on('status', onStatus);
|
|
191
|
+
this.on('connectionState', onConnectionState);
|
|
184
192
|
});
|
|
185
193
|
}
|
|
186
194
|
scheduleReconnect() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-pitpat-treadmill",
|
|
3
3
|
"displayName": "PitPat Treadmill",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": "Local Homebridge integration for PitPat treadmills over Bluetooth Low Energy.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|