node-red-contrib-hoymiles-home 0.1.1-dev.0 → 0.1.2
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 +115 -0
- package/nodes/hoymiles-config/hoymiles-config.js +18 -10
- package/nodes/hoymiles-watch/hoymiles-watch.js +38 -14
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# node-red-contrib-hoymiles-home
|
|
2
|
+
|
|
3
|
+
Node-RED nodes for the **Hoymiles S-Miles Home API** — continuously polls live power data and emits a message for every new reading.
|
|
4
|
+
|
|
5
|
+
## Nodes
|
|
6
|
+
|
|
7
|
+
### `hoymiles-config` (config node)
|
|
8
|
+
|
|
9
|
+
Manages authentication against the Hoymiles cloud. Enter your S-Miles Home **e-mail** and **password** — the node performs the same two-step login as the S-Miles Home app (pre-inspection → Argon2ID or MD5·SHA256 password encoding → token) and shares the session with all connected watch nodes.
|
|
10
|
+
|
|
11
|
+
### `hoymiles-watch` (input node)
|
|
12
|
+
|
|
13
|
+
Continuously polls live power data for a station and emits one message per reading.
|
|
14
|
+
|
|
15
|
+
**Configuration:**
|
|
16
|
+
|
|
17
|
+
| Field | Description |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Credentials | Select a `hoymiles-config` node |
|
|
20
|
+
| Station ID | Numeric station ID (`sid`) — find it in the S-Miles Home app or via the Hoymiles API |
|
|
21
|
+
| Interval (s) | Poll interval in seconds. Leave empty to use the `dly` value from each server response (recommended — mirrors the S-Miles Home app) |
|
|
22
|
+
|
|
23
|
+
**Output `msg.payload`:**
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"pv": 210,
|
|
28
|
+
"load": 340,
|
|
29
|
+
"grid": 130,
|
|
30
|
+
"bat": 0
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Field | Description |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `pv` | Solar generation (W) |
|
|
37
|
+
| `load` | Consumption (W) |
|
|
38
|
+
| `grid` | Grid exchange (W) — negative = feed-in |
|
|
39
|
+
| `bat` | Battery power (W) — negative = charging |
|
|
40
|
+
|
|
41
|
+
**Additional message properties:**
|
|
42
|
+
|
|
43
|
+
| Property | Value |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `msg.topic` | `hoymiles/watch/{sid}` |
|
|
46
|
+
| `msg.sid` | Station ID |
|
|
47
|
+
|
|
48
|
+
**Status indicators:**
|
|
49
|
+
|
|
50
|
+
| Colour | Text | Meaning |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| Yellow | authenticating… | Login in progress |
|
|
53
|
+
| Yellow | connecting… | Fetching live URI |
|
|
54
|
+
| Green | watching sid=… | Polling normally |
|
|
55
|
+
| Yellow | reconnecting… | Refreshing expired URI |
|
|
56
|
+
| Red | auth failed | Login failed — check credentials |
|
|
57
|
+
| Red | connect error | Initial connection failed |
|
|
58
|
+
| Grey | stopped | Node stopped / redeployed |
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install node-red-contrib-hoymiles-home
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or via the Node-RED palette manager: search for **hoymiles-home**.
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
1. Add a **hoymiles-watch** node to your flow
|
|
71
|
+
2. Open it and create a new **Hoymiles Credentials** config node
|
|
72
|
+
3. Enter your S-Miles Home **e-mail** and **password**
|
|
73
|
+
4. Enter your **Station ID**
|
|
74
|
+
5. Connect a **debug** node and click **Deploy**
|
|
75
|
+
|
|
76
|
+
## Local development with Docker
|
|
77
|
+
|
|
78
|
+
A Docker Compose setup is included for testing nodes without a full Node-RED installation.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# First start — builds the image and initialises the data volume
|
|
82
|
+
docker compose up --build
|
|
83
|
+
|
|
84
|
+
# After changing node code — restart is enough (nodes are bind-mounted)
|
|
85
|
+
docker compose restart
|
|
86
|
+
|
|
87
|
+
# After changing package.json or adding dependencies — rebuild
|
|
88
|
+
docker compose up --build
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Open **http://localhost:1880** — the **Hoymiles Test** flow is pre-loaded.
|
|
92
|
+
|
|
93
|
+
## Releasing
|
|
94
|
+
|
|
95
|
+
Versions are published to npm automatically via GitHub Actions when a tag is pushed.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Stable release
|
|
99
|
+
npm version minor # or patch / major
|
|
100
|
+
git push --follow-tags
|
|
101
|
+
|
|
102
|
+
# Dev release (published with npm tag "dev", not "latest")
|
|
103
|
+
npm version prerelease --preid=dev
|
|
104
|
+
git push --follow-tags
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Install a dev release:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install node-red-contrib-hoymiles-home@dev
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT © Robin Lenz
|
|
@@ -129,16 +129,24 @@ module.exports = function (RED) {
|
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
// doLogin resolves (never rejects) and updates node.client on success.
|
|
133
|
+
function doLogin(label = 'Login') {
|
|
134
|
+
const p = loginWithCredentials(email, password)
|
|
135
|
+
.then(token => {
|
|
136
|
+
node.client = new HoymilesClient(token);
|
|
137
|
+
node.log(`${label} successful for ${email}`);
|
|
138
|
+
})
|
|
139
|
+
.catch(err => {
|
|
140
|
+
node.error(`${label} failed: ${err.message}`);
|
|
141
|
+
});
|
|
142
|
+
node._loginReady = p;
|
|
143
|
+
return p;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// relogin() can be called by watch nodes when the token has expired.
|
|
147
|
+
node.relogin = () => doLogin('Re-login');
|
|
148
|
+
|
|
149
|
+
doLogin();
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
RED.nodes.registerType('hoymiles-config', HoymilesConfigNode, {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const MIN_INTERVAL_MS = 1000;
|
|
4
|
-
const DEFAULT_DELAY_MS = 5000;
|
|
1
|
+
const MIN_INTERVAL_MS = 1000;
|
|
2
|
+
const DEFAULT_DELAY_MS = 5000;
|
|
5
3
|
const URI_RETRY_DELAY_MS = 3000;
|
|
6
4
|
|
|
7
5
|
function sleep(ms) {
|
|
@@ -53,7 +51,17 @@ module.exports = function (RED) {
|
|
|
53
51
|
currentTimer = null;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
// Re-authenticates via the config node and returns the fresh client.
|
|
55
|
+
async function reauth() {
|
|
56
|
+
node.status({ fill: 'yellow', shape: 'ring', text: 're-authenticating…' });
|
|
57
|
+
await configNode.relogin();
|
|
58
|
+
return configNode.client;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function watchLoop() {
|
|
62
|
+
// client is mutable so re-auth can swap it in during the loop
|
|
63
|
+
let client = configNode.client;
|
|
64
|
+
|
|
57
65
|
node.status({ fill: 'yellow', shape: 'ring', text: 'connecting…' });
|
|
58
66
|
|
|
59
67
|
let uri, data;
|
|
@@ -79,15 +87,32 @@ module.exports = function (RED) {
|
|
|
79
87
|
try {
|
|
80
88
|
data = await fetchLive(client, uri);
|
|
81
89
|
|
|
82
|
-
// No "flow" field → URI
|
|
90
|
+
// No "flow" field → URI expired, get a fresh one
|
|
83
91
|
if (!data || !('flow' in data)) {
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
try {
|
|
93
|
+
uri = await getLiveUri(client);
|
|
94
|
+
data = await fetchLive(client, uri);
|
|
95
|
+
} catch (uriErr) {
|
|
96
|
+
// URI fetch failed → token likely expired, re-authenticate
|
|
97
|
+
node.warn(`URI refresh failed (${uriErr.message}) — re-authenticating`);
|
|
98
|
+
const fresh = await reauth();
|
|
99
|
+
if (fresh) {
|
|
100
|
+
client = fresh;
|
|
101
|
+
uri = await getLiveUri(client);
|
|
102
|
+
data = await fetchLive(client, uri);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
86
105
|
}
|
|
87
106
|
} catch (err) {
|
|
88
|
-
node.warn(`Poll error: ${err.message} —
|
|
89
|
-
node.status({ fill: 'yellow', shape: 'ring', text: '
|
|
90
|
-
try {
|
|
107
|
+
node.warn(`Poll error: ${err.message} — re-authenticating`);
|
|
108
|
+
node.status({ fill: 'yellow', shape: 'ring', text: 're-authenticating…' });
|
|
109
|
+
try {
|
|
110
|
+
const fresh = await reauth();
|
|
111
|
+
if (fresh) {
|
|
112
|
+
client = fresh;
|
|
113
|
+
uri = await getLiveUri(client);
|
|
114
|
+
}
|
|
115
|
+
} catch (_) { /* will retry next iteration */ }
|
|
91
116
|
await sleep(URI_RETRY_DELAY_MS);
|
|
92
117
|
node.status({ fill: 'green', shape: 'dot', text: `watching sid=${sid}` });
|
|
93
118
|
}
|
|
@@ -103,7 +128,6 @@ module.exports = function (RED) {
|
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
async function start() {
|
|
106
|
-
// Wait for the config node to finish logging in
|
|
107
131
|
if (configNode._loginReady) {
|
|
108
132
|
node.status({ fill: 'yellow', shape: 'ring', text: 'authenticating…' });
|
|
109
133
|
await configNode._loginReady;
|
|
@@ -111,11 +135,11 @@ module.exports = function (RED) {
|
|
|
111
135
|
|
|
112
136
|
if (!configNode.client) {
|
|
113
137
|
node.status({ fill: 'red', shape: 'ring', text: 'auth failed' });
|
|
114
|
-
return;
|
|
138
|
+
return;
|
|
115
139
|
}
|
|
116
140
|
|
|
117
141
|
running = true;
|
|
118
|
-
await watchLoop(
|
|
142
|
+
await watchLoop();
|
|
119
143
|
}
|
|
120
144
|
|
|
121
145
|
node.on('close', (done) => { stop(); done(); });
|
package/package.json
CHANGED