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 +35 -0
- package/config.schema.json +11 -0
- package/dist/platform.js +13 -2
- package/dist/shellyApi.js +40 -10
- package/package.json +1 -1
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:
|
package/config.schema.json
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
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 ? `¶ms=${encodeURIComponent(JSON.stringify(params))}` : '';
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
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:
|
|
123
|
-
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
|
-
|
|
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