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 npm name `homebridge-pitpat-treadmill` was unregistered when checked on 9 October 2026. Publication is manual and must not occur until hardware validation and release review are complete. No release workflow in this repository publishes automatically.
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 (!active) {
74
- await this.bleManager.stopTreadmill();
70
+ if (this.activeCommand) {
71
+ await this.activeCommand;
75
72
  return;
76
73
  }
77
- if (!this.config.allowRemoteStart) {
78
- throw new this.api.hap.HapStatusError(-70412 /* this.api.hap.HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE */);
79
- }
80
- await this.bleManager.startTreadmill(this.targetSpeedKph);
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.bleManager.stopTreadmill();
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
- await this.bleManager.setSpeed(this.targetSpeedKph);
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
- throw new this.api.hap.HapStatusError(-70412 /* this.api.hap.HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE */);
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
- this.fanService.updateCharacteristic(this.api.hap.Characteristic.StatusFault, state === 'ready'
116
- ? this.api.hap.Characteristic.StatusFault.NO_FAULT
117
- : this.api.hap.Characteristic.StatusFault.GENERAL_FAULT);
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.0",
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
  },