homebridge-shelly-blu-trv 1.0.7 → 1.0.10

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
@@ -97,6 +97,41 @@ To ensure coverage is sufficient:
97
97
  - Check coverage threshold (lines): `npm run check-coverage` (configured to require at least 80% lines by default, set `COVERAGE_THRESHOLD` env to override)
98
98
 
99
99
 
100
+ ## Publishing
101
+
102
+ ### Manual device configuration (fallback when discovery unavailable) ✅
103
+
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
+
100
135
  ## Publishing
101
136
 
102
137
  - The repository contains two publishing workflows:
@@ -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/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
@@ -46,10 +46,17 @@ class ShellyApi {
46
46
  async rpcCall(id, method, params) {
47
47
  // Try several RPC variants for wider compatibility with firmware differences
48
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
- ];
49
+ // Try direct GetStatus endpoints first (some firmwares expose dedicated GET endpoints)
50
+ const candidates = [];
51
+ if (method === 'TRV.GetStatus') {
52
+ candidates.push(`/rpc/BluTrv.GetStatus?id=${id}`);
53
+ candidates.push(`/rpc/BluTrv.GetStatus&id=${id}`);
54
+ }
55
+ // Common CALL variants (different firmware use call vs Call and different query separators)
56
+ candidates.push(`/rpc/BluTrv.Call?id=${id}&method=${method}${paramsStr}`);
57
+ candidates.push(`/rpc/BluTrv.call?id=${id}&method=${method}${paramsStr}`);
58
+ candidates.push(`/rpc/BluTrv.Call&id=${id}&method=${method}${paramsStr}`);
59
+ candidates.push(`/rpc/BluTrv.call&id=${id}&method=${method}${paramsStr}`);
53
60
  for (const path of candidates) {
54
61
  try {
55
62
  return await this.get(path);
@@ -112,15 +119,37 @@ class ShellyApi {
112
119
  async getTrvState(id) {
113
120
  this.log.debug(`[ShellyApi] Fetching state for TRV ${id}`);
114
121
  try {
115
- const rpc = await this.rpcCall(id, 'TRV.GetStatus');
116
- const status = await this.get("/status");
117
- const dev = status.ble?.devices?.find((d) => d.id === id);
122
+ // RPC variant may return battery/paired status directly (BluTrv.GetStatus). Use RPC result first.
123
+ const rpcAny = await this.rpcCall(id, 'TRV.GetStatus');
124
+ const rpc = rpcAny;
125
+ // Validate required fields
126
+ if (typeof rpc.current_C !== 'number' ||
127
+ typeof rpc.target_C !== 'number' ||
128
+ typeof rpc.pos !== 'number') {
129
+ throw new Error(`Missing required TRV state fields in RPC response: ${JSON.stringify(rpc)}`);
130
+ }
131
+ // Prefer battery/online information from RPC response; if missing, try /status as a graceful fallback
132
+ let battery = typeof rpc.battery === 'number' ? rpc.battery : undefined;
133
+ let online = typeof rpc.paired === 'boolean' ? rpc.paired : undefined;
134
+ if (battery === undefined || online === undefined) {
135
+ try {
136
+ const status = await this.get("/status");
137
+ const dev = status.ble?.devices?.find((d) => d.id === id);
138
+ battery = battery ?? dev?.battery ?? 0;
139
+ online = online ?? !!dev?.online;
140
+ }
141
+ catch (err) {
142
+ this.log.debug(`[ShellyApi] /status fallback failed for TRV ${id}: ${err instanceof Error ? err.message : String(err)}`);
143
+ battery = battery ?? 0;
144
+ online = online ?? true;
145
+ }
146
+ }
118
147
  const state = {
119
148
  currentTemp: rpc.current_C,
120
149
  targetTemp: rpc.target_C,
121
150
  valve: rpc.pos,
122
- battery: dev?.battery ?? 0,
123
- online: !!dev?.online
151
+ battery: battery ?? 0,
152
+ online: !!online
124
153
  };
125
154
  this.log.debug(`[ShellyApi] TRV ${id} state: temp=${state.currentTemp}°C, target=${state.targetTemp}°C, valve=${state.valve}%, battery=${state.battery}%, online=${state.online}`);
126
155
  return state;
@@ -133,7 +162,8 @@ class ShellyApi {
133
162
  async setTargetTemp(id, value) {
134
163
  this.log.debug(`[ShellyApi] Setting target temperature for TRV ${id} to ${value}°C`);
135
164
  try {
136
- await this.rpcCall(id, 'TRV.SetTarget', { target_C: value });
165
+ // Include an explicit id:0 in params (observed in some firmware examples)
166
+ await this.rpcCall(id, 'TRV.SetTarget', { id: 0, target_C: value });
137
167
  this.log.debug(`[ShellyApi] Successfully set target temperature for TRV ${id}`);
138
168
  }
139
169
  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.7",
4
+ "version": "1.0.10",
5
5
  "description": "Homebridge plugin for Shelly BLU TRV thermostats via Shelly BLU Gateway Gen3",
6
6
  "license": "MIT",
7
7
  "author": "agiplv",