node-red-contrib-hoymiles-home 0.1.1-dev.0

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/.dockerignore ADDED
@@ -0,0 +1,3 @@
1
+ .git
2
+ node_modules
3
+ *.md
@@ -0,0 +1,35 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: '20'
19
+ registry-url: 'https://registry.npmjs.org'
20
+
21
+ - run: npm ci --production
22
+
23
+ - name: Determine npm tag
24
+ id: npm-tag
25
+ run: |
26
+ VERSION="${GITHUB_REF#refs/tags/v}"
27
+ if [[ "$VERSION" == *"-"* ]]; then
28
+ echo "tag=dev" >> $GITHUB_OUTPUT
29
+ else
30
+ echo "tag=latest" >> $GITHUB_OUTPUT
31
+ fi
32
+
33
+ - run: npm publish --access public --tag ${{ steps.npm-tag.outputs.tag }}
34
+ env:
35
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/Dockerfile ADDED
@@ -0,0 +1,23 @@
1
+ FROM nodered/node-red:latest
2
+
3
+ USER root
4
+
5
+ # Copy the custom node source
6
+ COPY --chown=node-red:node-red . /usr/src/node-red-contrib-hoymiles-home/
7
+
8
+ # Install the node's own dependencies (axios etc.)
9
+ RUN cd /usr/src/node-red-contrib-hoymiles-home && npm install --production --no-fund
10
+
11
+ # Bootstrap the Node-RED data dir and install the custom node into it.
12
+ # Named volumes are initialised from the image on first run, so npm packages
13
+ # installed here survive container restarts without a rebuild.
14
+ RUN cd /data && \
15
+ echo '{"name":"nodered-data","private":true}' > package.json && \
16
+ npm install /usr/src/node-red-contrib-hoymiles-home --no-fund --no-save && \
17
+ chown -R node-red:node-red /data
18
+
19
+ # Copy dev settings and sample flows (only applied when /data volume is fresh)
20
+ COPY --chown=node-red:node-red docker/settings.js /data/settings.js
21
+ COPY --chown=node-red:node-red docker/flows.json /data/flows.json
22
+
23
+ USER node-red
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pinguin45orga
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,43 @@
1
+ [
2
+ {
3
+ "id": "tab-hoymiles",
4
+ "type": "tab",
5
+ "label": "Hoymiles Test",
6
+ "disabled": false,
7
+ "info": "1. Open 'Live Power', set credentials (email + password) and station ID\n2. Deploy and watch the Debug sidebar"
8
+ },
9
+ {
10
+ "id": "cfg-hoymiles",
11
+ "type": "hoymiles-config",
12
+ "name": "Hoymiles Account"
13
+ },
14
+ {
15
+ "id": "node-watch",
16
+ "type": "hoymiles-watch",
17
+ "name": "Live Power",
18
+ "server": "cfg-hoymiles",
19
+ "sid": "",
20
+ "interval": "",
21
+ "z": "tab-hoymiles",
22
+ "x": 180,
23
+ "y": 200,
24
+ "wires": [["node-debug"]]
25
+ },
26
+ {
27
+ "id": "node-debug",
28
+ "type": "debug",
29
+ "name": "Power (W)",
30
+ "active": true,
31
+ "tosidebar": true,
32
+ "console": false,
33
+ "tostatus": true,
34
+ "complete": "payload",
35
+ "targetType": "msg",
36
+ "statusVal": "payload.pv",
37
+ "statusType": "msg",
38
+ "z": "tab-hoymiles",
39
+ "x": 380,
40
+ "y": 200,
41
+ "wires": []
42
+ }
43
+ ]
@@ -0,0 +1,35 @@
1
+ module.exports = {
2
+ // ── Server ────────────────────────────────────────────────────────────────
3
+ uiPort: process.env.PORT || 1880,
4
+ uiHost: '0.0.0.0',
5
+
6
+ // ── Auth ──────────────────────────────────────────────────────────────────
7
+ // No admin auth for local dev. Add adminAuth to lock down in production.
8
+ // adminAuth: { type: 'credentials', users: [{ username: 'admin', password: '...', permissions: '*' }] },
9
+
10
+ // ── Credential encryption ─────────────────────────────────────────────────
11
+ // Value comes from NODE_RED_CREDENTIAL_SECRET env var (set in docker-compose).
12
+ credentialSecret: process.env.NODE_RED_CREDENTIAL_SECRET || 'hoymiles-dev-secret',
13
+
14
+ // ── Flows ─────────────────────────────────────────────────────────────────
15
+ flowFile: 'flows.json',
16
+ flowFilePretty: true,
17
+
18
+ // ── Editor ────────────────────────────────────────────────────────────────
19
+ editorTheme: {
20
+ tours: false,
21
+ projects: { enabled: false },
22
+ },
23
+
24
+ // ── Logging ───────────────────────────────────────────────────────────────
25
+ logging: {
26
+ console: {
27
+ level: 'info',
28
+ metric: false,
29
+ audit: false,
30
+ },
31
+ },
32
+
33
+ // ── Node settings ─────────────────────────────────────────────────────────
34
+ functionExternalModules: true,
35
+ };
@@ -0,0 +1,19 @@
1
+ services:
2
+ node-red:
3
+ build: .
4
+ image: node-red-contrib-hoymiles-home-dev
5
+ ports:
6
+ - "1880:1880"
7
+ volumes:
8
+ # Persistent data (flows, credentials, settings).
9
+ # Initialised from the image on first run; survives container restarts.
10
+ - node-red-data:/data
11
+ # Live-reload: mount source directly so node code changes are reflected
12
+ # after a container restart (no full rebuild needed).
13
+ - ./nodes:/usr/src/node-red-contrib-hoymiles-home/nodes:ro
14
+ environment:
15
+ NODE_RED_CREDENTIAL_SECRET: "${NODE_RED_CREDENTIAL_SECRET:-hoymiles-dev-secret}"
16
+ restart: unless-stopped
17
+
18
+ volumes:
19
+ node-red-data:
@@ -0,0 +1,47 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('hoymiles-config', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: '' },
6
+ },
7
+ credentials: {
8
+ email: { type: 'text' },
9
+ password: { type: 'password' },
10
+ },
11
+ label: function () {
12
+ return this.name || this.credentials?.email || 'Hoymiles Account';
13
+ },
14
+ });
15
+ </script>
16
+
17
+ <script type="text/html" data-template-name="hoymiles-config">
18
+ <div class="form-row">
19
+ <label for="node-config-input-name">
20
+ <i class="fa fa-tag"></i> Name
21
+ </label>
22
+ <input type="text" id="node-config-input-name" placeholder="Hoymiles Account" />
23
+ </div>
24
+ <div class="form-row">
25
+ <label for="node-config-input-email">
26
+ <i class="fa fa-envelope"></i> E-Mail
27
+ </label>
28
+ <input type="email" id="node-config-input-email" placeholder="you@example.com" />
29
+ </div>
30
+ <div class="form-row">
31
+ <label for="node-config-input-password">
32
+ <i class="fa fa-lock"></i> Password
33
+ </label>
34
+ <input type="password" id="node-config-input-password" placeholder="S-Miles Home password" />
35
+ </div>
36
+ <div class="form-tips">
37
+ Use the same credentials as in the <b>S-Miles Home</b> app.
38
+ The node logs in automatically and refreshes the session token as needed.
39
+ </div>
40
+ </script>
41
+
42
+ <script type="text/html" data-help-name="hoymiles-config">
43
+ <p>Stores the S-Miles Home login credentials and manages the API session token.</p>
44
+ <p>On deploy the node performs the same two-step login as the S-Miles Home app
45
+ (pre-inspection → Argon2ID or MD5·SHA256 encoding → token).</p>
46
+ <p>The resulting token is shared with all connected <b>hoymiles-watch</b> nodes.</p>
47
+ </script>
@@ -0,0 +1,153 @@
1
+ const axios = require('axios');
2
+ const crypto = require('crypto');
3
+
4
+ const DOMAINS = {
5
+ eu: 'https://euapi.hoymiles.com',
6
+ 'eu-rt': 'https://eud0.hoymiles.com',
7
+ global: 'https://neapi.hoymiles.com',
8
+ in: 'https://indapi.hoymiles.com',
9
+ };
10
+
11
+ const EU_PREFIXES = ['/iam/', '/dict/', '/hlm/', '/csc/', '/tpa/', '/pvm-ext/', '/iuc/'];
12
+ const EU_RT_PATHS = new Set([
13
+ '/pvmc/api/0/station_data/real_g_c',
14
+ '/pvmc/api/0/station_data/eq_day_month_s_c',
15
+ '/pvmc/api/0/station_data/eq_month_year_s_c',
16
+ '/pvmc/api/0/station_data/eq_total_year_s_c',
17
+ '/pvmc/api/0/station_data/p_day_s_c',
18
+ ]);
19
+
20
+ function resolveDomain(path) {
21
+ if (EU_RT_PATHS.has(path)) return 'eu-rt';
22
+ if (EU_PREFIXES.some(p => path.startsWith(p))) return 'eu';
23
+ return 'global';
24
+ }
25
+
26
+ class APIError extends Error {
27
+ constructor(message, status) {
28
+ super(message);
29
+ this.status = status;
30
+ }
31
+ }
32
+
33
+ class HoymilesClient {
34
+ constructor(token, language = 'en_us') {
35
+ this.token = token;
36
+ this.language = language;
37
+ }
38
+
39
+ _headers() {
40
+ const h = {
41
+ 'Content-Type': 'application/json',
42
+ 'Charset': 'UTF-8',
43
+ 'language': this.language,
44
+ 'User-Agent': 'sma/ad/2.9.0/159/0',
45
+ };
46
+ if (this.token) h['Authorization'] = this.token;
47
+ return h;
48
+ }
49
+
50
+ async post(path, data = {}, domain = null) {
51
+ const base = DOMAINS[domain || resolveDomain(path)];
52
+ return this._doPost(`${base}${path}`, data);
53
+ }
54
+
55
+ async postUrl(url, data = {}) {
56
+ return this._doPost(url, data);
57
+ }
58
+
59
+ async _doPost(url, data) {
60
+ const resp = await axios.post(url, data, {
61
+ headers: this._headers(),
62
+ timeout: 30000,
63
+ maxRedirects: 5,
64
+ });
65
+ const result = resp.data;
66
+ if (!['0', '100'].includes(String(result.status))) {
67
+ throw new APIError(result.message || 'Unknown error', String(result.status ?? '???'));
68
+ }
69
+ return result.data;
70
+ }
71
+ }
72
+
73
+ // ── Password encoding (mirrors the CLI exactly) ────────────────────────────
74
+
75
+ function encodePasswordFallback(password) {
76
+ const md5 = crypto.createHash('md5').update(password, 'utf8').digest('hex');
77
+ const sha256 = crypto.createHash('sha256').update(password, 'utf8').digest();
78
+ return `${md5}.${sha256.toString('base64')}`;
79
+ }
80
+
81
+ async function encodePasswordArgon2(password, saltHex) {
82
+ // Lazy-load hash-wasm so the module doesn't fail if the package isn't installed yet
83
+ const { argon2id } = require('hash-wasm');
84
+ return argon2id({
85
+ password,
86
+ salt: Buffer.from(saltHex, 'hex'),
87
+ parallelism: 1,
88
+ iterations: 3,
89
+ memorySize: 32768,
90
+ hashLength: 32,
91
+ outputType: 'hex',
92
+ });
93
+ }
94
+
95
+ async function loginWithCredentials(email, password) {
96
+ const client = new HoymilesClient(); // no token needed for auth endpoints
97
+
98
+ // Step 1: pre-inspection — determine password encoding scheme
99
+ const preinsp = await client.post('/iam/pub/3/auth/pre-insp', { u: email });
100
+ const { v, a, n } = preinsp || {};
101
+
102
+ // Step 2: encode password (Argon2ID when v=3, MD5.Base64(SHA256) otherwise)
103
+ const ch = (v === 3 && a)
104
+ ? await encodePasswordArgon2(password, a)
105
+ : encodePasswordFallback(password);
106
+
107
+ // Step 3: login
108
+ const data = await client.post('/iam/pub/3/auth/login', { u: email, ch, n });
109
+ const token = data?.token;
110
+ if (!token) throw new Error('Login succeeded but no token in response');
111
+ return token;
112
+ }
113
+
114
+ // ── Node-RED registration ──────────────────────────────────────────────────
115
+
116
+ module.exports = function (RED) {
117
+ function HoymilesConfigNode(config) {
118
+ RED.nodes.createNode(this, config);
119
+ const node = this;
120
+
121
+ const email = node.credentials?.email || '';
122
+ const password = node.credentials?.password || '';
123
+
124
+ node.client = null;
125
+
126
+ if (!email || !password) {
127
+ node.error('Hoymiles config: email and password are required');
128
+ node._loginReady = Promise.resolve(); // resolve immediately so dependents don't hang
129
+ return;
130
+ }
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
+ });
142
+ }
143
+
144
+ RED.nodes.registerType('hoymiles-config', HoymilesConfigNode, {
145
+ credentials: {
146
+ email: { type: 'text' },
147
+ password: { type: 'password' },
148
+ },
149
+ });
150
+ };
151
+
152
+ module.exports.APIError = APIError;
153
+ module.exports.HoymilesClient = HoymilesClient;
@@ -0,0 +1,92 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('hoymiles-watch', {
3
+ category: 'input',
4
+ color: '#f0a500',
5
+ defaults: {
6
+ name: { value: '' },
7
+ server: { value: '', type: 'hoymiles-config', required: true },
8
+ sid: { value: '', required: true, validate: RED.validators.number() },
9
+ interval: { value: '' },
10
+ },
11
+ inputs: 0,
12
+ outputs: 1,
13
+ icon: 'bolt.svg',
14
+ label: function () {
15
+ return this.name || `Hoymiles Watch (${this.sid || '?'})`;
16
+ },
17
+ labelStyle: function () {
18
+ return this.name ? 'node_label_italic' : '';
19
+ },
20
+ outputLabels: ['live power data'],
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="hoymiles-watch">
25
+ <div class="form-row">
26
+ <label for="node-input-name">
27
+ <i class="fa fa-tag"></i> Name
28
+ </label>
29
+ <input type="text" id="node-input-name" placeholder="Hoymiles Watch" />
30
+ </div>
31
+ <div class="form-row">
32
+ <label for="node-input-server">
33
+ <i class="fa fa-key"></i> Credentials
34
+ </label>
35
+ <input type="text" id="node-input-server" />
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-input-sid">
39
+ <i class="fa fa-bolt"></i> Station ID
40
+ </label>
41
+ <input type="number" id="node-input-sid" placeholder="e.g. 123456" />
42
+ </div>
43
+ <div class="form-row">
44
+ <label for="node-input-interval">
45
+ <i class="fa fa-clock-o"></i> Interval (s)
46
+ </label>
47
+ <input type="number" id="node-input-interval" placeholder="leave empty to use server value (dly)" min="1" />
48
+ </div>
49
+ <div class="form-tips">
50
+ Leave <b>Interval</b> empty to use the <code>dly</code> field from each server response (matches the S-Miles Home app behaviour).
51
+ </div>
52
+ </script>
53
+
54
+ <script type="text/html" data-help-name="hoymiles-watch">
55
+ <p>Continuously polls the Hoymiles S-Miles Home live power API and emits a message for every new data point.</p>
56
+
57
+ <h3>Configuration</h3>
58
+ <dl class="message-properties">
59
+ <dt>Credentials</dt>
60
+ <dd>Select or create a <em>Hoymiles Credentials</em> config node with your API token.</dd>
61
+ <dt>Station ID</dt>
62
+ <dd>The numeric station ID (<code>sid</code>). Use the CLI (<code>hoymiles station list</code>) to find it.</dd>
63
+ <dt>Interval</dt>
64
+ <dd>Poll interval in seconds. Leave empty to use the <code>dly</code> value returned by the server (recommended — this mirrors the S-Miles Home app).</dd>
65
+ </dl>
66
+
67
+ <h3>Output message</h3>
68
+ <dl class="message-properties">
69
+ <dt>payload <span class="property-type">object</span></dt>
70
+ <dd>
71
+ Power values from the station:
72
+ <ul>
73
+ <li><code>payload.pv</code> – Solar generation (W)</li>
74
+ <li><code>payload.load</code> – Consumption (W)</li>
75
+ <li><code>payload.grid</code> – Grid exchange (W, negative = export)</li>
76
+ <li><code>payload.bat</code> – Battery power (W)</li>
77
+ </ul>
78
+ </dd>
79
+ <dt>topic <span class="property-type">string</span></dt>
80
+ <dd><code>hoymiles/watch/{sid}</code></dd>
81
+ <dt>sid <span class="property-type">number</span></dt>
82
+ <dd>The station ID for this message.</dd>
83
+ </dl>
84
+
85
+ <h3>Status indicators</h3>
86
+ <ul>
87
+ <li><span style="color:green">●</span> <b>watching</b> – Polling normally</li>
88
+ <li><span style="color:yellow">◌</span> <b>connecting / reconnecting</b> – Fetching a fresh live URI</li>
89
+ <li><span style="color:red">●</span> <b>error</b> – Initial connect failed; check credentials and station ID</li>
90
+ <li><span style="color:grey">◌</span> <b>stopped</b> – Node was disabled or redeployed</li>
91
+ </ul>
92
+ </script>
@@ -0,0 +1,130 @@
1
+ const { APIError } = require('../hoymiles-config/hoymiles-config');
2
+
3
+ const MIN_INTERVAL_MS = 1000;
4
+ const DEFAULT_DELAY_MS = 5000;
5
+ const URI_RETRY_DELAY_MS = 3000;
6
+
7
+ function sleep(ms) {
8
+ return new Promise(resolve => setTimeout(resolve, ms));
9
+ }
10
+
11
+ module.exports = function (RED) {
12
+ function HoymilesWatchNode(config) {
13
+ RED.nodes.createNode(this, config);
14
+ const node = this;
15
+
16
+ const configNode = RED.nodes.getNode(config.server);
17
+ if (!configNode) {
18
+ node.error('No Hoymiles credentials configured');
19
+ node.status({ fill: 'red', shape: 'ring', text: 'no credentials' });
20
+ return;
21
+ }
22
+
23
+ const sid = parseInt(config.sid, 10);
24
+ const intervalOverride = config.interval ? parseInt(config.interval, 10) * 1000 : null;
25
+
26
+ if (!sid) {
27
+ node.error('Station ID (sid) is required');
28
+ node.status({ fill: 'red', shape: 'ring', text: 'no station ID' });
29
+ return;
30
+ }
31
+
32
+ let running = false;
33
+ let stopResolve = null;
34
+ let currentTimer = null;
35
+
36
+ async function getLiveUri(client) {
37
+ const data = await client.post('/pvmc/api/0/station/get_sd_uri_c', { sid });
38
+ const uri = data?.uri;
39
+ if (!uri) throw new Error('No live URI returned from get_sd_uri_c');
40
+ return uri;
41
+ }
42
+
43
+ async function fetchLive(client, uri) {
44
+ return client.postUrl(uri, { sid, m: 0 });
45
+ }
46
+
47
+ async function interruptibleSleep(ms) {
48
+ await new Promise(resolve => {
49
+ stopResolve = resolve;
50
+ currentTimer = setTimeout(resolve, ms);
51
+ });
52
+ stopResolve = null;
53
+ currentTimer = null;
54
+ }
55
+
56
+ async function watchLoop(client) {
57
+ node.status({ fill: 'yellow', shape: 'ring', text: 'connecting…' });
58
+
59
+ let uri, data;
60
+ try {
61
+ uri = await getLiveUri(client);
62
+ data = await fetchLive(client, uri);
63
+ } catch (err) {
64
+ node.error(`Initial connect failed: ${err.message}`);
65
+ node.status({ fill: 'red', shape: 'dot', text: 'connect error' });
66
+ return;
67
+ }
68
+
69
+ node.status({ fill: 'green', shape: 'dot', text: `watching sid=${sid}` });
70
+
71
+ while (running) {
72
+ node.send({ payload: data?.power ?? {}, topic: `hoymiles/watch/${sid}`, sid });
73
+
74
+ const dly = intervalOverride ?? Math.max(MIN_INTERVAL_MS, data?.dly ?? DEFAULT_DELAY_MS);
75
+ await interruptibleSleep(dly);
76
+
77
+ if (!running) break;
78
+
79
+ try {
80
+ data = await fetchLive(client, uri);
81
+
82
+ // No "flow" field → URI has expired, refresh
83
+ if (!data || !('flow' in data)) {
84
+ uri = await getLiveUri(client);
85
+ data = await fetchLive(client, uri);
86
+ }
87
+ } 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 */ }
91
+ await sleep(URI_RETRY_DELAY_MS);
92
+ node.status({ fill: 'green', shape: 'dot', text: `watching sid=${sid}` });
93
+ }
94
+ }
95
+
96
+ node.status({ fill: 'grey', shape: 'ring', text: 'stopped' });
97
+ }
98
+
99
+ function stop() {
100
+ running = false;
101
+ if (currentTimer) { clearTimeout(currentTimer); currentTimer = null; }
102
+ if (stopResolve) { stopResolve(); stopResolve = null; }
103
+ }
104
+
105
+ async function start() {
106
+ // Wait for the config node to finish logging in
107
+ if (configNode._loginReady) {
108
+ node.status({ fill: 'yellow', shape: 'ring', text: 'authenticating…' });
109
+ await configNode._loginReady;
110
+ }
111
+
112
+ if (!configNode.client) {
113
+ node.status({ fill: 'red', shape: 'ring', text: 'auth failed' });
114
+ return; // error already logged by the config node
115
+ }
116
+
117
+ running = true;
118
+ await watchLoop(configNode.client);
119
+ }
120
+
121
+ node.on('close', (done) => { stop(); done(); });
122
+
123
+ start().catch(err => {
124
+ node.error(`Startup failed: ${err.message}`);
125
+ node.status({ fill: 'red', shape: 'dot', text: 'crashed' });
126
+ });
127
+ }
128
+
129
+ RED.nodes.registerType('hoymiles-watch', HoymilesWatchNode);
130
+ };
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-110 -160 540 832">
2
+ <path fill="white" d="M296 160H180.6l42.6-129.8C227.2 15 215.7 0 200 0H56C44 0 33.8 8.9 32.2 20.8l-32 240C-1.7 275.2 9.5 288 24 288h118.7L96.6 482.5c-3.6 15.2 8 29.5 23.3 29.5 8.4 0 16.4-4.4 20.8-12l176-304c9.3-15.9-2.2-36-20.7-36z"/>
3
+ </svg>
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "node-red-contrib-hoymiles-home",
3
+ "version": "0.1.1-dev.0",
4
+ "description": "Node-RED nodes for Hoymiles S-Miles Home API – live power data watch",
5
+ "author": "Robin Lenz <pinguin45@web.de>",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "node-red",
9
+ "hoymiles",
10
+ "solar",
11
+ "photovoltaic",
12
+ "smiles",
13
+ "smiles home"
14
+ ],
15
+ "node-red": {
16
+ "version": ">=3.0.0",
17
+ "nodes": {
18
+ "hoymiles-config": "nodes/hoymiles-config/hoymiles-config.js",
19
+ "hoymiles-watch": "nodes/hoymiles-watch/hoymiles-watch.js"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "axios": "^1.7.0",
24
+ "hash-wasm": "^4.11.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ }
29
+ }