homebridge-shelly-blu-trv 1.0.8 → 1.1.1
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 +44 -44
- package/config.schema.json +4 -2
- package/dist/platform.js +6 -23
- package/dist/shellyApi.js +28 -29
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -3,86 +3,86 @@
|
|
|
3
3
|
[](https://github.com/agiplv/homebridge-shelly-blu-trv/actions)
|
|
4
4
|
|
|
5
5
|
Homebridge plugin for Shelly BLU Thermostatic Radiator Valve (TRV) devices
|
|
6
|
-
via the Shelly BLU Gateway Gen3.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
# Homebridge Shelly BLU TRV Platform Plugin
|
|
8
|
+
|
|
9
|
+
Homebridge plugin for controlling Shelly BLU TRV (Thermostatic Radiator Valve) devices via a Shelly Plus/Pro Gateway. **Only manual device configuration is supported.**
|
|
11
10
|
|
|
12
11
|
## Features
|
|
13
12
|
|
|
14
|
-
- BLU TRV
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Battery level
|
|
19
|
-
- Offline detection
|
|
20
|
-
- Multiple gateways
|
|
13
|
+
- Control Shelly BLU TRV devices from HomeKit
|
|
14
|
+
- Direct communication with TRVs (no cloud)
|
|
15
|
+
- Battery, temperature, and valve state reporting
|
|
16
|
+
- Fast polling and state updates
|
|
21
17
|
|
|
22
18
|
## Requirements
|
|
23
19
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- Shelly
|
|
27
|
-
- Shelly BLU TRV
|
|
20
|
+
- Homebridge v1.6+
|
|
21
|
+
- Node.js v18+
|
|
22
|
+
- Shelly Plus/Pro Gateway (with local network access)
|
|
23
|
+
- Shelly BLU TRV devices
|
|
28
24
|
|
|
29
25
|
## Installation
|
|
30
26
|
|
|
31
|
-
Install via npm:
|
|
27
|
+
Install via Homebridge UI or npm:
|
|
32
28
|
|
|
33
|
-
```
|
|
29
|
+
```sh
|
|
34
30
|
npm install -g homebridge-shelly-blu-trv
|
|
35
31
|
```
|
|
36
32
|
|
|
37
|
-
## Configuration
|
|
33
|
+
## Configuration
|
|
38
34
|
|
|
39
|
-
Add the platform
|
|
35
|
+
Add the platform to your Homebridge `config.json`. You **must** specify each gateway and the list of TRVs to control. **Automatic discovery is not supported.**
|
|
40
36
|
|
|
41
37
|
```json
|
|
42
38
|
{
|
|
43
|
-
"
|
|
39
|
+
"platform": "ShellyBluPlatform",
|
|
40
|
+
"gateways": [
|
|
44
41
|
{
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
42
|
+
"host": "192.168.1.10",
|
|
43
|
+
"token": "your-gateway-token",
|
|
44
|
+
"pollInterval": 60,
|
|
45
|
+
"devices": [
|
|
46
|
+
{ "id": 123, "name": "Living Room" },
|
|
47
|
+
{ "id": 456, "name": "Bedroom" }
|
|
52
48
|
]
|
|
53
49
|
}
|
|
54
50
|
]
|
|
55
51
|
}
|
|
56
52
|
```
|
|
57
53
|
|
|
58
|
-
|
|
54
|
+
### Config Options
|
|
59
55
|
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
56
|
+
- `host` (string, required): IP or hostname of your Shelly Plus/Pro Gateway
|
|
57
|
+
- `token` (string, required): Gateway authentication token
|
|
58
|
+
- `pollInterval` (number, optional): Polling interval in seconds (default: 60)
|
|
59
|
+
- `devices` (array, required): List of TRVs to control. Each device must have:
|
|
60
|
+
- `id` (number, required): TRV device ID (as shown in the Shelly app or web UI)
|
|
61
|
+
- `name` (string, required): Name for HomeKit
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
**Note:** You must manually add each TRV you want to control. The plugin will not scan or discover devices automatically.
|
|
65
64
|
|
|
66
|
-
##
|
|
65
|
+
## Usage
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
- Lint: `npm run lint`
|
|
70
|
-
- Test: `npm test`
|
|
67
|
+
Once configured, your Shelly BLU TRVs will appear in HomeKit as Thermostat accessories. You can control target temperature, view current temperature, battery, and valve state.
|
|
71
68
|
|
|
72
|
-
|
|
69
|
+
## Troubleshooting
|
|
73
70
|
|
|
74
|
-
|
|
71
|
+
- Ensure your Homebridge server can reach the Shelly Gateway and TRVs on the local network.
|
|
72
|
+
- Double-check the device IDs and gateway token.
|
|
73
|
+
- Check Homebridge logs for errors.
|
|
75
74
|
|
|
76
|
-
##
|
|
75
|
+
## Development & Testing
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
Run tests with:
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
```sh
|
|
80
|
+
npm test
|
|
81
|
+
```
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
## License
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
MIT
|
|
86
86
|
|
|
87
87
|
We include a simple fake Shelly BLU gateway and an E2E runner to validate discovery and polling locally:
|
|
88
88
|
|
package/config.schema.json
CHANGED
|
@@ -27,13 +27,15 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devices": {
|
|
29
29
|
"type": "array",
|
|
30
|
-
"title": "
|
|
30
|
+
"title": "TRV devices (required)",
|
|
31
|
+
"minItems": 1,
|
|
31
32
|
"items": {
|
|
32
33
|
"type": "object",
|
|
33
34
|
"properties": {
|
|
34
35
|
"id": { "type": "number", "title": "TRV ID", "required": true },
|
|
35
36
|
"name": { "type": "string", "title": "Display name" }
|
|
36
|
-
}
|
|
37
|
+
},
|
|
38
|
+
"required": ["id"]
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
}
|
package/dist/platform.js
CHANGED
|
@@ -31,32 +31,15 @@ class ShellyBluPlatform {
|
|
|
31
31
|
this.log.warn("[ShellyBluPlatform] No gateways configured in config.json");
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
-
this.log.info(`[ShellyBluPlatform]
|
|
34
|
+
this.log.info(`[ShellyBluPlatform] Registering manually configured TRVs for ${gateways.length} gateway(s)`);
|
|
35
35
|
for (const gw of gateways) {
|
|
36
|
-
|
|
36
|
+
if (!gw.devices || gw.devices.length === 0) {
|
|
37
|
+
this.log.warn(`[ShellyBluPlatform] No TRVs configured for gateway ${gw.host}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
37
40
|
const api = new shellyApi_1.ShellyApi(gw, this.log);
|
|
38
41
|
const pollMs = (gw.pollInterval ?? 60) * 1000;
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
trvs = await api.discoverTrvs();
|
|
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
|
-
}
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
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
|
-
}
|
|
58
|
-
}
|
|
59
|
-
for (const trv of trvs) {
|
|
42
|
+
for (const trv of gw.devices) {
|
|
60
43
|
const uuid = this.api.hap.uuid.generate(`${gw.host}-${trv.id}`);
|
|
61
44
|
let acc = this.accessories.get(uuid);
|
|
62
45
|
if (!acc) {
|
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);
|
|
@@ -90,37 +97,28 @@ class ShellyApi {
|
|
|
90
97
|
throw err;
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
|
-
async discoverTrvs() {
|
|
94
|
-
this.log.debug(`[ShellyApi] Discovering TRVs from gateway ${this.gw.host}`);
|
|
95
|
-
try {
|
|
96
|
-
const status = await this.get("/status");
|
|
97
|
-
const devices = status.ble?.devices ?? [];
|
|
98
|
-
const trvs = devices
|
|
99
|
-
.filter((d) => d.type === "trv")
|
|
100
|
-
.map((d) => ({
|
|
101
|
-
id: d.id ?? 0,
|
|
102
|
-
name: d.name || `BLU TRV ${d.id ?? 'unknown'}`
|
|
103
|
-
}));
|
|
104
|
-
this.log.debug(`[ShellyApi] Found ${trvs.length} TRV(s) on gateway ${this.gw.host}`);
|
|
105
|
-
return trvs;
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
this.log.error(`[ShellyApi] Failed to discover TRVs: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
100
|
async getTrvState(id) {
|
|
113
101
|
this.log.debug(`[ShellyApi] Fetching state for TRV ${id}`);
|
|
114
102
|
try {
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
103
|
+
// Only use direct RPC response; no /status fallback
|
|
104
|
+
const rpcAny = await this.rpcCall(id, 'TRV.GetStatus');
|
|
105
|
+
const rpc = rpcAny;
|
|
106
|
+
// Validate required fields
|
|
107
|
+
if (typeof rpc.current_C !== 'number' ||
|
|
108
|
+
typeof rpc.target_C !== 'number' ||
|
|
109
|
+
typeof rpc.pos !== 'number') {
|
|
110
|
+
throw new Error(`Missing required TRV state fields in RPC response: ${JSON.stringify(rpc)}`);
|
|
111
|
+
}
|
|
112
|
+
// Use battery/online from RPC response if present, else default
|
|
113
|
+
const battery = typeof rpc.battery === 'number' ? rpc.battery : 0;
|
|
114
|
+
// Accept either paired or online as online indicator
|
|
115
|
+
const online = typeof rpc.online === 'boolean' ? rpc.online : (typeof rpc.paired === 'boolean' ? rpc.paired : true);
|
|
118
116
|
const state = {
|
|
119
117
|
currentTemp: rpc.current_C,
|
|
120
118
|
targetTemp: rpc.target_C,
|
|
121
119
|
valve: rpc.pos,
|
|
122
|
-
battery
|
|
123
|
-
online
|
|
120
|
+
battery,
|
|
121
|
+
online
|
|
124
122
|
};
|
|
125
123
|
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
124
|
return state;
|
|
@@ -133,7 +131,8 @@ class ShellyApi {
|
|
|
133
131
|
async setTargetTemp(id, value) {
|
|
134
132
|
this.log.debug(`[ShellyApi] Setting target temperature for TRV ${id} to ${value}°C`);
|
|
135
133
|
try {
|
|
136
|
-
|
|
134
|
+
// Include an explicit id:0 in params (observed in some firmware examples)
|
|
135
|
+
await this.rpcCall(id, 'TRV.SetTarget', { id: 0, target_C: value });
|
|
137
136
|
this.log.debug(`[ShellyApi] Successfully set target temperature for TRV ${id}`);
|
|
138
137
|
}
|
|
139
138
|
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.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"description": "Homebridge plugin for Shelly BLU TRV thermostats via Shelly BLU Gateway Gen3",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "agiplv",
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"test": "vitest run --coverage",
|
|
40
40
|
"test:watch": "vitest",
|
|
41
41
|
"lint": "eslint . --ext .ts",
|
|
42
|
-
"e2e": "node scripts/run-e2e.js",
|
|
43
42
|
"check-coverage": "node scripts/check-coverage.js",
|
|
44
43
|
"release:patch": "npm version patch -m \"chore(release): %s\" && git push --follow-tags",
|
|
45
44
|
"semantic-release": "semantic-release"
|