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 CHANGED
@@ -3,86 +3,86 @@
3
3
  [![CI](https://github.com/agiplv/homebridge-shelly-blu-trv/actions/workflows/ci.yml/badge.svg)](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
- This plugin exposes BLU TRV devices to Apple HomeKit using a local Shelly BLU
9
- Gateway Gen3. It supports current temperature, target temperature control,
10
- valve position, battery level and offline detection.
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 discovery via Shelly BLU Gateway Gen3
15
- - Current temperature
16
- - Target temperature control
17
- - Valve position (percent)
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
- - Node.js 18+
25
- - Homebridge 1.6+
26
- - Shelly BLU Gateway Gen3
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
- ```bash
29
+ ```sh
34
30
  npm install -g homebridge-shelly-blu-trv
35
31
  ```
36
32
 
37
- ## Configuration (Homebridge UI)
33
+ ## Configuration
38
34
 
39
- Add the platform under `platforms` in Homebridge configuration. Example:
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
- "platforms": [
39
+ "platform": "ShellyBluPlatform",
40
+ "gateways": [
44
41
  {
45
- "platform": "ShellyBluTRV",
46
- "gateways": [
47
- {
48
- "host": "192.168.1.50",
49
- "token": "<optional-auth-token>",
50
- "pollInterval": 60
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
- Notes:
54
+ ### Config Options
59
55
 
60
- - Auth token is optional.
61
- - Valve position is reported as 0–100% (read-only).
62
- - Offline TRVs are shown as “Not Responding” in HomeKit.
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
- ## Development
65
+ ## Usage
67
66
 
68
- - Build: `npm run build`
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
- Contributions welcome — open an issue or a pull request.
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
- ## Local testing
75
+ ## Development & Testing
77
76
 
78
- To test the plugin locally in a running Homebridge instance:
77
+ Run tests with:
79
78
 
80
- - Build a tarball: `npm pack`
81
- - Install into Homebridge (on same machine): `npm i -g ./homebridge-shelly-blu-trv-<version>.tgz` or upload the tarball in Homebridge UI as a plugin file.
79
+ ```sh
80
+ npm test
81
+ ```
82
82
 
83
- To test in a development Homebridge container, you can mount the packed tarball or install from the working directory with `npm i -g` after `npm run build`.
83
+ ## License
84
84
 
85
- ### E2E (fake gateway) tests
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
 
@@ -27,13 +27,15 @@
27
27
  },
28
28
  "devices": {
29
29
  "type": "array",
30
- "title": "Manual TRV devices (optional)",
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] Starting device discovery for ${gateways.length} gateway(s)`);
34
+ this.log.info(`[ShellyBluPlatform] Registering manually configured TRVs for ${gateways.length} gateway(s)`);
35
35
  for (const gw of gateways) {
36
- this.log.debug(`[ShellyBluPlatform] Discovering devices on gateway: ${gw.host}`);
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
- let trvs;
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 ? `&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);
@@ -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
- 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);
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: dev?.battery ?? 0,
123
- online: !!dev?.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
- await this.rpcCall(id, 'TRV.SetTarget', { target_C: value });
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.0.8",
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"