├── .gitignore
├── LICENSE
├── README.md
├── changelog
├── config.sample
├── contributors.md
├── daemon3x.py
├── features-outdated
├── README.md
├── domoticz.py
├── ediplugs.py
├── influxdb.py
├── influxdb2.py
├── pvdata.py
├── pvdata_kostal_json.py
├── remotedebug.py
├── sample.py
├── sma_grafana.json
├── smamodbus.py
├── symcon.py
├── symcon_smaem_webhook.php
└── symcon_smawr_webhook.php
├── features
├── README.md
├── mqtt.py
└── simplefswriter.py
├── knownProblems.md
├── libs
└── smartplug.py
├── requirements.txt
├── sma-daemon.py
├── sma-em-capture-package.py
├── sma-em-measurement.py
├── speedwiredecoder.py
└── systemd-settings
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 |
26 | # PyInstaller
27 | # Usually these files are written by a python script from a template
28 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 | *.manifest
30 | *.spec
31 |
32 | # Installer logs
33 | pip-log.txt
34 | pip-delete-this-directory.txt
35 |
36 | # Unit test / coverage reports
37 | htmlcov/
38 | .tox/
39 | .coverage
40 | .coverage.*
41 | .cache
42 | nosetests.xml
43 | coverage.xml
44 | *,cover
45 |
46 | # Translations
47 | *.mo
48 | *.pot
49 |
50 | # Django stuff:
51 | *.log
52 |
53 | # Sphinx documentation
54 | docs/_build/
55 |
56 | # PyBuilder
57 | target/
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
341 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SMA-EM
2 |
3 | a detailed german description could be found here
4 | https://www.unifox.at/software/sma-em-daemon/
5 |
6 | translated by google
7 | https://translate.google.com/translate?sl=de&tl=en&u=https://www.unifox.at/software/sma-em-daemon/
8 |
9 |
10 | ## SMA Energymeter / Homemanager measurement
11 | sma-em-measurement.py: Python3 loop display SMA Energymeter measurement values
12 |
13 | sma-daemon.py: Python3 daemon writing consume and supply values to /run/shm/em-[serial]-[value]
14 |
15 | ```
16 | # HINT #
17 | Sma homemanager version 2.3.4R added 8 Byte of measurement data.
18 | This version trys to detect the measurement values on obis ids, so it should be save if new values were added or removed.
19 | ```
20 |
21 | ## Requirements
22 | python3
23 | sys
24 | time
25 | configparser (SafeConfigParser)
26 | signal
27 |
28 | some features require additional python modules
29 | features/README.md should give an overview of maintained features.
30 | features-outdated/README.md: other features untested because I do not have the appropriate hardware / software could be found in features-outdated.
31 |
32 |
33 | ## Configuration
34 | create a config file in /etc/smaemd/config
35 | Use UTF-8 encoded configfile
36 | Example:
37 | ```
38 | [SMA-EM]
39 | # serials of sma-ems the daemon should take notice
40 | # seperated by space
41 | serials=30028xxxxx
42 | # features could filter serials to, but wouldn't see serials if these serials was not defines in SMA-EM serials
43 | # list of features to load/run
44 | features=simplefswriter sample
45 |
46 | [DAEMON]
47 | pidfile=/run/smaemd.pid
48 | # listen on an interface with the given ip
49 | # use 0.0.0.0 for any interface
50 | ipbind=192.168.8.15
51 | # multicast ip and port of sma-datagrams
52 | # defaults
53 | mcastgrp=239.12.255.254
54 | mcastport=9522
55 |
56 | # each feature/plugin has its own section
57 | # called FEATURE-[featurename]
58 | # the feature section is required if a feature is listed in [SMA-EM]features
59 |
60 | [FEATURE-simplefswriter]
61 | # list serials simplefswriter notice
62 | serials=1900204522
63 | # measurement vars simplefswriter should write to filesystem (only from smas with serial in serials)
64 | values=pconsume psupply qsupply ssupply
65 |
66 | [FEATURE-sample]
67 | nothing=here
68 |
69 | ```
70 |
71 | ## Routing
72 | maybe you have to add a route (example: on hosts with more than one interface)
73 | ```
74 | sudo ip route add 224.0.0.0/4 dev interfacename
75 | ```
76 |
77 | ## Install / Copy (tested on Raspbian 9.1)
78 | ```
79 | sudo apt install git
80 | sudo apt install python3 cl-py-configparser
81 | sudo mkdir /opt/smaemd/
82 | sudo mkdir /etc/smaemd/
83 | sudo useradd -c "smaemd-user" -d /opt/smaemd -M -N -r -s /usr/sbin/nologin smaemd
84 | cd /opt/smaemd/
85 | sudo git clone https://github.com/datenschuft/SMA-EM.git .
86 | sudo cp systemd-settings /etc/systemd/system/smaemd.service
87 | ```
88 |
89 | Create a /etc/smaemd/config file
90 | ```
91 | sudo cp /opt/smaemd/config.sample /etc/smaemd/config
92 | ```
93 | Edit the /etc/smaemd/config file and customize it to suit your needs (e.g. set SMA energy meter serial number, IP address, enable features)
94 | ```
95 | sudo nano /etc/smaemd/config
96 | ```
97 |
98 | Update systemd
99 | ```
100 | sudo systemctl daemon-reload
101 | sudo systemctl enable smaemd.service
102 | sudo systemctl start smaemd.service
103 | ```
104 | feel lucky and read /run/shm/em--
105 |
106 |
107 |
108 | ## Testing
109 | sma-em-capture-package - trys to capture a SMA-EM or SMA-homemanager Datagram and display hex and ascii package-info and all recogniced measurement values.
110 | Cloud be helpful on package/software changes.
111 |
--------------------------------------------------------------------------------
/changelog:
--------------------------------------------------------------------------------
1 | SMA-EM-Daemon changelog-File
2 | 20240728001
3 | robustness check for dictionary content
4 | documentation switch from root to a new user (example smaemd in systemd-unit)
5 | focus on core competencies, remove features except mqtt and simplefswriter (other moved to features-outdated)
6 |
7 | 20210307001
8 | improve the feature init
9 | start_systemd; no doubleforking for Systemd
10 |
11 | 20200104001
12 | fixed issue 21 new datagramsize with homemanager version 2.3.4.R
13 | package split based on coding pull request 18 (david-m-m)
14 |
15 | 20190402001
16 | Cosmetics / variables naming #14
17 | replaced pregard with consume and surplus with supply
18 |
19 | 20180223001
20 | Merge branch 'Tommi2Day-master' https://github.com/datenschuft/SMA-EM/pull/13
21 | thanks to Tommi2Day
22 | - sma-daemon: allow read config from workdir, make status dir configurable, add config callback, exit if serials not set,small fixes
23 | - add feature "pvdata" for getting PV data from SMA Inverters via Modbus along SMA-EM/HM
24 | - add feature "mqtt" to send SMA EM and PV data to an MQTT broker
25 | - add feature "remotedebug" to allow remote debug from PyCharm
26 | - add feature "influxdb" and sample grafana dashboard based on this plugin
27 | - add feature "symcon" to supply SMA EM/HOM and PV data to "IP-Symcon" (https://www.symcon.de/en/ ) via WebHook and provide sample webhook scripts
28 | - add config.sample
29 |
30 | older versions were not tracked
31 |
--------------------------------------------------------------------------------
/config.sample:
--------------------------------------------------------------------------------
1 | [SMA-EM]
2 | # serials of sma-ems the daemon should take notice
3 | # separated by space
4 | serials=30028xxxxx
5 | # features could filter serials to, but wouldn't see serials if these serials was not defines in SMA-EM serials
6 | # list of features to load/run
7 | #features=simplefswriter sample pvdata ediplugs mqtt remotedebug symcon influxdb
8 | features=simplefswriter
9 |
10 | [DAEMON]
11 | pidfile=/run/smaemd.pid
12 | # listen on an interface with the given ip
13 | # use 0.0.0.0 for any interface
14 | ipbind=0.0.0.0
15 | # multicast ip and port of sma-datagrams
16 | # defaults
17 | mcastgrp=239.12.255.254
18 | mcastport=9522
19 | statusdir=
20 |
21 | # each feature/plugin has its own section
22 | # called FEATURE-[featurename]
23 | # the feature section is required if a feature is listed in [SMA-EM]features
24 |
25 | [FEATURE-simplefswriter]
26 | # list serials simplefswriter notice
27 | serials=30028xxxxx
28 | # measurement vars simplefswriter should write to filesystem (only from smas with serial in serials)
29 | values=pconsume psupply qsupply ssupply
30 | statusdir=
31 |
32 | [FEATURE-sample]
33 | nothing=here
34 |
35 | [FEATURE-mqtt]
36 | # MQTT broker details
37 | #mqtthost=::1
38 | mqtthost=mqtt
39 | mqttport=1883
40 | #mqttuser=
41 | #mqttpass=
42 |
43 | #The following list contains all possible field names that you can use with
44 | #the features mqtt, symcon, influxdb
45 | # prefix: p=real power, q=reactive power, s=apparent power, i=current, u=voltage
46 | # postfix: unit=the unit of the item, e.g. W, VA, VAr, Hz, A, V, kWh, kVArh, kVAh ...
47 | # postfix: counter=energy value (kWh, kVArh, kVAh)
48 | # without postfix counter=>power value (W, VAr, VA)
49 | #mqttfields=pconsume, pconsumeunit, pconsumecounter, pconsumecounterunit,
50 | # psupply, psupplyunit, psupplycounter, psupplycounterunit,
51 | # qconsume, qconsumeunit, qconsumecounter, qconsumecounterunit,
52 | # qsupply, qsupplyunit, qsupplycounter, qsupplycounterunit,
53 | # sconsume, sconsumeunit, sconsumecounter, sconsumecounterunit,
54 | # ssupply, ssupplyunit, ssupplycounter, ssupplycounterunit,
55 | # cosphi, cosphiunit,
56 | # frequency, frequencyunit,
57 | # p1consume, p1consumeunit, p1consumecounter, p1consumecounterunit,
58 | # p1supply, p1supplyunit, p1supplycounter, p1supplycounterunit,
59 | # q1consume, q1consumeunit, q1consumecounter, q1consumecounterunit,
60 | # q1supply, q1supplyunit, q1supplycounter, q1supplycounterunit,
61 | # s1consume, s1consumeunit, s1consumecounter, s1consumecounterunit,
62 | # s1supply, s1supplyunit, s1supplycounter, s1supplycounterunit,
63 | # i1, i1unit,
64 | # u1, u1unit,
65 | # cosphi1, cosphi1unit,
66 | # p2consume, p2consumeunit, p2consumecounter, p2consumecounterunit,
67 | # p2supply, p2supplyunit, p2supplycounter, p2supplycounterunit,
68 | # q2consume, q2consumeunit, q2consumecounter, q2consumecounterunit,
69 | # q2supply, q2supplyunit, q2supplycounter, q2supplycounterunit,
70 | # s2consume, s2consumeunit, s2consumecounter, s2consumecounterunit,
71 | # s2supply, s2supplyunit, s2supplycounter, s2supplycounterunit,
72 | # i2, i2unit,
73 | # u2, u2unit,
74 | # cosphi2, cosphi2unit,
75 | # p3consume, p3consumeunit, p3consumecounter, p3consumecounterunit,
76 | # p3supply, p3supplyunit, p3supplycounter, p3supplycounterunit,
77 | # q3consume, q3consumeunit, q3consumecounter, q3consumecounterunit,
78 | # q3supply, q3supplyunit, q3supplycounter, q3supplycounterunit,
79 | # s3consume, s3consumeunit, s3consumecounter, s3consumecounterunit,
80 | # s3supply, s3supplyunit, s3supplycounter, s3supplycounterunit,
81 | # i3, i3unit,
82 | # u3, u3unit,
83 | # cosphi3, cosphi3unit,
84 | # speedwire-version
85 | mqttfields=pconsume,pconsumecounter,psupply,psupplycounter
86 | #topic will be exteded with serial
87 | mqtttopic=SMA-EM/status
88 | pvtopic=SMA-PV/status
89 | # publish all values as single topics (0 or 1)
90 | publish_single=1
91 | # How frequently to send updates over (defaults to 20 sec)
92 | min_update=5
93 | #debug output
94 | debug=0
95 |
96 | # ssl support
97 | # adopt mqttport above to your ssl enabled mqtt port, usually 8883
98 | # options:
99 | # activate without certs=use tls_insecure
100 | # activate with ca_file, but without client_certs
101 | ssl_activate=0
102 | # ca file to verify
103 | ssl_ca_file=ca.crt
104 | # client certs
105 | ssl_certfile=
106 | ssl_keyfile=
107 | #TLSv1.1 or TLSv1.2 (default 2)
108 | tls_protocol=2
109 |
110 |
111 | [FEATURE-remotedebug]
112 | # Debug settings
113 | debughost=mypc
114 | debugport=9100
115 |
116 | [FEATURE-symcon]
117 | # symcon
118 | host=ips
119 | port=3777
120 | timeout=5
121 | user=Symcon
122 | password=SMA-EMdata
123 |
124 | #A list of possible field names can be found above under FEATURE-mqtt
125 | fields=pconsume,psupply,p1consume,p2consume,p3consume,p1supply,p2supply,p3supply,psupplycounter,pconsumecounter
126 | emhook=/hook/smaem
127 | pvfields=AC Power,grid frequency,DC input voltage,daily yield,total yield,Power L1,Power L2,Power L3,Status
128 | pvhook=/hook/smawr
129 |
130 | # How frequently to send updates over (defaults to 20 sec)
131 | min_update=30
132 |
133 | debug=0
134 |
135 | [FEATURE-influxdb]
136 | # influx
137 | host=influxdb
138 | port=8086
139 | ssl=
140 | db=SMA
141 |
142 | timeout=5
143 | user=
144 | password=
145 | # How frequently to send updates over (defaults to 20 sec)
146 | min_update=30
147 | debug=0
148 |
149 | # emdata
150 | # A list of possible field names can be found above under FEATURE-mqtt
151 | measurement=SMAEM
152 | fields=pconsume,psupply,p1consume,p2consume,p3consume,p1supply,p2supply,p3supply
153 |
154 | # pvdata
155 | # Fields can be any modbus register queried under FEATURE-pvdata except serial, DeviceID, and Device Name,
156 | # as those are used as tags in any case.
157 | pvmeasurement=SMAWR
158 | pvfields=AC Power,grid frequency,DC input voltage,daily yield,total yield,Power L1,Power L2,Power L3
159 |
160 | # ediplugs
161 | edimeasurement=edimax
162 |
163 | [FEATURE-influxdb2]
164 | debug=0
165 | url=hostname.tld
166 | token=long_token
167 | org=org_name
168 | bucket=bucket_name
169 |
170 | # emdata
171 | # A list of possible field names can be found above under FEATURE-mqtt
172 | measurement=SMAEM
173 | fields=pconsume,psupply,p1consume,p2consume,p3consume,p1supply,p2supply,p3supply
174 |
175 | # pvdata
176 | # Fields can be any modbus register queried under FEATURE-pvdata except serial, DeviceID, and Device Name,
177 | # as those are used as tags in any case.
178 | pvmeasurement=SMAWR
179 | pvfields=AC Power,AC Voltage,grid frequency,DC Power,DC input voltage,daily yield,total yield
180 | # How frequently to send updates over (defaults to 20 sec)
181 | min_update=30
182 |
183 | [FEATURE-pvdata]
184 | #Reads data from SMA inverter via Modbus.
185 | #Enable the mqtt feature to publish the data to a mqtt broker (features=pvdata mqtt),
186 | #and/or stored the data to a influx database (features=pvdata influxdb), and/or symcom ...
187 |
188 | # How frequently to send updates over (defaults to 20 sec)
189 | min_update=5
190 | # debug output
191 | debug=0
192 |
193 | # inverter connection
194 | # ['host', 'port', 'modbus_id', 'manufacturer']
195 | inverters = [
196 | ['', '502', '3', 'SMA'],
197 | ['', '502', '3', 'SMA']
198 | ]
199 |
200 | # For Modbus registers, see e.g. https://www.google.com/search?q=SMA_Modbus-TI-en-23.xlsx
201 | # ['Modbus register address', 'Type', 'Format', 'Name', 'Unit']
202 | # If the mqtt feature is used, 'Name' is included in the MQTT JSON payload as tag name.
203 | registers = [
204 | # Don't change names in this section as they are used by some features/*.py files
205 | # Alternatives for AC Power & daily yield in MQTT: 'SMA-EM/status/30028xxxxx/pvsum' & 'SMA-EM/status/30028xxxxx/pvdaily'
206 | # Also note that the daily yield register is broken for some inverters
207 | ['30057', 'U32', 'RAW', 'serial', ''],
208 | ['30201', 'U32', 'ENUM', 'Status',''],
209 | ['30051', 'U32', 'ENUM', 'DeviceClass',''],
210 | ['30053', 'U32', 'ENUM', 'DeviceID',''],
211 | ['40631', 'STR32', 'UTF8', 'Device Name', ''],
212 | ['30775', 'S32', 'FIX0', 'AC Power', 'W'],
213 | ['30517', 'U64', 'FIX3', 'daily yield', 'kWh'],
214 | ####################################################
215 | # ['30813', 'S32', 'FIX0', 'AC_Power_Apparent', 'VA'],
216 | ['30977', 'S32', 'FIX3', 'AC_Current', 'A'],
217 | # ['30783', 'S32', 'FIX2', 'AC_Voltage_L1', 'V'],
218 | # ['30785', 'S32', 'FIX2', 'AC_Voltage_L2', 'V'],
219 | # ['30787', 'S32', 'FIX2', 'AC_Voltage_L3', 'V'],
220 | # ['30777', 'S32', 'FIX0', 'AC_Power_L1', 'W'],
221 | # ['30779', 'S32', 'FIX0', 'AC_Power_L2', 'W'],
222 | # ['30781', 'S32', 'FIX0', 'AC_Power_L3', 'W'],
223 | ['30803', 'U32', 'FIX2', 'Grid_Frequency', 'Hz'],
224 | ['30773', 'S32', 'FIX0', 'DC_Input1_Power', 'W'],
225 | ['30771', 'S32', 'FIX2', 'DC_Input1_Voltage', 'V'],
226 | ['30769', 'S32', 'FIX3', 'DC_Input1_Current', 'A'],
227 | ['30961', 'S32', 'FIX0', 'DC_Input2_Power', 'W'],
228 | ['30959', 'S32', 'FIX2', 'DC_Input2_Voltage', 'V'],
229 | ['30957', 'S32', 'FIX3', 'DC_Input2_Current', 'A'],
230 | ['30953', 'S32', 'FIX1', 'Device_Temperature', u'\xb0C'],
231 | ['30513', 'U64', 'FIX3', 'Total_Yield', 'kWh'],
232 | ['30521', 'U64', 'FIX0', 'Operating_Time', 's'],
233 | ['30525', 'U64', 'FIX0', 'Feed-in_Time', 's'],
234 | ['30975', 'S32', 'FIX2', 'Intermediate_Circuit_Voltage', 'V'],
235 | ['30225', 'S32', 'FIX0', 'Isolation_Resistance', u'\u03a9']
236 | ]
237 |
238 | registers_batt = [
239 | # Don't change names in this section as they are used by some features/*.py files
240 | ['30057', 'U32', 'RAW', 'serial', ''],
241 | ['30201', 'U32', 'ENUM', 'Status',''],
242 | ['30051', 'U32', 'ENUM', 'DeviceClass',''],
243 | ['30053', 'U32', 'ENUM', 'DeviceID',''],
244 | ['40631', 'STR32', 'UTF8', 'Device Name', ''],
245 | ['30775', 'S32', 'FIX0', 'AC Power', 'W'],
246 | ['30517', 'U64', 'FIX3', 'daily yield', 'kWh'],
247 | ####################################################
248 | ['30953', 'S32', 'FIX1', 'Device_Temperature', u'\xb0C'],
249 | ['30849', 'S32', 'FIX1', 'BatteryTemp', u'\xb0C'],
250 | ['30843', 'S32', 'FIX3', 'BatteryAmp', 'A'],
251 | ['30851', 'U32', 'FIX2', 'BatteryVolt', 'V'],
252 | ['30845', 'U32', 'FIX0', 'BatteryCharge', u'\u0025'],
253 | ['30955', 'U32', 'ENUM', 'BatteryState', ''],
254 | ['31391', 'U32', 'ENUM', 'BatteryHealth', ''],
255 | ['30813', 'S32', 'FIX0', 'AC apparent power', 'VA'],
256 | ['30803', 'U32', 'FIX2', 'Grid_Frequency', 'Hz'],
257 | # ['30777', 'S32', 'FIX0', 'Power L1', 'W'],
258 | # ['30779', 'S32', 'FIX0', 'Power L2', 'W'],
259 | # ['30781', 'S32', 'FIX0', 'Power L3', 'W'],
260 | ['30513', 'U64', 'FIX3', 'Total_Yield', 'kWh'],
261 | ['30521', 'U64', 'FIX0', 'Operating_Time', 's'],
262 | ['30525', 'U64', 'FIX0', 'Feed-in_Time', 's'],
263 | ]
264 |
265 | [FEATURE-pvdata_kostal_json]
266 | # How frequently to send updates over (defaults to 20 sec)
267 | min_update=15
268 | #debug output
269 | debug=0
270 |
271 | #inverter connection
272 | inv_host =
273 | #['address', 'NONE', 'NONE' 'description', 'unit']
274 | # to get the same structure of sma pvdata feature
275 | registers = [
276 | ['33556736', 'NONE', 'NONE', 'DC Power', 'W'],
277 | ['33555202', 'NONE', 'NONE', 'DC string1 voltage', 'V'],
278 | ['33555201', 'NONE', 'NONE', 'DC string1 current', 'A'],
279 | ['33555203', 'NONE', 'NONE', 'DC string1 power', 'W'],
280 | ['67109120', 'NONE', 'NONE', 'AC Power', 'W'],
281 | ['67110400', 'NONE', 'NONE', 'AC frequency', 'Hz'],
282 | ['67110656', 'NONE', 'NONE', 'AC cosphi', u'\xb0C'],
283 | ['67110144', 'NONE', 'NONE', 'AC ptot limitation', ''],
284 | ['67109378', 'NONE', 'NONE', 'AC phase1 voltage', 'V'],
285 | ['67109377', 'NONE', 'NONE', 'AC phase1 current', 'A'],
286 | ['67109379', 'NONE', 'NONE', 'AC phase1 power', 'W'],
287 | ['251658754', 'NONE', 'NONE', 'yield today', 'Wh'],
288 | ['251658753', 'NONE', 'NONE', 'yield total', 'kWh'],
289 | ['251658496', 'NONE', 'NONE', 'operationtime', ''],
290 | ]
291 |
292 | [FEATURE-ediplugs]
293 | # How frequently to send updates over (defaults to 20 sec)
294 | min_update=15
295 | #debug output
296 | debug=0
297 |
298 | # Edimax SP-2101W V2 with Firmware 3.00c change their default password during initial setup.
299 | # Find the actual password using this hack: https://discourse.nodered.org/t/searching-for-help-to-read-status-of-edimax-smartplug/15789/6
300 | # ['', 'admin', '']
301 | plugs = [
302 | ['host1', 'admin', '1234'],
303 | ['host2', 'admin', '1234']
304 | ]
305 |
--------------------------------------------------------------------------------
/contributors.md:
--------------------------------------------------------------------------------
1 | SMA-EM-Daemon contributors
2 | ============================================
3 |
4 | * **[Wenger Florian](https://github.com/datenschuft)**
5 |
6 | * Initiator
7 | * Author and maintainer
8 |
9 | * **[jhagberg](https://github.com/jhagberg)**
10 |
11 | * Encoding-hints
12 |
13 | * **[mzealey](https://github.com/mzealey)**
14 |
15 | * domoticz support
16 |
17 | * **[Tommi2Day](https://github.com/Tommi2Day)**
18 |
19 | * many modifications to enhance this tool to meke more configurable to allow developing und running on windows and add a new mqtt feature and a remote debug feature
20 | * "pvdata" for getting PV data from SMA Inverters via Modbus along SMA-EM/HM
21 | * "mqtt" to send SMA EM and PV data to an MQTT broker
22 | * "remotedebug" to allow remote debug from PyCharm
23 | * "influxdb" and sample grafana dashboard based on this plugin
24 | * "symcon" to supply SMA EM/HOM and PV data to "IP-Symcon"
25 |
26 | * **[david-m-m](https://github.com/david-m-m)**
27 |
28 | * enhance mqtt module to export topics for all metrics, works with [mqtt_exporter](https://github.com/bendikwa/mqtt_exporter)
29 | * rewrite SMA HM2.0 datagram parser
30 | * parse SMA EMETER datagrams
31 |
32 |
33 | * **[sellth](https://github.com/sellth)**
34 |
35 | * improved reporting of missing module dependencies
36 |
37 | * **[AnotherDaniel](https://github.com/AnotherDaniel)**
38 |
39 | * robustness check for dictionary content
40 |
41 |
--------------------------------------------------------------------------------
/daemon3x.py:
--------------------------------------------------------------------------------
1 | """Generic linux daemon base class for python 3.x."""
2 |
3 | """
4 | Source: http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
5 | License Unknown
6 | * 2021-03-07 dervomsee improve the feature init
7 | """
8 | import sys, os, time, atexit, signal
9 |
10 | class daemon3x:
11 | """A generic daemon class.
12 |
13 | Usage: subclass the daemon class and override the run() method."""
14 |
15 | def __init__(self, pidfile): self.pidfile = pidfile
16 |
17 | def daemonize(self):
18 | """Deamonize class. UNIX double fork mechanism."""
19 |
20 | try:
21 | pid = os.fork()
22 | if pid > 0:
23 | # exit first parent
24 | sys.exit(0)
25 | except OSError as err:
26 | sys.stderr.write('fork #1 failed: {0}\n'.format(err))
27 | sys.exit(1)
28 |
29 | # decouple from parent environment
30 | os.chdir('/')
31 | os.setsid()
32 | os.umask(0)
33 |
34 | # do second fork
35 | try:
36 | pid = os.fork()
37 | if pid > 0:
38 |
39 | # exit from second parent
40 | sys.exit(0)
41 | except OSError as err:
42 | sys.stderr.write('fork #2 failed: {0}\n'.format(err))
43 | sys.exit(1)
44 |
45 | # redirect standard file descriptors
46 | sys.stdout.flush()
47 | sys.stderr.flush()
48 | si = open(os.devnull, 'r')
49 | so = open(os.devnull, 'a+')
50 | se = open(os.devnull, 'a+')
51 |
52 | os.dup2(si.fileno(), sys.stdin.fileno())
53 | os.dup2(so.fileno(), sys.stdout.fileno())
54 | os.dup2(se.fileno(), sys.stderr.fileno())
55 |
56 | # write pidfile
57 | atexit.register(self.delpid)
58 |
59 | pid = str(os.getpid())
60 | try:
61 | with open(self.pidfile,'w+') as f:
62 | f.write(pid + '\n')
63 | except PermissionError:
64 | message = "no access on pidfile"
65 | sys.stderr.write(message.format(self.pidfile))
66 | # my not work because of doubleforking
67 | sys.exit(1)
68 |
69 | def delpid(self):
70 | os.remove(self.pidfile)
71 |
72 | def start(self):
73 | """Start the daemon."""
74 |
75 | # Check for a pidfile to see if the daemon already runs
76 | try:
77 | with open(self.pidfile,'r') as pf:
78 | pid = int(pf.read().strip())
79 | except IOError:
80 | pid = None
81 |
82 | if pid:
83 | message = "pidfile {0} already exist. " + \
84 | "Daemon already running?\n"
85 | sys.stderr.write(message.format(self.pidfile))
86 | sys.exit(1)
87 | #check access to pid file, later checks my not generate readable output because of doubleforking
88 | pid = str(os.getpid())
89 | try:
90 | with open(self.pidfile,'w+') as f:
91 | f.write('checkpidaccess\n')
92 | except PermissionError:
93 | message = "no access on pidfile"
94 | sys.stderr.write(message.format(self.pidfile))
95 | sys.exit(1)
96 |
97 | # Start the daemon
98 | self.daemonize()
99 | self.config()
100 | self.run()
101 |
102 | def start_systemd(self):
103 | """Start the daemon."""
104 |
105 | # Check for a pidfile to see if the daemon already runs
106 | try:
107 | with open(self.pidfile,'r') as pf:
108 |
109 | pid = int(pf.read().strip())
110 | except IOError:
111 | pid = None
112 |
113 | if pid:
114 | message = "pidfile {0} already exist. " + \
115 | "Daemon already running?\n"
116 | sys.stderr.write(message.format(self.pidfile))
117 | sys.exit(1)
118 |
119 | #runc the main function without forking
120 | self.run()
121 |
122 | def stop(self):
123 | """Stop the daemon."""
124 |
125 | # Get the pid from the pidfile
126 | try:
127 | with open(self.pidfile,'r') as pf:
128 | pid = int(pf.read().strip())
129 | except IOError:
130 | pid = None
131 |
132 | if not pid:
133 | message = "pidfile {0} does not exist. " + \
134 | "Daemon not running?\n"
135 | sys.stderr.write(message.format(self.pidfile))
136 | return # not an error in a restart
137 |
138 | # Try killing the daemon process
139 | try:
140 | while 1:
141 | os.kill(pid, signal.SIGTERM)
142 | time.sleep(0.1)
143 | except OSError as err:
144 | e = str(err.args)
145 | if e.find("No such process") > 0:
146 | if os.path.exists(self.pidfile):
147 | os.remove(self.pidfile)
148 | else:
149 | print (str(err.args))
150 | sys.exit(1)
151 |
152 | def restart(self):
153 | """Restart the daemon."""
154 | self.stop()
155 | self.start()
156 |
157 | def restart_systemd(self):
158 | """Restart the daemon."""
159 | self.stop()
160 | self.start_systemd()
161 |
162 | def run(self):
163 | """You should override this method when you subclass Daemon.
164 |
165 | It will be called after the process has been daemonized by
166 | start() or restart()."""
167 |
168 | def config(self):
169 | """overwritten in subclass"""
170 |
171 |
--------------------------------------------------------------------------------
/features-outdated/README.md:
--------------------------------------------------------------------------------
1 | # SMA-EM daemon features (without maintenance)
2 |
3 | this page should give an overview of available features.
4 | I could not test all features, because I do not have the appropriate hardware / software.
5 |
6 | All the desired features must be activated in the configuration file
7 | ```
8 | [SMA-EM]
9 | # list of features to load/run
10 | features=simplefswriter nextfeature
11 | ```
12 | Each feature has it own configuration section in the configuration-file.
13 |
14 | [FEATURE-featurename]
15 |
16 | please have a look at the config.sample file or have a look at the features file (description) for supported configuration options.
17 |
18 | ```
19 | [FEATURE-simplefswriter]
20 | # list serials simplefswriter notice
21 | serials=1900204522
22 | # measurement vars simplefswriter should write to filesystem (only from smas with serial in serials)
23 | values=pconsume psupply qsupply ssupply
24 | ```
25 |
26 | Feature fist
27 |
28 | ## domoticz.py
29 | send SMA-measurement-values to domoticz.
30 |
31 | ## influxdb.py
32 | send SMA-measurement-values to an influxdb.
33 |
34 | ## mqtt.py
35 | send SMA-measurement-values to an mqtt broker.
36 |
37 | ## pvdata.py
38 | read sma inverter values via modbus.
39 |
40 | ## pvdata_kostal_json.py
41 | read kostal piko inverter values via http/json.
42 |
43 | ## remotedebug.py
44 | allow remote debug with PyCharm.
45 |
46 | ## sample.py
47 | a sample file; how to start writing a feature
48 |
49 | ## simplefswriter.py
50 | writes configureable measurement-values to the filesystem
51 |
52 | ## sma_grafana.json
53 | example grafana configuration to display SAM-measurement-values stored in influxdb
54 |
55 | ## smamodbus.py
56 | sma modbus library (required for pvdata.py)
57 |
58 | ## symcon.py
59 | send SMA-measurement-values to symcon
60 |
61 | ## symcon_smaem_webhook.php
62 | symcon webhook (required for symcon.py)
63 |
64 | ## symcon_smawr_webhook.php
65 | symcon webhook (required for symcon.py)
66 |
67 | ## influxdb2.py
68 | send SMA-measurement-values to an influxdb2.
69 |
70 | ## ediplugs.py
71 | get power consumption values of Edimax smartplugs
72 |
--------------------------------------------------------------------------------
/features-outdated/domoticz.py:
--------------------------------------------------------------------------------
1 | """
2 | Send SMA values over to domoticz. Configuration like:
3 |
4 | [FEATURE-domoticz]
5 | # Domoticz API endpoint
6 | api=http://127.0.1.1:8080/json.htm
7 |
8 | # How frequently to send updates over (defaults to 20 sec)
9 | min_update=30
10 |
11 | # List of items to send over. Each item should contain a string like :,:, ...
12 | pconsume=1234567869:73
13 | v1=1234567869:72
14 | """
15 |
16 | import urllib.request
17 | import json
18 | import time
19 |
20 | last_update = 0
21 | def run(emparts,config):
22 | global last_update
23 |
24 | # Only update every X seconds
25 | if time.time() < last_update + int(config.get('min_update', 20)):
26 | #print("skipping")
27 | return
28 |
29 | last_update = time.time()
30 |
31 | serial = format(emparts['serial'])
32 | for key in config:
33 | if key in ['api', 'min_update']:
34 | continue
35 |
36 | # Dictionary of serial: domoticz device id
37 | dom_ids = dict(item.split(':') for item in config[key].split(','))
38 |
39 | if serial not in dom_ids:
40 | continue
41 |
42 | url = "%s?type=command¶m=udevice&idx=%s&nvalue=0&svalue=" % (config['api'], dom_ids[serial])
43 | if key in ['pconsume', 'p1consume', 'p2consume', 'p3consume']:
44 | url += "%0.2f;%0.2f" % (emparts[key], emparts[key + "counter"] * 1000)
45 | else:
46 | url += "%0.2f" % emparts[key]
47 |
48 | try:
49 | urllib.request.urlopen( url )
50 | except Exception as e: # ignore if domoticz was down (URLError doesnt catch all io errors that may occur)
51 | print("Error from domoticz request")
52 | print(e)
53 | pass
54 |
55 | def stopping(emparts,config):
56 | pass
57 |
--------------------------------------------------------------------------------
/features-outdated/ediplugs.py:
--------------------------------------------------------------------------------
1 | """
2 | Get power consumption values of Edimax smartplugs
3 |
4 | 2020-08-19 thsell
5 |
6 | [FEATURE-ediplugs]
7 | plugs = [
8 | [ip, user, password]
9 | ]
10 | """
11 |
12 | import time
13 | from libs.smartplug import SmartPlug
14 |
15 | edi_last_update = 0
16 | edi_debug = 0
17 | edi_data=[]
18 |
19 | def run(emparts,config):
20 |
21 | global edi_debug
22 | global edi_last_update
23 | global edi_data
24 |
25 |
26 | # Only update every X seconds
27 | if time.time() < edi_last_update + int(config.get('min_update', 20)):
28 | if (edi_debug > 1):
29 | print("edi: data skipping")
30 | return
31 |
32 |
33 | edi_last_update = time.time()
34 |
35 | edi_data = []
36 | for inv in eval(config.get('plugs')):
37 | host, user, password = inv
38 | plug = SmartPlug(host, (user, password))
39 |
40 | try:
41 | mdata = {'state': plug.state, 'pconsume': float(plug.power), 'aconsume': float(plug.current)}
42 | edi_data.append({**plug.info, **mdata})
43 | except:
44 | print('Error connecting to Smartplug')
45 |
46 | # query
47 | if edi_data is None:
48 | if edi_debug > 0:
49 | print("Edi: no data" )
50 |
51 | if edi_debug > 0:
52 | for i in edi_data:
53 | i['timestamp'] = time.time()
54 | print("Edi:" + format(i))
55 |
56 |
57 | def stopping(emparts,config):
58 | pass
59 |
60 | def on_publish(client,userdata,result):
61 | pass
62 |
63 | def config(config):
64 | global edi_debug
65 | edi_debug=int(config.get('debug', 0))
66 | print('ediplugs: feature enabled')
67 |
--------------------------------------------------------------------------------
/features-outdated/influxdb.py:
--------------------------------------------------------------------------------
1 | """
2 | Send SMA values influxdb
3 |
4 | 2018-12-28 Tommi2Day
5 | 2020-09-22 Tommi2Day fix empty pv data and no ssl option
6 | 2021-01-02 sellth added support for multiple inverters
7 |
8 | Configuration:
9 | pip3 install influxdb datetime
10 |
11 | [SMA-EM]
12 | # serials of sma-ems the daemon should take notice
13 | # seperated by space
14 | serials=30028xxx
15 | # features could filter serials to, but wouldn't see serials if these serials was not defines in SMA-EM serials
16 | # list of features to load/run
17 | features=influxdb
18 |
19 | [FEATURE-influxdb]
20 | # symcon
21 | host=influxdb
22 | port=8086
23 | db=SMA
24 | measurement=SMAEM
25 | timeout=5
26 | user=
27 | password=
28 | fields=pconsume,psupply,p1consume,p2consume,p3consume,p1supply,p2supply,p3supply
29 |
30 | # How frequently to send updates over (defaults to 20 sec)
31 | min_update=30
32 |
33 | debug=0
34 | pvmeasurement=SMAWR
35 | pvvields=AC Power,AC Voltage,grid frequency,DC Power,DC input voltage,daily yield,total yield
36 |
37 |
38 | """
39 |
40 | import time
41 | import platform
42 | import datetime
43 | from influxdb import InfluxDBClient
44 | from influxdb.client import InfluxDBClientError
45 |
46 | influx_last_update = 0
47 | influx_debug = 0
48 |
49 |
50 | def run(emparts, config):
51 | global influx_last_update
52 | global influx_debug
53 |
54 | # Only update every X seconds
55 | if time.time() < influx_last_update + int(config.get('min_update', 20)):
56 | if (influx_debug > 1):
57 | print("InfluxDB: data skipping")
58 | return
59 |
60 | # db connect
61 | db = config.get('db', 'SMA')
62 | host = config.get('host', 'influxdb')
63 | port = int(config.get('port', 8086))
64 | ssl = bool(config.get('ssl'))
65 | timeout = int(config.get('timeout', 5))
66 | user = config.get('user', None)
67 | password = config.get('password', None)
68 | mesurement = config.get('measurement', 'SMAEM')
69 | fields = config.get('fields', 'pconsume,psupply')
70 | pvfields = config.get('pvfields')
71 | influx = None
72 |
73 | # connect to db, create one if needed
74 | try:
75 | if ssl == True:
76 | influx = InfluxDBClient(host=host, port=port, ssl=ssl, verify_ssl=ssl, username=user, password=password,
77 | timeout=timeout)
78 | if influx_debug > 0:
79 | print("Influxdb: use ssl")
80 | else:
81 | influx = InfluxDBClient(host=host, port=port, username=user, password=password, timeout=timeout)
82 |
83 | dbs = influx.get_list_database()
84 | if influx_debug > 1:
85 | print(dbs)
86 | if not {"name": db} in dbs:
87 | print(db + ' not in list, create')
88 | influx.create_database(db)
89 |
90 | influx.switch_database(db)
91 | if influx_debug > 1:
92 | print("Influxdb connected to '%s' @ '%s'(%s)" % (str(user), host, db))
93 |
94 | except InfluxDBClientError as e:
95 | if influx_debug > 0:
96 | print("InfluxDB: Connect Error to '%s' @ '%s'(%s)" % (str(user), host, db))
97 | print(format(e))
98 | return
99 | except Exception as e:
100 | if influx_debug > 0:
101 | print("InfluxDB: Error while connecting to '%s' @ '%s'(%s)" % (str(user), host, db))
102 | print(e)
103 | return
104 |
105 | myhostname = platform.node()
106 | influx_last_update = time.time()
107 | now = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
108 |
109 | # last aupdate
110 | influx_last_update = time.time()
111 | serial = emparts['serial']
112 |
113 | # data fields
114 | data = {}
115 | for f in fields.split(','):
116 | data[f] = emparts.get(f)
117 | if data[f] is None: data[f] = 0.0
118 |
119 | # data point
120 | influx_data = {}
121 | influx_data['measurement'] = mesurement
122 | influx_data['time'] = now
123 | influx_data['tags'] = {}
124 | influx_data['tags']["serial"] = serial
125 | pvpower = 0
126 | pdirectusage = 0
127 | pbattery = 0
128 |
129 | try:
130 | from features.pvdata import pv_data
131 |
132 | for inv in pv_data:
133 | # handle missing data during night hours
134 | if inv.get("AC Power") is None:
135 | pass
136 | elif inv.get("DeviceClass") == "Solar Inverter":
137 | pvpower += inv.get("AC Power")
138 | elif inv.get("DeviceClass") == "Battery Inverter":
139 | pbattery += inv.get("AC Power")
140 |
141 | pconsume = emparts.get('pconsume', 0)
142 | psupply = emparts.get('psupply', 0)
143 | pusage = pvpower + pconsume - psupply
144 | # total power consumption (grid + battery discharge)
145 | phouse = pvpower + pconsume - psupply + pbattery
146 |
147 | if pdirectusage is None: pdirectusage=0
148 | if pvpower > pusage:
149 | pdirectusage = pusage
150 | else:
151 | pdirectusage = pvpower
152 |
153 | data['pdirectusage'] = float(pdirectusage)
154 | data['pvpower'] = float(pvpower)
155 | data['pusage'] = float(pusage)
156 | data['pbattery'] = float(pbattery)
157 | data['phouse'] = float(phouse)
158 | except:
159 | # Kostal inverter? (pvdata_kostal_json)
160 | print("except - no sma - inverter")
161 | try:
162 | from features.pvdata_kostal_json import pv_data
163 | pvpower = pv_data.get("AC Power")
164 | if pvpower is None: pvpower = 0
165 | pconsume = emparts.get('pconsume', 0)
166 | psupply = emparts.get('psupply', 0)
167 | pusage = pvpower + pconsume - psupply
168 | if pdirectusage is None: pdirectusage=0
169 | if pvpower > pusage:
170 | pdirectusage = pusage
171 | else:
172 | pdirectusage = pvpower
173 | data['pdirectusage'] = pdirectusage
174 | data['pvpower'] = pvpower
175 | data['pusage'] = pusage
176 | except:
177 | pv_data = None
178 | print("no kostal inverter")
179 | pass
180 |
181 | influx_data['fields'] = data
182 | points = [influx_data]
183 |
184 | # send it
185 | try:
186 | influx.write_points(points, time_precision='s', protocol='json')
187 | except InfluxDBClientError as e:
188 | if influx_debug > 0:
189 | print('InfluxDBError: %s' % (format(e)))
190 | print("InfluxDB failed data:" + format(time.strftime("%H:%M:%S", time.localtime(influx_last_update))),
191 | format(points))
192 | pass
193 |
194 | else:
195 | if influx_debug > 0:
196 | print("InfluxDB: em data published %s:%s" % (
197 | format(time.strftime("%H:%M:%S", time.localtime(influx_last_update))), format(points)))
198 |
199 | pvmeasurement = config.get('pvmeasurement')
200 | if None in [pvfields, pv_data, pvmeasurement]: return
201 |
202 | influx_data = []
203 | datapoint = {
204 | 'measurement': pvmeasurement,
205 | 'time': now,
206 | 'tags': {},
207 | 'fields': {}
208 | }
209 | taglist = ['serial', 'DeviceID', 'Device Name']
210 | tags = {}
211 | fields = {}
212 |
213 | if pv_data is not None:
214 | for inv in pv_data:
215 | # add tag columns and remove from data list
216 | for t in taglist:
217 | tags[t] = inv.get(t)
218 |
219 | for f in pvfields.split(','):
220 | fields[f] = inv.get(f)
221 |
222 | datapoint['tags'] = tags.copy()
223 | datapoint['fields'] = fields.copy()
224 | influx_data.append(datapoint.copy())
225 |
226 | # send it
227 | try:
228 | influx.write_points(influx_data, time_precision='s', protocol='json')
229 | except InfluxDBClientError as e:
230 | if influx_debug > 0:
231 | print('InfluxDBError: %s' % (format(e)))
232 | print("InfluxDB failed pv data:" + format(time.strftime("%H:%M:%S", time.localtime(influx_last_update))),
233 | format(influx_data))
234 | pass
235 |
236 | else:
237 | if influx_debug > 0:
238 | print("InfluxDB: pv data published %s:%s" % (
239 | format(time.strftime("%H:%M:%S", time.localtime(influx_last_update))), format(influx_data)))
240 |
241 |
242 | # Edimax smartplug data #####
243 | from features.ediplugs import edi_data
244 | edimeasurement=config.get('edimeasurement')
245 |
246 | if None in [edi_data,edimeasurement]: return
247 |
248 | influx_data = []
249 | datapoint={
250 | 'measurement': edimeasurement,
251 | 'time': now,
252 | 'tags': {},
253 | 'fields': {}
254 | }
255 | taglist = ['vendor', 'model', 'mac', 'name']
256 | fieldlist = ['state', 'pconsume', 'aconsume']
257 | tags = {}
258 | fields = {}
259 |
260 | for inv in edi_data:
261 | for t in taglist:
262 | tags[t] = inv.get(t)
263 |
264 | for f in fieldlist:
265 | fields[f] = inv.get(f)
266 |
267 | datapoint['tags'] = tags.copy()
268 | datapoint['fields'] = fields.copy()
269 | influx_data.append(datapoint.copy())
270 |
271 | points = influx_data
272 |
273 | #send it
274 | try:
275 | influx.write_points(points, time_precision='s', protocol='json')
276 | except InfluxDBClientError as e:
277 | if influx_debug > 0:
278 | print('InfluxDBError: %s' % (format(e)))
279 | print("InfluxDB failed edi data:"
280 | + format(time.strftime("%H:%M:%S", time.localtime(influx_last_update)))
281 | + format(points)
282 | )
283 | pass
284 |
285 | else:
286 | if influx_debug > 0:
287 | print("InfluxDB: edi data published %s:%s"
288 | % (format(time.strftime("%H:%M:%S", time.localtime(influx_last_update))),
289 | format(points)
290 | )
291 | )
292 |
293 |
294 | def stopping(emparts, config):
295 | pass
296 |
297 |
298 | def config(config):
299 | global influx_debug
300 | influx_debug = int(config.get('debug', 0))
301 | print('influxdb: feature enabled')
302 |
--------------------------------------------------------------------------------
/features-outdated/influxdb2.py:
--------------------------------------------------------------------------------
1 | """
2 | Send SMA values influxdb 2.0
3 |
4 | 2021-02-25 dervomsee
5 |
6 | Configuration:
7 | pip3 install influxdb-client
8 |
9 | [SMA-EM]
10 | # serials of sma-ems the daemon should take notice
11 | # seperated by space
12 | serials=30028xxx
13 | # features could filter serials to, but wouldn't see serials if these serials was not defines in SMA-EM serials
14 | # list of features to load/run
15 | features=influxdb2
16 |
17 | [FEATURE-influxdb2]
18 | debug=0
19 | url=hostname.tld
20 | token=long_token
21 | org=org_name
22 | bucket=bucket_name
23 | measurement=SMAEM
24 | fields=pconsume,psupply,p1consume,p2consume,p3consume,p1supply,p2supply,p3supply
25 |
26 | # How frequently to send updates over (defaults to 20 sec)
27 | min_update=30
28 |
29 | #pv fields
30 | pvmeasurement=SMAWR
31 | pvfields=AC Power,AC_Current,Grid_Frequency
32 | # How frequently to send updates over (defaults to 20 sec)
33 |
34 | """
35 |
36 | import time
37 | from datetime import datetime
38 | from influxdb_client import InfluxDBClient, Point, WriteOptions, WritePrecision
39 | from influxdb_client.client.write_api import SYNCHRONOUS, WriteType
40 |
41 | influx2_client = InfluxDBClient(url="dummy", token="test", org="", debug=False)
42 | influx2_write_api = influx2_client.write_api(write_options=SYNCHRONOUS)
43 | influx2_last_update = 0
44 | influx2_debug = 0
45 |
46 |
47 | def run(emparts, config):
48 | global influx2_debug
49 | global influx2_last_update
50 | global influx2_client
51 | global influx2_write_api
52 |
53 | # Only update every X seconds
54 | if time.time() < influx2_last_update + int(config.get('min_update', 20)):
55 | if (influx2_debug > 1):
56 | print("InfluxDB: data skipping")
57 | return
58 | influx2_last_update = time.time()
59 |
60 | mesurement = config.get('measurement', 'SMAEM')
61 | fields = config.get('fields', 'pconsume,psupply')
62 | serial = emparts['serial']
63 |
64 | # data fields
65 | data = {}
66 | for f in fields.split(','):
67 | data[f] = emparts.get(f)
68 | if data[f] is None:
69 | data[f] = 0.0
70 |
71 | # inverter data
72 | pvpower = 0
73 | pdirectusage = 0
74 | try:
75 | from features.pvdata import pv_data
76 | for inv in pv_data:
77 | # handle missing data during night hours
78 | if inv.get("AC Power") is None:
79 | pass
80 | elif inv.get("DeviceClass") == "Solar Inverter":
81 | pvpower += inv.get("AC Power", 0)
82 | pconsume = emparts.get('pconsume', 0)
83 | psupply = emparts.get('psupply', 0)
84 | pusage = pvpower + pconsume - psupply
85 |
86 | if pdirectusage is None:
87 | pdirectusage = 0
88 | if pvpower > pusage:
89 | pdirectusage = pusage
90 | else:
91 | pdirectusage = pvpower
92 | data['pdirectusage'] = float(pdirectusage)
93 | data['pvpower'] = float(pvpower)
94 | data['pusage'] = float(pusage)
95 | except:
96 | # Kostal inverter? (pvdata_kostal_json)
97 | if influx2_debug > 0:
98 | print("InfluxDB2: " + "except - no sma - inverter")
99 | try:
100 | from features.pvdata_kostal_json import pv_data
101 | pvpower = pv_data.get("AC Power")
102 | if pvpower is None:
103 | pvpower = 0
104 | pconsume = emparts.get('pconsume', 0)
105 | psupply = emparts.get('psupply', 0)
106 | pusage = pvpower + pconsume - psupply
107 | if pdirectusage is None:
108 | pdirectusage = 0
109 | if pvpower > pusage:
110 | pdirectusage = pusage
111 | else:
112 | pdirectusage = pvpower
113 | data['pdirectusage'] = float(pdirectusage)
114 | data['pvpower'] = float(pvpower)
115 | data['pusage'] = float(pusage)
116 | except:
117 | pv_data = None
118 | if influx2_debug > 0:
119 | print("InfluxDB2: " + "no kostal inverter")
120 | pass
121 |
122 | # data point
123 | influx_data = {}
124 | influx_data['measurement'] = mesurement
125 | influx_data['time'] = datetime.utcnow()
126 | influx_data['tags'] = {}
127 | influx_data['tags']["serial"] = serial
128 | influx_data['fields'] = data
129 | points = [influx_data]
130 |
131 | # write em data
132 | org = config.get('org', "my-org")
133 | bucket = config.get('bucket', "my-bucket")
134 | influx2_write_api.write(bucket, org, points,
135 | write_precision=WritePrecision.S)
136 |
137 | # prepare pv data
138 | pvfields = config.get('pvfields')
139 | pvmeasurement = config.get('pvmeasurement')
140 | if None in [pvfields, pv_data, pvmeasurement]:
141 | return
142 |
143 | # pv data are empty on first call
144 | if not pv_data:
145 | if influx2_debug > 0:
146 | print("pv_data empty")
147 | return
148 |
149 | points = []
150 | influx_data = []
151 | datapoint = {
152 | 'measurement': pvmeasurement,
153 | 'time': datetime.utcnow(),
154 | 'tags': {},
155 | 'fields': {}
156 | }
157 | taglist = ['serial', 'DeviceID']
158 | tags = {}
159 | fields = {}
160 | for inv in pv_data:
161 | # add tag columns and remove from data list
162 | for t in taglist:
163 | if inv.get(t) is None:
164 | pass
165 | else:
166 | tags[t] = inv.get(t)
167 | inv.pop(t)
168 |
169 | # only if we have values
170 | if pv_data is not None:
171 | for f in pvfields.split(','):
172 | if inv.get(f) is None:
173 | pass
174 | else:
175 | fields[f] = inv.get(f)
176 |
177 | datapoint['tags'] = tags.copy()
178 | datapoint['fields'] = fields.copy()
179 | influx_data.append(datapoint.copy())
180 |
181 | # write pv data
182 | points = influx_data
183 | influx2_write_api.write(bucket, org, points,
184 | write_precision=WritePrecision.S)
185 |
186 |
187 | def stopping(emparts, config):
188 | global influx2_client
189 | influx2_client.close()
190 | global influx2_write_api
191 | influx2_write_api.close()
192 | pass
193 |
194 |
195 | def config(config):
196 | global influx2_debug
197 | global influx2_client
198 | global influx2_write_api
199 | influx2_debug = int(config.get('debug', 0))
200 | org = config.get('org', "my-org")
201 | url = config.get('url', "http://localhost:8086")
202 | token = config.get('token', "my-token")
203 |
204 | # create connection
205 | influx2_client.close()
206 | if influx2_debug > 0:
207 | influx2_client = InfluxDBClient(
208 | url=url, token=token, org=org, debug=True)
209 | else:
210 | influx2_client = InfluxDBClient(
211 | url=url, token=token, org=org, debug=False)
212 | influx2_write_api.close()
213 | influx2_write_api = influx2_client.write_api(write_options=WriteOptions(batch_size=200,
214 | flush_interval=120_000,
215 | jitter_interval=2_000,
216 | retry_interval=5_000,
217 | max_retries=5,
218 | max_retry_delay=30_000,
219 | exponential_base=2))
220 | print('influxdb2: feature enabled')
221 |
--------------------------------------------------------------------------------
/features-outdated/pvdata.py:
--------------------------------------------------------------------------------
1 | """
2 | Get inverter pv values via modbus
3 |
4 | 2018-12-28 Tommi2Day
5 | 2020-09-22 Tommi2Day fixes empty data exeptions
6 | 2021-01-02 sellth added support for multiple inverters
7 |
8 | Configuration:
9 | pip3 install pymodbus
10 |
11 | [FEATURE-pvdata]
12 |
13 | # How frequently to send updates over (defaults to 20 sec)
14 | min_update=20
15 | #debug output
16 | debug=0
17 |
18 | #inverter connection
19 | inv_host =
20 | inv_port = 502
21 | inv_modbus_id = 3
22 | inv_manufacturer = SMA
23 | #['address', 'type', 'format', 'description', 'unit', 'value']
24 | registers = [
25 | ['30057', 'U32', 'RAW', 'serial', ''],
26 | ['30201','U32','ENUM','Status',''],
27 | ['30051','U32','ENUM','DeviceClass',''],
28 | ['30053','U32','ENUM','DeviceID',''],
29 | ['40631', 'STR32', 'UTF8', 'Device Name', ''],
30 | ['30775', 'S32', 'FIX0', 'AC Power', 'W'],
31 | ['30813', 'S32', 'FIX0', 'AC apparent power', 'VA'],
32 | ['30977', 'S32', 'FIX3', 'AC current', 'A'],
33 | ['30783', 'S32', 'FIX2', 'AC voltage', 'V'],
34 | ['30803', 'U32', 'FIX2', 'grid frequency', 'Hz'],
35 | ['30773', 'S32', 'FIX0', 'DC power', 'W'],
36 | ['30771', 'S32', 'FIX2', 'DC input voltage', 'V'],
37 | ['30777', 'S32', 'FIX0', 'Power L1', 'W'],
38 | ['30779', 'S32', 'FIX0', 'Power L2', 'W'],
39 | ['30781', 'S32', 'FIX0', 'Power L3', 'W'],
40 | ['30953', 'S32', 'FIX1', u'device temperature', u'\xb0C'],
41 | ['30517', 'U64', 'FIX3', 'daily yield', 'kWh'],
42 | ['30513', 'U64', 'FIX3', 'total yield', 'kWh'],
43 | ['30521', 'U64', 'FIX0', 'operation time', 's'],
44 | ['30525', 'U64', 'FIX0', 'feed-in time', 's'],
45 | ['30975', 'S32', 'FIX2', 'intermediate voltage', 'V'],
46 | ['30225', 'S32', 'FIX0', 'Isolation resistance', u'\u03a9'],
47 | ['30581', 'U32', 'FIX0', u'energy from grid', 'Wh'],
48 | ['30583', 'U32', 'FIX0', u'energy to grid', 'Wh'],
49 | ['30865', 'S32', 'FIX0', 'Power from grid', 'W'],
50 | ['30867', 'S32', 'FIX0', 'Power to grid', 'W']
51 | ]
52 | """
53 |
54 | import time
55 | from features.smamodbus import get_device_class
56 | from features.smamodbus import get_pv_data
57 |
58 | pv_last_update = 0
59 | pv_debug = 0
60 | pv_data = []
61 |
62 |
63 | def run(emparts, config):
64 | global pv_debug
65 | global pv_last_update
66 | global pv_data
67 |
68 | # Only update every X seconds
69 | if time.time() < pv_last_update + int(config.get('min_update', 20)):
70 | if (pv_debug > 1):
71 | print("pv: data skipping")
72 | return
73 |
74 | pv_last_update = time.time()
75 | registers = eval(config.get('registers'))
76 |
77 | pv_data = []
78 | for inv in eval(config.get('inverters')):
79 | host, port, modbusid, manufacturer = inv
80 |
81 | device_class = get_device_class(host, int(port), int(modbusid))
82 | if device_class == "Solar Inverter":
83 | relevant_registers = eval(config.get('registers'))
84 | mdata = get_pv_data(host, int(port), int(modbusid), relevant_registers)
85 | pv_data.append(mdata)
86 | elif device_class == "Battery Inverter":
87 | relevant_registers = eval(config.get('registers_batt'))
88 | mdata = get_pv_data(host, int(port), int(modbusid), relevant_registers)
89 | pv_data.append(mdata)
90 | else:
91 | if (pv_debug > 1):
92 | print("pv: unknown device class; skipping")
93 | pass
94 |
95 | # query
96 | if pv_data is None:
97 | if pv_debug > 0:
98 | print("PV: no data")
99 | return
100 |
101 | timestamp = time.time()
102 | for i in pv_data:
103 | i['timestamp'] = timestamp
104 | if pv_debug > 0:
105 | print("PV:" + format(i))
106 |
107 |
108 | def stopping(emparts, config):
109 | pass
110 |
111 |
112 | def on_publish(client, userdata, result):
113 | pass
114 |
115 |
116 | def config(config):
117 | global pv_debug
118 | pv_debug = int(config.get('debug', 0))
119 | print('pvdata: feature enabled')
120 |
--------------------------------------------------------------------------------
/features-outdated/pvdata_kostal_json.py:
--------------------------------------------------------------------------------
1 | """
2 | Get inverter pv values http / Json on Kostal interters
3 |
4 | 2020-05-24 Wenger Florian
5 |
6 | Configuration:
7 | pip3 install requests json
8 |
9 | [FEATURE-pvdata_kostal_json]
10 |
11 | # How frequently to send updates over (defaults to 20 sec)
12 | # my kostal inverter updates the values only every 3 seconds
13 | #
14 | # How frequently to send updates over (defaults to 20 sec)
15 | min_update=15
16 | #debug output
17 | debug=0
18 |
19 | #inverter connection
20 | inv_host =
21 | #['address', 'NONE', 'NONE' 'description', 'unit']
22 | # to get the same structure of sma pvdata feature
23 | registers = [
24 | ['33556736', 'NONE', 'NONE', 'DC Power', 'W'],
25 | ['33555202', 'NONE', 'NONE', 'DC string1 voltage', 'V'],
26 | ['33555201', 'NONE', 'NONE', 'DC string1 current', 'A'],
27 | ['33555203', 'NONE', 'NONE', 'DC string1 power', 'W'],
28 | ['67109120', 'NONE', 'NONE', 'AC Power', 'W'],
29 | ['67110400', 'NONE', 'NONE', 'AC frequency', 'Hz'],
30 | ['67110656', 'NONE', 'NONE', 'AC cosphi', '°'],
31 | ['67110144', 'NONE', 'NONE', 'AC ptot limitation', ''],
32 | ['67109378', 'NONE', 'NONE', 'AC phase1 voltage', 'V'],
33 | ['67109377', 'NONE', 'NONE', 'AC phase1 current', 'A'],
34 | ['67109379', 'NONE', 'NONE', 'AC phase1 power', 'W'],
35 | ['251658754', 'NONE', 'NONE', 'yield today', 'Wh'],
36 | ['251658753', 'NONE', 'NONE', 'yield total', 'kWh'],
37 | ['251658496', 'NONE', 'NONE', 'operationtime', ''],
38 | ]
39 |
40 |
41 | r = requests.get(url='http://192.168.1.21/api/dxs.json?dxsEntries=67109120&sessionId=1234567890')
42 | cont = json.loads(r.content)
43 | #print(cont)
44 | print(cont["dxsEntries"][0]["value"],"W Kostal")
45 | sys.exit()
46 |
47 | """
48 |
49 | import requests, json, time
50 | pv_last_update = 0
51 | pv_debug = 0
52 | # pv_data
53 | pv_data={}
54 |
55 | def run(emparts,config):
56 | global pv_debug
57 | global pv_last_update
58 | #global pv_data_last
59 | global pv_data
60 |
61 | host = config.get('inv_host')
62 | registers = eval(config.get('registers'))
63 |
64 | # Only update every X seconds
65 | if time.time() < pv_last_update + int(config.get('min_update', 20)):
66 | if (pv_debug > 0):
67 | print("pv: data skipping")
68 | print("reuse last values")
69 | print("PV:" + format(pv_data))
70 | return
71 | pv_last_update = time.time()
72 | url = "http://"+host+"/api/dxs.json?sessionId=1234567890"
73 | for register in registers:
74 | if (pv_debug > 0):
75 | print (register[0])
76 | url=url+"&dxsEntries="+register[0]
77 | if (pv_debug > 1):
78 | print (url)
79 | r = requests.get(url)
80 | cont = json.loads(r.content)
81 | #print(cont)
82 | pv_data={}
83 | if cont['status']["code"] == 0:
84 |
85 | for pvjdata in cont["dxsEntries"]:
86 | #process the json values
87 | item=pvjdata["dxsId"]
88 | value=pvjdata["value"]
89 | for register in registers:
90 | if int(register[0])==int(item):
91 | if pv_debug > 0:
92 | print("---")
93 | print(str(item) + " " + register[3] + " " + str(value) + " " + register[4])
94 | pv_data[register[3]]=float(value)
95 | #pv_data_last=pv_data
96 | else:
97 | print ("PVdata-Result json, but not OK")
98 |
99 | if pv_data is None:
100 | if pv_debug > 0:
101 | print("PV: no data" )
102 |
103 | pv_data['timestamp'] = time.time()
104 | if pv_debug > 0:
105 | print("PV:" + format(pv_data))
106 |
107 |
108 | def stopping(emparts,config):
109 | pass
110 |
111 | def config(config):
112 | global pv_debug
113 | pv_debug=int(config.get('debug', 0))
114 | print('pvdata_kostal_json: feature enabled')
115 |
--------------------------------------------------------------------------------
/features-outdated/remotedebug.py:
--------------------------------------------------------------------------------
1 | """
2 | Allow remote debug with PyCharm
3 |
4 | 2020-09-20 Tommi2Day
5 |
6 | [FEATURE-debug]
7 | # Debug settings
8 | debughost=mypc
9 | debugport=9100
10 |
11 | """
12 | import pydevd_pycharm
13 |
14 |
15 | def run(emparts, config):
16 | pass
17 |
18 |
19 | def stopping(emparts, config):
20 | pass
21 |
22 |
23 | def config(config):
24 | # prepare mqtt settings
25 | print('debug feature enabled')
26 | debughost = config.get('debughost', None)
27 | debugport = config.get('debugport', None)
28 | if None not in (debughost, debugport):
29 | try:
30 | print('activate debug for ' + debughost + ' Port ' + str(debugport))
31 | pydevd_pycharm.settrace(debughost, port=int(debugport), stdoutToServer=True, stderrToServer=True)
32 | except Exception as e:
33 | print('...failed')
34 | print(e)
35 | pass
36 |
--------------------------------------------------------------------------------
/features-outdated/sample.py:
--------------------------------------------------------------------------------
1 | """
2 | * sample feature module / just an example
3 | * for sma-em daemon
4 | *
5 | """
6 | def run(emparts,config):
7 | """
8 | * sma-em daemon calls run for each measurement package
9 | * emparts: all measurements of one sma-em package
10 | * config: all config items from section FEATURE-[featurename] in /etc/smaemd/config
11 | *
12 | """
13 | #print("running sample feature")
14 | #print ('config')
15 | #print(config)
16 | pass
17 |
18 |
19 | def stopping(emparts,config):
20 | """
21 | * executed on daemon stop
22 | * do some cleanup / close filehandles if needed and so on...
23 | """
24 | print("quitting")
25 | #close filehandles
26 |
27 | def config(config):
28 | """
29 | * executed on daemon config init
30 | * do some configuration stuff...
31 | """
32 | global sw_debug
33 | sw_debug = int(config.get('debug', 0))
34 | print("sample: feature enabled")
35 |
--------------------------------------------------------------------------------
/features-outdated/sma_grafana.json:
--------------------------------------------------------------------------------
1 | {
2 | "__inputs": [
3 | {
4 | "name": "DS_INFLUXDB-SMA",
5 | "label": "InfluxDB-SMA",
6 | "description": "",
7 | "type": "datasource",
8 | "pluginId": "influxdb",
9 | "pluginName": "InfluxDB"
10 | }
11 | ],
12 | "__requires": [
13 | {
14 | "type": "grafana",
15 | "id": "grafana",
16 | "name": "Grafana",
17 | "version": "5.2.1"
18 | },
19 | {
20 | "type": "panel",
21 | "id": "graph",
22 | "name": "Graph",
23 | "version": "5.0.0"
24 | },
25 | {
26 | "type": "datasource",
27 | "id": "influxdb",
28 | "name": "InfluxDB",
29 | "version": "5.0.0"
30 | }
31 | ],
32 | "annotations": {
33 | "list": [
34 | {
35 | "builtIn": 1,
36 | "datasource": "-- Grafana --",
37 | "enable": true,
38 | "hide": true,
39 | "iconColor": "rgba(0, 211, 255, 1)",
40 | "name": "Annotations & Alerts",
41 | "type": "dashboard"
42 | }
43 | ]
44 | },
45 | "editable": true,
46 | "gnetId": null,
47 | "graphTooltip": 0,
48 | "id": null,
49 | "links": [],
50 | "panels": [
51 | {
52 | "aliasColors": {
53 | "SMAEM.Verbrauch": "#890f02"
54 | },
55 | "bars": false,
56 | "dashLength": 10,
57 | "dashes": false,
58 | "datasource": "${DS_INFLUXDB-SMA}",
59 | "fill": 1,
60 | "gridPos": {
61 | "h": 9,
62 | "w": 12,
63 | "x": 0,
64 | "y": 0
65 | },
66 | "id": 2,
67 | "legend": {
68 | "avg": false,
69 | "current": false,
70 | "max": false,
71 | "min": false,
72 | "show": true,
73 | "total": false,
74 | "values": false
75 | },
76 | "lines": true,
77 | "linewidth": 1,
78 | "links": [],
79 | "nullPointMode": "null",
80 | "percentage": false,
81 | "pointradius": 5,
82 | "points": false,
83 | "renderer": "flot",
84 | "seriesOverrides": [],
85 | "spaceLength": 10,
86 | "stack": false,
87 | "steppedLine": false,
88 | "targets": [
89 | {
90 | "groupBy": [
91 | {
92 | "params": [
93 | "1m"
94 | ],
95 | "type": "time"
96 | },
97 | {
98 | "params": [
99 | "null"
100 | ],
101 | "type": "fill"
102 | }
103 | ],
104 | "measurement": "SMAEM",
105 | "orderByTime": "ASC",
106 | "policy": "autogen",
107 | "refId": "A",
108 | "resultFormat": "time_series",
109 | "select": [
110 | [
111 | {
112 | "params": [
113 | "pconsume"
114 | ],
115 | "type": "field"
116 | },
117 | {
118 | "params": [],
119 | "type": "mean"
120 | },
121 | {
122 | "params": [
123 | "consume"
124 | ],
125 | "type": "alias"
126 | }
127 | ],
128 | [
129 | {
130 | "params": [
131 | "psupply"
132 | ],
133 | "type": "field"
134 | },
135 | {
136 | "params": [],
137 | "type": "mean"
138 | },
139 | {
140 | "params": [
141 | "supply"
142 | ],
143 | "type": "alias"
144 | }
145 | ],
146 | [
147 | {
148 | "params": [
149 | "pusage"
150 | ],
151 | "type": "field"
152 | },
153 | {
154 | "params": [],
155 | "type": "mean"
156 | },
157 | {
158 | "params": [
159 | "usage"
160 | ],
161 | "type": "alias"
162 | }
163 | ],
164 | [
165 | {
166 | "params": [
167 | "pvpower"
168 | ],
169 | "type": "field"
170 | },
171 | {
172 | "params": [],
173 | "type": "mean"
174 | },
175 | {
176 | "params": [
177 | "PV"
178 | ],
179 | "type": "alias"
180 | }
181 | ]
182 | ],
183 | "tags": [
184 | {
185 | "key": "serial",
186 | "operator": "=",
187 | "value": "3002849936"
188 | }
189 | ]
190 | }
191 | ],
192 | "thresholds": [],
193 | "timeFrom": null,
194 | "timeShift": null,
195 | "title": "SMA-EM Data",
196 | "tooltip": {
197 | "shared": true,
198 | "sort": 0,
199 | "value_type": "individual"
200 | },
201 | "type": "graph",
202 | "xaxis": {
203 | "buckets": null,
204 | "mode": "time",
205 | "name": null,
206 | "show": true,
207 | "values": []
208 | },
209 | "yaxes": [
210 | {
211 | "format": "watt",
212 | "label": "Watt",
213 | "logBase": 1,
214 | "max": null,
215 | "min": null,
216 | "show": true
217 | },
218 | {
219 | "format": "short",
220 | "label": "",
221 | "logBase": 1,
222 | "max": null,
223 | "min": null,
224 | "show": true
225 | }
226 | ],
227 | "yaxis": {
228 | "align": false,
229 | "alignLevel": null
230 | }
231 | }
232 | ],
233 | "schemaVersion": 16,
234 | "style": "dark",
235 | "tags": [],
236 | "templating": {
237 | "list": []
238 | },
239 | "time": {
240 | "from": "now-2d",
241 | "to": "now"
242 | },
243 | "timepicker": {
244 | "refresh_intervals": [
245 | "5s",
246 | "10s",
247 | "30s",
248 | "1m",
249 | "5m",
250 | "15m",
251 | "30m",
252 | "1h",
253 | "2h",
254 | "1d"
255 | ],
256 | "time_options": [
257 | "5m",
258 | "15m",
259 | "1h",
260 | "6h",
261 | "12h",
262 | "24h",
263 | "2d",
264 | "7d",
265 | "30d"
266 | ]
267 | },
268 | "timezone": "",
269 | "title": "SMA EM/HM",
270 | "uid": "L1e-KEymz",
271 | "version": 3
272 | }
273 |
--------------------------------------------------------------------------------
/features-outdated/smamodbus.py:
--------------------------------------------------------------------------------
1 | '''
2 | smaem modbus library
3 |
4 | 2018-12-28 Tommi2Day
5 |
6 | requires pymodbus
7 |
8 | huge parts taken from
9 | - https://github.com/transistorgrab/PyModMon
10 | - https://github.com/CodeKing/de.codeking.symcon.sma
11 |
12 | config sample:
13 | config={
14 | 'inv_host' : "sma",
15 | 'inv_port' : 502,
16 | 'inv_modbus_id' : 3,
17 | 'registers': [
18 | ['address', 'type', 'format', 'description', 'unit', 'value'],
19 | ['30057', 'U32', 'RAW', 'serial', ''],
20 | ['30201','U32','ENUM',Status',''],
21 | ['30775', 'S32', 'FIX0', 'AC Power', 'W']
22 | ]
23 | }
24 | '''
25 |
26 | from pymodbus.payload import BinaryPayloadDecoder
27 | from pymodbus.constants import Endian
28 | import datetime
29 | from pymodbus.client.sync import ModbusTcpClient as ModbusClient
30 | import traceback
31 |
32 | # defines
33 | MIN_SIGNED = -2147483648
34 | MAX_UNSIGNED = 4294967295
35 | modbusdatatype = { ## allowed data types, sent from target
36 | 'S32': 2,
37 | 'U32': 2,
38 | 'U64': 4,
39 | 'STR32': 16,
40 | 'S16': 1,
41 | 'U16': 1
42 | }
43 | pvenums = {
44 | 'Status': {
45 | 35: 'Error',
46 | 303: 'Off',
47 | 307: 'OK',
48 | 455: 'Warning'
49 | },
50 | 'DeviceClass': {
51 | 460: 'Solar Inverter',
52 | 8000: 'All Devices',
53 | 8001: 'Solar Inverter',
54 | 8002: 'Wind Turbine Inverter',
55 | 8007: 'Battery Inverter',
56 | 8033: 'Consumer',
57 | 8064: 'Sensor System in General',
58 | 8065: 'Electricity meter',
59 | 8128: 'Communication device'
60 | },
61 | 'DeviceID': {
62 | 9000: 'SWR 700',
63 | 9001: 'SWR 850',
64 | 9002: 'SWR 850E',
65 | 9003: 'SWR 1100',
66 | 9004: 'SWR 1100E',
67 | 9005: 'SWR 1100LV',
68 | 9006: 'SWR 1500',
69 | 9007: 'SWR 1600',
70 | 9008: 'SWR 1700E',
71 | 9009: 'SWR 1800U',
72 | 9010: 'SWR 2000',
73 | 9011: 'SWR 2400',
74 | 9012: 'SWR 2500',
75 | 9013: 'SWR 2500U',
76 | 9014: 'SWR 3000',
77 | 9015: 'SB 700',
78 | 9016: 'SB 700U',
79 | 9017: 'SB 1100',
80 | 9018: 'SB 1100U',
81 | 9019: 'SB 1100LV',
82 | 9020: 'SB 1700',
83 | 9021: 'SB 1900TLJ',
84 | 9022: 'SB 2100TL',
85 | 9023: 'SB 2500',
86 | 9024: 'SB 2800',
87 | 9025: 'SB 2800i',
88 | 9026: 'SB 3000',
89 | 9027: 'SB 3000US',
90 | 9028: 'SB 3300',
91 | 9029: 'SB 3300U',
92 | 9030: 'SB 3300TL',
93 | 9031: 'SB 3300TL HC',
94 | 9032: 'SB 3800',
95 | 9033: 'SB 3800U',
96 | 9034: 'SB 4000US',
97 | 9035: 'SB 4200TL',
98 | 9036: 'SB 4200TL HC',
99 | 9037: 'SB 5000TL',
100 | 9038: 'SB 5000TLW',
101 | 9039: 'SB 5000TL HC',
102 | 9040: 'Convert 2700',
103 | 9041: 'SMC 4600A',
104 | 9042: 'SMC 5000',
105 | 9043: 'SMC 5000A',
106 | 9044: 'SB 5000US',
107 | 9045: 'SMC 6000',
108 | 9046: 'SMC 6000A',
109 | 9047: 'SB 6000US',
110 | 9048: 'SMC 6000UL',
111 | 9049: 'SMC 6000TL',
112 | 9050: 'SMC 6500A',
113 | 9051: 'SMC 7000A',
114 | 9052: 'SMC 7000HV',
115 | 9053: 'SB 7000US',
116 | 9054: 'SMC 7000TL',
117 | 9055: 'SMC 8000TL',
118 | 9056: 'SMC 9000TL-10',
119 | 9057: 'SMC 10000TL-10',
120 | 9058: 'SMC 11000TL-10',
121 | 9059: 'SB 3000 K',
122 | 9060: 'Unknown device',
123 | 9061: 'SensorBox',
124 | 9062: 'SMC 11000TLRP',
125 | 9063: 'SMC 10000TLRP',
126 | 9064: 'SMC 9000TLRP',
127 | 9065: 'SMC 7000HVRP',
128 | 9066: 'SB 1200',
129 | 9067: 'STP 10000TL-10',
130 | 9068: 'STP 12000TL-10',
131 | 9069: 'STP 15000TL-10',
132 | 9070: 'STP 17000TL-10',
133 | 9071: 'SB 2000HF-30',
134 | 9072: 'SB 2500HF-30',
135 | 9073: 'SB 3000HF-30',
136 | 9074: 'SB 3000TL-21',
137 | 9075: 'SB 4000TL-21',
138 | 9076: 'SB 5000TL-21',
139 | 9077: 'SB 2000HFUS-30',
140 | 9078: 'SB 2500HFUS-30',
141 | 9079: 'SB 3000HFUS-30',
142 | 9080: 'SB 8000TLUS',
143 | 9081: 'SB 9000TLUS',
144 | 9082: 'SB 10000TLUS',
145 | 9083: 'SB 8000US',
146 | 9084: 'WB 3600TL-20',
147 | 9085: 'WB 5000TL-20',
148 | 9086: 'SB 3800US-10',
149 | 9087: 'Sunny Beam BT11',
150 | 9088: 'Sunny Central 500CP',
151 | 9089: 'Sunny Central 630CP',
152 | 9090: 'Sunny Central 800CP',
153 | 9091: 'Sunny Central 250U',
154 | 9092: 'Sunny Central 500U',
155 | 9093: 'Sunny Central 500HEUS',
156 | 9094: 'Sunny Central 760CP',
157 | 9095: 'Sunny Central 720CP',
158 | 9096: 'Sunny Central 910CP',
159 | 9097: 'SMU8',
160 | 9098: 'STP 5000TL-20',
161 | 9099: 'STP 6000TL-20',
162 | 9100: 'STP 7000TL-20',
163 | 9101: 'STP 8000TL-10',
164 | 9102: 'STP 9000TL-20',
165 | 9103: 'STP 8000TL-20',
166 | 9104: 'SB 3000TL-JP-21',
167 | 9105: 'SB 3500TL-JP-21',
168 | 9106: 'SB 4000TL-JP-21',
169 | 9107: 'SB 4500TL-JP-21',
170 | 9108: 'SCSMC',
171 | 9109: 'SB 1600TL-10',
172 | 9110: 'SSM US',
173 | 9111: 'SMA radio-controlled socket',
174 | 9112: 'WB 2000HF-30',
175 | 9113: 'WB 2500HF-30',
176 | 9114: 'WB 3000HF-30',
177 | 9115: 'WB 2000HFUS-30',
178 | 9116: 'WB 2500HFUS-30',
179 | 9117: 'WB 3000HFUS-30',
180 | 9118: 'VIEW-10',
181 | 9119: 'Sunny Home Manager',
182 | 9120: 'SMID',
183 | 9121: 'Sunny Central 800HE-20',
184 | 9122: 'Sunny Central 630HE-20',
185 | 9123: 'Sunny Central 500HE-20',
186 | 9124: 'Sunny Central 720HE-20',
187 | 9125: 'Sunny Central 760HE-20',
188 | 9126: 'SMC 6000A-11',
189 | 9127: 'SMC 5000A-11',
190 | 9128: 'SMC 4600A-11',
191 | 9129: 'SB 3800-11',
192 | 9130: 'SB 3300-11',
193 | 9131: 'STP 20000TL-10',
194 | 9132: 'SMA CT Meter',
195 | 9133: 'SB 2000HFUS-32',
196 | 9134: 'SB 2500HFUS-32',
197 | 9135: 'SB 3000HFUS-32',
198 | 9136: 'WB 2000HFUS-32',
199 | 9137: 'WB 2500HFUS-32',
200 | 9138: 'WB 3000HFUS-32',
201 | 9139: 'STP 20000TLHE-10',
202 | 9140: 'STP 15000TLHE-10',
203 | 9141: 'SB 3000US-12',
204 | 9142: 'SB 3800US-12',
205 | 9143: 'SB 4000US-12',
206 | 9144: 'SB 5000US-12',
207 | 9145: 'SB 6000US-12',
208 | 9146: 'SB 7000US-12',
209 | 9147: 'SB 8000US-12',
210 | 9148: 'SB 8000TLUS-12',
211 | 9149: 'SB 9000TLUS-12',
212 | 9150: 'SB 10000TLUS-12',
213 | 9151: 'SB 11000TLUS-12',
214 | 9152: 'SB 7000TLUS-12',
215 | 9153: 'SB 6000TLUS-12',
216 | 9154: 'SB 1300TL-10',
217 | 9155: 'Sunny Backup 2200',
218 | 9156: 'Sunny Backup 5000',
219 | 9157: 'Sunny Island 2012',
220 | 9158: 'Sunny Island 2224',
221 | 9159: 'Sunny Island 5048',
222 | 9160: 'SB 3600TL-20',
223 | 9161: 'SB 3000TL-JP-22',
224 | 9162: 'SB 3500TL-JP-22',
225 | 9163: 'SB 4000TL-JP-22',
226 | 9164: 'SB 4500TL-JP-22',
227 | 9165: 'SB 3600TL-21',
228 | 9167: 'Cluster Controller',
229 | 9168: 'SC630HE-11',
230 | 9169: 'SC500HE-11',
231 | 9170: 'SC400HE-11',
232 | 9171: 'WB 3000TL-21',
233 | 9172: 'WB 3600TL-21',
234 | 9173: 'WB 4000TL-21',
235 | 9174: 'WB 5000TL-21',
236 | 9175: 'SC 250',
237 | 9176: 'SMA Meteo Station',
238 | 9177: 'SB 240-10',
239 | 9178: 'SB 240-US-10',
240 | 9179: 'Multigate-10',
241 | 9180: 'Multigate-US-10',
242 | 9181: 'STP 20000TLEE-10',
243 | 9182: 'STP 15000TLEE-10',
244 | 9183: 'SB 2000TLST-21',
245 | 9184: 'SB 2500TLST-21',
246 | 9185: 'SB 3000TLST-21',
247 | 9186: 'WB 2000TLST-21',
248 | 9187: 'WB 2500TLST-21',
249 | 9188: 'WB 3000TLST-21',
250 | 9189: 'WTP 5000TL-20',
251 | 9190: 'WTP 6000TL-20',
252 | 9191: 'WTP 7000TL-20',
253 | 9192: 'WTP 8000TL-20',
254 | 9193: 'WTP 9000TL-20',
255 | 9194: 'STP 12000TL-US-10',
256 | 9195: 'STP 15000TL-US-10',
257 | 9196: 'STP 20000TL-US-10',
258 | 9197: 'STP 24000TL-US-10',
259 | 9198: 'SB 3000TLUS-22',
260 | 9199: 'SB 3800TLUS-22',
261 | 9200: 'SB 4000TLUS-22',
262 | 9201: 'SB 5000TLUS-22',
263 | 9202: 'WB 3000TLUS-22',
264 | 9203: 'WB 3800TLUS-22',
265 | 9204: 'WB 4000TLUS-22',
266 | 9205: 'WB 5000TLUS-22',
267 | 9206: 'SC 500CP-JP',
268 | 9207: 'SC 850CP',
269 | 9208: 'SC 900CP',
270 | 9209: 'SC 850 CP-US',
271 | 9210: 'SC 900 CP-US',
272 | 9211: 'SC 619CP',
273 | 9212: 'SMA Meteo Station',
274 | 9213: 'SC 800 CP-US',
275 | 9214: 'SC 630 CP-US',
276 | 9215: 'SC 500 CP-US',
277 | 9216: 'SC 720 CP-US',
278 | 9217: 'SC 750 CP-US',
279 | 9218: 'SB 240 Dev',
280 | 9219: 'SB 240-US BTF',
281 | 9220: 'Grid Gate-20',
282 | 9221: 'SC 500 CP-US/600V',
283 | 9222: 'STP 10000TLEE-JP-10',
284 | 9223: 'Sunny Island 6.0H',
285 | 9224: 'Sunny Island 8.0H',
286 | 9225: 'SB 5000SE-10',
287 | 9226: 'SB 3600SE-10',
288 | 9227: 'SC 800CP-JP',
289 | 9228: 'SC 630CP-JP',
290 | 9229: 'WebBox-30',
291 | 9230: 'Power Reducer Box',
292 | 9231: 'Sunny Sensor Counter',
293 | 9232: 'Sunny Boy Control',
294 | 9233: 'Sunny Boy Control Plus',
295 | 9234: 'Sunny Boy Control Light',
296 | 9235: 'Sunny Central 100 Outdoor',
297 | 9236: 'Sunny Central 1000MV',
298 | 9237: 'Sunny Central 100 LV',
299 | 9238: 'Sunny Central 1120MV',
300 | 9239: 'Sunny Central 125 LV',
301 | 9240: 'Sunny Central 150',
302 | 9241: 'Sunny Central 200',
303 | 9242: 'Sunny Central 200 HE',
304 | 9243: 'Sunny Central 250 HE',
305 | 9244: 'Sunny Central 350',
306 | 9245: 'Sunny Central 350 HE',
307 | 9246: 'Sunny Central 400 HE',
308 | 9247: 'Sunny Central 400MV',
309 | 9248: 'Sunny Central 500 HE',
310 | 9249: 'Sunny Central 500MV',
311 | 9250: 'Sunny Central 560 HE',
312 | 9251: 'Sunny Central 630 HE',
313 | 9252: 'Sunny Central 700MV',
314 | 9253: 'Sunny Central',
315 | 9254: 'Sunny Island 3324',
316 | 9255: 'Sunny Island 4.0M',
317 | 9256: 'Sunny Island 4248',
318 | 9257: 'Sunny Island 4248U',
319 | 9258: 'Sunny Island 4500',
320 | 9259: 'Sunny Island 4548U',
321 | 9260: 'Sunny Island 5.4M',
322 | 9261: 'Sunny Island 5048U',
323 | 9262: 'Sunny Island 6048U',
324 | 9263: 'Sunny Mini Central 7000HV-11',
325 | 9264: 'Sunny Solar Tracker',
326 | 9265: 'Sunny Beam',
327 | 9266: 'Sunny Boy SWR 700/150',
328 | 9267: 'Sunny Boy SWR 700/200',
329 | 9268: 'Sunny Boy SWR 700/250',
330 | 9269: 'Sunny WebBox für SC',
331 | 9270: 'Sunny WebBox',
332 | 9271: 'STP 20000TLEE-JP-11',
333 | 9272: 'STP 10000TLEE-JP-11',
334 | 9273: 'SB 6000TL-21',
335 | 9274: 'SB 6000TL-US-22',
336 | 9275: 'SB 7000TL-US-22',
337 | 9276: 'SB 7600TL-US-22',
338 | 9277: 'SB 8000TL-US-22',
339 | 9278: 'SI 3.0M',
340 | 9279: 'SI 4.4M',
341 | 9281: 'STP 10000TL-20',
342 | 9282: 'STP 11000TL-20',
343 | 9283: 'STP 12000TL-20',
344 | 9284: 'STP 20000TL-30',
345 | 9285: 'STP 25000TL-30',
346 | 9286: 'SCS-500',
347 | 9287: 'SCS-630',
348 | 9288: 'SCS-720',
349 | 9289: 'SCS-760',
350 | 9290: 'SCS-800',
351 | 9291: 'SCS-850',
352 | 9292: 'SCS-900',
353 | 9293: 'SB 7700TL-US-22',
354 | 9294: 'SB20.0-3SP-40',
355 | 9295: 'SB30.0-3SP-40',
356 | 9296: 'SC 1000 CP',
357 | 9297: 'Zeversolar 1000',
358 | 9298: 'SC 2200-10',
359 | 9299: 'SC 2200-US-10',
360 | 9300: 'SC 2475-EV-10',
361 | 9301: 'SB1.5-1VL-40',
362 | 9302: 'SB2.5-1VL-40',
363 | 9303: 'SB2.0-1VL-40',
364 | 9304: 'SB5.0-1SP-US-40',
365 | 9305: 'SB6.0-1SP-US-40',
366 | 9306: 'SB8.0-1SP-US-40',
367 | 9307: 'Energy Meter',
368 | 9308: 'ZoneMonitoring',
369 | 9309: 'STP 27kTL-US-10',
370 | 9310: 'STP 30kTL-US-10',
371 | 9311: 'STP 25kTL-JP-30',
372 | 9312: 'SSM30',
373 | 9313: 'SB50.0-3SP-40',
374 | 9314: 'PlugwiseCircle',
375 | 9315: 'PlugwiseSting',
376 | 9316: 'SCS-1000',
377 | 9317: 'SB 5400TL-JP-22',
378 | 9319: 'SB3.0-1AV-40',
379 | 9320: 'SB3.6-1AV-40',
380 | 9321: 'SB4.0-1AV-40',
381 | 9322: 'SB5.0-1AV-40',
382 | 9324: 'SBS1.5-1VL-10',
383 | 9325: 'SBS2.0-1VL-10',
384 | 9326: 'SBS2.5-1VL-10',
385 | 9344: 'STP4.0-3AV-40',
386 | 9345: 'STP5.0-3AV-40',
387 | 9346: 'STP6.0-3AV-40',
388 | 9356: 'SBS3.7-10',
389 | 9358: 'SBS5.0-10',
390 | 9359: 'SBS6.0-10',
391 | 9360: 'SBS3.8-US-10',
392 | 9361: 'SBS5.0-US-10',
393 | 9362: 'SBS6.0-US-10',
394 | 9366: 'STP3.0-3AV-40',
395 | 9401: 'SB3.0-1AV-41',
396 | 9402: 'SB3.6-1AV-41',
397 | 9403: 'SB4.0-1AV-41',
398 | 9404: 'SB5.0-1AV-41',
399 | 9405: 'SB6.0-1AV-41'
400 | },
401 | 'BatteryState': {
402 | 303: 'Off',
403 | 2291: 'Standby',
404 | 2292: 'Charging',
405 | 2293: 'Discharging',
406 | 16777213: 'NA'
407 | },
408 | 'BatteryHealth': {
409 | 35: 'Fault',
410 | 303: 'Off',
411 | 307: 'OK',
412 | 455: 'Warning',
413 | 16777213: 'NA'
414 | }
415 | }
416 |
417 |
418 | def get_device_class(host, port, modbusid):
419 | client = ModbusClient(host=host, port=port)
420 |
421 | # connects even within if clause
422 | if client.connect() == False:
423 | print('Modbus Connection Error: Could not connect to', host)
424 | return None
425 |
426 | try:
427 | received = client.read_input_registers(address=30051, count=2, unit=3)
428 | except:
429 | thisdate = str(datetime.datetime.now()).partition('.')[0]
430 | thiserrormessage = thisdate + ': Connection not possible. Check settings or connection.'
431 | print(thiserrormessage)
432 | return None
433 |
434 | message = BinaryPayloadDecoder.fromRegisters(received.registers, byteorder=Endian.Big, wordorder=Endian.Big)
435 | interpreted = message.decode_32bit_uint()
436 | dclass = pvenums["DeviceClass"].get(interpreted)
437 |
438 | client.close()
439 | return dclass
440 |
441 |
442 | def get_pv_data(host, port, modbusid, registers):
443 | client = ModbusClient(host=host, port=port)
444 |
445 | # connects even within if clause
446 | if client.connect() == False:
447 | print('Modbus Connection Error: Could not connect to', host)
448 | return None
449 |
450 | data = {} ## empty data store for current values
451 |
452 | for myreg in registers:
453 | ## if the connection is somehow not possible (e.g. target not responding)
454 | # show a error message instead of excepting and stopping
455 | try:
456 | addr = int(myreg[0])
457 | dt = myreg[1]
458 | received = client.read_input_registers(address=addr, count=modbusdatatype[dt], unit=int(modbusid))
459 | except Exception as e:
460 | thisdate = str(datetime.datetime.now()).partition('.')[0]
461 | thiserrormessage = thisdate + 'Modbus: Connection not possible. Check settings or connection.'
462 | print(thiserrormessage)
463 | print(traceback.format_exc())
464 | return None ## prevent further execution of this function
465 |
466 | name = myreg[3]
467 | message = BinaryPayloadDecoder.fromRegisters(received.registers, byteorder=Endian.Big, wordorder=Endian.Big)
468 | ## provide the correct result depending on the defined datatype
469 | if myreg[1] == 'S32':
470 | interpreted = message.decode_32bit_int()
471 | elif myreg[1] == 'U32':
472 | interpreted = message.decode_32bit_uint()
473 | elif myreg[1] == 'U64':
474 | interpreted = message.decode_64bit_uint()
475 | elif myreg[1] == 'STR32':
476 | interpreted = message.decode_string(32)
477 | elif myreg[1] == 'S16':
478 | interpreted = message.decode_16bit_int()
479 | elif myreg[1] == 'U16':
480 | interpreted = message.decode_16bit_uint()
481 | else: ## if no data type is defined do raw interpretation of the delivered data
482 | interpreted = message.decode_16bit_uint()
483 |
484 | ## check for "None" data before doing anything else
485 | if ((interpreted == MIN_SIGNED) or (interpreted == MAX_UNSIGNED)):
486 | value = None
487 | else:
488 | ## put the data with correct formatting into the data table
489 | if myreg[2] == 'FIX3':
490 | value = float(interpreted) / 1000
491 | elif myreg[2] == 'FIX2':
492 | value = float(interpreted) / 100
493 | elif myreg[2] == 'FIX1':
494 | value = float(interpreted) / 10
495 | elif myreg[2] == 'UTF8':
496 | value = str(interpreted,'UTF-8',errors='ignore').rstrip("\x00")
497 | elif myreg[2] == 'ENUM':
498 | e = pvenums.get(name, {})
499 | value = e.get(interpreted, str(interpreted))
500 | else:
501 | value = interpreted
502 | data[name] = value
503 |
504 | client.close()
505 | return data
506 |
--------------------------------------------------------------------------------
/features-outdated/symcon.py:
--------------------------------------------------------------------------------
1 | """
2 | Send SMA values to symcon (www.symcon.de) via web hook
3 | need Symcon 4.0+
4 |
5 | 2018-12-23 Tommi2Day
6 | 2020-09-22 Tommi2Day fix empty pv data
7 | 2021-01-07 sellth added support for multiple inverters
8 |
9 | Configuration:
10 | [FEATURE-symcon]
11 | # symcon
12 | host=ips
13 | port=3777
14 | emhook=/hook/smaem
15 | pvhook=/hook/smawr
16 | timeout=5
17 | user=
18 | password=
19 | fields=pconsume,psupply,p1consume,p2consume,p3consume,p1supply,p2supply,p3supply
20 | pvfields=AC Power,AC Voltage,grid frequency,DC Power,DC input voltage,daily yield,total yield,Power L1,Power L2,Power L3
21 |
22 | # How frequently to send updates over (defaults to 20 sec)
23 | min_update=30
24 |
25 | debug=0
26 |
27 | """
28 |
29 | import urllib.request, urllib.error
30 | import json
31 | import time
32 | import platform
33 |
34 | symcon_last_update = 0
35 | symcon_debug = 0
36 |
37 |
38 | def run(emparts, config):
39 | global symcon_last_update
40 | global symcon_debug
41 |
42 | # Only update every X seconds
43 | if time.time() < symcon_last_update + int(config.get('min_update', 20)):
44 | if (symcon_debug > 1):
45 | print("Symcon: data skipping")
46 | return
47 |
48 | # prepare hook settings
49 | host = config.get('host', 'ips')
50 | port = config.get('port', 3777)
51 | timeout = config.get('timeout', 5)
52 | emhook = config.get('emhook', '/hook/smaem')
53 | user = config.get('user', None)
54 | password = config.get('password', None)
55 | fields = config.get('fields', 'pconsume,psupply')
56 |
57 | # mqtt client settings
58 | myhostname = platform.node()
59 | symcon_last_update = time.time()
60 |
61 | url = 'http://' + host + ':' + str(port) + emhook
62 |
63 | serial = emparts['serial']
64 | data = {}
65 | for f in fields.split(','):
66 | data[f] = emparts.get(f, 0)
67 |
68 | data['timestamp'] = symcon_last_update
69 | data['sender'] = myhostname
70 | data['serial'] = str(serial)
71 | payload = json.dumps(data)
72 |
73 | # prepare request
74 | req = urllib.request.Request(url)
75 | req.add_header('Content-Type', 'application/json; charset=utf-8')
76 | dataasbytes = payload.encode('utf-8') # needs to be bytes
77 | req.add_header('Content-Length', str(len(dataasbytes)))
78 | # print(dataasbytes)
79 | req.add_header("User-Agent", "SMEM")
80 |
81 | # prepare auth
82 | if None not in [user, password]:
83 | passman = urllib.request.HTTPPasswordMgrWithDefaultRealm()
84 | passman.add_password(None, url, user, password)
85 | authhandler = urllib.request.HTTPBasicAuthHandler(passman)
86 | opener = urllib.request.build_opener(authhandler)
87 | urllib.request.install_opener(opener)
88 | if symcon_debug > 2:
89 | print('Symcon EM: use Basic auth')
90 |
91 | # send it
92 | try:
93 | response = urllib.request.urlopen(req, data=dataasbytes, timeout=int(timeout))
94 |
95 | except urllib.error.HTTPError as e:
96 | if symcon_debug > 0:
97 | print('Symcon EM: HTTPError: {%s} to %s' % (format(e.code), url))
98 | pass
99 | except urllib.error.URLError as e:
100 | if symcon_debug > 0:
101 | print('Symcon EM: URLError: {%s} to %s ' % (format(e.reason), url))
102 | pass
103 | except Exception as e:
104 | if symcon_debug > 0:
105 | print("Symcon EM: Error from symcon request")
106 | print(e)
107 | pass
108 | else:
109 | if symcon_debug > 0:
110 | print("Symcon EM: data published %s:%s to %s" % (
111 | format(time.strftime("%H:%M:%S", time.localtime(symcon_last_update))), payload, url))
112 |
113 | # send pv data
114 | data = {}
115 | pvhook = config.get('pvhook')
116 | pvfields = config.get('pvfields', 'AC Power,daily yield')
117 | if None in [pvhook, pvfields]: return
118 | pvurl = 'http://' + host + ':' + str(port) + pvhook
119 |
120 | try:
121 | from features.pvdata import pv_data
122 | except:
123 | if symcon_debug > 0:
124 | print("Symcon EM: Error importing PV Data")
125 | return
126 | if pv_data == None:
127 | if symcon_debug > 0:
128 | print("Symcon EM: No PV Data")
129 |
130 | return
131 |
132 | # prepare auth
133 | if None not in [user, password]:
134 | passman = urllib.request.HTTPPasswordMgrWithDefaultRealm()
135 | passman.add_password(None, pvurl, user, password)
136 | authhandler = urllib.request.HTTPBasicAuthHandler(passman)
137 | opener = urllib.request.build_opener(authhandler)
138 | urllib.request.install_opener(opener)
139 | if symcon_debug > 2:
140 | print('Symcon PV: use Basic auth')
141 |
142 | # prepare request
143 | pvreq = urllib.request.Request(pvurl)
144 | pvreq.add_header('Content-Type', 'application/json; charset=utf-8')
145 | pvreq.add_header("User-Agent", "SMWR")
146 |
147 | for inv in pv_data:
148 | serial = inv.get("serial")
149 | if serial is not None:
150 | for f in pvfields.split(','):
151 | data[f] = inv.get(f, 0)
152 |
153 | data['timestamp'] = symcon_last_update
154 | data['sender'] = myhostname
155 | data['serial'] = str(serial)
156 | pvpayload = json.dumps(data)
157 | pvdataasbytes = pvpayload.encode('utf-8') # needs to be bytes
158 | pvreq.add_header('Content-Length', str(len(pvdataasbytes)))
159 | # print(dataasbytes)
160 |
161 | # send it
162 | try:
163 | response = urllib.request.urlopen(pvreq, data=pvdataasbytes, timeout=int(timeout))
164 | except urllib.error.HTTPError as e:
165 | if symcon_debug > 0:
166 | print('Symcon PV : HTTPError: {%s} to %s ' % (format(e.reason), pvurl))
167 | pass
168 | except urllib.error.URLError as e:
169 | if symcon_debug > 0:
170 | print('Symcon PV: URLError: {%s} to %s ' % (format(e.reason), pvurl))
171 | pass
172 | except Exception as e:
173 | if symcon_debug > 0:
174 | print("Symcon PV: Error from symcon request")
175 | print(e)
176 | pass
177 | else:
178 | if symcon_debug > 0:
179 | print("Symcon PV: data published %s:%s to %s" % (
180 | format(time.strftime("%H:%M:%S", time.localtime(symcon_last_update))), pvpayload, pvurl))
181 |
182 |
183 | def stopping(emparts, config):
184 | pass
185 |
186 |
187 | def config(config):
188 | global symcon_debug
189 | symcon_debug = int(config.get('debug', 0))
190 | print('symcon: feature enabled')
191 |
--------------------------------------------------------------------------------
/features-outdated/symcon_smaem_webhook.php:
--------------------------------------------------------------------------------
1 | array('type'=>3,'profile'=>''),
70 | 'pconsume'=>array('type'=>2,'profile'=>'~Watt.14490'),
71 | 'p1consume'=>array('type'=>2,'profile'=>'~Watt.14490'),
72 | 'p2consume'=>array('type'=>2,'profile'=>'~Watt.14490'),
73 | 'p3consume'=>array('type'=>2,'profile'=>'~Watt.14490'),
74 | 'psupply'=>array('type'=>2,'profile'=>'~Watt.14490'),
75 | 'psupply'=>array('type'=>2,'profile'=>'~Watt.14490'),
76 | 'p1supply'=>array('type'=>2,'profile'=>'~Watt.14490'),
77 | 'p2supply'=>array('type'=>2,'profile'=>'~Watt.14490'),
78 | 'p3supply'=>array('type'=>2,'profile'=>'~Watt.14490'),
79 | 'pconsumecounter'=>array('type'=>2,'profile'=>'~Electricity'),
80 | 'psupplycounter'=>array('type'=>2,'profile'=>'~Electricity'),
81 | 'timestamp'=>array('type'=>1,'profile'=>'~UnixTimestamp')
82 | );
83 | //auth only if called by webhook
84 | //IPS_LogMessage('SMA-EM WebHook Server',print_r($_SERVER,true));
85 | if (($_IPS['SENDER']=='WebHook') && $hook_user && $hook_password){
86 | if(!isset($_SERVER['PHP_AUTH_USER']))
87 | $_SERVER['PHP_AUTH_USER'] = "";
88 | if(!isset($_SERVER['PHP_AUTH_PW']))
89 | $_SERVER['PHP_AUTH_PW'] = "";
90 |
91 | if(($_SERVER['PHP_AUTH_USER'] != $hook_user) || ($_SERVER['PHP_AUTH_PW'] != $hook_password)) {
92 | header('WWW-Authenticate: Basic Realm="SMA-EM WebHook"');
93 | header('HTTP/1.0 401 Unauthorized');
94 | echo "Authorization required";
95 | return;
96 | }
97 |
98 | $raw=file_get_contents("php://input");
99 | }else {
100 | $raw=$test;
101 | }
102 |
103 | //sanity
104 | $data=@json_decode($raw);
105 | if (!is_object($data)) {
106 | IPS_LogMessage('SMA-EM WebHook','json_decode error'.print_r($raw,true));
107 | return;
108 | }
109 |
110 | if (!isset($data->{'serial'})) {
111 | IPS_LogMessage('SMA-EM WebHook','missing serial field'.print_r($data,true));
112 | return;
113 | }
114 | $serial=$data->{'serial'};
115 | $varids=get_ips_vars($serial,$vartypes,$cat,$prefix);
116 | if (is_null($varids)) {
117 | IPS_LogMessage($cat, "cannot get ids for device $serial");
118 | print($cat. " cannot get ids for device $serial");
119 | //no vars available, maybe autocreate disabled
120 | return;
121 | }
122 | $fields=array_keys($vartypes);
123 | foreach ($fields as $f) {
124 | if (isset($data->{"$f"})) {
125 | $ident=fix_ident($f);
126 | SetValue($varids["$ident"]['id'],$data->{"$f"});
127 | }
128 |
129 | }
130 | return;
131 |
132 | //----- end main ---------------------------------------
133 |
134 | /**
135 | * function fix_ident
136 | * remove unwanted chars from name for ips_setIdent
137 | * @param string $name
138 | * @returns string
139 | */
140 | function fix_ident($name) {
141 | $chars=array(" ","_","-","%");
142 | $ident=str_replace($chars,"",$name);
143 | return $ident;
144 | }
145 | /**
146 | * IPS Variablen handler
147 | * creates variables as needed
148 | * returns assoc. Array with IPS Variable ID and Value
149 | * @param string $serial Device serial
150 | * @param array $vartypes Array with Variable Names, Types and Profiles
151 | * @param string $cat Master Categorie Name
152 | * @param string $prefix default name, will be extended with $addr
153 | */
154 | function get_ips_vars($serial,$vartypes,$cat,$prefix) {
155 |
156 | $varids=null;
157 | $master=@IPS_GetCategoryIDByName($cat,0);
158 | //no master cat, create new
159 | if (!$master) {
160 | $master=IPS_CreateCategory();
161 | IPS_SetName($master,$cat);
162 | IPS_SetParent($master,0);
163 | if ($master>0) {
164 | IPS_LogMessage($cat, "Master category created, ID=$master\n");
165 | }else{
166 | IPS_LogMessage($cat, "Can't create Master Category\n");
167 | return null;
168 | }
169 | }
170 |
171 | $id=0;
172 |
173 | if ($master>0) {
174 | //get chilren devices
175 | $devices=IPS_GetChildrenIDs($master);
176 | foreach($devices as $dev) {
177 | $name=IPS_GetName($dev);
178 |
179 | $vars=IPS_GetChildrenIDs($dev);
180 | foreach($vars as $vid) {
181 | $obj=IPS_GetObject($vid);
182 | $vname=$obj['ObjectIdent'];
183 | $typ=$obj['ObjectType'];
184 | if ($typ==2) { //Variable
185 | //if ID, here is the address
186 | if ($vname="serial") {
187 | $i=GetValue($vid);
188 | //go out if matches, $id returns the sensor categorie id
189 | if ($i===$serial) {
190 | $id=$dev;
191 | break;
192 | }
193 | }
194 | }
195 | }
196 | if ($id>0) break;
197 | }
198 | if ($id==0) {
199 | //Sensor with address $addr not found in IPS
200 | if ($GLOBALS['autocreate']==false) {
201 | //autocreate disable, ignore new device
202 | return null;
203 | }
204 | //create new sensor
205 | $id=ips_createCategory();
206 | ips_setName($id,$prefix.' '.$serial);
207 | $ident=fix_ident($prefix.$serial);
208 | ips_setIdent($id,$ident);
209 | ips_setParent($id,$master);
210 | //creates all needed variables for the new sensor
211 | foreach (array_keys($vartypes) as $name) {
212 | $ident=fix_ident($name);
213 | $typ=$vartypes["$name"]['type'];
214 | $profile=$vartypes["$name"]['profile'];
215 | $vid=IPS_CreateVariable($typ);
216 | ips_setname($vid,"$name");
217 | ips_setident($vid,"$ident");
218 | ips_setParent($vid,$id);
219 | IPS_SetVariableCustomProfile($vid,$profile);
220 | //preload variables
221 | SetValue($vid,0);
222 | $varids["$ident"]['id']=$vid;
223 | $varids["$ident"]['val']=0;
224 | //Store address in $ID for next time
225 | if ($name=='serial') {
226 | SetValue($vid,$serial);
227 | $varids["$ident"]['val']=$serial;
228 | }
229 | }
230 | }else{
231 | //found matching cat, collect ids and vals for this sensor
232 | $vars=IPS_GetChildrenIDs($id);
233 | foreach($vars as $vid) {
234 | $obj=IPS_GetObject($vid);
235 | $ident=$obj['ObjectIdent'];
236 | $typ=$obj['ObjectType'];
237 | if ($typ==2) { //Variable
238 | $val=GetValue($vid);
239 | $varids["$ident"]['id']=$vid;
240 | $varids["$ident"]['val']=$val;
241 | }
242 |
243 | }
244 |
245 | }
246 | //returns IDs and Values of this Sensor, Name is Key
247 | return $varids;
248 | }
249 | }
250 |
251 | /**
252 | * list existing device categories
253 | * will be used for deletion
254 | * @param $catname master category
255 | * @return array of devices=>id
256 | */
257 | function list_cats($catname) {
258 | $master=@IPS_GetCategoryIDByName($catname,0);
259 | $ret=null;
260 | if ($master>0) {
261 | //get chilren sensors
262 | $devices=IPS_GetChildrenIDs($master);
263 | foreach($devices as $ids) {
264 | $name=IPS_GetName($ids);
265 | $ret{$name}=$ids;
266 | }
267 | }
268 | return $ret;
269 | }
270 |
--------------------------------------------------------------------------------
/features-outdated/symcon_smawr_webhook.php:
--------------------------------------------------------------------------------
1 | array('type'=>3,'profile'=>''),
70 | 'Status'=>array('type'=>3,'profile'=>''),
71 | 'AC Power'=>array('type'=>2,'profile'=>'~Watt.14490'),
72 | 'DC input voltage'=>array('type'=>2,'profile'=>'~Volt'),
73 | 'Power L1'=>array('type'=>2,'profile'=>'~Watt.14490'),
74 | 'Power L2'=>array('type'=>2,'profile'=>'~Watt.14490'),
75 | 'Power L3'=>array('type'=>2,'profile'=>'~Watt.14490'),
76 | 'daily yield'=>array('type'=>2,'profile'=>'~Electricity'),
77 | 'total yield'=>array('type'=>2,'profile'=>'~Electricity'),
78 | 'grid frequency'=>array('type'=>2,'profile'=>'~Hertz.50'),
79 | 'timestamp'=>array('type'=>1,'profile'=>'~UnixTimestamp')
80 | );
81 |
82 | //auth only if called by webhook
83 | //IPS_LogMessage('SMA-WR WebHook Server',print_r($_SERVER,true));
84 | if (($_IPS['SENDER']=='WebHook') && $hook_user && $hook_password){
85 | if(!isset($_SERVER['PHP_AUTH_USER']))
86 | $_SERVER['PHP_AUTH_USER'] = "";
87 | if(!isset($_SERVER['PHP_AUTH_PW']))
88 | $_SERVER['PHP_AUTH_PW'] = "";
89 |
90 | if(($_SERVER['PHP_AUTH_USER'] != $hook_user) || ($_SERVER['PHP_AUTH_PW'] != $hook_password)) {
91 | header('WWW-Authenticate: Basic Realm="SMA-WR WebHook"');
92 | header('HTTP/1.0 401 Unauthorized');
93 | echo "Authorization required";
94 | return;
95 | }
96 |
97 | $raw=file_get_contents("php://input");
98 | }else {
99 | $raw=$test;
100 | }
101 |
102 | //sanity
103 |
104 | $data=@json_decode($raw);
105 | if (!is_object($data)) {
106 | IPS_LogMessage('SMA-WR WebHook','json_decode error'.print_r($raw,true));
107 | return;
108 | }
109 |
110 | if (!isset($data->{'serial'})) {
111 | IPS_LogMessage('SMA-WR WebHook','missing serial field'.print_r($data,true));
112 | return;
113 | }
114 |
115 | $serial=$data->{'serial'};
116 | $varids=get_ips_vars($serial,$vartypes,$cat,$prefix);
117 | if (is_null($varids)) {
118 | IPS_LogMessage($cat, "cannot get ids for device $serial");
119 | print($cat. " cannot get ids for device $serial");
120 | //no vars available, maybe autocreate disabled
121 | return;
122 | }
123 | $fields=array_keys($vartypes);
124 | foreach ($fields as $f) {
125 | if (isset($data->{"$f"})) {
126 | $ident=fix_ident($f);
127 | SetValue($varids["$ident"]['id'],$data->{"$f"});
128 | }
129 | }
130 | return;
131 | //----- end main ---------------------------------------
132 |
133 | /**
134 | * function fix_ident
135 | * remove unwanted chars from name for ips_setIdent
136 | * @param string $name
137 | * @returns string
138 | */
139 | function fix_ident($name) {
140 | $chars=array(" ","_","-","%");
141 | $ident=str_replace($chars,"",$name);
142 | return $ident;
143 | }
144 | /**
145 | * IPS Variablen handler
146 | * creates variables as needed
147 | * returns assoc. Array with IPS Variable ID and Value
148 | * @param string $serial Device serial
149 | * @param array $vartypes Array with Variable Names, Types and Profiles
150 | * @param string $cat Master Categorie Name
151 | * @param string $prefix default name, will be extended with $addr
152 | */
153 | function get_ips_vars($serial,$vartypes,$cat,$prefix) {
154 |
155 | $varids=null;
156 | $master=@IPS_GetCategoryIDByName($cat,0);
157 | //no master cat, create new
158 | if (!$master) {
159 | $master=IPS_CreateCategory();
160 | IPS_SetName($master,$cat);
161 | IPS_SetParent($master,0);
162 | if ($master>0) {
163 | IPS_LogMessage($cat, "Master category created, ID=$master\n");
164 | }else{
165 | IPS_LogMessage($cat, "Can't create Master Category\n");
166 | return null;
167 | }
168 | }
169 |
170 | $id=0;
171 |
172 | if ($master>0) {
173 | //get chilren devices
174 | $devices=IPS_GetChildrenIDs($master);
175 | foreach($devices as $dev) {
176 | $name=IPS_GetName($dev);
177 |
178 | $vars=IPS_GetChildrenIDs($dev);
179 | foreach($vars as $vid) {
180 | $obj=IPS_GetObject($vid);
181 | $vname=$obj['ObjectIdent'];
182 | $typ=$obj['ObjectType'];
183 | if ($typ==2) { //Variable
184 | //if ID, here is the address
185 | if ($vname="serial") {
186 | $i=GetValue($vid);
187 | //go out if matches, $id returns the sensor categorie id
188 | if ($i===$serial) {
189 | $id=$dev;
190 | break;
191 | }
192 | }
193 | }
194 | }
195 | if ($id>0) break;
196 | }
197 | if ($id==0) {
198 | //Sensor with address $addr not found in IPS
199 | if ($GLOBALS['autocreate']==false) {
200 | //autocreate disable, ignore new device
201 | return null;
202 | }
203 | //create new sensor
204 | $id=ips_createCategory();
205 | ips_setName($id,$prefix.' '.$serial);
206 | $ident=fix_ident($prefix.$serial);
207 | ips_setIdent($id,$ident);
208 | ips_setParent($id,$master);
209 | //creates all needed variables for the new sensor
210 | foreach (array_keys($vartypes) as $name) {
211 | $ident=fix_ident($name);
212 | $typ=$vartypes["$name"]['type'];
213 | $profile=$vartypes["$name"]['profile'];
214 | $vid=IPS_CreateVariable($typ);
215 | ips_setname($vid,"$name");
216 | ips_setident($vid,"$ident");
217 | ips_setParent($vid,$id);
218 | IPS_SetVariableCustomProfile($vid,$profile);
219 | //preload variables
220 | SetValue($vid,0);
221 | $varids["$ident"]['id']=$vid;
222 | $varids["$ident"]['val']=0;
223 | //Store address in $ID for next time
224 | if ($name=='serial') {
225 | SetValue($vid,$serial);
226 | $varids["$ident"]['val']=$serial;
227 | }
228 | }
229 | }else{
230 | //found matching cat, collect ids and vals for this sensor
231 | $vars=IPS_GetChildrenIDs($id);
232 | foreach($vars as $vid) {
233 | $obj=IPS_GetObject($vid);
234 | $ident=$obj['ObjectIdent'];
235 | $typ=$obj['ObjectType'];
236 | if ($typ==2) { //Variable
237 | $val=GetValue($vid);
238 | $varids["$ident"]['id']=$vid;
239 | $varids["$ident"]['val']=$val;
240 | }
241 |
242 | }
243 |
244 | }
245 | //returns IDs and Values of this Sensor, Name is Key
246 | return $varids;
247 | }
248 | }
249 |
250 | /**
251 | * list existing device categories
252 | * will be used for deletion
253 | * @param $catname master category
254 | * @return array of devices=>id
255 | */
256 | function list_cats($catname) {
257 | $master=@IPS_GetCategoryIDByName($catname,0);
258 | $ret=null;
259 | if ($master>0) {
260 | //get chilren sensors
261 | $devices=IPS_GetChildrenIDs($master);
262 | foreach($devices as $ids) {
263 | $name=IPS_GetName($ids);
264 | $ret{$name}=$ids;
265 | }
266 | }
267 | return $ret;
268 | }
269 |
--------------------------------------------------------------------------------
/features/README.md:
--------------------------------------------------------------------------------
1 | # SMA-EM daemon features
2 |
3 | this page should give an overview of maintained features.
4 | other features untested because I do not have the appropriate hardware / software could be found in features-outdated.
5 |
6 | All the desired features must be activated in the configuration file
7 | ```
8 | [SMA-EM]
9 | # list of features to load/run
10 | features=simplefswriter nextfeature
11 | ```
12 | Each feature has it own configuration section in the configuration-file.
13 |
14 | [FEATURE-featurename]
15 |
16 | please have a look at the config.sample file or have a look at the features file (description) for supported configuration options.
17 |
18 | ```
19 | [FEATURE-simplefswriter]
20 | # list serials simplefswriter notice
21 | serials=1900204522
22 | # measurement vars simplefswriter should write to filesystem (only from smas with serial in serials)
23 | values=pconsume psupply qsupply ssupply
24 | ```
25 |
26 | Feature fist
27 |
28 | ## mqtt.py
29 | send SMA-measurement-values to an mqtt broker.
30 |
31 | ## simplefswriter.py
32 | writes configureable measurement-values to the filesystem
33 |
34 |
--------------------------------------------------------------------------------
/features/mqtt.py:
--------------------------------------------------------------------------------
1 | """
2 | Send SMA values to mqtt broker.
3 |
4 | 2018-12-23 Tommi2Day
5 | 2019-03-02 david-m-m
6 | 2020-09-22 Tommi2Day ssl support
7 | 2021-01-07 sellth added support for multiple inverters
8 |
9 | Configuration:
10 |
11 | [FEATURE-mqtt]
12 | # MQTT broker details
13 | mqtthost=mqtt
14 | mqttport=1883
15 | #mqttuser=
16 | #mqttpass=
17 | mqttfields=pconsume,psupply,p1consume,p2consume,p3consume,p1supply,p2supply,p3supply
18 | #topic will be exted3ed with serial
19 | mqtttopic=SMA-EM/status
20 | pvtopic=SMA-PV/status
21 | # publish all values as single topics (0 or 1)
22 | publish_single=1
23 | # How frequently to send updates over (defaults to 20 sec)
24 | min_update=30
25 | #debug output
26 | debug=0
27 |
28 | # ssl support
29 | # adopt mqttport above to your ssl enabled mqtt port, usually 8883
30 | # options:
31 | # activate without certs=use tls_insecure
32 | # activate with ca_file, but without client_certs
33 | ssl_activate=0
34 | # ca file to verify
35 | ssl_ca_file=ca.crt
36 | # client certs
37 | ssl_certfile=
38 | ssl_keyfile=
39 | #TLSv1.1 or TLSv1.2 (default 2)
40 | tls_protocol=2
41 |
42 | """
43 |
44 | import paho.mqtt.client as mqtt
45 | import platform
46 | import json
47 | import time
48 | import ssl
49 | import traceback
50 |
51 | mqtt_last_update = 0
52 | mqtt_debug = 0
53 |
54 |
55 | def run(emparts, config):
56 | global mqtt_last_update
57 | global mqtt_debug
58 |
59 | # Only update every X seconds
60 | if time.time() < mqtt_last_update + int(config.get('min_update', 20)):
61 | if (mqtt_debug > 1):
62 | print("mqtt: data skipping")
63 | return
64 |
65 | # prepare mqtt settings
66 | mqtthost = config.get('mqtthost', 'mqtt')
67 | mqttport = config.get('mqttport', 1883)
68 | mqttuser = config.get('mqttuser', None)
69 | mqttpass = config.get('mqttpass', None)
70 | mqtttopic = config.get('mqtttopic', "SMA-EM/status")
71 | mqttfields = config.get('mqttfields', 'pconsume,psupply')
72 | publish_single = int(config.get('publish_single', 0))
73 |
74 | ssl_activate = config.get('ssl_activate', False)
75 | ssl_ca_file = config.get('ssl_ca_file', None)
76 | ssl_certfile = config.get('ssl_certfile', None)
77 | ssl_keyfile = config.get('ssl_keyfile', None)
78 | tls_protocol = config.get('tls_protocol', "2")
79 | if tls_protocol == "1":
80 | tls = ssl.PROTOCOL_TLSv1_1
81 | elif tls_protocol == "2":
82 | tls = ssl.PROTOCOL_TLSv1_2
83 | else:
84 | tls = ssl.PROTOCOL_TLSv1_2
85 | if mqtt_debug > 0:
86 | print("tls_protocol %s unsupported, use (TLSv1.)2" % tls_protocol)
87 |
88 | # mqtt client settings
89 | myhostname = platform.node()
90 | mqtt_clientID = 'SMA-EM@' + myhostname
91 | client = mqtt.Client(mqtt_clientID)
92 | if None not in [mqttuser, mqttpass]:
93 | client.username_pw_set(username=mqttuser, password=mqttpass)
94 |
95 | if ssl_activate == "1":
96 | # and ssl_ca_file:
97 | if ssl_certfile and ssl_keyfile and ssl_ca_file:
98 | # use client cert
99 | client.tls_set(ssl_ca_file, certfile=ssl_certfile, keyfile=ssl_keyfile, tls_version=tls)
100 | if mqtt_debug > 0:
101 | print("mqtt: ssl ca and client verify enabled")
102 | elif ssl_ca_file:
103 | # no client cert
104 | client.tls_set(ssl_ca_file, tls_version=tls)
105 | if mqtt_debug > 0:
106 | print("mqtt: ssl ca verify enabled")
107 | else:
108 | # disable certificat verify as there is no certificate
109 | client.tls_set(tls_version=tls)
110 | client.tls_insecure_set(True)
111 | if mqtt_debug > 0:
112 | print("mqtt: ssl verify disabled")
113 | else:
114 | if mqtt_debug > 0:
115 | print("mqtt: ssl disabled")
116 |
117 | # last aupdate
118 | # last aupdate
119 | mqtt_last_update = time.time()
120 |
121 | serial = emparts['serial']
122 | data = {}
123 | for f in mqttfields.split(','):
124 | data[f] = emparts.get(f, 0)
125 |
126 | # add pv data
127 | pvpower = 0
128 | daily = 0
129 | try:
130 | from features.pvdata import pv_data
131 |
132 | for inv in pv_data:
133 | # handle missing data during night hours
134 | if inv.get("AC Power") is None:
135 | pass
136 | elif inv.get("DeviceClass") == "Solar Inverter":
137 | pvpower += inv.get("AC Power", 0)
138 | # NOTE: daily yield is broken for some inverters
139 | daily += inv.get("daily yield", 0)
140 |
141 | pconsume = emparts.get('pconsume', 0)
142 | psupply = emparts.get('psupply', 0)
143 | pusage = pvpower + pconsume - psupply
144 | data['pvsum'] = pvpower
145 | data['pusage'] = pusage
146 | data['pvdaily'] = daily
147 | except:
148 | pv_data = None
149 | pass
150 |
151 | data['timestamp'] = mqtt_last_update
152 | payload = json.dumps(data)
153 | topic = mqtttopic + '/' + str(serial)
154 | try:
155 | # mqtt connect
156 | client.connect(str(mqtthost), int(mqttport))
157 | client.loop_start()
158 | client.publish(topic, payload)
159 | if mqtt_debug > 0:
160 | print("mqtt: sma-em topic %s data published %s:%s" % (topic,
161 | format(time.strftime("%H:%M:%S", time.localtime(
162 | mqtt_last_update))), payload))
163 | # publish each value as separate topic
164 | if publish_single == 1:
165 | for item in data.keys():
166 | itemtopic = topic + '/' + item
167 | if mqtt_debug > 0:
168 | print("mqtt: publishing %s:%s" % (itemtopic, data[item]))
169 | client.publish(itemtopic, str(data[item]))
170 |
171 | # pvoption
172 | mqttpvtopic = config.get('pvtopic', None)
173 | if None not in [pv_data, mqttpvtopic]:
174 | if pv_data is not None:
175 | for inv in pv_data:
176 | pvserial = inv.get("serial")
177 | pvtopic = mqttpvtopic + '/' + str(pvserial)
178 | payload = json.dumps(inv)
179 | # sendf pv topic
180 | client.publish(pvtopic, payload)
181 | if mqtt_debug > 0:
182 | print("mqtt: sma-pv topic %s data published %s:%s" % (
183 | pvtopic,
184 | format(time.strftime("%H:%M:%S",
185 | time.localtime(
186 | mqtt_last_update))),
187 | payload))
188 | client.loop_stop()
189 | client.disconnect()
190 |
191 | except Exception as e:
192 | print("mqtt: Error publishing")
193 | print(traceback.format_exc())
194 | pass
195 |
196 |
197 | def stopping(emparts, config):
198 | pass
199 |
200 |
201 | def config(config):
202 | global mqtt_debug
203 | mqtt_debug = int(config.get('debug', 0))
204 | print('mqtt: feature enabled')
205 |
--------------------------------------------------------------------------------
/features/simplefswriter.py:
--------------------------------------------------------------------------------
1 | """
2 | * Feature-Module for SMA-EM daemon
3 | * Simple measurement to file writer
4 | * by Wenger Florian 2018-01-30
5 | *
6 | *
7 | * this software is released under GNU General Public License, version 2.
8 | * This program is free software;
9 | * you can redistribute it and/or modify it under the terms of the GNU General Public License
10 | * as published by the Free Software Foundation; version 2 of the License.
11 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
12 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 | * See the GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License along with this program;
16 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 | *
18 | */
19 | """
20 |
21 | import os,time
22 | sw_debug=0
23 |
24 | def run(emparts,config):
25 | global sw_debug
26 | values=config['values'].split(' ')
27 | serials=config['serials'].split(' ')
28 | statusdir = config.get('statusdir','')
29 | #prefere shm
30 | if (statusdir==''):
31 | statusdir="/run/shm/"
32 | #fallback to local dir
33 | if not os.path.isdir(statusdir):
34 | statusdir=''
35 | for serial in serials:
36 | if serial==format(emparts['serial']):
37 | ts=(format(time.strftime("%H:%M:%S", time.localtime())))
38 | for value in values:
39 | if value in emparts.keys():
40 | if sw_debug >0:
41 | print ('simplewriter: '+ts+" - "+format(value)+': '+('%.4f' % emparts[value]))
42 | file = open(statusdir+"em-"+format(serial)+"-"+format(value), "w")
43 | file.write('%.4f' % emparts[value])
44 | file.close()
45 | elif sw_debug > 0:
46 | print ('simplefswriter: could not find value for '+format(value))
47 |
48 | def stopping(emparts,config):
49 | print("quitting")
50 | #close files
51 | def config(config):
52 | global sw_debug
53 | sw_debug = int(config.get('debug', 0))
54 | print("simplefswriter: feature enabled")
55 |
--------------------------------------------------------------------------------
/knownProblems.md:
--------------------------------------------------------------------------------
1 | ## Known Problems
2 | Systemd starts daemon to early
3 |
4 | systemd[1]: Started SMA Energymeter measurement daemon.
5 | sma-daemon.py[574]: Traceback (most recent call last):
6 | sma-daemon.py[574]: File "/opt/smaem/sma-daemon.py", line 24, in
7 | sma-daemon.py[574]: import smaem
8 | sma-daemon.py[574]: File "/opt/smaem/smaem.py", line 49, in
9 | sma-daemon.py[574]: sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
10 | sma-daemon.py[574]: OSError: [Errno 19] No such device
11 | systemd[1]: smaemd.service: main process exited, code=exited, status=1/FAILURE
12 |
13 | should create a .socket file with ListenNetlink= MCastGroup
14 | need to import more systemd - stuff to brain.
15 |
--------------------------------------------------------------------------------
/libs/smartplug.py:
--------------------------------------------------------------------------------
1 | ##
2 | # The MIT License (MIT)
3 | #
4 | # Copyright (c) 2018 Stefan Wendler
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | ##
24 |
25 |
26 | import requests as req
27 | import optparse as par
28 | import logging as log
29 |
30 | from xml.dom.minidom import getDOMImplementation
31 | from xml.dom.minidom import parseString
32 | from requests.auth import HTTPDigestAuth
33 |
34 | __author__ = 'Stefan Wendler, sw@kaltpost.de'
35 |
36 |
37 | class SmartPlug(object):
38 | """
39 | Simple class to access a "EDIMAX Smart Plug Switch SP1101W/SP2101W"
40 |
41 | Usage example when used as library:
42 |
43 | p = SmartPlug("172.16.100.75", ('admin', '1234'))
44 |
45 | # get device info
46 | print(p.info)
47 |
48 | # change state of plug
49 | p.state = "OFF"
50 | p.state = "ON"
51 |
52 | # query and print current state of plug
53 | print(p.state)
54 |
55 | # get power consumption (only SP2101W)
56 | print(p.power)
57 |
58 | # get current consumption (only SP2101W)
59 | print(p.current)
60 |
61 | # read and print complete week schedule from plug
62 | print(p.schedule.__str__())
63 |
64 | # write schedule for on day to plug (Saturday, 11:15 - 11:45)
65 | p.schedule = {'state': u'ON', 'sched': [[[11, 15], [11, 45]]], 'day': 6}
66 |
67 | # write schedule for the whole week
68 | p.schedule = [
69 | {'state': u'ON', 'sched': [[[0, 3], [0, 4]]], 'day': 0},
70 | {'state': u'ON', 'sched': [[[0, 10], [0, 20]], [[10, 16], [11, 55]],
71 | [[15, 19], [15, 32]], [[21, 0], [23, 8]], [[23, 17], [23, 59]]], 'day': 1},
72 | {'state': u'OFF', 'sched': [[[19, 59], [21, 1]]], 'day': 2},
73 | {'state': u'OFF', 'sched': [[[20, 59], [21, 12]]], 'day': 3},
74 | {'state': u'OFF', 'sched': [], 'day': 4},
75 | {'state': u'OFF', 'sched': [[[0, 0], [0, 30]], [[11, 14], [14, 31]]], 'day': 5},
76 | {'state': u'ON', 'sched': [[[1, 42], [2, 41]]], 'day': 6}]
77 |
78 |
79 | Usage example when used as command line utility:
80 |
81 | Get device info:
82 |
83 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -i
84 |
85 | turn plug on:
86 |
87 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -s ON
88 |
89 | turn plug off:
90 |
91 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -s OFF
92 |
93 | get plug state:
94 |
95 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -g
96 |
97 | get power consumption (only SP2101W)
98 |
99 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -w
100 |
101 | get current consumption (only SP2101W)
102 |
103 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -a
104 |
105 | get schedule of the whole week:
106 |
107 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -G
108 |
109 | get schedule of the whole week as python array:
110 |
111 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -P
112 |
113 | set schedule for one day:
114 |
115 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -S
116 | "{'state': u'ON', 'sched': [[[11, 0], [11, 45]]], 'day': 6}"
117 |
118 | set schedule for the whole week:
119 |
120 | python smartplug.py -H 172.16.100.75 -l admin -p 1234 -S "[
121 | {'state': u'ON', 'sched': [[[1, 0], [1, 1]]], 'day': 0},
122 | {'state': u'ON', 'sched': [[[2, 0], [2, 2]]], 'day': 1},
123 | {'state': u'ON', 'sched': [[[3, 0], [3, 3]]], 'day': 2},
124 | {'state': u'ON', 'sched': [[[4, 0], [4, 4]]], 'day': 3},
125 | {'state': u'ON', 'sched': [[[5, 0], [5, 5]]], 'day': 4},
126 | {'state': u'ON', 'sched': [[[6, 0], [6, 6]]], 'day': 5},
127 | {'state': u'ON', 'sched': [[[7, 0], [7, 7]]], 'day': 6},
128 | ]"
129 | """
130 |
131 | def __init__(self, host, auth):
132 | """
133 | Create a new SmartPlug instance identified by the given URL.
134 |
135 | :rtype: object
136 | :param host: The IP/hostname of the SmartPlug. E.g. '172.16.100.75'
137 | :param auth: User and password to authenticate with the plug. E.g. ('admin', '1234')
138 | """
139 | object.__init__(self)
140 |
141 | self.url = "http://%s:10000/smartplug.cgi" % host
142 | self.auth = auth
143 | self.domi = getDOMImplementation()
144 |
145 | # Make a request to detect if Authentication type is Digest
146 | res = req.head(self.url)
147 | if res.headers['WWW-Authenticate'][0:6] == 'Digest':
148 | self.auth = HTTPDigestAuth(auth[0], auth[1])
149 |
150 | self.log = log.getLogger("SmartPlug")
151 |
152 | def _xml_cmd_setget_state(self, cmdId, cmdStr):
153 | """
154 | Create XML representation of a state command.
155 |
156 | :type self: object
157 | :type cmdId: str
158 | :type cmdStr: str
159 | :rtype: str
160 | :param cmdId: Use 'get' to request plug state, use 'setup' change plug state.
161 | :param cmdStr: Empty string for 'get', 'ON' or 'OFF' for 'setup'
162 | :return: XML representation of command
163 | """
164 |
165 | assert (cmdId == "setup" and cmdStr in ["ON", "OFF"]) or (cmdId == "get" and cmdStr == "")
166 |
167 | doc = self.domi.createDocument(None, "SMARTPLUG", None)
168 | doc.documentElement.setAttribute("id", "edimax")
169 |
170 | cmd = doc.createElement("CMD")
171 | cmd.setAttribute("id", cmdId)
172 | state = doc.createElement("Device.System.Power.State")
173 | cmd.appendChild(state)
174 | state.appendChild(doc.createTextNode(cmdStr))
175 |
176 | doc.documentElement.appendChild(cmd)
177 |
178 | xml = doc.toxml()
179 | self.log.debug("Request: %s" % xml)
180 |
181 | return xml
182 |
183 | def _xml_cmd_get_pc(self, what):
184 | """
185 | Get power or current consumption (only SP2101W).
186 |
187 | :type self: object
188 | :type what: str
189 | :rtype: str
190 | :param what: What to retrieve: "NowPower" or "NowCurrent
191 | :return: XML representation of command
192 | """
193 |
194 | assert what in ["NowPower", "NowCurrent"]
195 |
196 | doc = self.domi.createDocument(None, "SMARTPLUG", None)
197 | doc.documentElement.setAttribute("id", "edimax")
198 |
199 | cmd = doc.createElement("CMD")
200 | cmd.setAttribute("id", "get")
201 | pwr = doc.createElement("NOW_POWER")
202 | cmd.appendChild(pwr)
203 | state = doc.createElement("Device.System.Power.%s" % what)
204 | pwr.appendChild(state)
205 |
206 | doc.documentElement.appendChild(cmd)
207 |
208 | xml = doc.toxml()
209 | self.log.debug("Request: %s" % xml)
210 |
211 | return xml
212 |
213 | def _xml_cmd_get_info(self):
214 | """
215 | Create XML representation of a command to query some information
216 |
217 | :type self: object
218 | :rtype: str
219 | :return: XML representation of command
220 | """
221 |
222 | doc = self.domi.createDocument(None, "SMARTPLUG", None)
223 | doc.documentElement.setAttribute("id", "edimax")
224 |
225 | cmd = doc.createElement("CMD")
226 | cmd.setAttribute("id", "get")
227 | si = doc.createElement("SYSTEM_INFO")
228 | cmd.appendChild(si)
229 | doc.documentElement.appendChild(cmd)
230 |
231 | xml = doc.toxml()
232 | self.log.debug("Request: %s" % xml)
233 |
234 | return xml
235 |
236 | def _xml_cmd_get_sched(self):
237 | """
238 | Create XML representation of a command to query schedule of whole week from plug.
239 |
240 | :type self: object
241 | :rtype: str
242 | :return: XML representation of command
243 | """
244 |
245 | doc = self.domi.createDocument(None, "SMARTPLUG", None)
246 | doc.documentElement.setAttribute("id", "edimax")
247 |
248 | cmd = doc.createElement("CMD")
249 | cmd.setAttribute("id", "get")
250 | sched = doc.createElement("SCHEDULE")
251 | cmd.appendChild(sched)
252 | doc.createElement("Device.System.Power.State")
253 |
254 | doc.documentElement.appendChild(cmd)
255 |
256 | xml = doc.toxml()
257 | self.log.debug("Request: %s" % xml)
258 |
259 | return xml
260 |
261 | def _xml_cmd_set_sched(self, sched_days):
262 | """
263 | Create XML representation of a command to set scheduling for one day or whole week.
264 |
265 | :type self: object
266 | :type sched_days: list
267 | :rtype: str
268 | :param sched_day: Single day or whole week
269 | :return: XML representation of command
270 | """
271 |
272 | doc = self.domi.createDocument(None, "SMARTPLUG", None)
273 | doc.documentElement.setAttribute("id", "edimax")
274 |
275 | cmd = doc.createElement("CMD")
276 | cmd.setAttribute("id", "setup")
277 | sched = doc.createElement("SCHEDULE")
278 | cmd.appendChild(sched)
279 |
280 | if isinstance(sched_days, list):
281 | # more then one day
282 |
283 | for one_sched_day in sched_days:
284 |
285 | dev_sched = doc.createElement("Device.System.Power.Schedule.%d" % one_sched_day["day"])
286 | dev_sched.appendChild(doc.createTextNode(self._render_schedule(one_sched_day["sched"])))
287 | dev_sched.attributes["value"] = one_sched_day["state"]
288 |
289 | sched.appendChild(dev_sched)
290 |
291 | else:
292 | # one day
293 | dev_sched = doc.createElement("Device.System.Power.Schedule.%d" % sched_days["day"])
294 | dev_sched.appendChild(doc.createTextNode(self._render_schedule(sched_days["sched"])))
295 | dev_sched.attributes["value"] = sched_days["state"]
296 |
297 | sched.appendChild(dev_sched)
298 |
299 | doc.documentElement.appendChild(cmd)
300 |
301 | xml = doc.toxml()
302 | self.log.debug("Request: %s" % xml)
303 |
304 | return xml
305 |
306 | def _post_xml(self, xml):
307 | """
308 | Post XML command as multipart file to SmartPlug, parse XML response.
309 |
310 | :type self: object
311 | :type xml: str
312 | :rtype: str
313 | :param xml: XML representation of command (as generated by _xml_cmd)
314 | :return: 'OK' on success, 'FAILED' otherwise
315 | """
316 |
317 | files = {'file': xml}
318 |
319 | res = req.post(self.url, auth=self.auth, files=files)
320 |
321 | self.log.debug("Status code: %d" % res.status_code)
322 | self.log.debug("Response: %s" % res.text)
323 |
324 | if res.status_code == req.codes.ok:
325 | dom = parseString(res.text)
326 |
327 | try:
328 | val = dom.getElementsByTagName("CMD")[0].firstChild.nodeValue
329 |
330 | if val is None:
331 | val = dom.getElementsByTagName("CMD")[0].getElementsByTagName("Device.System.Power.State")[0].\
332 | firstChild.nodeValue
333 |
334 | return val
335 |
336 | except Exception as e:
337 |
338 | print(e.__str__())
339 |
340 | return None
341 |
342 | def _post_xml_dom(self, xml):
343 | """
344 | Post XML command as multipart file to SmartPlug, return response as raw dom.
345 |
346 | :type self: object
347 | :type xml: str
348 | :rtype: object
349 | :param xml: XML representation of command (as generated by _xml_cmd)
350 | :return: dom representation of XML response
351 | """
352 |
353 | files = {'file': xml}
354 |
355 | res = req.post(self.url, auth=self.auth, files=files)
356 |
357 | self.log.debug("Status code: %d" % res.status_code)
358 | self.log.debug("Response: %s" % res.text)
359 |
360 | if res.status_code == req.codes.ok:
361 | return parseString(res.text)
362 |
363 | return None
364 |
365 | @property
366 | def info(self):
367 | """
368 | Get device info (vendor, model, version, mac and system name (if available)).
369 |
370 | :type self: object
371 | :rtype: dictonary
372 | :return: dictonary with the following keys: vendor, model, version, mac, name
373 | """
374 |
375 | dom = self._post_xml_dom(self._xml_cmd_get_info())
376 |
377 | vendor = dom.getElementsByTagName("Run.Cus")[0].firstChild.nodeValue
378 | model = dom.getElementsByTagName("Run.Model")[0].firstChild.nodeValue
379 | version = dom.getElementsByTagName("Run.FW.Version")[0].firstChild.nodeValue
380 | mac = dom.getElementsByTagName("Run.LAN.Client.MAC.Address")[0].firstChild.nodeValue
381 |
382 | inf = {"vendor":vendor, "model":model, "version":version, "mac":mac}
383 |
384 | # not all plugs/fw versions seem to return the system name ...
385 | try:
386 | inf["name"] = dom.getElementsByTagName("Device.System.Name")[0].firstChild.nodeValue
387 | except IndexError:
388 | pass
389 |
390 | return inf
391 |
392 | @property
393 | def state(self):
394 | """
395 | Get the current state of the SmartPlug.
396 |
397 | :type self: object
398 | :rtype: str
399 | :return: 'ON' or 'OFF'
400 | """
401 |
402 | res = self._post_xml(self._xml_cmd_setget_state("get", ""))
403 |
404 | if res != "ON" and res != "OFF":
405 | raise Exception("Failed to communicate with SmartPlug")
406 |
407 | return res
408 |
409 | @state.setter
410 | def state(self, value):
411 | """
412 | Set the state of the SmartPlug
413 |
414 | :type self: object
415 | :type value: str
416 | :param value: 'ON', 'on', 'OFF' or 'off'
417 | """
418 |
419 | if value == "ON" or value == "on":
420 | res = self._post_xml(self._xml_cmd_setget_state("setup", "ON"))
421 | else:
422 | res = self._post_xml(self._xml_cmd_setget_state("setup", "OFF"))
423 |
424 | if res != "OK":
425 | raise Exception("Failed to communicate with SmartPlug")
426 |
427 | @property
428 | def power(self):
429 | """
430 | Get the power consumption of the SmartPlug (only SP2101W).
431 |
432 | :type self: object
433 | :rtype: tuple (str, float)
434 | :return: power consumption in W
435 | """
436 |
437 | dom = self._post_xml_dom(self._xml_cmd_get_pc("NowPower"))
438 |
439 | try:
440 | power = dom.getElementsByTagName("Device.System.Power.NowPower")[0].firstChild.nodeValue
441 | except:
442 | raise Exception("Failed to communicate with SmartPlug")
443 |
444 | return power
445 |
446 | @property
447 | def current(self):
448 | """
449 | Get the current consumption of the SmartPlug (only SP2101W).
450 |
451 | :type self: object
452 | :rtype: tuple (str, float)
453 | :return: current consumption in A
454 | """
455 |
456 | dom = self._post_xml_dom(self._xml_cmd_get_pc("NowCurrent"))
457 |
458 | try:
459 | current = dom.getElementsByTagName("Device.System.Power.NowCurrent")[0].firstChild.nodeValue
460 | except:
461 | raise Exception("Failed to communicate with SmartPlug")
462 |
463 | return current
464 |
465 | def _parse_schedule(self, sched):
466 | """
467 | Parse the plugs internal scheduling format string to python array
468 |
469 | :type self: object
470 | :type sched: str
471 | :rtype: list
472 | :param sched: scheduling string (of one day) as returned by plug
473 | :return: Python array with scheduling: [[[start_hh:start_mm],[end_hh:end_mm]], ... ]
474 | """
475 |
476 | sched_unpacked = [0] * 60 * 24
477 | hours = []
478 |
479 | idx_sched = 0
480 |
481 | # first, unpack the packed schedule from the plug
482 | for packed in sched:
483 |
484 | int_packed = int(packed, 16)
485 |
486 | sched_unpacked[idx_sched+0] = (int_packed >> 3) & 1
487 | sched_unpacked[idx_sched+1] = (int_packed >> 2) & 1
488 | sched_unpacked[idx_sched+2] = (int_packed >> 1) & 1
489 | sched_unpacked[idx_sched+3] = (int_packed >> 0) & 1
490 |
491 | idx_sched += 4
492 |
493 | idx_hours = 0
494 |
495 | hour = 0
496 | min = 0
497 |
498 | found_range = False
499 |
500 | # second build time array from unpacked schedule
501 | for m in sched_unpacked:
502 |
503 | if m == 1 and not found_range:
504 | found_range = True
505 | hours.append([[hour, min], [23, 59]])
506 |
507 | elif m == 0 and found_range:
508 | found_range = False
509 | hours[idx_hours][1][0] = hour
510 | hours[idx_hours][1][1] = min
511 | idx_hours += 1
512 |
513 | min += 1
514 |
515 | if min > 59:
516 | min = 0
517 | hour += 1
518 |
519 | return hours
520 |
521 | def _render_schedule(self, hours):
522 | """
523 | Render Python scheduling array back to plugs internal format
524 |
525 | :type self: object
526 | :type hours: list
527 | :rtype: str
528 | :param hours: Python array with scheduling hours: [[[start_hh:start_mm],[end_hh:end_mm]], ... ]
529 | :return: scheduling string (of one day) as needed by plug
530 | """
531 |
532 | sched = [0] * 60 * 24
533 | sched_str = ''
534 |
535 | # first, set every minute we found a schedule to 1 in the sched array
536 | for times in hours:
537 |
538 | idx_start = times[0][0] * 60 + times[0][1]
539 | idx_end = times[1][0] * 60 + times[1][1]
540 |
541 | if idx_end < idx_start:
542 | idx_end = 60 * 24
543 |
544 | for i in range(idx_start, idx_end):
545 | sched[i] = 1
546 |
547 | # second, pack the minute array from above into the plug format and make a string out of it
548 | for i in range(0, 60 * 24, 4):
549 | packed = (sched[i] << 3) + (sched[i+1] << 2) + (sched[i+2] << 1) + (sched[i+3] << 0)
550 | sched_str += "%X" % packed
551 |
552 | return sched_str
553 |
554 | @property
555 | def schedule(self):
556 | """
557 | Get scheduling for all days of week from plug as python list.
558 | Note: it looks like the plug only is able to return a whole week.
559 |
560 | :type self: object
561 | :rtype: list
562 | :return: List with scheduling for each day of week:
563 |
564 | [
565 | {'state': u'ON|OFF', 'sched': [[[hh, mm], [hh, mm]], ...], 'day': 0..6},
566 | ...
567 | ]
568 | """
569 |
570 | sched = []
571 |
572 | dom = self._post_xml_dom(self._xml_cmd_get_sched())
573 |
574 | if dom is None:
575 | return sched
576 |
577 | try:
578 |
579 | dom_sched = dom.getElementsByTagName("CMD")[0].getElementsByTagName("SCHEDULE")[0]
580 |
581 | for i in range(0, 7):
582 |
583 | sched.append(
584 | {"day": i,
585 | "state": dom_sched.getElementsByTagName("Device.System.Power.Schedule.%d" % i)[0].attributes[
586 | "value"].
587 | firstChild.nodeValue,
588 | "sched": self._parse_schedule(
589 | dom_sched.getElementsByTagName("Device.System.Power.Schedule.%d" % i)[0].
590 | firstChild.nodeValue)})
591 |
592 | except Exception as e:
593 |
594 | print(e.__str__())
595 |
596 | return sched
597 |
598 | @schedule.setter
599 | def schedule(self, sched):
600 | """
601 | Set scheduling for ony day of week or for whole week on the plug.
602 | Note: it seams not to be possible to schedule anything else then one day or a whole week.
603 |
604 | :type self: object
605 | :type sched: list
606 | :rtype: str
607 | :param sched: Array with scheduling hours for ons day:
608 |
609 | {'day': 0..6, 'state': 'ON' | 'OFF', [[start_hh:start_mm],[end_hh:end_mm]], ... ]}
610 |
611 | Or whole week:
612 |
613 | [{'day': 0..6, 'state': 'ON' | 'OFF', [[start_hh:start_mm],[end_hh:end_mm]], ... ]}, ...]
614 |
615 | :return: 'OK' (or exception on error)
616 | """
617 |
618 | res = self._post_xml(self._xml_cmd_set_sched(sched))
619 |
620 | if res != "OK":
621 | raise Exception("Failed to communicate with SmartPlug")
622 |
623 | return res
624 |
625 | if __name__ == "__main__":
626 |
627 | usage = "%prog [options]"
628 |
629 | parser = par.OptionParser(usage)
630 |
631 | parser.add_option("-v", "--verbose", action="store_true", help="Print debug information")
632 |
633 | parser.add_option("-H", "--host", default="172.16.100.75", help="Base URL of the SmartPlug")
634 | parser.add_option("-l", "--login", default="admin", help="Login user to authenticate with SmartPlug")
635 | parser.add_option("-p", "--password", default="1234", help="Password to authenticate with SmartPlug")
636 |
637 | parser.add_option("-i", "--info", action="store_true", help="Get plug information")
638 | parser.add_option("-g", "--get", action="store_true", help="Get state of plug")
639 | parser.add_option("-s", "--set", help="Set state of plug: ON or OFF")
640 |
641 | parser.add_option("-w", "--power", action="store_true", help="Get plug power consumption (only SP2101W)")
642 | parser.add_option("-a", "--current", action="store_true", help="Get plug current consumption (only SP2101W)")
643 |
644 | parser.add_option("-G", "--getsched", action="store_true", help="Get schedule from Plug")
645 | parser.add_option("-P", "--getschedpy", action="store_true", help="Get schedule from Plug as Python list")
646 | parser.add_option("-S", "--setsched", help="Set schedule of Plug")
647 |
648 | (options, args) = parser.parse_args()
649 |
650 | # this turns on debugging
651 | level = log.ERROR
652 |
653 | if options.verbose:
654 | level = log.DEBUG
655 |
656 | log.basicConfig(level=level, format='%(asctime)s - %(levelname) 8s [%(module) 15s] - %(message)s')
657 |
658 | p = SmartPlug(options.host, (options.login, options.password))
659 |
660 | if options.info:
661 |
662 | print("Plug info:")
663 | for i in sorted(p.info.items()):
664 | print("- %s: %s" % i)
665 |
666 | if options.get:
667 |
668 | print(p.state)
669 |
670 | elif options.set:
671 |
672 | p.state = options.set
673 |
674 | if options.power:
675 |
676 | print("%s W" % p.power)
677 |
678 | if options.current:
679 |
680 | print("%s A" % p.current)
681 |
682 | elif options.getsched:
683 |
684 | days = {0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday",
685 | 4: "Thursday", 5: "Friday", 6: "Saturday"}
686 |
687 | for day in p.schedule:
688 |
689 | if len(day["sched"]) > 0:
690 | print("Schedules for: %s (%s)" % (days[day["day"]], day["state"]))
691 |
692 | for sched in day["sched"]:
693 | print(" * %02d:%02d - %02d:%02d" % (sched[0][0], sched[0][1], sched[1][0], sched[1][1]))
694 |
695 | elif options.getschedpy:
696 |
697 | print(p.schedule.__str__())
698 |
699 | elif options.setsched:
700 |
701 | try:
702 |
703 | sched = eval(options.setsched)
704 | p.schedule = sched
705 |
706 | except Exception as e:
707 |
708 | print("Wrong input format: %s" % e.__str__())
709 | exit(-1)
710 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | paho-mqtt~=1.5.0
2 | influxdb~=5.3.0
3 | influxdb-client~=1.14.0
4 | pymodbus~=2.4.0
5 | requests~=2.24.0
--------------------------------------------------------------------------------
/sma-daemon.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # coding=utf-8
3 | """
4 | *
5 | * by Wenger Florian 2018-01-30
6 | * wenger@unifox.at
7 | *
8 | * this software is released under GNU General Public License, version 2.
9 | * This program is free software;
10 | * you can redistribute it and/or modify it under the terms of the GNU General Public License
11 | * as published by the Free Software Foundation; version 2 of the License.
12 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14 | * See the GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License along with this program;
17 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 | *
19 | * 2020-01-04 datenschuft changes to tun with speedwiredecoder
20 | * 2020-09-21 Tommi2Day add traceback for exception analysis
21 | * 2021-03-07 datenschuft add config to run section (required beause of seperating moduleconfig to extra section by dervomsee
22 | */
23 | """
24 | import sys, time,os
25 | from daemon3x import daemon3x
26 | from configparser import ConfigParser
27 | #import smaem
28 | import socket
29 | import struct
30 | from speedwiredecoder import *
31 | import traceback
32 | import importlib
33 |
34 | #read configuration
35 | parser = ConfigParser()
36 | #alternate config locations
37 | parser.read(['/etc/smaemd/config','config'])
38 | try:
39 | smaemserials=parser.get('SMA-EM', 'serials')
40 | except:
41 | print('Cannot find base config entry SMA-EM serials')
42 | sys.exit(1)
43 |
44 | serials=smaemserials.split(' ')
45 | #smavalues=parser.get('SMA-EM', 'values')
46 | #values=smavalues.split(' ')
47 | pidfile=parser.get('DAEMON', 'pidfile')
48 | ipbind=parser.get('DAEMON', 'ipbind')
49 | MCAST_GRP = parser.get('DAEMON', 'mcastgrp')
50 | MCAST_PORT = int(parser.get('DAEMON', 'mcastport'))
51 | features=parser.get('SMA-EM', 'features')
52 | features=features.split(' ')
53 | statusdir=''
54 | try:
55 | statusdir=parser.get('DAEMON','statusdir')
56 | except:
57 | statusdir="/run/shm/"
58 |
59 | if os.path.isdir(statusdir):
60 | statusfile=statusdir+"em-status"
61 | else:
62 | statusfile = "em-status"
63 |
64 | #feature list
65 | featurelist = {}
66 | featurecounter=0
67 |
68 | #set defaults
69 | if MCAST_GRP == "":
70 | MCAST_GRP = '239.12.255.254'
71 | if MCAST_PORT == 0:
72 | MCAST_PORT = 9522
73 |
74 | class MyDaemon(daemon3x):
75 | def config(self):
76 | global featurelist
77 | global featurecounter
78 | global features
79 | # Check features and load
80 | for feature in features:
81 | print ('import ' + feature + '.py')
82 | featureitem = {'name': feature}
83 | try:
84 | featureitem['feature'] = importlib.import_module('features.' + feature)
85 | except ModuleNotFoundError as e:
86 | print('Dependency problem: ' + str(e))
87 | sys.exit()
88 | except (ImportError, FileNotFoundError, TypeError):
89 | print('feature '+feature+ ' not found')
90 | sys.exit()
91 | try:
92 | featureitem['config']=dict(parser.items('FEATURE-'+feature))
93 | #print (featureitem['config'])
94 | except:
95 | print('feature '+feature+ ' not configured')
96 | sys.exit()
97 | try:
98 | # run config action, if any
99 | featureitem['feature'].config(featureitem['config'])
100 | except:
101 | pass
102 | featurelist[featurecounter]=featureitem
103 | featurecounter += 1
104 | def run(self):
105 | # prepare listen to socket-Multicast
106 | print("config")
107 | self.config()
108 | socketconnected = False
109 | while not socketconnected:
110 | #try:
111 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
112 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
113 | sock.bind(('', MCAST_PORT))
114 | try:
115 | mreq = struct.pack("4s4s", socket.inet_aton(MCAST_GRP), socket.inet_aton(ipbind))
116 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
117 | file = open(statusfile, "w")
118 | file.write('multicastgroup connected')
119 | file.close()
120 | socketconnected = True
121 | except BaseException:
122 | print('could not connect to mulicast group... rest a bit and retry')
123 | file = open(statusfile, "w")
124 | file.write('could not connect to mulicast group... rest a bit and retry')
125 | file.close()
126 | time.sleep(5)
127 | emparts = {}
128 | while True:
129 | #getting sma values
130 | try:
131 | #emparts=smaem.readem(sock)
132 | emparts=decode_speedwire(sock.recv(608))
133 | for serial in serials:
134 | # process only known sma serials
135 | if 'serial' in emparts:
136 | if serial==format(emparts['serial']):
137 | # running all enabled features
138 | for featurenr in featurelist:
139 | #print('>>> starting '+featurelist[featurenr]['name'])
140 | featurelist[featurenr]['feature'].run(emparts,featurelist[featurenr]['config'])
141 | except Exception as e:
142 | print("Daemon: Exception occured")
143 | print(traceback.format_exc())
144 | pass
145 | #Daemon - Coding
146 | if __name__ == "__main__":
147 | daemon = MyDaemon(pidfile)
148 | if len(sys.argv) == 2:
149 | if 'start' == sys.argv[1]:
150 | daemon.start()
151 | elif 'start_systemd' == sys.argv[1]:
152 | daemon.start_systemd()
153 | elif 'stop' == sys.argv[1]:
154 | for featurenr in featurelist:
155 | print('>>> stopping '+featurelist[featurenr]['name'])
156 | featurelist[featurenr]['feature'].stopping({},featurelist[featurenr]['config'])
157 | daemon.stop()
158 | elif 'restart' == sys.argv[1]:
159 | daemon.restart()
160 | elif 'restart_systemd' == sys.argv[1]:
161 | daemon.restart_systemd()
162 | elif 'run' == sys.argv[1]:
163 | daemon.run()
164 | else:
165 | print ("Unknown command")
166 | sys.exit(2)
167 | sys.exit(0)
168 | else:
169 | print ("usage: %s start|start_systemd|stop|restart|restart_systemd|run" % sys.argv[0])
170 | print (pidfile)
171 | sys.exit(2)
172 |
--------------------------------------------------------------------------------
/sma-em-capture-package.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # coding=utf-8
3 | """
4 | *
5 | * by Wenger Florian 2020-01-04
6 | * wenger@unifox.at
7 | *
8 | *
9 | * this software is released under GNU General Public License, version 2.
10 | * This program is free software;
11 | * you can redistribute it and/or modify it under the terms of the GNU General Public License
12 | * as published by the Free Software Foundation; version 2 of the License.
13 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
14 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
15 | * See the GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License along with this program;
18 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 | *
20 | * 2018-12-22 Tommi2Day small enhancements
21 | * 2019-08-13 datenschuft run without config
22 | *
23 | */
24 | """
25 |
26 | import signal
27 | import sys
28 | import socket
29 | import struct
30 | import binascii
31 | from configparser import ConfigParser
32 | from speedwiredecoder import *
33 |
34 | # clean exit
35 | def abortprogram(signal,frame):
36 | # Housekeeping -> nothing to cleanup
37 | print('STRG + C = end program')
38 | sys.exit(0)
39 |
40 | # abort-signal
41 | signal.signal(signal.SIGINT, abortprogram)
42 |
43 |
44 | #read configuration
45 | parser = ConfigParser()
46 | #default values
47 | smaserials = ""
48 | ipbind = '0.0.0.0'
49 | MCAST_GRP = '239.12.255.254'
50 | MCAST_PORT = 9522
51 | parser.read(['/etc/smaemd/config','config'])
52 | try:
53 | smaemserials=parser.get('SMA-EM', 'serials')
54 | ipbind=parser.get('DAEMON', 'ipbind')
55 | MCAST_GRP = parser.get('DAEMON', 'mcastgrp')
56 | MCAST_PORT = int(parser.get('DAEMON', 'mcastport'))
57 | except:
58 | print('Cannot find config /etc/smaemd/config... using defaults')
59 |
60 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
61 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
62 | sock.bind(('', MCAST_PORT))
63 | try:
64 | mreq = struct.pack("4s4s", socket.inet_aton(MCAST_GRP), socket.inet_aton(ipbind))
65 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
66 | except BaseException:
67 | print('could not connect to mulicast group or bind to given interface')
68 | sys.exit(1)
69 |
70 | # processing received messages
71 | smainfo=sock.recv(1024)
72 |
73 | #test-datagrem sma-em-1.2.4.R
74 | #smainfo=b'SMA\x00\x00\x04\x02\xa0\x00\x00\x00\x01\x02D\x00\x10`i\x01\x0eqB\xd1\xeb_\xc9\r\xd0\x00\x01\x04\x00\x00\x00\x87\x13\x00\x01\x08\x00\x00\x00\x00\x16R/\x00h\x00\x02\x04\x00\x00\x00\x00\x00\x00\x02\x08\x00\x00\x00\x00\x08\x9d\x14\xb8`\x00\x03\x04\x00\x00\x00\x00\x00\x00\x03\x08\x00\x00\x00\x00\x00\xc1%\xc1H\x00\x04\x04\x00\x00\x00\x11Q\x00\x04\x08\x00\x00\x00\x00\no\xce|\xe0\x00\t\x04\x00\x00\x00\x88.\x00\t\x08\x00\x00\x00\x00\x19\xdfo\x07\x18\x00\n\x04\x00\x00\x00\x00\x00\x00\n\x08\x00\x00\x00\x00\tJ\xc5x\xc8\x00\r\x04\x00\x00\x00\x03\xe0\x00\x15\x04\x00\x00\x00}P\x00\x15\x08\x00\x00\x00\x00\n.\x07o`\x00\x16\x04\x00\x00\x00\x00\x00\x00\x16\x08\x00\x00\x00\x00\n\xcb\xab\xf4 \x00\x17\x04\x00\x00\x00\x00\x00\x00\x17\x08\x00\x00\x00\x00\x00bF\x05p\x00\x18\x04\x00\x00\x00\r\xe4\x00\x18\x08\x00\x00\x00\x00\x04\xf3E\'\xc8\x00\x1d\x04\x00\x00\x00~\x14\x00\x1d\x08\x00\x00\x00\x00\x0c\x0f\xb7\xbd`\x00\x1e\x04\x00\x00\x00\x00\x00\x00\x1e\x08\x00\x00\x00\x00\x0b"p\x95\x90\x00\x1f\x04\x00\x00\x008G\x00 \x04\x00\x00\x03l\xd4\x00!\x04\x00\x00\x00\x03\xe2\x00)\x04\x00\x00\x00\x07,\x00)\x08\x00\x00\x00\x00\t\xdfT;\xf0\x00*\x04\x00\x00\x00\x00\x00\x00*\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x04\x00\x00\x00\x00\x00\x00+\x08\x00\x00\x00\x00\x00\x03\x12\xb1H\x00,\x04\x00\x00\x00\x03\x89\x00,\x08\x00\x00\x00\x00\x05)\x8a\x93h\x001\x04\x00\x00\x00\x07\xff\x001\x08\x00\x00\x00\x00\x0c4\xa7\x9b@\x002\x04\x00\x00\x00\x00\x00\x002\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\x04\x00\x00\x00\x03\xba\x004\x04\x00\x00\x03\x85\t\x005\x04\x00\x00\x00\x03\x81\x00=\x04\x00\x00\x00\x02\x97\x00=\x08\x00\x00\x00\x00\x04sj\xe2h\x00>\x04\x00\x00\x00\x00\x00\x00>\x08\x00\x00\x00\x00\x00\x00\x00o\x18\x00?\x04\x00\x00\x00\x00\x1c\x00?\x08\x00\x00\x00\x00\x00\xe2\xa0\xb1\xe8\x00@\x04\x00\x00\x00\x00\x00\x00@\x08\x00\x00\x00\x00\x00\xd9\xd2`\x98\x00E\x04\x00\x00\x00\x02\x97\x00E\x08\x00\x00\x00\x00\x05K\xf5vp\x00F\x04\x00\x00\x00\x00\x00\x00F\x08\x00\x00\x00\x00\x00\x00\x00o\x18\x00G\x04\x00\x00\x00\x01\xe6\x00H\x04\x00\x00\x03\x84.\x00I\x04\x00\x00\x00\x03\xe7\x90\x00\x00\x00\x01\x02\x04R\x00\x00\x00\x00'
75 |
76 | #test-datagram sma-homemanager-2.3.4.R
77 | #smainfo=b'SMA\x00\x00\x04\x02\xa0\x00\x00\x00\x01\x02L\x00\x10`i\x01t\xb2\xfb\xdb\na3\xfe\xa4\x00\x01\x04\x00\x00\x00\x00\x00\x00\x01\x08\x00\x00\x00\x00\x01\xd6[.\xf8\x00\x02\x04\x00\x00\x00\xbe\x80\x00\x02\x08\x00\x00\x00\x00\x07\x81\x86E`\x00\x03\x04\x00\x00\x00\x17x\x00\x03\x08\x00\x00\x00\x00\x0144\xf6\x90\x00\x04\x04\x00\x00\x00\x00\x00\x00\x04\x08\x00\x00\x00\x00\x00\xd5#\xa7P\x00\t\x04\x00\x00\x00\x00\x00\x00\t\x08\x00\x00\x00\x00\x02\x0fv\xbb\xf8\x00\n\x04\x00\x00\x00\xbf\xf0\x00\n\x08\x00\x00\x00\x00\x07\xac\xcc\x0c\xa0\x00\r\x04\x00\x00\x00\x03\xe0\x00\x0e\x04\x00\x00\x00\xc3<\x00\x15\x04\x00\x00\x00\x00\x00\x00\x15\x08\x00\x00\x00\x00\x00jb\xee\x80\x00\x16\x04\x00\x00\x00B9\x00\x16\x08\x00\x00\x00\x00\x02\xb4\xc4\xfa \x00\x17\x04\x00\x00\x00\x07\x16\x00\x17\x08\x00\x00\x00\x00\x00d>U\x08\x00\x18\x04\x00\x00\x00\x00\x00\x00\x18\x08\x00\x00\x00\x00\x00EC\xec0\x00\x1d\x04\x00\x00\x00\x00\x00\x00\x1d\x08\x00\x00\x00\x00\x00\x87z=H\x00\x1e\x04\x00\x00\x00B\x99\x00\x1e\x08\x00\x00\x00\x00\x02\xc4G\xe0 \x00\x1f\x04\x00\x00\x00\x1d\x15\x00 \x04\x00\x00\x03\x80\xbb\x00!\x04\x00\x00\x00\x03\xe2\x00)\x04\x00\x00\x00\x00\x00\x00)\x08\x00\x00\x00\x00\x00\xcc\xc2b\xe0\x00*\x04\x00\x00\x00=j\x00*\x08\x00\x00\x00\x00\x02n\xbf)\x88\x00+\x04\x00\x00\x00\tt\x00+\x08\x00\x00\x00\x00\x00u\x08\xddh\x00,\x04\x00\x00\x00\x00\x00\x00,\x08\x00\x00\x00\x00\x00P\x11\xe9x\x001\x04\x00\x00\x00\x00\x00\x001\x08\x00\x00\x00\x00\x00\xe7\xdb\xc0\xa8\x002\x04\x00\x00\x00>#\x002\x08\x00\x00\x00\x00\x02~E=\xc0\x003\x04\x00\x00\x00\x1a\xc2\x004\x04\x00\x00\x03\x8e\xb2\x005\x04\x00\x00\x00\x03\xdc\x00=\x04\x00\x00\x00\x00\x00\x00=\x08\x00\x00\x00\x00\x00\xc6T\xc4 \x00>\x04\x00\x00\x00>\xdd\x00>\x08\x00\x00\x00\x00\x02\x85!\t\xa8\x00?\x04\x00\x00\x00\x06\xed\x00?\x08\x00\x00\x00\x00\x00x~\xa8\xd8\x00@\x04\x00\x00\x00\x00\x00\x00@\x08\x00\x00\x00\x00\x00]^\xb90\x00E\x04\x00\x00\x00\x00\x00\x00E\x08\x00\x00\x00\x00\x00\xe1K,\x88\x00F\x04\x00\x00\x00?>\x00F\x08\x00\x00\x00\x00\x02\x97>i(\x00G\x04\x00\x00\x00\x1bi\x00H\x04\x00\x00\x03\x88i\x00I\x04\x00\x00\x00\x03\xe2\x90\x00\x00\x00\x02\x03\x04R\x00\x00\x00\x00'
78 |
79 | smainfoasci=binascii.b2a_hex(smainfo)
80 |
81 |
82 | emparts=decode_speedwire(smainfo)
83 |
84 |
85 | print ('----raw-output---')
86 | print (smainfo)
87 | print ('----asci-output---')
88 | print (smainfoasci)
89 |
90 | print ('----all-found-values---')
91 | for val in emparts:
92 | print ('{}: value:{}'.format(val,emparts[val]))
93 |
--------------------------------------------------------------------------------
/sma-em-measurement.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # coding=utf-8
3 | """
4 | *
5 | * by Wenger Florian 2015-09-02
6 | * wenger@unifox.at
7 | *
8 | * endless loop (until ctrl+c) displays measurement from SMA Energymeter
9 | *
10 | *
11 | * this software is released under GNU General Public License, version 2.
12 | * This program is free software;
13 | * you can redistribute it and/or modify it under the terms of the GNU General Public License
14 | * as published by the Free Software Foundation; version 2 of the License.
15 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
16 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17 | * See the GNU General Public License for more details.
18 | *
19 | * You should have received a copy of the GNU General Public License along with this program;
20 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 | *
22 | * 2018-12-22 Tommi2Day small enhancements
23 | * 2019-08-13 datenschuft run without config
24 | * 2020-01-04 datenschuft changes to tun with speedwiredecoder
25 | *
26 | */
27 | """
28 |
29 | import signal
30 | import sys
31 | #import smaem
32 | import socket
33 | import struct
34 | from configparser import ConfigParser
35 | from speedwiredecoder import *
36 |
37 | # clean exit
38 | def abortprogram(signal,frame):
39 | # Housekeeping -> nothing to cleanup
40 | print('STRG + C = end program')
41 | sys.exit(0)
42 |
43 | # abort-signal
44 | signal.signal(signal.SIGINT, abortprogram)
45 |
46 |
47 | #read configuration
48 | parser = ConfigParser()
49 | #default values
50 | smaserials = ""
51 | ipbind = '0.0.0.0'
52 | MCAST_GRP = '239.12.255.254'
53 | MCAST_PORT = 9522
54 | parser.read(['/etc/smaemd/config','config'])
55 | try:
56 | smaemserials=parser.get('SMA-EM', 'serials')
57 | ipbind=parser.get('DAEMON', 'ipbind')
58 | MCAST_GRP = parser.get('DAEMON', 'mcastgrp')
59 | MCAST_PORT = int(parser.get('DAEMON', 'mcastport'))
60 | except:
61 | print('Cannot find config /etc/smaemd/config... using defaults')
62 |
63 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
64 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
65 | sock.bind(('', MCAST_PORT))
66 | try:
67 | mreq = struct.pack("4s4s", socket.inet_aton(MCAST_GRP), socket.inet_aton(ipbind))
68 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
69 | except BaseException:
70 | print('could not connect to mulicast group or bind to given interface')
71 | sys.exit(1)
72 | # processing received messages
73 | while True:
74 | emparts = {}
75 | emparts=decode_speedwire(sock.recv(608))
76 | # Output...
77 | # don't know what P,Q and S means:
78 | # http://en.wikipedia.org/wiki/AC_power or http://de.wikipedia.org/wiki/Scheinleistung
79 | # thd = Total_Harmonic_Distortion http://de.wikipedia.org/wiki/Total_Harmonic_Distortion
80 | # cos phi is always positive, no matter what quadrant
81 | print ('\n')
82 | print ('SMA-EM Serial:{}'.format(emparts['serial']))
83 | print ('----sum----')
84 | print ('P: consume:{}W {}kWh supply:{}W {}kWh'.format(emparts['pconsume'],emparts['pconsumecounter'],emparts['psupply'],emparts['psupplycounter']))
85 | print ('S: consume:{}VA {}kVAh supply:{}VA {}VAh'.format(emparts['sconsume'],emparts['sconsumecounter'],emparts['ssupply'],emparts['ssupplycounter']))
86 | print ('Q: cap {}var {}kvarh ind {}var {}kvarh'.format(emparts['qconsume'],emparts['qconsumecounter'],emparts['qsupply'],emparts['qsupplycounter']))
87 | print ('cos phi:{}°'.format(emparts['cosphi']))
88 | if emparts['speedwire-version']=="2.3.4.R|020304":
89 | print ('frequency:{}Hz'.format(emparts['frequency']))
90 | print ('----L1----')
91 | print ('P: consume:{}W {}kWh supply:{}W {}kWh'.format(emparts['p1consume'],emparts['p1consumecounter'],emparts['p1supply'],emparts['p1supplycounter']))
92 | print ('S: consume:{}VA {}kVAh supply:{}VA {}kVAh'.format(emparts['s1consume'],emparts['s1consumecounter'],emparts['s1supply'],emparts['s1supplycounter']))
93 | print ('Q: cap {}var {}kvarh ind {}var {}kvarh'.format(emparts['q1consume'],emparts['q1consumecounter'],emparts['q1supply'],emparts['q1supplycounter']))
94 | print ('U: {}V I:{}A cos phi:{}°'.format(emparts['u1'],emparts['i1'],emparts['cosphi1']))
95 | print ('----L2----')
96 | print ('P: consume:{}W {}kWh supply:{}W {}kWh'.format(emparts['p2consume'],emparts['p2consumecounter'],emparts['p2supply'],emparts['p2supplycounter']))
97 | print ('S: consume:{}VA {}kVAh supply:{}VA {}kVAh'.format(emparts['s2consume'],emparts['s2consumecounter'],emparts['s2supply'],emparts['s2supplycounter']))
98 | print ('Q: cap {}var {}kvarh ind {}var {}kvarh'.format(emparts['q2consume'],emparts['q2consumecounter'],emparts['q2supply'],emparts['q2supplycounter']))
99 | print ('U: {}V I:{}A cos phi:{}°'.format(emparts['u2'],emparts['i2'],emparts['cosphi2']))
100 | print ('----L3----')
101 | print ('P: consume:{}W {}kWh supply:{}W {}kWh'.format(emparts['p3consume'],emparts['p3consumecounter'],emparts['p3supply'],emparts['p3supplycounter']))
102 | print ('S: consume:{}VA {}kVAh supply:{}VA {}kVAh'.format(emparts['s3consume'],emparts['s3consumecounter'],emparts['s3supply'],emparts['s3supplycounter']))
103 | print ('Q: cap {}var {}kvarh ind {}var {}kvarh'.format(emparts['q3consume'],emparts['q3consumecounter'],emparts['q3supply'],emparts['q3supplycounter']))
104 | print ('U: {}V I:{}A cos phi:{}°'.format(emparts['u3'],emparts['i3'],emparts['cosphi3']))
105 | print ('Version: {}'.format(emparts['speedwire-version']))
106 |
--------------------------------------------------------------------------------
/speedwiredecoder.py:
--------------------------------------------------------------------------------
1 | """
2 | *
3 | * by david-m-m 2019-Mar-17
4 | * by datenschuft 2020-Jan-04
5 | *
6 | * this software is released under GNU General Public License, version 2.
7 | * This program is free software;
8 | * you can redistribute it and/or modify it under the terms of the GNU General Public License
9 | * as published by the Free Software Foundation; version 2 of the License.
10 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
11 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12 | * See the GNU General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU General Public License along with this program;
15 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 | *
17 | */
18 | """
19 |
20 | import binascii
21 |
22 | # unit definitions with scaling
23 | sma_units={
24 | "W": 10,
25 | "VA": 10,
26 | "VAr": 10,
27 | "kWh": 3600000,
28 | "kVAh": 3600000,
29 | "kVArh": 3600000,
30 | "A": 1000,
31 | "V": 1000,
32 | "°": 1000,
33 | "Hz": 1000,
34 | }
35 |
36 | # map of all defined SMA channels
37 | # format: :(emparts_name>,,)
38 | sma_channels={
39 | # totals
40 | 1:('pconsume','W','kWh'),
41 | 2:('psupply','W','kWh'),
42 | 3:('qconsume','VAr','kVArh'),
43 | 4:('qsupply','VAr','kVArh'),
44 | 9:('sconsume','VA','kVAh'),
45 | 10:('ssupply','VA','kVAh'),
46 | 13:('cosphi','°'),
47 | 14:('frequency','Hz'),
48 | # phase 1
49 | 21:('p1consume','W','kWh'),
50 | 22:('p1supply','W','kWh'),
51 | 23:('q1consume','VAr','kVArh'),
52 | 24:('q1supply','VAr','kVArh'),
53 | 29:('s1consume','VA','kVAh'),
54 | 30:('s1supply','VA','kVAh'),
55 | 31:('i1','A'),
56 | 32:('u1','V'),
57 | 33:('cosphi1','°'),
58 | # phase 2
59 | 41:('p2consume','W','kWh'),
60 | 42:('p2supply','W','kWh'),
61 | 43:('q2consume','VAr','kVArh'),
62 | 44:('q2supply','VAr','kVArh'),
63 | 49:('s2consume','VA','kVAh'),
64 | 50:('s2supply','VA','kVAh'),
65 | 51:('i2','A'),
66 | 52:('u2','V'),
67 | 53:('cosphi2','°'),
68 | # phase 3
69 | 61:('p3consume','W','kWh'),
70 | 62:('p3supply','W','kWh'),
71 | 63:('q3consume','VAr','kVArh'),
72 | 64:('q3supply','VAr','kVArh'),
73 | 69:('s3consume','VA','kVAh'),
74 | 70:('s3supply','VA','kVAh'),
75 | 71:('i3','A'),
76 | 72:('u3','V'),
77 | 73:('cosphi3','°'),
78 | # common
79 | 36864:('speedwire-version',''),
80 | }
81 |
82 | def decode_OBIS(obis):
83 | measurement=int.from_bytes(obis[0:2], byteorder='big' )
84 | raw_type=int.from_bytes(obis[2:3], byteorder='big')
85 | if raw_type==4:
86 | datatype='actual'
87 | elif raw_type==8:
88 | datatype='counter'
89 | elif raw_type==0 and measurement==36864:
90 | datatype='version'
91 | else:
92 | datatype='unknown'
93 | print('unknown datatype: measurement {} datatype {} raw_type {}'.format(measurement,datatype,raw_type))
94 | return (measurement,datatype)
95 |
96 | def decode_speedwire(datagram):
97 | emparts={}
98 | # process data only of SMA header is present
99 | if datagram[0:3]==b'SMA':
100 | # datagram length
101 | datalength=int.from_bytes(datagram[12:14],byteorder='big')+16
102 | #print('data lenght: {}'.format(datalength))
103 | if datalength != 54:
104 | # serial number
105 | emID=int.from_bytes(datagram[20:24],byteorder='big')
106 | #print('seral: {}'.format(emID))
107 | emparts['serial']=emID
108 | # timestamp
109 | timestamp=int.from_bytes(datagram[24:28],byteorder='big')
110 | #print('timestamp: {}'.format(timestamp))
111 | # decode OBIS data blocks
112 | # start with header
113 | position=28
114 | while position