e3dc-ess-modbus 0.1.0__tar.gz

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.
Files changed (29) hide show
  1. e3dc_ess_modbus-0.1.0/.github/renovate.json +47 -0
  2. e3dc_ess_modbus-0.1.0/.github/workflows/ci.yml +39 -0
  3. e3dc_ess_modbus-0.1.0/.github/workflows/publish.yml +46 -0
  4. e3dc_ess_modbus-0.1.0/.gitignore +10 -0
  5. e3dc_ess_modbus-0.1.0/LICENSE +202 -0
  6. e3dc_ess_modbus-0.1.0/PKG-INFO +289 -0
  7. e3dc_ess_modbus-0.1.0/README.md +273 -0
  8. e3dc_ess_modbus-0.1.0/RELEASING.md +28 -0
  9. e3dc_ess_modbus-0.1.0/pyproject.toml +63 -0
  10. e3dc_ess_modbus-0.1.0/script/query.py +126 -0
  11. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/__init__.py +69 -0
  12. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/control.py +520 -0
  13. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/device.py +311 -0
  14. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/enums.py +68 -0
  15. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/errors.py +45 -0
  16. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/holds.py +346 -0
  17. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/identity.py +41 -0
  18. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/inverter.py +56 -0
  19. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/meters.py +45 -0
  20. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/model.py +50 -0
  21. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/power.py +73 -0
  22. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/py.typed +0 -0
  23. e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/wallbox.py +108 -0
  24. e3dc_ess_modbus-0.1.0/tests/__init__.py +0 -0
  25. e3dc_ess_modbus-0.1.0/tests/conftest.py +116 -0
  26. e3dc_ess_modbus-0.1.0/tests/test_device.py +581 -0
  27. e3dc_ess_modbus-0.1.0/tests/test_holds.py +530 -0
  28. e3dc_ess_modbus-0.1.0/tests/test_writes.py +840 -0
  29. e3dc_ess_modbus-0.1.0/uv.lock +458 -0
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "description": "Renovate configuration for e3dc-ess-modbus",
4
+ "dependencyDashboard": false,
5
+ "extends": [
6
+ "config:recommended",
7
+ ":separateMajorReleases",
8
+ ":combinePatchMinorReleases",
9
+ "helpers:pinGitHubActionDigests"
10
+ ],
11
+ "rebaseWhen": "conflicted",
12
+ "ignorePaths": [
13
+ "**/node_modules/**"
14
+ ],
15
+ "github-actions": {
16
+ "enabled": true
17
+ },
18
+ "lockFileMaintenance": {
19
+ "enabled": true,
20
+ "schedule": [
21
+ "before 5am on monday"
22
+ ]
23
+ },
24
+ "packageRules": [
25
+ {
26
+ "matchManagers": [
27
+ "github-actions"
28
+ ],
29
+ "groupName": "GitHub Actions",
30
+ "minimumReleaseAge": "3 days"
31
+ },
32
+ {
33
+ "matchDepTypes": [
34
+ "dev",
35
+ "test"
36
+ ],
37
+ "groupName": "Dev & Test Dependencies",
38
+ "minimumReleaseAge": "3 days"
39
+ },
40
+ {
41
+ "matchPackageNames": [
42
+ "modbus-connection"
43
+ ],
44
+ "groupName": "Core Protocol Dependencies"
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ lint:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
17
+ with:
18
+ persist-credentials: false
19
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
20
+ - name: Install dependencies
21
+ run: uv sync --locked
22
+ - name: Ruff format
23
+ run: uv run ruff format --check .
24
+ - name: Ruff lint
25
+ run: uv run ruff check .
26
+ - name: Mypy
27
+ run: uv run mypy
28
+
29
+ test:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
33
+ with:
34
+ persist-credentials: false
35
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
36
+ - name: Install dependencies
37
+ run: uv sync --locked
38
+ - name: Run tests
39
+ run: uv run pytest
@@ -0,0 +1,46 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ env:
13
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
14
+ steps:
15
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
16
+ with:
17
+ persist-credentials: false
18
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
19
+ with:
20
+ version: "0.11.17"
21
+ enable-cache: false
22
+ - run: uv sync --locked
23
+ - run: uv run pytest
24
+ - name: Set the package version from the release tag
25
+ run: |
26
+ uv version --frozen "$RELEASE_TAG"
27
+ test "$(uv version --short)" = "$RELEASE_TAG"
28
+ - run: uv build
29
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+
34
+ publish:
35
+ needs: build
36
+ runs-on: ubuntu-latest
37
+ # The pypi environment only accepts version tags and needs an approval
38
+ environment: pypi
39
+ permissions:
40
+ id-token: write
41
+ steps:
42
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .coverage
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2025 William Swann
190
+ Copyright 2026 darkrain-nl
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,289 @@
1
+ Metadata-Version: 2.5
2
+ Name: e3dc-ess-modbus
3
+ Version: 0.1.0
4
+ Summary: Read and control E3/DC home power stations over Modbus TCP.
5
+ Project-URL: Homepage, https://github.com/AlCalzone/e3dc-ess-modbus
6
+ Author: AlCalzone
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ Keywords: battery,e3dc,energy-storage,hagerenergy,modbus,solar
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Home Automation
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.12
14
+ Requires-Dist: modbus-connection<5,>=4.12.4
15
+ Description-Content-Type: text/markdown
16
+
17
+ # e3dc-ess-modbus
18
+
19
+ Read and control E3/DC home power stations over Modbus TCP.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install e3dc-ess-modbus "modbus-connection[tmodbus]"
25
+ ```
26
+
27
+ It needs Python 3.12 or later and [modbus-connection](https://github.com/home-assistant-libs/modbus-connection) 4.12.4 or later. The examples use its tmodbus backend, which `modbus-connection[tmodbus]` installs.
28
+
29
+ ## Device setup
30
+
31
+ - **Protocol:** turn Modbus on under **Smart Functions** > **Smart Home** > **Modbus**, and set **Protocol** to **E3DC**. SunSpec mode is not supported.
32
+ - **Connection:** Modbus TCP, port 502 by default. The port is set in the same menu.
33
+ - **Unit ID:** 1.
34
+ - **Firmware:** grid control needs release 2025_06 or later, battery control 2025_40, the inverter operating mode 2025_43, and battery limits, stored energy and the battery power percentage 2026_04.
35
+ - **Supported devices:** E3/DC home power stations such as the S10 E, S10 E PRO, S10 X and S20 X PRO, and systems on the Q10 and P10 firmware lines.
36
+ - **Network:** the power station answers only clients in its own subnet.
37
+
38
+ ## Usage
39
+
40
+ The library builds on modbus-connection. You open the connection and pass the library a `ModbusUnit`. The library never opens or closes the connection itself.
41
+
42
+ ```python
43
+ import asyncio
44
+
45
+ from modbus_connection import ModbusTcpParams
46
+ from modbus_connection.tmodbus import ModbusConnection
47
+ from e3dc_ess_modbus import E3dcPowerStation
48
+
49
+
50
+ async def main() -> None:
51
+ connection = ModbusConnection(ModbusTcpParams(host="192.168.1.50"))
52
+ try:
53
+ station = await E3dcPowerStation.async_detect(connection.for_unit(1))
54
+ await station.async_update()
55
+ print("PV:", station.power.pv_power, "W")
56
+ print("Battery:", station.power.battery_soc, "%")
57
+ print("Grid:", station.power.grid_power, "W")
58
+ if station.battery_control is not None:
59
+ await station.battery_control.async_write_battery_power(3000)
60
+ finally:
61
+ await connection.close()
62
+
63
+
64
+ asyncio.run(main())
65
+ ```
66
+
67
+ Every value is `None` until the first update. Each update returns an `UpdateReport`. `report.updated` names the components that refreshed. `report.failed` maps the others to their error, and those keep their previous values. A lost connection raises `ModbusConnectionError`.
68
+
69
+ ## Gotchas
70
+
71
+ - **Request rate:** the device accepts at most 10 requests per second. The library spaces its requests by 100 ms to stay under that.
72
+ - **Optional components:** setup probes which components the firmware serves and logs the missing ones at `INFO`. Those it doesn't serve stay `None`.
73
+ - **Setpoints expire:** the feed-in limit and the battery power lapse 30 seconds after the last write. The active power limit and the reactive power setpoints hold for the failsafe timeouts, 30 seconds by default. A timeout of 0 keeps them until the next write.
74
+ - **Holding a setpoint:** a component's write method writes a setpoint once. To hold one, use `SetpointHolds`, which writes it again every 10 seconds until it is stopped. The `*_setpoint` attributes keep the last value written that the device acknowledged, as encoded. They don't clear when the setpoint lapses, only when the connection drops.
75
+ - **Reading back a write:** a write doesn't refresh the cached values. Call `await async_update_settings()` to read back what took effect.
76
+ - **Two-register writes:** the feed-in limit and the battery power go out as one FC16 write. If the device refuses FC16 as an illegal function, the library logs it once and sends two single-register writes from then on. Between those two writes, the register pair holds a value that is neither the old nor the new one. If the pair may be left that way, by an error or a cancellation, the library ends remote battery control, or writes the last feed-in limit back, before it raises. Writes to the battery and the feed-in limit wait for each other, so they reach the device in the order they were made.
77
+ - **Feed-in limit:** `feed_in_limit` reads back the last value written. The limit in effect may differ.
78
+ - **Wallbox bits:** which bits a wallbox uses depends on its model. Unused bits stay unset.
79
+ - **Testing:** built against the vendor's Modbus TCP manual, version 2.80, and tested against modbus-connection's mock and an E3/DC one with firmware H20_2026_44.
80
+
81
+ ## API
82
+
83
+ ### `E3dcPowerStation(unit, *, timeout=None)`
84
+
85
+ An E3/DC home power station. `timeout` asks the connection for a per-request timeout, in seconds.
86
+
87
+ - `await async_detect(unit, *, timeout=None)`: build the device, read its identity and find out which optional components the firmware serves.
88
+ - `await async_update()`: refresh every component.
89
+ - `await async_update_readings()`: refresh what the device measures and reports on its own: power, DC strings, wallboxes, meters, power output and inverter mode status, battery limits and inverters.
90
+ - `await async_update_settings()`: refresh the values that change only when written: the feed-in limit, the failsafe timeouts and the battery remote control flag.
91
+ - `await async_read_raw()`: every register read, undecoded.
92
+ - `serial_number`, `model`: from the identity block.
93
+ - `readings_components`, `settings_components`: the components this firmware serves, once setup has run.
94
+ - `power_meter_readings`: the eight meter slots as `PowerMeterReading`, keyed by slot 0 to 7.
95
+ - `identity`, `power`, `dc_strings`, `wallboxes`, `power_meters`, `wide_power_meters`, `grid_status`, `grid_control`, `battery_control`, `inverter_mode_control`, `battery_limits`, `inverters`: the components below.
96
+
97
+ Numbered parts are mappings keyed by the manual's numbering: DC strings 1 to 3, and wallboxes, power meters and inverters 0 to 7.
98
+
99
+ ### `identity` (`Identity`)
100
+
101
+ - `manufacturer`: the manufacturer name.
102
+ - `model`: the model, such as `S10 X`.
103
+ - `serial_number`: the serial number.
104
+ - `firmware_release`: the firmware release, such as `S10_2026_04`.
105
+ - `modbus_version`: the Modbus interface version, such as `2.3`.
106
+
107
+ ### `power` (`PowerFlows`)
108
+
109
+ - `pv_power`: solar power, in W.
110
+ - `battery_power`: positive while charging, negative while discharging, in W.
111
+ - `house_power`: house consumption, in W.
112
+ - `grid_power`: positive while importing, negative while feeding in, in W.
113
+ - `additional_power`: power from additional feeders, such as a second PV inverter, in W.
114
+ - `wallbox_power`: wallbox power, in W.
115
+ - `wallbox_solar_power`: the solar share of the wallbox power, in W.
116
+ - `battery_soc`: state of charge, in %.
117
+ - `autarky`: self-sufficiency, in %.
118
+ - `self_consumption`: self-consumption, in %.
119
+ - `emergency_power_status`: an `EmergencyPowerStatus`.
120
+ - `ems_status`: `EmsStatus` flags.
121
+
122
+ ### `dc_strings` (`DcStrings`)
123
+
124
+ A mapping of string number 1 to 3 to `DcString`. The manual marks string 3 as not used.
125
+
126
+ - `voltage`: string voltage, in V.
127
+ - `current`: string current, in A.
128
+ - `power`: string power, in W.
129
+
130
+ ### `wallboxes` (`Wallboxes`)
131
+
132
+ - `slots`: eight `Wallbox` objects, in the order the device's wallbox configuration sets.
133
+ - `configured`: the configured wallboxes, keyed by slot.
134
+
135
+ Each `Wallbox` has:
136
+
137
+ - `slot`: its slot, from 0 to 7.
138
+ - `available`: a wallbox is configured in this slot.
139
+ - `charging_allowed`: charging is allowed. It reads the same bit as `charging_aborted`, inverted.
140
+ - `charging_aborted`: charging is not allowed.
141
+ - `charging`: a vehicle is drawing power. The wallbox sets this bit itself.
142
+ - `solar_mode`: charging from solar only. Unset means hybrid mode.
143
+ - `one_phase`: charging on one phase. Unset means three.
144
+ - `type2_plugged`, `type2_locked`: the type 2 plug is plugged in, or locked.
145
+ - `schuko_on`, `schuko_plugged`, `schuko_locked`: the type F socket is on, plugged in, or locked. Only the Wallbox classic can switch the socket.
146
+ - `relay_16a_1p`, `relay_16a_3p`, `relay_32a_3p`: which relay is closed.
147
+ - `await async_write_charging_allowed(allowed)`: allow or abort charging.
148
+ - `await async_write_solar_mode(enabled)`: charge from solar only, or in hybrid mode.
149
+ - `await async_write_one_phase(enabled)`: charge on one phase, or on three.
150
+ - `await async_write_schuko_on(on)`: switch the type F socket on or off, on a Wallbox classic.
151
+
152
+ ### `power_meters` (`PowerMeters`)
153
+
154
+ - `meters`: eight `PowerMeter` slots with `meter_type` and `power_l1` to `power_l3`. Read them through `power_meter_readings`, which uses the 32-bit values when they refreshed in the last poll.
155
+
156
+ A `PowerMeterReading` has:
157
+
158
+ - `meter_type`: a `PowerMeterType`, or `None` for an empty slot.
159
+ - `is_installed`: a meter is configured in this slot.
160
+ - `power_l1`, `power_l2`, `power_l3`: power per phase, in W.
161
+
162
+ ### `wide_power_meters` (`WidePowerMeters`)
163
+
164
+ Optional. Present when the firmware serves the 32-bit meter values.
165
+
166
+ - `sg_ready_status`: the SG Ready status code. The manual documents no values.
167
+
168
+ ### `grid_status` (`GridStatus`)
169
+
170
+ Optional. Present on firmware 2025_06 or later.
171
+
172
+ - `power_output_ready`: the inverter, DC-DC converter and battery can deliver power.
173
+
174
+ ### `grid_control` (`GridControl`)
175
+
176
+ Optional. Present on firmware 2025_06 or later.
177
+
178
+ - `feed_in_limit`: the last feed-in limit written, in W.
179
+ - `active_power_failsafe_timeout`: how long an active power setpoint holds, in s.
180
+ - `reactive_power_failsafe_timeout`: how long a reactive power setpoint holds, in s.
181
+ - `active_power_limit_setpoint`, `active_power_failsafe_setpoint`, `reactive_power_failsafe_setpoint`: the last values acknowledged, in %.
182
+ - `reactive_power_setpoint` (var), `cos_phi_setpoint`, `reactive_power_percent_setpoint` (%): the last reactive power acknowledged, in the form it was written. The other two forms are `None`.
183
+ - `await async_write_feed_in_limit(watts)`: cap the power fed into the grid.
184
+ - `await async_write_active_power_limit(percent)`: cap the inverter's output, in % of rated power.
185
+ - `await async_write_active_power_failsafe(percent)`: the active power limit after a setpoint expires.
186
+ - `await async_write_reactive_power(var)`: set the reactive power, in var.
187
+ - `await async_write_reactive_power_percent(percent)`: set the reactive power, in % of rated apparent power.
188
+ - `await async_write_cos_phi(value)`: set the reactive power as cos φ, from -600 to 600.
189
+ - `await async_write_reactive_power_failsafe(percent)`: the reactive power after a setpoint expires.
190
+ - `await async_write_active_power_failsafe_timeout(seconds)`: how long an active power setpoint holds. 0 turns the failsafe off.
191
+ - `await async_write_reactive_power_failsafe_timeout(seconds)`: how long a reactive power setpoint holds. 0 turns the failsafe off.
192
+
193
+ ### `battery_control` (`BatteryControl`)
194
+
195
+ Optional. Present on firmware 2025_40 or later.
196
+
197
+ - `remote_control_disabled`: register 40199. Writing 1 ends remote battery control at once. The manual says 0 has no effect.
198
+ - `battery_power_setpoint`: the last battery power acknowledged, in W. `None` after a stop or a percentage write.
199
+ - `await async_write_battery_power(watts)`: charge (positive) or discharge (negative) at a fixed power.
200
+ - `await async_stop_remote_control()`: hand control back at once, by writing 1 to 40199.
201
+ - `await async_write_remote_control_disabled(disabled)`: write 40199. True does what `async_stop_remote_control()` does.
202
+
203
+ On firmware 2026_04 and later, `battery_limits` sets the battery power as a percentage too.
204
+
205
+ ### `inverter_mode_control` (`InverterModeControl`)
206
+
207
+ Optional. Present on firmware 2025_43 or later.
208
+
209
+ - `inverter_mode_request`: the forced inverter mode, an `InverterModeRequest`.
210
+ - `inverter_mode`: the inverter mode in effect, an `InverterMode`.
211
+ - `await async_write_inverter_mode_request(mode)`: force an `InverterModeRequest`.
212
+
213
+ ### `battery_limits` (`BatteryLimits`)
214
+
215
+ Optional. Present on firmware 2026_04 or later.
216
+
217
+ - `max_charge_power`: what the battery can take right now, in W.
218
+ - `max_discharge_power`: what the battery can give right now, negative, in W.
219
+ - `remaining_energy`: stored energy, in Wh.
220
+ - `full_charge_energy`: capacity when full, in Wh.
221
+ - `battery_power_percent_setpoint`: the last battery power percentage acknowledged. `None` after a stop or a watt write.
222
+ - `await async_write_battery_power_percent(percent)`: charge (positive) or discharge (negative) at a share of full power. The watt write is on `battery_control`.
223
+
224
+ ### `inverters` (`Inverters`)
225
+
226
+ Optional. Present when any inverter slot answers.
227
+
228
+ A mapping of slot number to `Inverter`, with every slot that answers its registers. Inverter 0 is the device's own. Slots 1 to 7 are for additional solar inverters, and an empty slot may answer with zeros. A slot that fails during setup stays absent until a new `E3dcPowerStation` sets up.
229
+
230
+ - `is_connected`: the inverter reports a grid voltage.
231
+ - `active_power_l1` to `active_power_l3`: active power per phase, in W.
232
+ - `apparent_power_l1` to `apparent_power_l3`: apparent power per phase, in VA.
233
+ - `reactive_power_l1` to `reactive_power_l3`: reactive power per phase, in var.
234
+ - `voltage_l1` to `voltage_l3`: AC voltage per phase, in V.
235
+ - `current_l1` to `current_l3`: AC current per phase, in A.
236
+ - `frequency`: grid frequency, in Hz.
237
+ - `dc_power_1` to `dc_power_3`: DC power per tracker, in W.
238
+ - `dc_voltage_1` to `dc_voltage_3`: DC voltage per tracker, in V.
239
+ - `dc_current_1` to `dc_current_3`: DC current per tracker, in A.
240
+
241
+ ### `SetpointHolds(station, *, interval=10.0, on_change=None)`
242
+
243
+ Keeps setpoints alive against the 30 second watchdog. One lock serializes starting, stopping and rewriting. With `interval=None` it runs no loop of its own, and the caller calls `async_rewrite()` every 10 seconds or less. An interval must be above 0 and at most 20 seconds. If the active power failsafe timeout (40194) is shorter than the interval, the active power limit lapses between rewrites. `on_change` is called whenever a hold changes, and an exception it raises is logged.
244
+
245
+ - `Setpoint`: `BATTERY_POWER` (W), `FEED_IN_LIMIT` (W), `ACTIVE_POWER_LIMIT` (%).
246
+ - `get(setpoint)`: the `Hold`, a frozen snapshot with `value`, `failing` (the last rewrite failed), `releasing` (a hand-back failed and is retried) and `confirmed` (neither), or `None`.
247
+ - `requested(setpoint)`: the last value written or remembered, held or not.
248
+ - `remember(setpoint, value)`: remember a value for `async_resume` without writing it.
249
+ - `await async_start(setpoint, value)`: write a value and hold it. A failed write keeps the current hold, except a half-written one, which ends it. Raises `E3dcError` if the firmware lacks the setpoint's registers.
250
+ - `await async_resume(setpoint)`: hold the last requested value again.
251
+ - `await async_stop(setpoint)`: hand control back. The battery leaves remote control, the active power limit goes to 100 %, and the feed-in limit lapses within 30 seconds. A failed hand-back stays as releasing and is retried on each rewrite. A failed battery hand-back is dropped after 30 seconds, when the watchdog has dropped the setpoint anyway.
252
+ - `await async_rewrite()`: write every hold again. A device refusing one setpoint doesn't stop the others. A connection error or timeout ends the pass, and the setpoints it skipped count as failing.
253
+ - `await async_close(timeout=5.0)`: hand everything back and refuse new holds. Waiting for a running rewrite and the hand-backs take at most `timeout` together. Afterwards nothing is held, even if a hand-back failed.
254
+
255
+ ### Enums
256
+
257
+ - `EmergencyPowerStatus`: `NOT_SUPPORTED`, `ACTIVE`, `INACTIVE`, `NOT_AVAILABLE`, `MOTOR_SWITCH_OFF`.
258
+ - `EmsStatus` (flags): `CHARGING_LOCKED`, `DISCHARGING_LOCKED`, `EMERGENCY_POWER_READY`, `WEATHER_BASED_CHARGING`, `FEED_IN_LIMITED`, `CHARGE_LOCK_TIME`, `DISCHARGE_LOCK_TIME`.
259
+ - `PowerMeterType`: `ROOT`, `EXTERNAL_PRODUCTION`, `BIDIRECTIONAL`, `EXTERNAL_CONSUMPTION`, `FARM`, `NOT_USED`, `WALLBOX`, `EXTERNAL_FARM`, `DATA_DISPLAY`, `CONTROL_BYPASS`.
260
+ - `InverterModeRequest`: `DISABLE_OVERWRITE`, `FORCE_IDLE_OFF_GRID_POWER_SAVE`.
261
+ - `InverterMode`: `INIT`, `IDLE_OFF_GRID_POWER_SAVE`.
262
+
263
+ ### Errors
264
+
265
+ All errors the library raises itself derive from `E3dcError`. Transport and device errors come from modbus-connection.
266
+
267
+ - `NotAnE3dcError`: the device refused the identity block, or didn't identify as an E3/DC power station in E3DC mode. Raised by `async_detect()` and the first update.
268
+ - `InvalidValueError`: a value to write is out of range or of the wrong type. It is also a `ValueError`. Nothing is written.
269
+ - `InconsistentWriteError`: on a device that refuses FC16, a two-register setpoint failed after its first half may have landed. The library has written 1 to 40199 for the battery power, or the last feed-in limit for the feed-in limit, and forgotten the setpoint.
270
+ - `HoldsClosedError`: `SetpointHolds` was closed.
271
+ - `E3dcError` itself: `SetpointHolds` was asked to hold a setpoint the firmware has no registers for.
272
+ - `NoSetpointError`: `async_resume` has no value to hold.
273
+ - `ModbusConnectionError`: the connection was lost.
274
+ - `ModbusTimeoutError`: the device didn't answer before any component had refreshed.
275
+ - `ModbusExceptionError`: the device refused a write, for example a setpoint on firmware that doesn't support it.
276
+
277
+ The register addresses are in the component modules under [`src/e3dc_ess_modbus`](src/e3dc_ess_modbus), and in the vendor's Modbus TCP manual on the [E3/DC customer portal](https://my.e3dc.com).
278
+
279
+ ## Troubleshooting
280
+
281
+ `script/query.py` reads the device once and prints every value:
282
+
283
+ ```bash
284
+ uv run script/query.py 192.168.1.50
285
+ ```
286
+
287
+ Add `--raw` to include the undecoded registers. A bug report about a wrong value needs them.
288
+
289
+ The `e3dc_ess_modbus` logger reports at `DEBUG` the components setup settled on. It logs each refused block at `INFO`, and a refused FC16 write or a half-written setpoint at `WARNING`.