wattsonic-modbus-docs 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,2 @@
1
+ """Read-only MCP documentation server for the Modbus interface of Wattsonic MATIC inverters."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ main()
@@ -0,0 +1,359 @@
1
+ """Client code templates generated from a recipe (Modbus TCP only).
2
+
3
+ Libraries follow the examples on modbus.wattsonic.com: Python pymodbus (ModbusTcpClient), Node.js modbus-serial,
4
+ C# NModbus, Go github.com/goburrow/modbus. The generated program is a template: review the values, then run it
5
+ against your own inverter. This module only produces text; it never connects anywhere.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ from . import docs
12
+
13
+ LANGS = ("python", "nodejs", "csharp", "go")
14
+ KEEPALIVE_S = 10 # well below 30 s and at most RvrtTms/3 for the documented RvrtTms of 60 s
15
+
16
+
17
+ def _expected_int(e: str) -> int | None:
18
+ e = e.strip()
19
+ if re.fullmatch(r"-?\d+", e):
20
+ return int(e)
21
+ if re.fullmatch(r"0x[0-9A-Fa-f]+", e):
22
+ return int(e, 16)
23
+ return None
24
+
25
+
26
+ def _plan(recipe: dict) -> dict:
27
+ pre = [p for p in recipe["preconditions"] if "read" in p]
28
+ steps, keep = [], []
29
+ for i, s in enumerate(recipe["steps"], 1):
30
+ st = dict(s, n=i)
31
+ if s["op"] == "write":
32
+ if s["function_code"] == "0x10":
33
+ st["vals"] = list(s["values"])
34
+ else:
35
+ v = s["value"] if s["value"] is not None else s.get("example_value")
36
+ st["vals"] = [v]
37
+ st["wire"] = [v & 0xFFFF for v in st["vals"]]
38
+ steps.append(st)
39
+ if s.get("repeat"):
40
+ keep.append(st)
41
+ return {"pre": pre, "steps": steps, "keep": keep, "confirm": recipe["confirm"]}
42
+
43
+
44
+ def _comment_lines(text: str, prefix: str, width: int = 100) -> list[str]:
45
+ words, lines, cur = text.split(), [], ""
46
+ for w in words:
47
+ if len(cur) + len(w) + 1 > width:
48
+ lines.append(prefix + cur)
49
+ cur = w
50
+ else:
51
+ cur = (cur + " " + w).strip()
52
+ if cur:
53
+ lines.append(prefix + cur)
54
+ return lines
55
+
56
+
57
+ def _header(recipe: dict, c: str) -> list[str]:
58
+ v = docs.version()
59
+ h = ["Recipe %s: %s" % (recipe["id"], recipe["title"]),
60
+ "Wattsonic MATIC, ARM firmware %s or later (documentation edition %s)." % (v["firmware"], v["edition"]),
61
+ "Template generated from the published documentation. It WRITES to a live inverter when run:",
62
+ "check every value against your installation first. Values are raw register values;",
63
+ "negative values are sent as 16-bit two's complement."]
64
+ return [c + x for x in h]
65
+
66
+
67
+ def generate(recipe_id: str, language: str, transport: str = "tcp", host: str = "192.168.1.100",
68
+ unit_id: int = 1) -> dict:
69
+ lang = (language or "").lower().replace(".", "").replace("#", "sharp").replace(" ", "")
70
+ lang = {"py": "python", "node": "nodejs", "js": "nodejs", "javascript": "nodejs", "cs": "csharp",
71
+ "c": "csharp", "golang": "go"}.get(lang, lang)
72
+ if lang not in LANGS:
73
+ return {"error": "language must be one of %s" % ", ".join(LANGS)}
74
+ if (transport or "tcp").lower() != "tcp":
75
+ return {"error": "Only transport='tcp' (Modbus TCP, port 502) is generated."}
76
+ ids = [r["id"] for r in docs.data("recipes.v1130.json")["recipes"]]
77
+ if recipe_id not in ids:
78
+ return {"error": "Unknown recipe id %r; use list_recipes." % recipe_id, "recipes": ids}
79
+ recipe = docs.get_recipe(recipe_id)
80
+ plan = _plan(recipe)
81
+ gen = {"python": _python, "nodejs": _node, "csharp": _csharp, "go": _go}[lang]
82
+ code = gen(recipe, plan, host, int(unit_id))
83
+ return {"recipe": recipe["id"], "language": lang, "transport": "tcp", "code": code,
84
+ "note": "Template only; " + docs.SAFETY + " Library: " + {
85
+ "python": "pymodbus 3.x", "nodejs": "modbus-serial", "csharp": "NModbus",
86
+ "go": "github.com/goburrow/modbus"}[lang] + "."}
87
+
88
+
89
+ # ------------------------------------------------------------------------------------------------ Python
90
+ def _python(recipe, plan, host, unit):
91
+ L = ['"""'] + _header(recipe, "") + ['"""', "import inspect", "import time", "",
92
+ "from pymodbus.client import ModbusTcpClient", "",
93
+ 'HOST, PORT, UNIT = "%s", 502, %d' % (host, unit),
94
+ "KEEPALIVE_S = %d # %s" % (KEEPALIVE_S, "well below 30 s and at most RvrtTms/3"), "",
95
+ "client = ModbusTcpClient(HOST, port=PORT)",
96
+ "if not client.connect():", ' raise SystemExit("cannot connect to %s:%d" % (HOST, PORT))',
97
+ "# pymodbus up to 3.9 names the unit 'slave', 3.10 and later 'device_id'",
98
+ '_UNIT_KW = "device_id" if "device_id" in inspect.signature(client.read_holding_registers).parameters else "slave"',
99
+ "", "",
100
+ "def rd(address, count=1):",
101
+ " r = client.read_holding_registers(address, count=count, **{_UNIT_KW: UNIT})",
102
+ " if r.isError():",
103
+ ' raise RuntimeError("read %d failed: %s" % (address, r))',
104
+ " return r.registers", "", "",
105
+ "def wr(address, value): # function code 0x06",
106
+ " r = client.write_register(address, value & 0xFFFF, **{_UNIT_KW: UNIT})",
107
+ " if r.isError():",
108
+ ' raise RuntimeError("write %d failed: %s" % (address, r))', "", "",
109
+ "def wr_block(address, values): # function code 0x10",
110
+ " r = client.write_registers(address, [v & 0xFFFF for v in values], **{_UNIT_KW: UNIT})",
111
+ " if r.isError():",
112
+ ' raise RuntimeError("write %d+%d failed: %s" % (address, len(values), r))', "", ""]
113
+ L.append("# --- preconditions")
114
+ for p in plan["pre"]:
115
+ a, e = p["read"]["address"], p["read"]["expected"]
116
+ L += _comment_lines(p["text"], "# ")
117
+ ei = _expected_int(e)
118
+ L.append("v = rd(%d)[0]" % a)
119
+ if ei is not None:
120
+ L.append("if v != %d:" % (ei & 0xFFFF))
121
+ L.append(' raise SystemExit("precondition %d: read %%d, expected %s" %% v)' % (a, e))
122
+ else:
123
+ L.append('print("%d =", v, "(expected: %s)")' % (a, e.replace('"', "'")))
124
+ L.append("")
125
+ L.append("# --- write sequence")
126
+ for s in plan["steps"]:
127
+ L += _comment_lines("Step %d: %s" % (s["n"], s["description"]), "# ")
128
+ ind = ""
129
+ if s.get("condition"):
130
+ L.append("DO_STEP_%d = False # set True when: %s" % (s["n"], s["condition"]))
131
+ L.append("if DO_STEP_%d:" % s["n"])
132
+ ind = " "
133
+ if s.get("wait_before_s"):
134
+ L.append("%stime.sleep(%s)" % (ind, s["wait_before_s"]))
135
+ if s["op"] == "read":
136
+ L.append('%sprint("%d..%d:", rd(%d, %d)) # check: %s' % (ind, s["address"], s["address"] + s.get("count", 1) - 1,
137
+ s["address"], s.get("count", 1), s["check"]))
138
+ elif s["function_code"] == "0x10":
139
+ L.append("%swr_block(%d, %r) # check: %s" % (ind, s["address"], s["vals"], s["check"]))
140
+ else:
141
+ rng = (" range %s..%s," % tuple(s["value_range"])) if s.get("value_range") else ""
142
+ L.append("%swr(%d, %d) #%s check: %s" % (ind, s["address"], s["vals"][0], rng, s["check"]))
143
+ L.append("")
144
+ L.append("# --- confirm")
145
+ L.append("time.sleep(2)")
146
+ for c in plan["confirm"]:
147
+ L.append('print("%d =", rd(%d)[0], "expected: %s (%s)")' % (c["read"], c["read"], c["expected"].replace('"', "'"),
148
+ c["meaning"].replace('"', "'")))
149
+ if plan["keep"]:
150
+ L += ["", "# --- keep-alive (stop with Ctrl+C)", "try:", " while True:", " time.sleep(KEEPALIVE_S)"]
151
+ for s in plan["keep"]:
152
+ if s["function_code"] == "0x10":
153
+ L.append(" wr_block(%d, %r) # %s" % (s["address"], s["vals"], s["repeat"]["interval"]))
154
+ else:
155
+ L.append(" wr(%d, %d) # %s" % (s["address"], s["vals"][0], s["repeat"]["interval"]))
156
+ L += ["except KeyboardInterrupt:", " pass"]
157
+ L += ["", "client.close()", ""]
158
+ return "\n".join(L)
159
+
160
+
161
+ # ------------------------------------------------------------------------------------------------ Node.js
162
+ def _node(recipe, plan, host, unit):
163
+ L = ["// " + x[2:] if x.startswith(" ") else "// " + x for x in _header(recipe, "")]
164
+ L += ["const ModbusRTU = require('modbus-serial');", "",
165
+ "const HOST = '%s', PORT = 502, UNIT = %d;" % (host, unit),
166
+ "const KEEPALIVE_MS = %d; // well below 30 s and at most RvrtTms/3" % (KEEPALIVE_S * 1000),
167
+ "const sleep = (ms) => new Promise((r) => setTimeout(r, ms));",
168
+ "const u16 = (v) => v & 0xFFFF;", "",
169
+ "async function main() {",
170
+ " const client = new ModbusRTU();",
171
+ " await client.connectTCP(HOST, { port: PORT });",
172
+ " client.setID(UNIT);",
173
+ " const rd = async (a, n = 1) => (await client.readHoldingRegisters(a, n)).data;",
174
+ " const wr = (a, v) => client.writeRegister(a, u16(v)); // 0x06",
175
+ " const wrBlock = (a, vs) => client.writeRegisters(a, vs.map(u16)); // 0x10", ""]
176
+ L.append(" // --- preconditions")
177
+ for p in plan["pre"]:
178
+ a, e = p["read"]["address"], p["read"]["expected"]
179
+ L += _comment_lines(p["text"], " // ")
180
+ ei = _expected_int(e)
181
+ if ei is not None:
182
+ L.append(" { const v = (await rd(%d))[0]; if (v !== %d) throw new Error(`precondition %d: read ${v}, expected %s`); }"
183
+ % (a, ei & 0xFFFF, a, e))
184
+ else:
185
+ L.append(" console.log('%d =', (await rd(%d))[0], '(expected: %s)');" % (a, a, e.replace("'", '"')))
186
+ L.append("")
187
+ L.append(" // --- write sequence")
188
+ for s in plan["steps"]:
189
+ L += _comment_lines("Step %d: %s" % (s["n"], s["description"]), " // ")
190
+ ind = " "
191
+ if s.get("condition"):
192
+ L.append(" const DO_STEP_%d = false; // set true when: %s" % (s["n"], s["condition"]))
193
+ L.append(" if (DO_STEP_%d) {" % s["n"])
194
+ ind = " "
195
+ if s.get("wait_before_s"):
196
+ L.append("%sawait sleep(%d);" % (ind, int(s["wait_before_s"] * 1000)))
197
+ if s["op"] == "read":
198
+ L.append("%sconsole.log('%d:', await rd(%d, %d)); // check: %s" % (ind, s["address"], s["address"],
199
+ s.get("count", 1), s["check"]))
200
+ elif s["function_code"] == "0x10":
201
+ L.append("%sawait wrBlock(%d, [%s]); // check: %s" % (ind, s["address"], ", ".join(map(str, s["vals"])), s["check"]))
202
+ else:
203
+ L.append("%sawait wr(%d, %d); // check: %s" % (ind, s["address"], s["vals"][0], s["check"]))
204
+ if s.get("condition"):
205
+ L.append(" }")
206
+ L.append("")
207
+ L.append(" // --- confirm")
208
+ L.append(" await sleep(2000);")
209
+ for c in plan["confirm"]:
210
+ L.append(" console.log('%d =', (await rd(%d))[0], 'expected: %s');" % (c["read"], c["read"], c["expected"].replace("'", '"')))
211
+ if plan["keep"]:
212
+ L.append("")
213
+ L.append(" // --- keep-alive")
214
+ L.append(" for (;;) {")
215
+ L.append(" await sleep(KEEPALIVE_MS);")
216
+ for s in plan["keep"]:
217
+ if s["function_code"] == "0x10":
218
+ L.append(" await wrBlock(%d, [%s]);" % (s["address"], ", ".join(map(str, s["vals"]))))
219
+ else:
220
+ L.append(" await wr(%d, %d); // %s" % (s["address"], s["vals"][0], s["repeat"]["interval"]))
221
+ L.append(" }")
222
+ else:
223
+ L.append(" client.close(() => {});")
224
+ L += ["}", "", "main().catch((e) => { console.error(e); process.exit(1); });", ""]
225
+ return "\n".join(L)
226
+
227
+
228
+ # ------------------------------------------------------------------------------------------------ C#
229
+ def _csharp(recipe, plan, host, unit):
230
+ L = ["// " + x for x in _header(recipe, "")]
231
+ L += ["using System;", "using System.Net.Sockets;", "using System.Threading;", "using NModbus;", "",
232
+ 'const string Host = "%s";' % host, "const byte Unit = %d;" % unit,
233
+ "const int KeepAliveMs = %d; // well below 30 s and at most RvrtTms/3" % (KEEPALIVE_S * 1000), "",
234
+ "using var tcp = new TcpClient(Host, 502);",
235
+ "var master = new ModbusFactory().CreateMaster(tcp);",
236
+ "ushort U16(int v) => (ushort)(v & 0xFFFF);",
237
+ "ushort[] Rd(ushort a, ushort n = 1) => master.ReadHoldingRegisters(Unit, a, n);",
238
+ "void Wr(ushort a, int v) => master.WriteSingleRegister(Unit, a, U16(v)); // 0x06",
239
+ "void WrBlock(ushort a, int[] vs) => master.WriteMultipleRegisters(Unit, a, Array.ConvertAll(vs, U16)); // 0x10",
240
+ ""]
241
+ L.append("// --- preconditions")
242
+ for p in plan["pre"]:
243
+ a, e = p["read"]["address"], p["read"]["expected"]
244
+ L += _comment_lines(p["text"], "// ")
245
+ ei = _expected_int(e)
246
+ if ei is not None:
247
+ L.append('{ var v = Rd(%d)[0]; if (v != %d) throw new Exception($"precondition %d: read {v}, expected %s"); }'
248
+ % (a, ei & 0xFFFF, a, e))
249
+ else:
250
+ L.append('Console.WriteLine($"%d = {Rd(%d)[0]} (expected: %s)");' % (a, a, e.replace('"', "'")))
251
+ L.append("")
252
+ L.append("// --- write sequence")
253
+ for s in plan["steps"]:
254
+ L += _comment_lines("Step %d: %s" % (s["n"], s["description"]), "// ")
255
+ ind = ""
256
+ if s.get("condition"):
257
+ L.append("bool doStep%d = false; // set true when: %s" % (s["n"], s["condition"]))
258
+ L.append("if (doStep%d) {" % s["n"])
259
+ ind = " "
260
+ if s.get("wait_before_s"):
261
+ L.append("%sThread.Sleep(%d);" % (ind, int(s["wait_before_s"] * 1000)))
262
+ if s["op"] == "read":
263
+ L.append('%sConsole.WriteLine(string.Join(",", Rd(%d, %d))); // check: %s' % (ind, s["address"], s.get("count", 1), s["check"]))
264
+ elif s["function_code"] == "0x10":
265
+ L.append("%sWrBlock(%d, new[] { %s }); // check: %s" % (ind, s["address"], ", ".join(map(str, s["vals"])), s["check"]))
266
+ else:
267
+ L.append("%sWr(%d, %d); // check: %s" % (ind, s["address"], s["vals"][0], s["check"]))
268
+ if s.get("condition"):
269
+ L.append("}")
270
+ L.append("")
271
+ L.append("// --- confirm")
272
+ L.append("Thread.Sleep(2000);")
273
+ for c in plan["confirm"]:
274
+ L.append('Console.WriteLine($"%d = {Rd(%d)[0]} (expected: %s)");' % (c["read"], c["read"], c["expected"].replace('"', "'")))
275
+ if plan["keep"]:
276
+ L += ["", "// --- keep-alive", "while (true) {", " Thread.Sleep(KeepAliveMs);"]
277
+ for s in plan["keep"]:
278
+ if s["function_code"] == "0x10":
279
+ L.append(" WrBlock(%d, new[] { %s });" % (s["address"], ", ".join(map(str, s["vals"]))))
280
+ else:
281
+ L.append(" Wr(%d, %d); // %s" % (s["address"], s["vals"][0], s["repeat"]["interval"]))
282
+ L.append("}")
283
+ L.append("")
284
+ return "\n".join(L)
285
+
286
+
287
+ # ------------------------------------------------------------------------------------------------ Go
288
+ def _go(recipe, plan, host, unit):
289
+ L = ["// " + x for x in _header(recipe, "")]
290
+ L += ["package main", "", "import (", '\t"encoding/binary"', '\t"fmt"', '\t"log"', '\t"time"', "",
291
+ '\t"github.com/goburrow/modbus"', ")", "",
292
+ 'const host = "%s:502"' % host, "const unit = %d" % unit,
293
+ "const keepAlive = %d * time.Second // well below 30 s and at most RvrtTms/3" % KEEPALIVE_S, "",
294
+ "func main() {",
295
+ "\th := modbus.NewTCPClientHandler(host)",
296
+ "\th.SlaveId = unit",
297
+ "\th.Timeout = 5 * time.Second",
298
+ "\tif err := h.Connect(); err != nil {", "\t\tlog.Fatal(err)", "\t}", "\tdefer h.Close()",
299
+ "\tc := modbus.NewClient(h)",
300
+ "\trd := func(a, n uint16) []uint16 {",
301
+ "\t\tb, err := c.ReadHoldingRegisters(a, n)",
302
+ "\t\tif err != nil {", "\t\t\tlog.Fatalf(\"read %d: %v\", a, err)", "\t\t}",
303
+ "\t\tout := make([]uint16, n)",
304
+ "\t\tfor i := range out {", "\t\t\tout[i] = binary.BigEndian.Uint16(b[2*i:])", "\t\t}",
305
+ "\t\treturn out", "\t}",
306
+ "\twr := func(a uint16, v int) { // 0x06",
307
+ "\t\tif _, err := c.WriteSingleRegister(a, uint16(v&0xFFFF)); err != nil {",
308
+ "\t\t\tlog.Fatalf(\"write %d: %v\", a, err)", "\t\t}", "\t}",
309
+ "\twrBlock := func(a uint16, vs []int) { // 0x10",
310
+ "\t\tb := make([]byte, 2*len(vs))",
311
+ "\t\tfor i, v := range vs {", "\t\t\tbinary.BigEndian.PutUint16(b[2*i:], uint16(v&0xFFFF))", "\t\t}",
312
+ "\t\tif _, err := c.WriteMultipleRegisters(a, uint16(len(vs)), b); err != nil {",
313
+ "\t\t\tlog.Fatalf(\"write %d: %v\", a, err)", "\t\t}", "\t}",
314
+ "\t_, _, _ = rd, wr, wrBlock", "\t_ = fmt.Sprint", ""]
315
+ L.append("\t// --- preconditions")
316
+ for p in plan["pre"]:
317
+ a, e = p["read"]["address"], p["read"]["expected"]
318
+ L += _comment_lines(p["text"], "\t// ")
319
+ ei = _expected_int(e)
320
+ if ei is not None:
321
+ L.append("\tif v := rd(%d, 1)[0]; v != %d {" % (a, ei & 0xFFFF))
322
+ L.append('\t\tlog.Fatalf("precondition %d: read %%d, expected %s", v)' % (a, e))
323
+ L.append("\t}")
324
+ else:
325
+ L.append('\tfmt.Println("%d =", rd(%d, 1)[0], "(expected: %s)")' % (a, a, e.replace('"', "'")))
326
+ L.append("")
327
+ L.append("\t// --- write sequence")
328
+ for s in plan["steps"]:
329
+ L += _comment_lines("Step %d: %s" % (s["n"], s["description"]), "\t// ")
330
+ ind = "\t"
331
+ if s.get("condition"):
332
+ L.append("\tdoStep%d := false // set true when: %s" % (s["n"], s["condition"]))
333
+ L.append("\tif doStep%d {" % s["n"])
334
+ ind = "\t\t"
335
+ if s.get("wait_before_s"):
336
+ L.append("%stime.Sleep(%d * time.Millisecond)" % (ind, int(s["wait_before_s"] * 1000)))
337
+ if s["op"] == "read":
338
+ L.append("%sfmt.Println(%d, rd(%d, %d)) // check: %s" % (ind, s["address"], s["address"], s.get("count", 1), s["check"]))
339
+ elif s["function_code"] == "0x10":
340
+ L.append("%swrBlock(%d, []int{%s}) // check: %s" % (ind, s["address"], ", ".join(map(str, s["vals"])), s["check"]))
341
+ else:
342
+ L.append("%swr(%d, %d) // check: %s" % (ind, s["address"], s["vals"][0], s["check"]))
343
+ if s.get("condition"):
344
+ L.append("\t}")
345
+ L.append("")
346
+ L.append("\t// --- confirm")
347
+ L.append("\ttime.Sleep(2 * time.Second)")
348
+ for c in plan["confirm"]:
349
+ L.append('\tfmt.Println("%d =", rd(%d, 1)[0], "expected: %s")' % (c["read"], c["read"], c["expected"].replace('"', "'")))
350
+ if plan["keep"]:
351
+ L += ["", "\t// --- keep-alive", "\tfor {", "\t\ttime.Sleep(keepAlive)"]
352
+ for s in plan["keep"]:
353
+ if s["function_code"] == "0x10":
354
+ L.append("\t\twrBlock(%d, []int{%s})" % (s["address"], ", ".join(map(str, s["vals"]))))
355
+ else:
356
+ L.append("\t\twr(%d, %d) // %s" % (s["address"], s["vals"][0], s["repeat"]["interval"]))
357
+ L.append("\t}")
358
+ L += ["}", ""]
359
+ return "\n".join(L)
@@ -0,0 +1,16 @@
1
+ {
2
+ "data_version": "M013V1130B01D05+2026-10-23",
3
+ "firmware": "M013V1130B01D05",
4
+ "firmware_applicability": "ARM >= M013V1130B01D05 (13021 >= 1130)",
5
+ "edition": "2026-10-23",
6
+ "build_date": "2026-10-23",
7
+ "source": "https://modbus.wattsonic.com/",
8
+ "files_md5": {
9
+ "native.md": "b875fba9bf377eba39e4301e728e6896",
10
+ "sunspec.md": "46aab3e67ee6f8b0a402ffea3ccb3e47",
11
+ "llms.txt": "541f89007883f98accb29f44ea29de36",
12
+ "registers.v1130.json": "1f2ba96c1f2480694c31efb7286584e8",
13
+ "sunspec-models.v1130.json": "8eef2dd804776ba14e4522154eeeb01d",
14
+ "recipes.v1130.json": "aa02ca59ae101488b4eae8f9eb3165e9"
15
+ }
16
+ }
@@ -0,0 +1,26 @@
1
+ # Wattsonic MATIC Modbus interface
2
+
3
+ > Modbus register reference for the Wattsonic MATIC three-phase hybrid PV and storage inverter (10-25 kW, one battery input). The inverter serves Modbus TCP on port 502 (Ethernet and Wi-Fi; unit identifier 1) and Modbus RTU on RS485. Two register interfaces share the same port and connection and are selected by the start address of each request: the native Wattsonic EMS map (10000-19800: monitoring, settings, EMS dispatch, communications watchdog) and the SunSpec map (40000-40478: models 1, 123, 124, 103, 203, 120, 121, 122, 160). Applies to ARM firmware M013V1130B01D05 or later (native register 13021 >= 1130). Edition 2026-10-23.
4
+
5
+ Control writes follow a closed loop: meet the preconditions, write the sequence, read back to confirm the command is in force, and know how it is released. Use function code 0x03 for reads; write the control registers 11300-11303, 11320-11324 and 11330 with 0x06 (one register per request). SunSpec control writes need the control gate 11790 = 0x5355.
6
+
7
+ ## Documentation
8
+
9
+ - [Native EMS protocol (Markdown)](native.md): register map, data types, EMS session, watchdog, scenarios with Python / Node.js / C# / Go examples. Sections: Overview; Data Types; Monitoring Registers; Settings Registers; Control Commands; Device Information; System Status; EMS Setpoint Shadow; Alarms & Faults; Quick Start Guide; Scenarios.
10
+ - [SunSpec interface (Markdown)](sunspec.md): sessions, model chain, all points, control interface, verification blocks, worked frames. Sections: Overview; Interface and sessions; Data representation; Model chain discovery; Polling; Model 1 · Common; Model 123 · Immediate controls; Model 124 · Storage; Model 103 · Inverter, three-phase; Model 203 · Meter, three-phase wye; Model 120 · Nameplate; Model 121 · Basic settings; Model 122 · Measurements and status; Model 160 · Multiple MPPT inverter extension; Control interface; Checking that a command is in force; Worked examples; Implementation notes; What's new in this edition; Terms and abbreviations.
11
+
12
+ ## Data
13
+
14
+ - [Native register map with integration fields](registers.v1130.json): every native register with type, scale, unit, access, range, and where documented: write function, effective conditions, read-back registers, persistence.
15
+ - [SunSpec model chain](sunspec-models.v1130.json): the device's SunSpec models in the SunSpec Alliance JSON layout with addresses, scale factors, access and per-point device notes.
16
+ - [Control recipes](recipes.v1130.json): closed-loop sequences (EMS session, active-power setpoint, power limit, reactive power, Eco schedule, SunSpec gate, SunSpec limit, forced charge/discharge, recovery, emergency stop).
17
+
18
+ ## Pages
19
+
20
+ - [Native EMS protocol](Wattsonic_EMS_Modbus_Protocol.html): the HTML reference (also served as index.html); English, Chinese, German and French.
21
+ - [SunSpec interface](sunspec.html): the HTML reference of the SunSpec map.
22
+
23
+ ## Optional
24
+
25
+ - [Full text](llms-full.txt): both Markdown documents in one file.
26
+ - [Register map as published by the page](Wattsonic_MATIC_Modbus_Registers.json): the original register map file offered by the Download JSON button.