homebridge-pitpat-treadmill 0.1.0 → 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,23 @@
|
|
|
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
|
+
|
|
15
|
+
## [0.1.1] - 2026-10-09
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Publish releases through GitHub Actions and npm Trusted Publishing with OIDC provenance.
|
|
20
|
+
- Document the automated release procedure.
|
|
21
|
+
|
|
5
22
|
## [0.1.0] - 2026-10-09
|
|
6
23
|
|
|
7
24
|
### Added
|
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ The initial packet structure was reimplemented from the MIT-licensed [`azmke/pit
|
|
|
176
176
|
|
|
177
177
|
## Publishing
|
|
178
178
|
|
|
179
|
-
The
|
|
179
|
+
The package is published on [npm](https://www.npmjs.com/package/homebridge-pitpat-treadmill). Future releases use npm Trusted Publishing from GitHub Actions, without a stored npm token. See [docs/PUBLISHING.md](docs/PUBLISHING.md).
|
|
180
180
|
|
|
181
181
|
## License
|
|
182
182
|
|
|
@@ -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() {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Publishing
|
|
2
|
+
|
|
3
|
+
Releases are published to npm through GitHub Actions and npm Trusted Publishing. No npm token is stored in GitHub.
|
|
4
|
+
|
|
5
|
+
## One-time npm configuration
|
|
6
|
+
|
|
7
|
+
Configure a GitHub Actions trusted publisher for `homebridge-pitpat-treadmill` with:
|
|
8
|
+
|
|
9
|
+
- Organization or user: `Alexxanddr`
|
|
10
|
+
- Repository: `homebridge-pitpat-treadmill`
|
|
11
|
+
- Workflow filename: `release.yml`
|
|
12
|
+
- Environment: leave empty
|
|
13
|
+
|
|
14
|
+
The workflow path is `.github/workflows/release.yml`. npm matches the filename, repository, and owner exactly.
|
|
15
|
+
|
|
16
|
+
## Release procedure
|
|
17
|
+
|
|
18
|
+
1. Update `CHANGELOG.md`.
|
|
19
|
+
2. Run the complete local check:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run check
|
|
23
|
+
npm pack --dry-run
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. Create the version commit and tag:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm version patch
|
|
30
|
+
git push origin master --follow-tags
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use `minor` or `major` instead of `patch` when appropriate.
|
|
34
|
+
|
|
35
|
+
4. Create and publish the matching GitHub release, for example `v0.1.1`.
|
|
36
|
+
5. The `Publish to npm` workflow verifies that the release tag equals `v` plus the package version, reruns all checks, and publishes with OIDC credentials.
|
|
37
|
+
6. Verify the registry:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm view homebridge-pitpat-treadmill version dist-tags
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Trusted Publishing automatically adds npm provenance. Draft releases do not publish until they are changed to published. Re-running a failed workflow is safe only when that version does not already exist on npm; npm versions are immutable.
|
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",
|
|
@@ -47,6 +47,9 @@
|
|
|
47
47
|
"bugs": {
|
|
48
48
|
"url": "https://github.com/Alexxanddr/homebridge-pitpat-treadmill/issues"
|
|
49
49
|
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
50
53
|
"dependencies": {
|
|
51
54
|
"@stoprocent/noble": "2.8.0"
|
|
52
55
|
},
|