iobroker.advansol-optimizer 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TheBam1990
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.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # ioBroker AdvanSol Optimizer Adapter
2
+
3
+ ioBroker adapter for AdvanSol DCON-WIFI / MRO/MR optimizers connected through a TCP-to-RS485 bridge, for example a Waveshare ETH-to-RS485 adapter.
4
+
5
+ The adapter is based on the original ioBroker JavaScript script `Advinsol Optimierer2` and moves the logic into a real ioBroker adapter namespace.
6
+
7
+ ![System overview](docs/images/system-overview.svg)
8
+
9
+ ## Deutsch
10
+
11
+ ### Funktion
12
+
13
+ Der Adapter verbindet sich per TCP mit einer RS485-Bridge und spricht darueber mit dem AdvanSol Controller und den angeschlossenen Optimierern. Beim Start wird die Controller-Seriennummer gelesen, danach wird die Modulliste abgefragt. Fuer jedes gefundene Modul werden automatisch ioBroker-Datenpunkte angelegt.
14
+
15
+ Der Adapter kann:
16
+
17
+ - TCP-Verbindung zur RS485-Bridge aufbauen.
18
+ - Controller-Seriennummer lesen.
19
+ - Angeschlossene Optimierer automatisch erkennen.
20
+ - Modulwerte zyklisch abfragen.
21
+ - MOS je Optimierer ueber `module_X.switch` schalten.
22
+ - Polling in einem einstellbaren Nachtfenster aussetzen.
23
+ - Verbindungsstatus und Nachtmodus als Datenpunkte bereitstellen.
24
+
25
+ ### Typischer Aufbau
26
+
27
+ 1. ioBroker laeuft im lokalen Netzwerk.
28
+ 2. Eine TCP-RS485-Bridge ist per LAN/WLAN erreichbar.
29
+ 3. Die RS485-Seite der Bridge ist mit dem AdvanSol Controller verbunden.
30
+ 4. Der Controller kommuniziert mit den Optimierer-Modulen.
31
+
32
+ Empfohlene Bridge-Einstellung:
33
+
34
+ - Modus: TCP Server
35
+ - Port: passend zur Adaptereinstellung, Standard `502`
36
+ - Serielle Schnittstelle: wie vom AdvanSol Controller/RS485-Bus benoetigt
37
+ - RS485 A/B korrekt anschliessen
38
+ - Nur ein aktiver Master auf dem RS485-Bus
39
+
40
+ ### Adapter-Einstellungen
41
+
42
+ ![Adapter settings](docs/images/adapter-settings.svg)
43
+
44
+ | Einstellung | Bedeutung | Standard |
45
+ | --- | --- | --- |
46
+ | `Host` | IP-Adresse oder Hostname der TCP-RS485-Bridge | leer |
47
+ | `TCP port` | TCP-Port der Bridge | `502` |
48
+ | `Polling interval` | Zeit zwischen zwei Polling-Zyklen in Millisekunden | `10000` |
49
+ | `Request timeout` | Maximale Wartezeit auf eine Antwort | `5000` |
50
+ | `Switch retries` | Anzahl wiederholter MOS-Schaltbefehle | `3` |
51
+ | `Switch retry delay` | Pause zwischen Schaltversuchen | `4100` |
52
+ | `Night mode starts` | Stunde, ab der nicht mehr gepollt wird | `22` |
53
+ | `Night mode ends` | Stunde, ab der Polling wieder startet | `5` |
54
+
55
+ Das Nachtfenster verhindert unnoetige Fehlermeldungen, wenn die Optimierer nachts oder ohne PV-Spannung nicht antworten.
56
+
57
+ ### Datenpunkte
58
+
59
+ ![Object tree](docs/images/object-tree.svg)
60
+
61
+ Allgemeine Datenpunkte:
62
+
63
+ | Datenpunkt | Bedeutung |
64
+ | --- | --- |
65
+ | `info.connection` | Verbindung zur TCP-RS485-Bridge |
66
+ | `connection` | zusaetzlicher Verbindungsstatus |
67
+ | `controller.sn` | Seriennummer des Controllers |
68
+ | `module_count` | Anzahl erkannter Optimierer |
69
+ | `last_poll` | Zeitpunkt der letzten erfolgreichen Polling-Runde |
70
+ | `night_mode` | Adapter hat Nachtmodus erkannt |
71
+
72
+ Pro Optimierer wird ein Kanal `module_1`, `module_2`, `module_3` usw. angelegt.
73
+
74
+ | Datenpunkt | Bedeutung | Einheit |
75
+ | --- | --- | --- |
76
+ | `module_X.sn` | Seriennummer des Optimierers | |
77
+ | `module_X.switch` | MOS Ein/Aus, beschreibbar | |
78
+ | `module_X.mos` | MOS Status, `0` aus und `1` ein | |
79
+ | `module_X.software` | Softwareversion | |
80
+ | `module_X.hardware` | Hardwareversion | |
81
+ | `module_X.output_voltage` | Ausgangsspannung | V |
82
+ | `module_X.output_current` | Ausgangsstrom | A |
83
+ | `module_X.input_voltage` | Eingangsspannung | V |
84
+ | `module_X.input_current` | Eingangsstrom | A |
85
+ | `module_X.power` | Leistung | W |
86
+ | `module_X.energy` | Gesamtertrag | kWh |
87
+ | `module_X.temperature` | Temperatur | degC |
88
+ | `module_X.raw` | rohe Antwort als Hex-String | |
89
+ | `module_X.last_update` | letzte Aktualisierung des Moduls | |
90
+
91
+ ### Schalten der Optimierer
92
+
93
+ Der Datenpunkt `module_X.switch` ist beschreibbar. Wird er auf `true` gesetzt, sendet der Adapter einen MOS-Ein-Befehl fuer die Seriennummer dieses Moduls. Wird er auf `false` gesetzt, sendet der Adapter den MOS-Aus-Befehl.
94
+
95
+ Der Adapter wiederholt den Schaltbefehl entsprechend `Switch retries`. Zwischen den Versuchen wartet er `Switch retry delay`. Das ist bewusst so umgesetzt, weil RS485/TCP-Wandler und Optimierer nicht immer jede Telegrammfolge sofort bestaetigen.
96
+
97
+ ### Installation
98
+
99
+ Aus einem lokalen Paket:
100
+
101
+ ```bash
102
+ iobroker url /path/to/iobroker.advansol-optimizer-0.1.2.tgz
103
+ ```
104
+
105
+ Aus einem Projektordner:
106
+
107
+ ```bash
108
+ iobroker url /root/iobroker.advansol-optimizer
109
+ ```
110
+
111
+ Nach der Installation eine Instanz anlegen, Host/Port der Bridge eintragen und die Instanz starten.
112
+
113
+ ### Fehlersuche
114
+
115
+ - Keine Verbindung: IP-Adresse, Port und TCP-Server-Modus der Bridge pruefen.
116
+ - `TCP connect timeout`: Bridge nicht erreichbar oder falscher Port.
117
+ - Keine Module erkannt: RS485 A/B pruefen, Controller eingeschaltet, PV-Seite versorgt.
118
+ - Tagsueber keine Antworten: RS485-Parameter und Verdrahtung pruefen.
119
+ - Nachts keine Antworten: normal, wenn die Optimierer ohne PV-Spannung schlafen. Nachtfenster passend einstellen.
120
+ - Schalten funktioniert nicht: Seriennummer muss gelesen sein, Modul muss antworten, `Switch retries` ggf. erhoehen.
121
+ - Mehrere Systeme am Bus: sicherstellen, dass nicht mehrere Master gleichzeitig Telegramme senden.
122
+
123
+ ## English
124
+
125
+ ### Purpose
126
+
127
+ The adapter connects to a TCP-to-RS485 bridge and communicates with the AdvanSol controller and connected optimizer modules. On startup it reads the controller serial number and then discovers the module list. For every discovered module, ioBroker states are created automatically.
128
+
129
+ The adapter can:
130
+
131
+ - Connect to a TCP RS485 bridge.
132
+ - Read the controller serial number.
133
+ - Discover connected optimizer modules.
134
+ - Poll module values cyclically.
135
+ - Switch each optimizer MOS through `module_X.switch`.
136
+ - Skip polling during a configurable night window.
137
+ - Expose connection state and night mode states.
138
+
139
+ ### Typical Setup
140
+
141
+ 1. ioBroker runs in the local network.
142
+ 2. A TCP-RS485 bridge is reachable via LAN/Wi-Fi.
143
+ 3. The RS485 side of the bridge is connected to the AdvanSol controller.
144
+ 4. The controller communicates with the optimizer modules.
145
+
146
+ Recommended bridge configuration:
147
+
148
+ - Mode: TCP server
149
+ - Port: same as configured in the adapter, default `502`
150
+ - Serial settings: matching the AdvanSol controller/RS485 bus
151
+ - RS485 A/B connected correctly
152
+ - Only one active master on the RS485 bus
153
+
154
+ ### Adapter Settings
155
+
156
+ | Setting | Meaning | Default |
157
+ | --- | --- | --- |
158
+ | `Host` | IP address or host name of the TCP-RS485 bridge | empty |
159
+ | `TCP port` | TCP port of the bridge | `502` |
160
+ | `Polling interval` | Time between polling cycles in milliseconds | `10000` |
161
+ | `Request timeout` | Maximum wait time for a response | `5000` |
162
+ | `Switch retries` | Number of repeated MOS switch commands | `3` |
163
+ | `Switch retry delay` | Delay between switch attempts | `4100` |
164
+ | `Night mode starts` | Hour where polling is skipped | `22` |
165
+ | `Night mode ends` | Hour where polling resumes | `5` |
166
+
167
+ The night window avoids unnecessary errors when optimizers do not respond at night or without PV-side voltage.
168
+
169
+ ### States
170
+
171
+ General states:
172
+
173
+ | State | Meaning |
174
+ | --- | --- |
175
+ | `info.connection` | Connection to the TCP-RS485 bridge |
176
+ | `connection` | Additional connection state |
177
+ | `controller.sn` | Controller serial number |
178
+ | `module_count` | Number of discovered optimizers |
179
+ | `last_poll` | Time of the last successful poll cycle |
180
+ | `night_mode` | Adapter detected night mode |
181
+
182
+ Each optimizer gets a channel named `module_1`, `module_2`, `module_3` and so on.
183
+
184
+ | State | Meaning | Unit |
185
+ | --- | --- | --- |
186
+ | `module_X.sn` | Optimizer serial number | |
187
+ | `module_X.switch` | MOS on/off, writable | |
188
+ | `module_X.mos` | MOS status, `0` off and `1` on | |
189
+ | `module_X.software` | Software version | |
190
+ | `module_X.hardware` | Hardware version | |
191
+ | `module_X.output_voltage` | Output voltage | V |
192
+ | `module_X.output_current` | Output current | A |
193
+ | `module_X.input_voltage` | Input voltage | V |
194
+ | `module_X.input_current` | Input current | A |
195
+ | `module_X.power` | Power | W |
196
+ | `module_X.energy` | Total energy | kWh |
197
+ | `module_X.temperature` | Temperature | degC |
198
+ | `module_X.raw` | Raw response as hex string | |
199
+ | `module_X.last_update` | Last module update | |
200
+
201
+ ### Switching Optimizers
202
+
203
+ The state `module_X.switch` is writable. Setting it to `true` sends the MOS-on command for the module serial number. Setting it to `false` sends the MOS-off command.
204
+
205
+ The adapter repeats the command according to `Switch retries` and waits `Switch retry delay` between attempts. This is intentional because TCP-RS485 converters and optimizer modules may not acknowledge every command immediately.
206
+
207
+ ### Installation
208
+
209
+ From a local package:
210
+
211
+ ```bash
212
+ iobroker url /path/to/iobroker.advansol-optimizer-0.1.2.tgz
213
+ ```
214
+
215
+ From a project folder:
216
+
217
+ ```bash
218
+ iobroker url /root/iobroker.advansol-optimizer
219
+ ```
220
+
221
+ After installation, create an instance, enter the bridge host/port and start the instance.
222
+
223
+ ### Troubleshooting
224
+
225
+ - No connection: check IP address, port and TCP server mode of the bridge.
226
+ - `TCP connect timeout`: bridge is not reachable or the port is wrong.
227
+ - No modules discovered: check RS485 A/B, controller power and PV-side supply.
228
+ - No daytime responses: check RS485 parameters and wiring.
229
+ - No nighttime responses: usually normal if optimizers sleep without PV voltage. Adjust the night window.
230
+ - Switching does not work: serial number must be known, module must respond, increase `Switch retries` if needed.
231
+ - Multiple systems on the bus: make sure there is not more than one active master sending frames.
232
+
233
+ ## Changelog
234
+
235
+ ### 0.1.2
236
+
237
+ - Updated package metadata for ioBroker adapter checker compatibility.
238
+ - Added repository, testing, license information, tier and extended translations.
239
+
240
+ ### 0.1.1
241
+
242
+ - Added adapter icon and localized admin configuration labels.
243
+
244
+ ### 0.1.0
245
+
246
+ - Initial adapter version based on the existing ioBroker JavaScript optimizer script.
@@ -0,0 +1,19 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="AdvanSol Optimizer">
2
+ <defs>
3
+ <linearGradient id="sky" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#1d4ed8"/>
5
+ <stop offset="1" stop-color="#0f766e"/>
6
+ </linearGradient>
7
+ <linearGradient id="panel" x1="24" y1="50" x2="101" y2="94" gradientUnits="userSpaceOnUse">
8
+ <stop offset="0" stop-color="#164e63"/>
9
+ <stop offset="1" stop-color="#0f172a"/>
10
+ </linearGradient>
11
+ </defs>
12
+ <rect width="128" height="128" rx="24" fill="url(#sky)"/>
13
+ <circle cx="91" cy="34" r="17" fill="#facc15"/>
14
+ <path d="M91 9v11M91 48v11M66 34h11M105 34h11M73 16l8 8M101 44l8 8M109 16l-8 8M81 44l-8 8" stroke="#fde68a" stroke-width="5" stroke-linecap="round"/>
15
+ <path d="M22 52h65c5 0 9 4 9 9v33H31c-5 0-9-4-9-9z" fill="url(#panel)"/>
16
+ <path d="M31 61h56M27 72h67M24 83h70M44 54l-13 39M62 53 49 94M80 54 67 94" stroke="#38bdf8" stroke-width="3" stroke-linecap="round" opacity=".9"/>
17
+ <path d="M79 84h18l-7-12h17L86 107l6-17H76z" fill="#facc15" stroke="#fff7ad" stroke-width="2" stroke-linejoin="round"/>
18
+ <path d="M22 102h84" stroke="#d1fae5" stroke-width="7" stroke-linecap="round" opacity=".9"/>
19
+ </svg>
@@ -0,0 +1,121 @@
1
+ {
2
+ "type": "panel",
3
+ "items": {
4
+ "host": {
5
+ "type": "text",
6
+ "label": {
7
+ "en": "Host",
8
+ "de": "Host"
9
+ },
10
+ "help": {
11
+ "en": "IP address or host name of the TCP RS485 bridge.",
12
+ "de": "IP-Adresse oder Hostname der TCP-RS485-Bridge."
13
+ },
14
+ "newLine": true,
15
+ "xs": 12,
16
+ "sm": 6
17
+ },
18
+ "tcpPort": {
19
+ "type": "number",
20
+ "label": {
21
+ "en": "TCP port",
22
+ "de": "TCP-Port"
23
+ },
24
+ "help": {
25
+ "en": "TCP port of the Waveshare ETH-to-RS485 bridge.",
26
+ "de": "TCP-Port der Waveshare ETH-to-RS485-Bridge."
27
+ },
28
+ "min": 1,
29
+ "max": 65535,
30
+ "xs": 12,
31
+ "sm": 6
32
+ },
33
+ "pollMs": {
34
+ "type": "number",
35
+ "label": {
36
+ "en": "Polling interval",
37
+ "de": "Abfrageintervall"
38
+ },
39
+ "help": {
40
+ "en": "Time between polling cycles in milliseconds.",
41
+ "de": "Zeit zwischen zwei Abfragen in Millisekunden."
42
+ },
43
+ "min": 1000,
44
+ "xs": 12,
45
+ "sm": 6
46
+ },
47
+ "requestTimeoutMs": {
48
+ "type": "number",
49
+ "label": {
50
+ "en": "Request timeout",
51
+ "de": "Anfrage-Timeout"
52
+ },
53
+ "help": {
54
+ "en": "Maximum wait time for a device response in milliseconds.",
55
+ "de": "Maximale Wartezeit auf eine Geräteantwort in Millisekunden."
56
+ },
57
+ "min": 500,
58
+ "xs": 12,
59
+ "sm": 6
60
+ },
61
+ "switchRetries": {
62
+ "type": "number",
63
+ "label": {
64
+ "en": "Switch retries",
65
+ "de": "Schaltversuche"
66
+ },
67
+ "help": {
68
+ "en": "Number of repeated switch commands.",
69
+ "de": "Anzahl der wiederholten Schaltbefehle."
70
+ },
71
+ "min": 1,
72
+ "max": 20,
73
+ "xs": 12,
74
+ "sm": 6
75
+ },
76
+ "switchRetryDelayMs": {
77
+ "type": "number",
78
+ "label": {
79
+ "en": "Switch retry delay",
80
+ "de": "Pause zwischen Schaltversuchen"
81
+ },
82
+ "help": {
83
+ "en": "Delay between switch retries in milliseconds.",
84
+ "de": "Pause zwischen Schaltversuchen in Millisekunden."
85
+ },
86
+ "min": 0,
87
+ "xs": 12,
88
+ "sm": 6
89
+ },
90
+ "nightStart": {
91
+ "type": "number",
92
+ "label": {
93
+ "en": "Night mode starts",
94
+ "de": "Nachtmodus beginnt"
95
+ },
96
+ "help": {
97
+ "en": "Hour when polling is skipped, from 0 to 23.",
98
+ "de": "Stunde, ab der nicht mehr abgefragt wird, von 0 bis 23."
99
+ },
100
+ "min": 0,
101
+ "max": 23,
102
+ "xs": 12,
103
+ "sm": 6
104
+ },
105
+ "nightEnd": {
106
+ "type": "number",
107
+ "label": {
108
+ "en": "Night mode ends",
109
+ "de": "Nachtmodus endet"
110
+ },
111
+ "help": {
112
+ "en": "Hour when polling resumes, from 0 to 23.",
113
+ "de": "Stunde, ab der wieder abgefragt wird, von 0 bis 23."
114
+ },
115
+ "min": 0,
116
+ "max": 23,
117
+ "xs": 12,
118
+ "sm": 6
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,29 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="980" height="540" viewBox="0 0 980 540" role="img" aria-label="AdvanSol ioBroker adapter settings">
2
+ <rect width="980" height="540" fill="#f6f8fb"/>
3
+ <rect x="42" y="34" width="896" height="470" rx="8" fill="#fff" stroke="#cfd7e3"/>
4
+ <text x="72" y="76" font-family="Arial, sans-serif" font-size="25" font-weight="700" fill="#24384d">AdvanSol Optimierer - Adapter Einstellungen</text>
5
+ <text x="72" y="116" font-family="Arial, sans-serif" font-size="15" fill="#506274">TCP-Verbindung zur RS485-Bridge und Polling-Verhalten</text>
6
+ <text x="82" y="168" font-family="Arial, sans-serif" font-size="15" fill="#4e5f70">Host / IP der Bridge</text>
7
+ <rect x="82" y="182" width="330" height="40" rx="4" fill="#fff" stroke="#b9c6d3"/>
8
+ <text x="102" y="208" font-family="Arial, sans-serif" font-size="16" fill="#24384d">bridge.local</text>
9
+ <text x="458" y="168" font-family="Arial, sans-serif" font-size="15" fill="#4e5f70">TCP-Port</text>
10
+ <rect x="458" y="182" width="160" height="40" rx="4" fill="#fff" stroke="#b9c6d3"/>
11
+ <text x="478" y="208" font-family="Arial, sans-serif" font-size="16" fill="#24384d">502</text>
12
+ <text x="82" y="262" font-family="Arial, sans-serif" font-size="15" fill="#4e5f70">Abfrageintervall</text>
13
+ <rect x="82" y="276" width="250" height="40" rx="4" fill="#fff" stroke="#b9c6d3"/>
14
+ <text x="102" y="302" font-family="Arial, sans-serif" font-size="16" fill="#24384d">10000 ms</text>
15
+ <text x="378" y="262" font-family="Arial, sans-serif" font-size="15" fill="#4e5f70">Anfrage-Timeout</text>
16
+ <rect x="378" y="276" width="250" height="40" rx="4" fill="#fff" stroke="#b9c6d3"/>
17
+ <text x="398" y="302" font-family="Arial, sans-serif" font-size="16" fill="#24384d">5000 ms</text>
18
+ <text x="82" y="356" font-family="Arial, sans-serif" font-size="15" fill="#4e5f70">Schaltversuche</text>
19
+ <rect x="82" y="370" width="250" height="40" rx="4" fill="#fff" stroke="#b9c6d3"/>
20
+ <text x="102" y="396" font-family="Arial, sans-serif" font-size="16" fill="#24384d">3</text>
21
+ <text x="378" y="356" font-family="Arial, sans-serif" font-size="15" fill="#4e5f70">Pause zwischen Schaltversuchen</text>
22
+ <rect x="378" y="370" width="250" height="40" rx="4" fill="#fff" stroke="#b9c6d3"/>
23
+ <text x="398" y="396" font-family="Arial, sans-serif" font-size="16" fill="#24384d">4100 ms</text>
24
+ <text x="690" y="262" font-family="Arial, sans-serif" font-size="15" fill="#4e5f70">Nachtmodus</text>
25
+ <rect x="690" y="276" width="160" height="40" rx="4" fill="#fff" stroke="#b9c6d3"/>
26
+ <text x="710" y="302" font-family="Arial, sans-serif" font-size="16" fill="#24384d">22 bis 5 Uhr</text>
27
+ <rect x="690" y="370" width="180" height="40" rx="4" fill="#2d7dd2"/>
28
+ <text x="735" y="396" font-family="Arial, sans-serif" font-size="16" font-weight="700" fill="#fff">Speichern</text>
29
+ </svg>
@@ -0,0 +1,36 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="980" height="560" viewBox="0 0 980 560" role="img" aria-label="AdvanSol ioBroker object tree">
2
+ <rect width="980" height="560" fill="#f6f8fb"/>
3
+ <rect x="42" y="34" width="896" height="488" rx="8" fill="#fff" stroke="#cfd7e3"/>
4
+ <text x="72" y="76" font-family="Arial, sans-serif" font-size="25" font-weight="700" fill="#24384d">Objektbaum</text>
5
+ <g font-family="Arial, sans-serif" font-size="17" fill="#26384a">
6
+ <text x="92" y="126">advansol-optimizer.0</text>
7
+ <text x="132" y="166">info.connection</text>
8
+ <text x="132" y="196">controller.sn</text>
9
+ <text x="132" y="226">module_count</text>
10
+ <text x="132" y="256">last_poll</text>
11
+ <text x="132" y="286">night_mode</text>
12
+ <text x="132" y="336">module_1.sn</text>
13
+ <text x="132" y="366">module_1.switch</text>
14
+ <text x="132" y="396">module_1.output_voltage / output_current</text>
15
+ <text x="132" y="426">module_1.input_voltage / input_current</text>
16
+ <text x="132" y="456">module_1.power / energy / temperature</text>
17
+ <text x="582" y="336">module_2.*</text>
18
+ <text x="582" y="366">module_3.*</text>
19
+ <text x="582" y="396">module_n.*</text>
20
+ </g>
21
+ <g stroke="#9aabbc" stroke-width="2" fill="none">
22
+ <path d="M106 134v326"/>
23
+ <path d="M110 160h16"/>
24
+ <path d="M110 190h16"/>
25
+ <path d="M110 220h16"/>
26
+ <path d="M110 250h16"/>
27
+ <path d="M110 280h16"/>
28
+ <path d="M110 330h16"/>
29
+ <path d="M110 360h16"/>
30
+ <path d="M110 390h16"/>
31
+ <path d="M110 420h16"/>
32
+ <path d="M110 450h16"/>
33
+ </g>
34
+ <rect x="562" y="304" width="250" height="120" rx="6" fill="#eef8f1" stroke="#b9d8c0"/>
35
+ <text x="590" y="458" font-family="Arial, sans-serif" font-size="15" fill="#506274">Jedes gefundene Modul bekommt denselben Satz Datenpunkte.</text>
36
+ </svg>
@@ -0,0 +1,33 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="980" height="430" viewBox="0 0 980 430" role="img" aria-label="AdvanSol ioBroker system overview">
2
+ <rect width="980" height="430" fill="#f6f8fb"/>
3
+ <rect x="42" y="48" width="180" height="100" rx="8" fill="#fff" stroke="#c9d4df"/>
4
+ <text x="82" y="88" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#24384d">ioBroker</text>
5
+ <text x="64" y="118" font-family="Arial, sans-serif" font-size="15" fill="#506274">AdvanSol Adapter</text>
6
+ <rect x="300" y="48" width="230" height="100" rx="8" fill="#fff" stroke="#c9d4df"/>
7
+ <text x="346" y="88" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#24384d">TCP-RS485 Bridge</text>
8
+ <text x="340" y="118" font-family="Arial, sans-serif" font-size="15" fill="#506274">z. B. Waveshare ETH</text>
9
+ <rect x="622" y="48" width="300" height="100" rx="8" fill="#fff" stroke="#c9d4df"/>
10
+ <text x="672" y="88" font-family="Arial, sans-serif" font-size="20" font-weight="700" fill="#24384d">AdvanSol Controller</text>
11
+ <text x="676" y="118" font-family="Arial, sans-serif" font-size="15" fill="#506274">DCON-WIFI / MRO/MR Bus</text>
12
+ <path d="M222 98h78" stroke="#2d7dd2" stroke-width="4"/>
13
+ <path d="M530 98h92" stroke="#2d7dd2" stroke-width="4"/>
14
+ <text x="242" y="82" font-family="Arial, sans-serif" font-size="14" fill="#2d7dd2">TCP/IP</text>
15
+ <text x="548" y="82" font-family="Arial, sans-serif" font-size="14" fill="#2d7dd2">RS485</text>
16
+ <rect x="108" y="240" width="150" height="76" rx="8" fill="#eef8f1" stroke="#b9d8c0"/>
17
+ <text x="134" y="272" font-family="Arial, sans-serif" font-size="17" font-weight="700" fill="#254a32">Modul 1</text>
18
+ <text x="132" y="296" font-family="Arial, sans-serif" font-size="14" fill="#506274">U / I / P / kWh</text>
19
+ <rect x="314" y="240" width="150" height="76" rx="8" fill="#eef8f1" stroke="#b9d8c0"/>
20
+ <text x="340" y="272" font-family="Arial, sans-serif" font-size="17" font-weight="700" fill="#254a32">Modul 2</text>
21
+ <text x="338" y="296" font-family="Arial, sans-serif" font-size="14" fill="#506274">MOS Schalter</text>
22
+ <rect x="520" y="240" width="150" height="76" rx="8" fill="#eef8f1" stroke="#b9d8c0"/>
23
+ <text x="546" y="272" font-family="Arial, sans-serif" font-size="17" font-weight="700" fill="#254a32">Modul 3</text>
24
+ <text x="544" y="296" font-family="Arial, sans-serif" font-size="14" fill="#506274">Temperatur</text>
25
+ <rect x="726" y="240" width="150" height="76" rx="8" fill="#eef8f1" stroke="#b9d8c0"/>
26
+ <text x="752" y="272" font-family="Arial, sans-serif" font-size="17" font-weight="700" fill="#254a32">Modul n</text>
27
+ <text x="750" y="296" font-family="Arial, sans-serif" font-size="14" fill="#506274">Auto-Erkennung</text>
28
+ <path d="M772 148v60H183v32" stroke="#7a8b9d" stroke-width="3" fill="none"/>
29
+ <path d="M389 208v32" stroke="#7a8b9d" stroke-width="3"/>
30
+ <path d="M595 208v32" stroke="#7a8b9d" stroke-width="3"/>
31
+ <path d="M801 208v32" stroke="#7a8b9d" stroke-width="3"/>
32
+ <text x="354" y="384" font-family="Arial, sans-serif" font-size="16" fill="#506274">Der Adapter liest seriell ueber TCP und legt gefundene Optimierer automatisch als ioBroker-Objekte an.</text>
33
+ </svg>
@@ -0,0 +1,98 @@
1
+ {
2
+ "common": {
3
+ "name": "advansol-optimizer",
4
+ "version": "0.1.2",
5
+ "news": {
6
+ "0.1.2": {
7
+ "en": "Updates package metadata for ioBroker adapter checker compatibility",
8
+ "de": "Aktualisiert Paket-Metadaten fuer die Kompatibilitaet mit dem ioBroker Adapter-Checker",
9
+ "ru": "Updates package metadata for ioBroker adapter checker compatibility",
10
+ "pt": "Updates package metadata for ioBroker adapter checker compatibility",
11
+ "nl": "Updates package metadata for ioBroker adapter checker compatibility",
12
+ "fr": "Updates package metadata for ioBroker adapter checker compatibility",
13
+ "it": "Updates package metadata for ioBroker adapter checker compatibility",
14
+ "es": "Updates package metadata for ioBroker adapter checker compatibility",
15
+ "pl": "Updates package metadata for ioBroker adapter checker compatibility",
16
+ "uk": "Updates package metadata for ioBroker adapter checker compatibility",
17
+ "zh-cn": "Updates package metadata for ioBroker adapter checker compatibility"
18
+ }
19
+ },
20
+ "titleLang": {
21
+ "en": "AdvanSol Optimizer",
22
+ "de": "AdvanSol Optimierer",
23
+ "ru": "AdvanSol Optimizer",
24
+ "pt": "AdvanSol Optimizer",
25
+ "nl": "AdvanSol Optimizer",
26
+ "fr": "AdvanSol Optimizer",
27
+ "it": "AdvanSol Optimizer",
28
+ "es": "AdvanSol Optimizer",
29
+ "pl": "AdvanSol Optimizer",
30
+ "uk": "AdvanSol Optimizer",
31
+ "zh-cn": "AdvanSol Optimizer"
32
+ },
33
+ "desc": {
34
+ "en": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge.",
35
+ "de": "Liest und steuert AdvanSol DCON-WIFI / MRO/MR Optimierer ueber eine TCP-RS485-Bridge.",
36
+ "ru": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge.",
37
+ "pt": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge.",
38
+ "nl": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge.",
39
+ "fr": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge.",
40
+ "it": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge.",
41
+ "es": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge.",
42
+ "pl": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge.",
43
+ "uk": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge.",
44
+ "zh-cn": "Reads and controls AdvanSol DCON-WIFI / MRO/MR optimizers via a TCP RS485 bridge."
45
+ },
46
+ "authors": [
47
+ "TheBam1990"
48
+ ],
49
+ "keywords": [
50
+ "advansol",
51
+ "optimizer",
52
+ "solar",
53
+ "rs485",
54
+ "tcp"
55
+ ],
56
+ "licenseInformation": {
57
+ "type": "free",
58
+ "license": "MIT"
59
+ },
60
+ "platform": "Javascript/Node.js",
61
+ "icon": "advansol-optimizer.svg",
62
+ "enabled": true,
63
+ "extIcon": "https://raw.githubusercontent.com/TheBam1990/ioBroker.advansol-optimizer/main/admin/advansol-optimizer.svg",
64
+ "readme": "https://github.com/TheBam1990/ioBroker.advansol-optimizer/blob/main/README.md",
65
+ "loglevel": "info",
66
+ "mode": "daemon",
67
+ "type": "energy",
68
+ "tier": 3,
69
+ "compact": true,
70
+ "connectionType": "local",
71
+ "dataSource": "poll",
72
+ "adminUI": {
73
+ "config": "json"
74
+ },
75
+ "dependencies": [
76
+ {
77
+ "js-controller": ">=5.0.19"
78
+ }
79
+ ],
80
+ "globalDependencies": [
81
+ {
82
+ "admin": ">=6.0.0"
83
+ }
84
+ ]
85
+ },
86
+ "native": {
87
+ "host": "",
88
+ "tcpPort": 502,
89
+ "pollMs": 10000,
90
+ "requestTimeoutMs": 5000,
91
+ "switchRetries": 3,
92
+ "switchRetryDelayMs": 4100,
93
+ "nightStart": 22,
94
+ "nightEnd": 5
95
+ },
96
+ "objects": [],
97
+ "instanceObjects": []
98
+ }
package/main.js ADDED
@@ -0,0 +1,534 @@
1
+ "use strict";
2
+
3
+ const net = require("net");
4
+ const utils = require("@iobroker/adapter-core");
5
+
6
+ class AdvansolOptimizer extends utils.Adapter {
7
+ constructor(options = {}) {
8
+ super({
9
+ ...options,
10
+ name: "advansol-optimizer",
11
+ });
12
+
13
+ this.socket = null;
14
+ this.connected = false;
15
+ this.requestQueue = Promise.resolve();
16
+ this.modules = [];
17
+ this.moduleErrorCount = {};
18
+ this.nightMode = false;
19
+ this.nightCheckCounter = 0;
20
+ this.initialized = false;
21
+ this.polling = false;
22
+ this.pollTimer = null;
23
+ this.switchSubscriptions = new Set();
24
+
25
+ this.nightFailLimit = 5;
26
+ this.nightCheckEvery = 180;
27
+ this.moduleReadDelayMs = 900;
28
+ this.finishDelayMs = 350;
29
+
30
+ this.on("ready", this.onReady.bind(this));
31
+ this.on("stateChange", this.onStateChange.bind(this));
32
+ this.on("unload", this.onUnload.bind(this));
33
+ }
34
+
35
+ get cfg() {
36
+ return {
37
+ host: String(this.config.host || "").trim(),
38
+ tcpPort: Number(this.config.tcpPort || 502),
39
+ pollMs: Number(this.config.pollMs || 10000),
40
+ requestTimeoutMs: Number(this.config.requestTimeoutMs || 5000),
41
+ switchRetries: Number(this.config.switchRetries || 3),
42
+ switchRetryDelayMs: Number(this.config.switchRetryDelayMs || 4100),
43
+ nightStart: Number(this.config.nightStart ?? 22),
44
+ nightEnd: Number(this.config.nightEnd ?? 5),
45
+ };
46
+ }
47
+
48
+ async onReady() {
49
+ await this.initBaseStates();
50
+ await this.setStateAsync("info.connection", false, true);
51
+
52
+ try {
53
+ if (!this.cfg.host) {
54
+ this.log.warn("No TCP RS485 bridge host configured. Please enter the bridge IP address or host name in the adapter settings.");
55
+ return;
56
+ }
57
+
58
+ await this.connectTcp();
59
+ await this.poll();
60
+ this.pollTimer = this.setInterval(() => void this.poll(), this.cfg.pollMs);
61
+ } catch (error) {
62
+ this.log.error(`AdvanSol start error: ${error.message}`);
63
+ }
64
+ }
65
+
66
+ onUnload(callback) {
67
+ try {
68
+ if (this.pollTimer) {
69
+ this.clearInterval(this.pollTimer);
70
+ this.pollTimer = null;
71
+ }
72
+ if (this.socket) {
73
+ this.socket.destroy();
74
+ this.socket = null;
75
+ }
76
+ callback();
77
+ } catch {
78
+ callback();
79
+ }
80
+ }
81
+
82
+ async onStateChange(id, state) {
83
+ if (!state || state.ack) {
84
+ return;
85
+ }
86
+
87
+ const ownPrefix = `${this.namespace}.`;
88
+ if (!id.startsWith(ownPrefix)) {
89
+ return;
90
+ }
91
+
92
+ const rel = id.slice(ownPrefix.length);
93
+ const match = rel.match(/^module_(\d+)\.switch$/);
94
+ if (!match) {
95
+ return;
96
+ }
97
+
98
+ const index = Number(match[1]);
99
+ try {
100
+ const snState = await this.getStateAsync(`module_${index}.sn`);
101
+ const sn = snState && snState.val;
102
+ if (!sn) {
103
+ this.log.warn(`No serial number found for module ${index}`);
104
+ return;
105
+ }
106
+
107
+ await this.switchModule(String(sn), state.val === true);
108
+ } catch (error) {
109
+ this.log.error(`Switching module ${index} failed: ${error.message}`);
110
+ }
111
+ }
112
+
113
+ crc16(buf) {
114
+ let crc = 0xffff;
115
+ for (const b of buf) {
116
+ crc ^= b;
117
+ for (let i = 0; i < 8; i++) {
118
+ crc = (crc & 1) ? (crc >> 1) ^ 0xa001 : crc >> 1;
119
+ }
120
+ }
121
+ return crc & 0xffff;
122
+ }
123
+
124
+ makeFrame(payload) {
125
+ const frame = Buffer.alloc(256, 0xff);
126
+ Buffer.from(payload).copy(frame, 0);
127
+ const crc = this.crc16(frame.subarray(0, 254));
128
+ frame[254] = crc & 0xff;
129
+ frame[255] = (crc >> 8) & 0xff;
130
+ return frame;
131
+ }
132
+
133
+ hex(buf) {
134
+ return Buffer.from(buf).toString("hex").toUpperCase();
135
+ }
136
+
137
+ u16(buf, pos) {
138
+ return buf.readUInt16BE(pos);
139
+ }
140
+
141
+ i16(buf, pos) {
142
+ return buf.readInt16BE(pos);
143
+ }
144
+
145
+ u32(buf, pos) {
146
+ return buf.readUInt32BE(pos);
147
+ }
148
+
149
+ wait(ms) {
150
+ return new Promise(resolve => this.setTimeout(resolve, ms));
151
+ }
152
+
153
+ async ensureState(id, name, role, type, unit = "", write = false) {
154
+ await this.setObjectNotExistsAsync(id, {
155
+ type: "state",
156
+ common: {
157
+ name,
158
+ role,
159
+ type,
160
+ unit,
161
+ read: true,
162
+ write,
163
+ },
164
+ native: {},
165
+ });
166
+ }
167
+
168
+ async ensureChannel(id, name) {
169
+ await this.setObjectNotExistsAsync(id, {
170
+ type: "channel",
171
+ common: { name },
172
+ native: {},
173
+ });
174
+ }
175
+
176
+ async setVal(id, val) {
177
+ await this.setStateAsync(id, val, true);
178
+ }
179
+
180
+ async initBaseStates() {
181
+ await this.ensureChannel("info", "Information");
182
+ await this.ensureState("info.connection", "Connection", "indicator.connected", "boolean");
183
+ await this.ensureChannel("controller", "Controller");
184
+ await this.ensureState("controller.sn", "Controller Seriennummer", "text", "string");
185
+ await this.ensureState("module_count", "Anzahl Optimierer", "value", "number");
186
+ await this.ensureState("last_poll", "Letzter Poll", "date", "string");
187
+ await this.ensureState("connection", "Verbindungsstatus", "indicator.connected", "boolean");
188
+ await this.ensureState("night_mode", "Nachtmodus", "indicator", "boolean");
189
+ }
190
+
191
+ async initStatesForModule(index) {
192
+ const base = `module_${index}`;
193
+ await this.ensureChannel(base, `Optimierer ${index}`);
194
+ await this.ensureState(`${base}.sn`, "Seriennummer", "text", "string");
195
+ await this.ensureState(`${base}.switch`, "MOS Ein/Aus", "switch", "boolean", "", true);
196
+ await this.ensureState(`${base}.mos`, "MOS Status 0=aus 1=ein", "value", "number");
197
+ await this.ensureState(`${base}.software`, "Softwareversion", "text", "string");
198
+ await this.ensureState(`${base}.hardware`, "Hardwareversion", "text", "string");
199
+ await this.ensureState(`${base}.output_voltage`, "Spannung", "value.voltage", "number", "V");
200
+ await this.ensureState(`${base}.output_current`, "Strom", "value.current", "number", "A");
201
+ await this.ensureState(`${base}.temperature`, "Temperatur", "value.temperature", "number", "degC");
202
+ await this.ensureState(`${base}.power`, "Leistung", "value.power", "number", "W");
203
+ await this.ensureState(`${base}.energy`, "Gesamtertrag", "value.energy", "number", "kWh");
204
+ await this.ensureState(`${base}.input_voltage`, "Eingangsspannung", "value.voltage", "number", "V");
205
+ await this.ensureState(`${base}.input_current`, "Eingangsstrom", "value.current", "number", "A");
206
+ await this.ensureState(`${base}.raw`, "Raw Antwort", "text", "string");
207
+ await this.ensureState(`${base}.last_update`, "Letzte Aktualisierung", "date", "string");
208
+ }
209
+
210
+ connectTcp() {
211
+ const cfg = this.cfg;
212
+ return new Promise((resolve, reject) => {
213
+ this.socket = new net.Socket();
214
+ this.socket.setNoDelay(true);
215
+
216
+ const connectTimer = this.setTimeout(() => {
217
+ reject(new Error(`TCP connect timeout to ${cfg.host}:${cfg.tcpPort}`));
218
+ try {
219
+ this.socket.destroy();
220
+ } catch {
221
+ // ignore cleanup errors
222
+ }
223
+ }, cfg.requestTimeoutMs);
224
+
225
+ this.socket.connect(cfg.tcpPort, cfg.host, () => {
226
+ this.clearTimeout(connectTimer);
227
+ this.connected = true;
228
+ void this.setStateAsync("info.connection", true, true);
229
+ void this.setVal("connection", true);
230
+ this.log.info(`AdvanSol connected to ${cfg.host}:${cfg.tcpPort}`);
231
+ resolve();
232
+ });
233
+
234
+ this.socket.on("error", err => {
235
+ this.connected = false;
236
+ void this.setStateAsync("info.connection", false, true);
237
+ void this.setVal("connection", false);
238
+ this.log.warn(`AdvanSol TCP error: ${err.message}`);
239
+ });
240
+
241
+ this.socket.on("close", () => {
242
+ this.connected = false;
243
+ void this.setStateAsync("info.connection", false, true);
244
+ void this.setVal("connection", false);
245
+ this.log.warn("AdvanSol TCP connection closed");
246
+ });
247
+
248
+ this.socket.setTimeout(30000);
249
+ this.socket.on("timeout", () => {
250
+ this.log.warn("AdvanSol TCP timeout, reconnecting");
251
+ try {
252
+ this.socket.destroy();
253
+ } catch {
254
+ // ignore cleanup errors
255
+ }
256
+ });
257
+ });
258
+ }
259
+
260
+ async reconnectIfNeeded() {
261
+ if (this.connected && this.socket && !this.socket.destroyed) {
262
+ return;
263
+ }
264
+
265
+ try {
266
+ if (this.socket) {
267
+ this.socket.destroy();
268
+ }
269
+ } catch {
270
+ // ignore cleanup errors
271
+ }
272
+
273
+ await this.wait(1000);
274
+ await this.connectTcp();
275
+ }
276
+
277
+ doRequest(payload, timeout = this.cfg.requestTimeoutMs) {
278
+ return new Promise((resolve, reject) => {
279
+ if (!this.socket || !this.connected || this.socket.destroyed) {
280
+ reject(new Error("TCP not connected"));
281
+ return;
282
+ }
283
+
284
+ const chunks = [];
285
+ let finishTimer = null;
286
+ let hardTimer = null;
287
+ let finished = false;
288
+
289
+ const cleanup = () => {
290
+ if (finishTimer) this.clearTimeout(finishTimer);
291
+ if (hardTimer) this.clearTimeout(hardTimer);
292
+ this.socket.off("data", onData);
293
+ };
294
+
295
+ const finish = () => {
296
+ if (finished) return;
297
+ finished = true;
298
+ cleanup();
299
+ resolve(Buffer.concat(chunks));
300
+ };
301
+
302
+ const onData = data => {
303
+ chunks.push(data);
304
+ const total = Buffer.concat(chunks);
305
+ if (total.length >= 256) {
306
+ finish();
307
+ return;
308
+ }
309
+ if (finishTimer) this.clearTimeout(finishTimer);
310
+ finishTimer = this.setTimeout(finish, this.finishDelayMs);
311
+ };
312
+
313
+ this.socket.on("data", onData);
314
+ const frame = this.makeFrame(payload);
315
+
316
+ this.socket.write(frame, err => {
317
+ if (err) {
318
+ cleanup();
319
+ reject(err);
320
+ return;
321
+ }
322
+ hardTimer = this.setTimeout(finish, timeout);
323
+ });
324
+ });
325
+ }
326
+
327
+ request(payload, timeout = this.cfg.requestTimeoutMs) {
328
+ this.requestQueue = this.requestQueue
329
+ .then(async () => {
330
+ await this.reconnectIfNeeded();
331
+ return this.doRequest(payload, timeout);
332
+ })
333
+ .catch(async err => {
334
+ this.log.warn(`Request error: ${err.message}`);
335
+ throw err;
336
+ });
337
+ return this.requestQueue;
338
+ }
339
+
340
+ async readControllerSn() {
341
+ const res = await this.request([0x00, 0x03, 0xff, 0xe7, 0x00, 0x03]);
342
+ if (res.length >= 9 && res[0] === 0x01 && res[1] === 0x03 && res[2] === 0x06) {
343
+ const sn = this.hex(res.subarray(3, 9));
344
+ await this.setVal("controller.sn", sn);
345
+ return sn;
346
+ }
347
+ throw new Error(`Invalid controller SN response: ${this.hex(res)}`);
348
+ }
349
+
350
+ async readDeviceList(controllerSn) {
351
+ const snBytes = Buffer.from(controllerSn, "hex");
352
+ const res = await this.request([0x00, 0x42, 0x01, ...snBytes], 2500);
353
+
354
+ if (!(res.length > 20 && res[0] === 0x01 && res[1] === 0x42)) {
355
+ throw new Error(`Invalid device list response: ${this.hex(res)}`);
356
+ }
357
+
358
+ const count = this.u16(res, 6);
359
+ const modules = [];
360
+
361
+ for (let i = 0; i < count; i++) {
362
+ const pos = 10 + i * 10;
363
+ if (pos + 6 > res.length) break;
364
+ const sn = this.hex(res.subarray(pos, pos + 6));
365
+ if (sn !== "FFFFFFFFFFFF" && sn.length === 12) {
366
+ modules.push({ index: i + 1, sn });
367
+ }
368
+ }
369
+
370
+ await this.setVal("module_count", modules.length);
371
+ return modules;
372
+ }
373
+
374
+ async parseModuleResponse(index, res) {
375
+ if (!(res.length > 40 && res[0] === 0x01 && res[1] === 0x43)) {
376
+ throw new Error(`Module ${index}: invalid response: ${this.hex(res)}`);
377
+ }
378
+
379
+ const base = `module_${index}`;
380
+ const sn = this.hex(res.subarray(3, 9));
381
+ const mos = this.u16(res, 9);
382
+ const sw = [...res.subarray(11, 15)].map(x => x.toString(16).padStart(2, "0").toUpperCase()).join(".");
383
+ const hw = [...res.subarray(15, 19)].map(x => x.toString(16).padStart(2, "0").toUpperCase()).join(".");
384
+
385
+ await this.setVal(`${base}.sn`, sn);
386
+ await this.setVal(`${base}.mos`, mos);
387
+ await this.setVal(`${base}.switch`, mos === 1);
388
+ await this.setVal(`${base}.software`, sw);
389
+ await this.setVal(`${base}.hardware`, hw);
390
+ await this.setVal(`${base}.output_voltage`, this.i16(res, 19) / 100);
391
+ await this.setVal(`${base}.output_current`, this.i16(res, 21) / 100);
392
+ await this.setVal(`${base}.temperature`, this.u16(res, 23) - 100);
393
+ await this.setVal(`${base}.power`, this.i16(res, 25));
394
+ await this.setVal(`${base}.energy`, this.u32(res, 27) / 100);
395
+ await this.setVal(`${base}.input_voltage`, this.i16(res, 31) / 100);
396
+ await this.setVal(`${base}.input_current`, this.i16(res, 33) / 100);
397
+ await this.setVal(`${base}.raw`, this.hex(res));
398
+ await this.setVal(`${base}.last_update`, new Date().toISOString());
399
+ }
400
+
401
+ async readModule(index) {
402
+ const res = await this.request([0x00, 0x43, 0x00, index, 0x00, 0x17], 2500);
403
+ await this.parseModuleResponse(index, res);
404
+ }
405
+
406
+ async switchModule(sn, targetOn) {
407
+ const cfg = this.cfg;
408
+ const snBytes = Buffer.from(sn, "hex");
409
+ if (snBytes.length !== 6) {
410
+ throw new Error(`Invalid serial number: ${sn}`);
411
+ }
412
+
413
+ const payload = targetOn
414
+ ? [0x00, 0x05, 0xff, 0xef, 0xff, 0x00, ...snBytes]
415
+ : [0x00, 0x05, 0xff, 0xef, 0x00, 0x00, ...snBytes];
416
+
417
+ this.log.info(`Switch optimizer ${sn} -> ${targetOn ? "ON" : "OFF"}`);
418
+ for (let i = 1; i <= cfg.switchRetries; i++) {
419
+ try {
420
+ const res = await this.request(payload, 1200);
421
+ this.log.info(`Switch command try ${i}/${cfg.switchRetries}, response: ${this.hex(res)}`);
422
+ } catch (error) {
423
+ this.log.warn(`Switch command try ${i}/${cfg.switchRetries} failed: ${error.message}`);
424
+ }
425
+
426
+ if (i < cfg.switchRetries) {
427
+ await this.wait(cfg.switchRetryDelayMs);
428
+ }
429
+ }
430
+ }
431
+
432
+ async discover() {
433
+ const controllerSn = await this.readControllerSn();
434
+ this.log.info(`AdvanSol controller SN: ${controllerSn}`);
435
+
436
+ this.modules = await this.readDeviceList(controllerSn);
437
+ this.log.info(`Found optimizers: ${this.modules.map(m => `${m.index}:${m.sn}`).join(", ")}`);
438
+
439
+ for (const m of this.modules) {
440
+ await this.initStatesForModule(m.index);
441
+ await this.setVal(`module_${m.index}.sn`, m.sn);
442
+ await this.registerSwitchHandler(m.index);
443
+ }
444
+
445
+ this.initialized = true;
446
+ }
447
+
448
+ async registerSwitchHandler(index) {
449
+ const stateId = `module_${index}.switch`;
450
+ if (this.switchSubscriptions.has(stateId)) {
451
+ return;
452
+ }
453
+ this.switchSubscriptions.add(stateId);
454
+ await this.subscribeStatesAsync(stateId);
455
+ }
456
+
457
+ isNightHour(hour) {
458
+ const { nightStart, nightEnd } = this.cfg;
459
+ return nightStart <= nightEnd
460
+ ? hour >= nightStart && hour < nightEnd
461
+ : hour >= nightStart || hour < nightEnd;
462
+ }
463
+
464
+ async poll() {
465
+ if (this.isNightHour(new Date().getHours())) {
466
+ return;
467
+ }
468
+ if (this.polling) {
469
+ return;
470
+ }
471
+
472
+ this.polling = true;
473
+ try {
474
+ if (!this.initialized || this.modules.length === 0) {
475
+ await this.discover();
476
+ }
477
+
478
+ if (this.nightMode) {
479
+ this.nightCheckCounter++;
480
+ if (this.nightCheckCounter < this.nightCheckEvery) {
481
+ return;
482
+ }
483
+
484
+ this.nightCheckCounter = 0;
485
+ try {
486
+ await this.readModule(1);
487
+ this.nightMode = false;
488
+ await this.setVal("night_mode", false);
489
+ this.log.info("AdvanSol night mode ended, optimizers respond again");
490
+ } catch {
491
+ return;
492
+ }
493
+ }
494
+
495
+ let successCount = 0;
496
+ let failCount = 0;
497
+
498
+ for (const m of this.modules) {
499
+ try {
500
+ await this.readModule(m.index);
501
+ this.moduleErrorCount[m.index] = 0;
502
+ successCount++;
503
+ } catch {
504
+ this.moduleErrorCount[m.index] = (this.moduleErrorCount[m.index] || 0) + 1;
505
+ failCount++;
506
+
507
+ if (!this.nightMode && this.moduleErrorCount[m.index] === 5) {
508
+ this.log.warn(`Module ${m.index} has had no valid response for 5 tries`);
509
+ }
510
+ }
511
+
512
+ await this.wait(this.moduleReadDelayMs);
513
+ }
514
+
515
+ if (failCount >= this.nightFailLimit && successCount === 0) {
516
+ this.nightMode = true;
517
+ await this.setVal("night_mode", true);
518
+ this.log.info("AdvanSol night mode active: optimizers do not respond");
519
+ }
520
+
521
+ await this.setVal("last_poll", new Date().toISOString());
522
+ } catch (error) {
523
+ this.log.warn(`AdvanSol poll error: ${error.message}`);
524
+ } finally {
525
+ this.polling = false;
526
+ }
527
+ }
528
+ }
529
+
530
+ if (require.main !== module) {
531
+ module.exports = options => new AdvansolOptimizer(options);
532
+ } else {
533
+ new AdvansolOptimizer();
534
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "iobroker.advansol-optimizer",
3
+ "version": "0.1.2",
4
+ "description": "ioBroker adapter for AdvanSol DCON-WIFI / MRO/MR optimizers via TCP RS485 bridge",
5
+ "author": "TheBam1990",
6
+ "homepage": "https://github.com/TheBam1990/ioBroker.advansol-optimizer",
7
+ "license": "MIT",
8
+ "main": "main.js",
9
+ "keywords": [
10
+ "ioBroker",
11
+ "advansol",
12
+ "optimizer",
13
+ "solar",
14
+ "rs485"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/TheBam1990/ioBroker.advansol-optimizer.git"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "dependencies": {
24
+ "@iobroker/adapter-core": "^3.2.3"
25
+ },
26
+ "devDependencies": {
27
+ "@iobroker/testing": "^4.1.3"
28
+ },
29
+ "scripts": {
30
+ "start": "node main.js",
31
+ "test": "node --check main.js"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/TheBam1990/ioBroker.advansol-optimizer/issues"
35
+ },
36
+ "readmeFilename": "README.md"
37
+ }