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 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
- // loginReady resolves (never rejects) once the login attempt finishes.
133
- // Dependents await this, then check node.client for success/failure.
134
- node._loginReady = loginWithCredentials(email, password)
135
- .then(token => {
136
- node.client = new HoymilesClient(token);
137
- node.log(`Logged in as ${email}`);
138
- })
139
- .catch(err => {
140
- node.error(`Login failed: ${err.message}`);
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 { APIError } = require('../hoymiles-config/hoymiles-config');
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
- async function watchLoop(client) {
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 has expired, refresh
90
+ // No "flow" field → URI expired, get a fresh one
83
91
  if (!data || !('flow' in data)) {
84
- uri = await getLiveUri(client);
85
- data = await fetchLive(client, uri);
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} — refreshing URI`);
89
- node.status({ fill: 'yellow', shape: 'ring', text: 'reconnecting…' });
90
- try { uri = await getLiveUri(client); } catch (_) { /* ignore */ }
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; // error already logged by the config node
138
+ return;
115
139
  }
116
140
 
117
141
  running = true;
118
- await watchLoop(configNode.client);
142
+ await watchLoop();
119
143
  }
120
144
 
121
145
  node.on('close', (done) => { stop(); done(); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hoymiles-home",
3
- "version": "0.1.1-dev.0",
3
+ "version": "0.1.2",
4
4
  "description": "Node-RED nodes for Hoymiles S-Miles Home API – live power data watch",
5
5
  "author": "Robin Lenz <pinguin45@web.de>",
6
6
  "license": "MIT",