homebridge-shelly-blu-trv 1.0.6 → 1.0.8

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/README.md CHANGED
@@ -99,22 +99,73 @@ To ensure coverage is sufficient:
99
99
 
100
100
  ## Publishing
101
101
 
102
- Automatic publishing is configured to run on new GitHub releases (see `.github/workflows/publish.yml`).
103
- To publish manually:
102
+ ### Manual device configuration (fallback when discovery unavailable)
104
103
 
105
- 1. Ensure you're logged in to npm (`npm login`).
104
+ If your Shelly BLU Gateway Gen3 does not expose the `/status` discovery endpoint (returns 404) or discovery is otherwise unavailable, you can manually configure TRV devices in the gateway entry of your Homebridge config. The plugin will use this list when discovery fails or returns no devices.
105
+
106
+ Example gateway configuration with manual devices:
107
+
108
+ ```json
109
+ {
110
+ "platforms": [
111
+ {
112
+ "platform": "ShellyBluTRV",
113
+ "gateways": [
114
+ {
115
+ "host": "10.0.0.171",
116
+ "token": "<optional-auth-token>",
117
+ "pollInterval": 60,
118
+ "devices": [
119
+ { "id": 100, "name": "Living TRV" },
120
+ { "id": 101, "name": "Kitchen TRV" }
121
+ ]
122
+ }
123
+ ]
124
+ }
125
+ ]
126
+ }
127
+ ```
128
+
129
+ Notes:
130
+
131
+ - `id` is the TRV numeric id assigned by the gateway; the plugin uses this id for polling and state updates.
132
+ - If `name` is omitted, a default name `BLU TRV <id>` will be used.
133
+ - When both discovery and manual `devices` are available, discovery takes precedence.
134
+
135
+ ## Publishing
136
+
137
+ - The repository contains two publishing workflows:
138
+ - **Publish** (`.github/workflows/publish.yml`) — runs when a GitHub Release is *created* and performs an `npm publish` step.
139
+ - **Semantic Release** (`.github/workflows/semantic-release.yml`) — when enabled, automatically creates releases, changelogs and publishes to npm on `main` via `semantic-release`.
140
+
141
+ **NOTE:** The automatic semantic-release runs are currently **paused** (workflow trigger switched to `workflow_dispatch`). To run it manually: go to the Actions tab → **Semantic Release** → **Run workflow** (choose `main`).
142
+
143
+ Manual publish steps
144
+
145
+ 1. Ensure you're logged in to npm: `npm login`.
106
146
  2. Build the package: `npm run build`.
107
- 3. Bump the version and push tags, or run `npm version <patch|minor|major>` and push tags.
108
- 4. Publish: `npm publish --access public`.
147
+ 3. Bump the package version (e.g. `npm version patch`) and push the tag.
148
+ 4. Create a GitHub Release (or run `npm publish --access public` manually).
149
+
150
+ Developer notes about `NPM_TOKEN` and automation
109
151
 
110
- Automated semantic releases are configured with `semantic-release`. The workflow runs on pushes to `main` and publishes a GitHub Release + npm package when a new release is performed.
152
+ - For CI-based publishing you must set the repository secret `NPM_TOKEN` (Settings Secrets Actions) to a valid **npm automation token** with publish rights.
153
+ - Create it on npm: Settings → Access Tokens → **Create New Token** → choose **Automation** and ensure it has publish rights.
154
+ - If your npm account uses Two-Factor Authentication (2FA), pick a token that is usable for CI (set 2FA to **Authorization only** for automation tokens). See https://docs.npmjs.com/getting-started/working_with_tokens for details.
155
+ - If semantic-release reports `EINVALIDNPMTOKEN` or `401 Unauthorized`, try re-creating the token and updating `NPM_TOKEN` in repo secrets.
156
+ - The repository also includes a Publish workflow (triggered on Release creation) which can publish even if semantic-release is paused — this is useful for one-off releases.
111
157
 
112
- Note: You must add the following repository secrets for automatic publishing to work:
158
+ E2E and local verification (developer)
113
159
 
114
- - `NPM_TOKEN` — npm token with publish permissions
115
- - `GITHUB_TOKEN` typically provided automatically by GitHub Actions (`secrets.GITHUB_TOKEN`) but keep it available if you customize tokens
160
+ - Run the E2E runner locally: `npm run e2e` — this will install deps, build the project, install a local shim, start the fake gateway and a headless Homebridge instance and look for discovery/polling logs.
161
+ - Run unit tests + coverage: `npm test` (creates `coverage/lcov.info` and an `lcov-report` directory).
162
+ - Check coverage threshold: `npm run check-coverage` (defaults to 80% lines; use `COVERAGE_THRESHOLD` to override).
163
+
164
+ If you'd like, I can add a small diagnostic Action that runs `npm whoami` using `NPM_TOKEN` (safely) so you can quickly confirm which npm account the token maps to before re-enabling semantic-release.
165
+
166
+ ---
116
167
 
117
- If you'd like me to open a PR with this setup and add a draft release, say so and I will create the branch and PR now.
168
+ If you want this documentation expanded into a `DEVELOPING.md` file or want me to add the diagnostic Action, tell me which and I'll add it in a PR.
118
169
 
119
170
  ## License
120
171
 
@@ -24,6 +24,17 @@
24
24
  "title": "Polling interval (seconds)",
25
25
  "default": 60,
26
26
  "minimum": 15
27
+ },
28
+ "devices": {
29
+ "type": "array",
30
+ "title": "Manual TRV devices (optional)",
31
+ "items": {
32
+ "type": "object",
33
+ "properties": {
34
+ "id": { "type": "number", "title": "TRV ID", "required": true },
35
+ "name": { "type": "string", "title": "Display name" }
36
+ }
37
+ }
27
38
  }
28
39
  }
29
40
  }
package/dist/accessory.js CHANGED
@@ -52,7 +52,7 @@ class ShellyTrvAccessory {
52
52
  this.log.warn(`[${this.accessory.displayName}] Device offline, unable to retrieve ${key}`);
53
53
  throw new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
54
54
  }
55
- // accessing dynamic state keys
55
+ // accessing typed state keys
56
56
  const value = s[key];
57
57
  this.log.debug(`[${this.accessory.displayName}] Retrieved ${key}: ${value}`);
58
58
  return value;
package/dist/platform.js CHANGED
@@ -40,10 +40,21 @@ class ShellyBluPlatform {
40
40
  try {
41
41
  trvs = await api.discoverTrvs();
42
42
  this.log.info(`[ShellyBluPlatform] Discovered ${trvs.length} TRV(s) on gateway ${gw.host}`);
43
+ // If discovery returned nothing but user provided manual devices, use them
44
+ if ((!trvs || trvs.length === 0) && gw.devices && gw.devices.length > 0) {
45
+ this.log.warn(`[ShellyBluPlatform] Discovery returned no devices; using manual device list for gateway ${gw.host}`);
46
+ trvs = gw.devices.map((d) => ({ id: d.id, name: d.name || `BLU TRV ${d.id}` }));
47
+ }
43
48
  }
44
49
  catch (error) {
45
- this.log.error(`[ShellyBluPlatform] Failed to discover devices on gateway ${gw.host}: ${error instanceof Error ? error.message : String(error)}`);
46
- continue;
50
+ if (gw.devices && gw.devices.length > 0) {
51
+ this.log.warn(`[ShellyBluPlatform] Discovery failed for gateway ${gw.host}, using manual device list: ${error instanceof Error ? error.message : String(error)}`);
52
+ trvs = gw.devices.map((d) => ({ id: d.id, name: d.name || `BLU TRV ${d.id}` }));
53
+ }
54
+ else {
55
+ this.log.error(`[ShellyBluPlatform] Failed to discover devices on gateway ${gw.host}: ${error instanceof Error ? error.message : String(error)}`);
56
+ continue;
57
+ }
47
58
  }
48
59
  for (const trv of trvs) {
49
60
  const uuid = this.api.hap.uuid.generate(`${gw.host}-${trv.id}`);
package/dist/shellyApi.js CHANGED
@@ -21,8 +21,16 @@ class ShellyApi {
21
21
  signal: AbortSignal.timeout(this.requestTimeout)
22
22
  });
23
23
  if (!res.ok) {
24
- this.log.error(`[ShellyApi] HTTP ${res.status} from gateway ${this.gw.host}`);
25
- throw new Error(`HTTP ${res.status}`);
24
+ // Try to extract body text for better diagnostics
25
+ let bodyText = '';
26
+ try {
27
+ bodyText = await res.text();
28
+ }
29
+ catch {
30
+ bodyText = '<no body>';
31
+ }
32
+ this.log.error(`[ShellyApi] HTTP ${res.status} from gateway ${this.gw.host}${path} - ${bodyText}`);
33
+ throw new Error(`HTTP ${res.status}: ${bodyText}`);
26
34
  }
27
35
  const data = await res.json();
28
36
  this.log.debug(`[ShellyApi] Successfully fetched from ${this.gw.host}${path}`);
@@ -30,11 +38,58 @@ class ShellyApi {
30
38
  }
31
39
  catch (error) {
32
40
  if (error instanceof Error) {
33
- this.log.error(`[ShellyApi] Request failed to ${this.gw.host}: ${error.message}`);
41
+ this.log.error(`[ShellyApi] Request failed to ${this.gw.host}${path}: ${error.message}`);
34
42
  }
35
43
  throw error;
36
44
  }
37
45
  }
46
+ async rpcCall(id, method, params) {
47
+ // Try several RPC variants for wider compatibility with firmware differences
48
+ const paramsStr = params ? `&params=${encodeURIComponent(JSON.stringify(params))}` : '';
49
+ const candidates = [
50
+ `/rpc/BluTrv.call?id=${id}&method=${method}${paramsStr}`,
51
+ `/rpc/BluTrv.call&id=${id}&method=${method}${paramsStr}`
52
+ ];
53
+ for (const path of candidates) {
54
+ try {
55
+ return await this.get(path);
56
+ }
57
+ catch (err) {
58
+ // If 404, try next candidate; otherwise propagate
59
+ if (err instanceof Error && err.message.includes('HTTP 404')) {
60
+ this.log.debug(`[ShellyApi] RPC variant failed (404), trying next: ${path}`);
61
+ continue;
62
+ }
63
+ throw err;
64
+ }
65
+ }
66
+ // Fallback: try POST to /rpc/BluTrv.call with JSON body
67
+ try {
68
+ const url = buildUrl(this.gw.host, '/rpc/BluTrv.call', this.gw.token);
69
+ const res = await fetch(url, {
70
+ method: 'POST',
71
+ body: JSON.stringify({ id, method, params }),
72
+ headers: { 'Content-Type': 'application/json' },
73
+ signal: AbortSignal.timeout(this.requestTimeout)
74
+ });
75
+ if (!res.ok) {
76
+ let bodyText = '';
77
+ try {
78
+ bodyText = await res.text();
79
+ }
80
+ catch {
81
+ bodyText = '<no body>';
82
+ }
83
+ this.log.error(`[ShellyApi] HTTP ${res.status} from gateway ${this.gw.host} (POST /rpc/BluTrv.call): ${bodyText}`);
84
+ throw new Error(`HTTP ${res.status}: ${bodyText}`);
85
+ }
86
+ return res.json();
87
+ }
88
+ catch (err) {
89
+ this.log.error(`[ShellyApi] RPC call failed for TRV ${id} method ${method}: ${err instanceof Error ? err.message : String(err)}`);
90
+ throw err;
91
+ }
92
+ }
38
93
  async discoverTrvs() {
39
94
  this.log.debug(`[ShellyApi] Discovering TRVs from gateway ${this.gw.host}`);
40
95
  try {
@@ -57,7 +112,7 @@ class ShellyApi {
57
112
  async getTrvState(id) {
58
113
  this.log.debug(`[ShellyApi] Fetching state for TRV ${id}`);
59
114
  try {
60
- const rpc = await this.get(`/rpc/BluTrv.call&id=${id}&method=TRV.GetStatus`);
115
+ const rpc = await this.rpcCall(id, 'TRV.GetStatus');
61
116
  const status = await this.get("/status");
62
117
  const dev = status.ble?.devices?.find((d) => d.id === id);
63
118
  const state = {
@@ -78,8 +133,7 @@ class ShellyApi {
78
133
  async setTargetTemp(id, value) {
79
134
  this.log.debug(`[ShellyApi] Setting target temperature for TRV ${id} to ${value}°C`);
80
135
  try {
81
- await this.get(`/rpc/BluTrv.call&id=${id}&method=TRV.SetTarget&params=` +
82
- encodeURIComponent(JSON.stringify({ target_C: value })));
136
+ await this.rpcCall(id, 'TRV.SetTarget', { target_C: value });
83
137
  this.log.debug(`[ShellyApi] Successfully set target temperature for TRV ${id}`);
84
138
  }
85
139
  catch (error) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-shelly-blu-trv",
3
3
  "displayName": "Shelly BLU TRV",
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "description": "Homebridge plugin for Shelly BLU TRV thermostats via Shelly BLU Gateway Gen3",
6
6
  "license": "MIT",
7
7
  "author": "agiplv",