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.
- e3dc_ess_modbus-0.1.0/.github/renovate.json +47 -0
- e3dc_ess_modbus-0.1.0/.github/workflows/ci.yml +39 -0
- e3dc_ess_modbus-0.1.0/.github/workflows/publish.yml +46 -0
- e3dc_ess_modbus-0.1.0/.gitignore +10 -0
- e3dc_ess_modbus-0.1.0/LICENSE +202 -0
- e3dc_ess_modbus-0.1.0/PKG-INFO +289 -0
- e3dc_ess_modbus-0.1.0/README.md +273 -0
- e3dc_ess_modbus-0.1.0/RELEASING.md +28 -0
- e3dc_ess_modbus-0.1.0/pyproject.toml +63 -0
- e3dc_ess_modbus-0.1.0/script/query.py +126 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/__init__.py +69 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/control.py +520 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/device.py +311 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/enums.py +68 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/errors.py +45 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/holds.py +346 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/identity.py +41 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/inverter.py +56 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/meters.py +45 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/model.py +50 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/power.py +73 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/py.typed +0 -0
- e3dc_ess_modbus-0.1.0/src/e3dc_ess_modbus/wallbox.py +108 -0
- e3dc_ess_modbus-0.1.0/tests/__init__.py +0 -0
- e3dc_ess_modbus-0.1.0/tests/conftest.py +116 -0
- e3dc_ess_modbus-0.1.0/tests/test_device.py +581 -0
- e3dc_ess_modbus-0.1.0/tests/test_holds.py +530 -0
- e3dc_ess_modbus-0.1.0/tests/test_writes.py +840 -0
- 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,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`.
|