Compare commits

...
212 Commits
Author SHA1 Message Date
Nigel 2aba206c6f Merge branch 'master' of github.com:ndokter/dsmr_parser 2022-04-20 22:10:27 +02:00
Nigel 247a7446f5 Prepare release 0.33 2022-04-20 22:10:06 +02:00
Nigel 5c378f3419 Update README.rst 2022-04-20 22:02:35 +02:00
Nigel b825faa719 Update README.rst 2022-04-20 22:00:58 +02:00
Nigel 7f35cd3c73 Merge pull request #63 from mjkl-gh/master
Improve documentation asyncio
2022-04-20 21:58:59 +02:00
Nigel 63338fbf06 Merge branch 'master' into master 2022-04-20 21:58:51 +02:00
Nigel dd9e264f5c Merge pull request #107 from dennissiemensma/feature/python-3.10
Test Python 3.10 in CI + legacy badge fix
2022-04-20 21:48:09 +02:00
Nigel 5c4d6ed98b Merge pull request #106 from gigatexel/master
Update telegram_specifications.py
2022-04-20 21:47:27 +02:00
Dennis Siemensma f238eb14a1 Converted markdown link to restructured link 2022-04-20 21:32:41 +02:00
Dennis Siemensma 179a75e58c Typo fix 2022-04-20 21:30:50 +02:00
Dennis Siemensma 32c20b61ac Refer to CI config for the currently supported/tested Python versions 2022-04-20 21:29:08 +02:00
Dennis Siemensma 602ed4928a YAML fix for CI config 2022-04-20 21:28:31 +02:00
Dennis Siemensma f5bedb1e6e CI now tests Python 3.10 as well 2022-04-20 21:26:07 +02:00
Dennis Siemensma bc6eab73de Replace legacy Travis status badge with GitHub Actions 2022-04-20 21:25:24 +02:00
gigatexel e5eddd006e Update telegram_specifications.py 2022-04-08 14:20:11 +02:00
gigatexel 0ed5fc0220 Update telegram_specifications.py 2022-04-08 14:09:56 +02:00
Nigel c0a509dab3 Merge pull request #105 from gigatexel/master
Improve compatiblity with Belgian standard
2022-04-08 10:06:02 +02:00
gigatexel 3d5599289e Update obis_name_mapping.py 2022-04-08 09:57:02 +02:00
gigatexel 99ab86fffb Update telegram_specifications.py 2022-04-08 09:56:36 +02:00
gigatexel 15b3653a02 Update telegram_specifications.py 2022-04-06 13:30:58 +02:00
gigatexel 527730781c Update obis_name_mapping.py 2022-04-06 13:30:12 +02:00
gigatexel 3f41a73b9d Update obis_references.py 2022-04-06 13:28:57 +02:00
Nigel ddbcb67088 Merge pull request #104 from ndokter/issue-101
Issue 101
2022-02-23 13:39:30 +01:00
Nigel 746fbc4fff Merge pull request #103 from vladimirvrabely/issue-101
Add FileReader test
2022-02-22 20:35:03 +01:00
Nigel 304593c246 Merge pull request #102 from ndokter/issue-101
fix FileReader from not completing
2022-02-22 20:33:31 +01:00
Vladimir 7610646e10 Add FileReader test 2022-02-22 09:50:11 +01:00
Nigel Dokter a6bc66667b fix FileReader from not completing 2022-02-21 20:05:23 +01:00
Nigel 0340b25733 Update README.rst 2022-02-21 13:38:06 +01:00
Nigel 5c838efbe1 Fix CHANGELOG formatting 2022-01-04 21:16:03 +01:00
Nigel 9f66c075d6 Prepare version 0.32 2022-01-04 21:14:53 +01:00
Nigel 2dafc7e7e4 Merge branch 'master' of github.com:ndokter/dsmr_parser 2022-01-04 21:10:44 +01:00
Nigel b74572238e Merge pull request #98 from rhpijnacker/feature/rfxtrx
Support DSMR data read via RFXtrx with integrated P1 reader
2022-01-04 09:59:54 +01:00
Ronald Pijnacker cedf71dbb5 Merge remote-tracking branch 'upstream/master' into feature/rfxtrx 2022-01-04 09:55:00 +01:00
Ronald Pijnacker eace91b591 Fix coding style issues 2022-01-04 09:53:49 +01:00
Ronald Pijnacker dd6d26670e Rewrite for compatibility with python 3.6 2022-01-04 09:49:11 +01:00
Ronald Pijnacker 7d28d0e370 Update according to coding style 2022-01-03 21:25:19 +01:00
Ronald Pijnacker c082cf4868 Add test case 2022-01-01 20:19:24 +01:00
Ronald Pijnacker 188cac5287 Small update 2021-12-30 20:32:44 +01:00
Ronald Pijnacker c7ed4acb03 Refactor into separate file 2021-12-30 20:23:58 +01:00
Ronald Pijnacker 8b64adb80c Small rename 2021-12-30 17:23:31 +01:00
Ronald Pijnacker 3eed3654d4 Wrap DSMR protocol in RFXtrx wrapper 2021-12-30 17:01:29 +01:00
Nigel b562510e40 Merge pull request #96 from lowdef/master
resolve name clash as reported in issue #95
2021-11-28 12:27:22 +01:00
Hans Erik van Elburg f0e035c8ed fix newer version pylama errors 2021-11-28 01:12:32 +01:00
Hans Erik van Elburg 83ef354c12 resolve name clash as reported in issue #95 2021-11-27 19:55:58 +01:00
Nigel dc2f493739 Merge pull request #70 from bremme/master
Add SocketReader to connect to remote serial port
2021-11-21 18:10:17 +01:00
Nigel 0a20dea7ed Merge pull request #93 from lowdef/master
fix pylama errors on PR92 + editorial fix
2021-11-21 17:56:01 +01:00
Hans Erik van Elburg 28e3c51f0d fix pylama errors on PR92 + editorial fix 2021-11-21 16:08:47 +01:00
Nigel 60af45be25 Prepare version 0.31 2021-11-21 14:06:11 +01:00
Nigel 00248f490e Merge pull request #92 from Aeroid/master
Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway
2021-11-21 13:59:35 +01:00
Gunnar Klauberg 45cc88942a latin-1 changes folded into data_received 2021-11-21 11:25:05 +00:00
Gunnar Klauberg 26ac27c347 re-adding ascii decoding at telegram level 2021-11-14 22:29:37 +00:00
Gunnar Klauberg 7a89d6e97b re-adding ascii decoding at telegram level 2021-11-14 22:20:00 +00:00
Gunnar Klauberg de4dc2ec98 Q3D COM-1 test telegrams 2021-11-14 22:05:27 +00:00
Gunnar Klauberg 6866e7d585 leaving version as it was 2021-11-14 21:52:38 +00:00
Gunnar Klauberg df59f3a498 decode latin1 and added ELECTRICITY_EXPORTED_TOTAL 2021-11-14 21:49:43 +00:00
Gunnar Klauberg 4073917d3e decode latin1 and added ELECTRICITY_EXPORTED_TOTAL 2021-11-14 21:29:24 +00:00
Nigel 3a4fcd7da1 Merge pull request #89 from dennissiemensma/feature/github-actions-with-fixes
Replace Travis with GitHub Actions (+ minor fixes)
2021-11-14 21:08:09 +01:00
Nigel d5b427ba4f Merge pull request #82 from Lennart99/master
add special option for Landis+Gyr E360 (DMSR 5 with serial version 4)
2021-11-14 21:06:58 +01:00
Gunnar Klauberg c1202f33e9 clean-up re-format 2021-11-12 20:12:47 +00:00
Gunnar Klauberg 3092ba8b1f clean-up re-format 2021-11-12 19:24:44 +00:00
Gunnar Klauberg 8e2bdd3279 clean-up re-format 2021-11-12 19:23:38 +00:00
Gunnar Klauberg 2b724b87c3 clean-up re-format 2021-11-12 19:21:32 +00:00
Gunnar Klauberg 17b56ae07b clean-up re-format 2021-11-12 17:51:24 +00:00
Gunnar Klauberg e4f384c2b7 clean-up re-format 2021-11-12 17:48:21 +00:00
Gunnar Klauberg 8b8b952ce1 clean-up re-format 2021-11-12 17:38:16 +00:00
Gunnar Klauberg 007b3ea089 ignoring trailing \xff in byte stream 2021-11-12 18:13:48 +01:00
Gunnar Klauberg 761aaccb3f adding EasyMeter Q3D support 2021-11-12 15:19:43 +00:00
Nigel dc608f9da3 Update README.rst 2021-10-31 13:08:42 +01:00
Dennis Siemensma 88ef4f4921 Split dependency installation step in Actions 2021-09-22 21:38:24 +02:00
Dennis Siemensma 61de170a79 Tox no longer relies on every Python version installed, due to multiple CI runners 2021-09-22 21:36:46 +02:00
Dennis Siemensma 947fd64377 E501 line too long (170 > 120 characters) [pycodestyle] 2021-09-22 21:33:22 +02:00
Dennis Siemensma 62aa0ac3ab W0611 'asyncio' imported but unused [pyflakes] 2021-09-22 21:31:11 +02:00
Dennis Siemensma da98cf1d05 Drop Travis (travis.org is EOL) 2021-09-22 21:28:36 +02:00
Dennis Siemensma fcb0dc600b CI tests using Github Actions 2021-09-22 21:28:17 +02:00
Dennis Siemensma b3a705a74d "pytest-catchlog plugin has been merged into the core, please remove it from your requirements" 2021-09-22 21:27:22 +02:00
Dennis Siemensma 4688cf9058 test/test_protocol.py:6:1: W0611 'dsmr_parser.telegram_specifications' imported but unused [pyflakes]
test/test_protocol.py:7:1: W0611 'dsmr_parser.parsers.TelegramParser' imported but unused [pyflakes]
2021-09-22 21:26:54 +02:00
Lennart99 fb9e488102 Merge branch 'ndokter:master' into master 2021-09-07 20:40:35 +02:00
Nigel d7e1f41162 Prepare version 0.30 2021-08-18 16:59:19 +02:00
Nigel 45de349062 Prepare version 0.29 2021-08-18 16:54:44 +02:00
Nigel f46f88bdb8 Merge pull request #86 from emontnemery/dsmr_sweden
Add support for Swedish smart meters
2021-08-16 17:47:47 +02:00
Erik fa70ada0bf Add support for Swedish smart meters 2021-08-14 17:08:27 +02:00
Lennart99 e30c951c7f add special option for Landis+Gyr E360 (DMSR 5 with serial version 4) 2021-06-10 12:23:20 +02:00
Nigel d989cfd0e6 Merge pull request #78 from fabaff/patch-1
Add license tag
2021-04-22 10:24:02 +02:00
Fabian Affolter ada02bf993 Add license tag 2021-04-21 23:36:25 +02:00
Nigel e607b62fa2 Merge pull request #71 from lowdef/fix_ProfileGenericObject_for_telegram_iterator
add value and unit properties to ProfileGenericObject to make sure they do not break
2021-03-04 21:49:03 +01:00
Nigel 247860be16 Merge pull request #76 from frenck/frenck-2021-0772
Remove deprecated asyncio coroutine decorator
2021-03-04 21:48:04 +01:00
Nigel c590109a1d Update README.rst
Fixed small typo
2021-03-04 21:47:12 +01:00
Franck Nijhof adaa2dcad5 Remove deprecated asyncio coroutine decorator 2021-03-03 11:48:49 +01:00
Hans Erik van Elburg 8c861ee308 resolved comment 2021-02-14 22:08:35 +01:00
Nigel f806cc01d3 Preparing release 0.28 2021-02-12 17:56:33 +01:00
Nigel 399532f244 Merge pull request #73 from hogend/keep-alive
Optional keep alive monitoring for TCP/IP connections
2021-02-12 17:38:48 +01:00
Rene Hogendoorn b901b3f74e Add unit test for keep-alive 2021-02-11 11:08:19 +01:00
Nigel a255380953 Merge pull request #74 from lowdef/issue72_robustness_fix
catch parse errors in TelegramParser, ignore lines that can not be parsed
2021-02-07 14:52:45 +01:00
Hans Erik van Elburg 1cdda2eaba catch parse errors in TelegramParser, ignore lines that can not be parsed 2021-02-07 13:40:20 +01:00
Rene Hogendoorn 7453534927 Raised log level to warning for failed keep-alive check 2021-01-19 10:55:20 +01:00
Rene Hogendoorn bbd73897a0 Optional keep alive monitoring for TCP/IP connections
* Since dsmr-parser is listen-only, it will not notice interrupted connections
  and DSMR device restarts. The connection will be reset after an (optional)
  keep-alive interval if no messages were received from the device.
2021-01-19 08:26:28 +01:00
Hans Erik van Elburg 804747c370 add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break 2021-01-03 19:58:19 +01:00
bremme 97786576cf Add SocketReader documentation 2020-12-27 18:58:14 +01:00
bremme e12aff5c0d Add SocketReader for reading ipv4 tcp sockets 2020-12-27 18:57:21 +01:00
Nigel 3dc77a8231 Prepare for release 0.27 2020-12-24 21:46:41 +01:00
Nigel d61f2229c8 Merge pull request #69 from lowdef/issue57_revisited
Issue57 revisited
2020-12-24 21:42:08 +01:00
Hans Erik van Elburg 3ddf0366e6 small fix 2020-12-24 12:44:49 +01:00
Hans Erik van Elburg 629767590b finished tox tests for issue 57 fix 2020-12-24 12:37:45 +01:00
Hans Erik van Elburg 81cccbd228 fix tox tests 2020-12-24 01:52:17 +01:00
Hans Erik van Elburg 5b1e830018 make sure that for the special case (actually invalid syntax) where a ProfileGeneric line only contains (); an empty ProfileGenericObject is created 2020-12-24 00:22:29 +01:00
albert 2d712b506d referring to parent method from _parse_values in ProfileGenericParser 2020-12-23 18:47:34 +01:00
albert 1318204d0c tempfix for empty profileGenericParser 2020-12-23 18:03:49 +01:00
Nigel feb0f88ddc Preparing release 0.26 2020-12-15 15:03:22 +01:00
Nigel 602129a665 Merge pull request #68 from ndokter/revert-57-master
Revert "fix for empty parentheses in ProfileGenericParser"
2020-12-15 15:00:07 +01:00
Nigel af9a99d995 Revert "fix for empty parentheses in ProfileGenericParser" 2020-12-15 14:59:35 +01:00
Nigel a7b4929eab preparation for v0.25 2020-12-14 17:21:16 +01:00
Nigel 8999b1c924 Merge branch 'master' of github.com:ndokter/dsmr_parser 2020-12-14 17:16:55 +01:00
Nigel 2391f38c4a Merge pull request #57 from avandermeer/master
fix for empty parentheses in ProfileGenericParser
2020-12-14 16:55:38 +01:00
Maarten Kleijwegt bc7961f840 add asyncio read as object 2020-12-13 16:36:30 +01:00
Nigel d1a6e15c68 Merge pull request #66 from GuyFoetz/foreign_name_mapping
adding the # Alternate codes for foreign countries.
2020-12-13 13:22:15 +01:00
Guy Foetz b7c8626d0c adding the # Alternate codes for foreign countries. 2020-12-13 12:15:51 +00:00
Nigel 684023d0b6 Preparing for release 0.24 2020-11-27 21:18:59 +01:00
Your Name 58851b5c5c Merge branch 'master' of https://github.com/mjkl-gh/dsmr_parser 2020-11-22 23:11:32 +01:00
Your Name ba9f3f3c25 First draft addin async documentation 2020-11-22 23:10:04 +01:00
Nigel 1a886aa03b Merge pull request #62 from RobBie1221/add_lux_eq
Add Luxembourg equipment identifier
2020-11-12 17:10:54 +01:00
Nigel 1c7535448e Updated changelog and version 2020-11-07 08:56:24 +01:00
Rob Bierbooms 2a95d33c02 Add Luxembourg equipment identifier 2020-11-04 20:42:38 +01:00
Nigel de6725dff8 Merge pull request #58 from dunajski/master
Corrects example telegrams.
2020-11-03 21:29:35 +01:00
Nigel 1b1a3f505e Merge pull request #59 from scop/dont-install-tests
Do not install tests
2020-11-03 21:27:21 +01:00
Nigel ebea385753 Merge pull request #61 from lowdef/issue60
issue#60 resolved issue with x-x:24.3.0 where it contains non-integer…
2020-11-03 21:25:39 +01:00
Hans Erik van Elburg 700cf6a2b3 issue#60 resolved issue with x-x:24.3.0 where it contains non-integer characters, parse 2nd value as string instead of integer 2020-11-02 00:29:03 +01:00
Ville Skyttä 7a687a99a6 Do not install tests 2020-10-18 10:49:09 +03:00
dunajski 86d665df39 Change README.md according to change in ec1d37e
In ec1d37ea08 changed example telegrams,
so example telegram from README.md should be corrected too.
2020-09-28 11:35:47 +02:00
dunajski ec1d37ea08 Corrects example telegrams.
In example telegrams there a few mistakes in header strings. Using '\'
in strings have special feature to add to string some non-printable
characters, for example to add new line we can add \n for tab \t etc. So
whenever we want to just print backslash as normal character we have to
add another '\' before. In short string "On\\Off" prints "On\Off" ascii
string.

In enquired telegrams there are few strings with \2 sign in header,
'/ISk5\2MT382-1000\r\n' and when we calculate CRC for whole given
telegram part of "..\2.." is interpreted as one character (value 0x02)
instead of two characters ('\', '2').

I think in a header in the example enlosed in P1 DSMR Companion Standard
/ISk5\2.. stand for printable '\' and '2' character instead of
"converted" to special '\2' character.
2020-09-28 10:22:16 +02:00
albert dc902a83e9 referring to parent method from _parse_values in ProfileGenericParser 2020-09-08 12:30:51 +02:00
albert be4386bc59 tempfix for empty profileGenericParser 2020-09-07 18:15:49 +02:00
Nigel 0427ace079 Updated changelog 2020-08-23 09:36:31 +02:00
Nigel addf9b590e Merge pull request #55 from aequitas/exception
Pass exception to log.exception as it is not called from within an ex…
2020-08-23 09:32:44 +02:00
Nigel 51f821a7fc Merge pull request #49 from dupondje/fix_pycrc
Improve CRC speed
2020-08-23 09:30:22 +02:00
Johan Bloemberg 5630520771 Pass exception to log.exception as it is not called from within an except: clause. Add Python 3.8 test support, drop Python 3.4 test support. 2020-07-21 10:31:40 +02:00
Nigel f9e188812f Merge pull request #54 from lowdef/fix_json_serialization
Fix json serialization
2020-05-29 17:22:53 +02:00
Hans Erik van Elburg 837ba3b6f7 add json serialization 2020-05-25 01:38:14 +02:00
Hans Erik van Elburg 94447c3571 GenericProfileObject value: make embedded buffer items also appear as dicts 2020-05-17 16:58:28 +02:00
Hans Erik van Elburg c2dea29c83 add a value property to GenericProfileObject, return a dict 2020-05-17 16:49:29 +02:00
Nigel bb486fc76f Merge pull request #53 from lowdef/profile_generic_parser
Profile generic parser
2020-05-17 09:12:56 +02:00
Hans Erik van Elburg 789871899c ProfileGeneric parser working, ProfileGenericObject implemented and Test for V4 telegram completed. 2020-05-17 01:25:02 +02:00
Hans Erik van Elburg b6278a8991 ProfileGeneric parser working, TODO complete ProfileGenericObject + Test 2020-05-16 16:31:26 +02:00
Hans Erik van Elburg a0ce89054a make all objects able to print their own values 2020-05-12 23:45:16 +02:00
Hans Erik van Elburg 9610fbc3c1 Merge branch 'add_missing_elements_DSRM_Specification_4' 2020-05-11 22:22:00 +02:00
Hans Erik van Elburg d1ad4fa585 igonre venv/ 2020-05-11 21:08:20 +02:00
Hans Erik van Elburg a44afb1a59 ignore .venv 2020-05-10 20:47:11 +02:00
Hans Erik van Elburg d98c93a57f modified changelog 2020-05-03 11:37:53 +02:00
Hans Erik van Elburg fc4a96ebab bump version 2020-05-03 11:18:08 +02:00
Hans Erik van Elburg c4331f6cd6 add tests for the missing elements and correct some test bugs 2020-05-02 15:50:00 +02:00
Hans Erik van Elburg 88c9ccd83d Add following missing signatures to the V4 telegram specification SHORT_POWER_FAILURE_COUNT, INSTANTANEOUS_CURRENT_L1, INSTANTANEOUS_CURRENT_L2, INSTANTANEOUS_CURRENT_L3. 2020-05-01 20:36:35 +02:00
Jean-Louis Dupond 78419f6cc7 Improve CRC speed 2020-02-16 09:31:06 +01:00
Nigel d423070853 Merge pull request #45 from lowdef/extend_filereader
Extend filereader
2020-02-03 17:31:15 +01:00
Nigel 987d35332a Merge branch 'master' into extend_filereader 2020-02-03 17:29:39 +01:00
lowdef fe710e9272 redo essential change 2020-02-02 22:54:21 +01:00
lowdef a7b0b03391 remove conflicting entries 2020-02-02 22:52:06 +01:00
Hans Erik van Elburg 5d88284d8d remove requires property again 2020-02-02 22:34:17 +01:00
Hans Erik van Elburg b6537678a7 cleaned up based on pylama complaints / pinpointed to coverage version 4.5.4 as next version is incompatible 2020-02-02 22:12:25 +01:00
Hans Erik van Elburg fee3f696c4 merged upstream 0.18 version and resolved conflict 2020-02-02 17:26:47 +01:00
Nigel dc6c35a0b6 Updated changelog 2020-01-28 19:40:27 +01:00
Nigel 47c20e763f Merge branch 'master' of github.com:ndokter/dsmr_parser 2020-01-28 19:36:23 +01:00
Nigel e7164d9af7 Merge pull request #48 from dupondje/fix_pycrc
Include needed PyCRC code
2020-01-28 19:35:42 +01:00
Jean-Louis Dupond d714528c5a Include needed PyCRC code 2020-01-28 17:26:45 +01:00
Hans Erik van Elburg 0675a6e2e7 add tailer dependency 2019-12-27 16:22:10 +01:00
Hans Erik van Elburg e6625df4a7 add documentation to FileTailReader 2019-12-27 15:18:35 +01:00
Hans Erik van Elburg 8c2485d70f added file tail reader 2019-12-24 00:57:44 +01:00
Nigel eac9681e07 updated changelog 2019-12-21 17:37:50 +01:00
Nigel 659560222a Merge pull request #40 from lowdef/add_a_true_telegram_object
Add a true telegram object
2019-12-21 17:36:28 +01:00
Nigel 74fe1f2d1e Merge branch 'master' into add_a_true_telegram_object 2019-12-21 17:34:58 +01:00
Nigel a01e673646 updated changelog for new release 2019-12-21 15:02:13 +01:00
Nigel 09614860a6 Merge pull request #44 from dupondje/master
Add support for Belgian and Smarty meters
2019-12-21 14:57:02 +01:00
Jean-Louis Dupond 3bfb555d0e Add support for Belgian and Smarty meters 2019-12-21 14:53:26 +01:00
Hans Erik van Elburg 2d4b0d8e72 fix documentation FileInputReader 2019-12-16 15:41:24 +01:00
Hans Erik van Elburg 43500e6bc2 fix failing import 2019-12-16 15:36:13 +01:00
Hans Erik van Elburg 50cef2646b add fileinputreader 2019-12-16 15:30:55 +01:00
Nigel 3a8b4d2458 Corrected changelog 2019-12-12 22:33:14 +01:00
Nigel 12aa003799 Updated version number 2019-12-12 22:29:18 +01:00
Nigel 86690ef44b Merge pull request #43 from WoLpH/patch-1
`create_tcp_dsmr_reader` accepts `loop=None` but always expects a loop. Fixes: #36
2019-12-12 22:22:47 +01:00
Rick van Hattem 7c9c59308e create_tcp_dsmr_reader accepts loop=None but always expects a loop. Fixes #36 2019-12-12 22:20:16 +01:00
Hans Erik van Elburg c1d7ba151d add some documentation for the file reader 2019-12-09 23:48:48 +01:00
Hans Erik van Elburg a65949823d fix filereader 2019-12-09 23:27:57 +01:00
Hans Erik van Elburg 3c78b0b6c4 add file reader 2019-12-09 22:36:46 +01:00
Hans Erik van Elburg 5313edd6cb add file reader 2019-12-09 22:31:40 +01:00
Hans Erik van Elburg aa4f9fb967 update version 2019-12-01 18:55:53 +01:00
Hans Erik van Elburg ea804d485e add some documentation for the use of the telegram as an object 2019-12-01 18:39:21 +01:00
Hans Erik van Elburg 1b522fc7f0 add some documentation for the use of the telegram as an object 2019-12-01 18:34:21 +01:00
Hans Erik van Elburg a137ef0e02 add some documentation for the use of the telegram as an object 2019-12-01 17:47:22 +01:00
Hans Erik van Elburg d712d468ac remove useless space 2019-11-25 20:44:25 +01:00
Hans Erik van Elburg 95bab6a51a Merge branch 'master' into add_a_true_telegram_object 2019-11-25 20:30:35 +01:00
Hans Erik van Elburg 49999618ea Merge remote-tracking branch 'upstream/master' 2019-11-25 20:29:20 +01:00
Hans Erik van Elburg 8ba400800b small fixes 2019-11-25 01:53:54 +01:00
Hans Erik van Elburg f7ba363b93 small fixes 2019-11-25 01:37:48 +01:00
Nigel b31bcd61fc Updated version number 2019-10-08 22:13:58 +02:00
Hans Erik van Elburg f9e9e70771 experimentation file 2019-10-03 21:33:11 +02:00
Nigel Dokter 50a5cb9203 Merge pull request #37 from tneele/master
Improve performance: read more data from serial port at once
2019-07-29 10:13:13 +02:00
Thomas Neele 8388624721 Read more data from serial port at once
A telegram can contain dozens of lines. Reading them one by one is somewhat inefficient.
With this change, the client tries to read all data that is available.
This significantly reduced CPU load for me.
2019-07-22 21:33:15 +02:00
Hans Erik van Elburg c36f68a884 working version of the Telegram object 2019-06-06 05:41:55 +02:00
Hans Erik van Elburg 8bdf77c78d ensure build and dist directories are not synced 2019-04-06 12:56:27 +02:00
Nigel c04b0a5add Updated version number 2019-03-04 20:31:51 +01:00
Nigel Dokter ed2290a597 Merge pull request #33 from mleenaerts/patch-1
Fix DSMR v5.0 serial settings which were not used
2019-01-22 19:27:56 +01:00
Mark Leenaerts 85c67464a1 Fix DSMR v5.0 serial settings which were not used
While analysing some CRC check errors I encounter within the home-assistant plugin (which uses this component) I encountered this oversight in the code.
2019-01-19 16:11:12 +01:00
Nigel Dokter 48783acc00 Update CHANGELOG.rst 2018-09-23 13:01:22 +02:00
Nigel Dokter 41f6a7ac16 Update CHANGELOG.rst 2018-09-23 13:01:04 +02:00
Nigel Dokter 59811dbf9f Update setup.py 2018-09-23 12:59:02 +02:00
Nigel Dokter 9b09969763 Merge pull request #31 from tneele/master
Add serial settings for DSMR v5.0
2018-09-23 12:50:01 +02:00
Nigel Dokter 64f5dc2e87 Merge pull request #32 from bossjl/master
Lux-creos-obis-1.8.0
2018-09-23 12:49:42 +02:00
bossjl 887dd3a2aa Lux-creos-obis-1.8.0 2018-09-21 18:37:32 +02:00
Thomas Neele ad6ab304f5 Added serial settings for DSMR v5.0 2018-09-15 21:23:22 +02:00
Nigel Dokter 8582395017 Merge pull request #30 from jk-5/sagemcom-t210-d
Added missing values for 3-phase sagemcom TD210-D meter
2018-09-12 23:45:29 +02:00
jk-5 3327c78c0e Updated unittests 2018-09-06 15:49:11 +02:00
jk-5 472c54968e Corrected meter types 2018-09-06 15:40:02 +02:00
jk-5 d534b1d8b0 Added missing values for 3-phase sagemcom TD210-D meter 2018-09-06 14:53:30 +02:00
Nigel Dokter d94bc8de03 updated version 2017-09-18 12:02:36 +02:00
30 changed files with 1963 additions and 128 deletions
+46
View File
@@ -0,0 +1,46 @@
name: Tests
on:
push: ~
pull_request: ~
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10 # Don't run forever when stale
strategy:
matrix:
python-version:
- '3.6'
- '3.7'
- '3.8'
- '3.9'
- '3.10'
name: Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v2
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Cached PIP dependencies
uses: actions/cache@v2
with:
path: |
~/.cache/pip
~/.tox/python/.pytest_cache
key: pip-${{ matrix.python-version }}-${{ hashFiles('setup.py', 'tox.ini') }}
restore-keys: pip-${{ matrix.python-version }}-
- name: Install dependencies
run: pip install tox
- name: Run tests
run: tox
- name: Code coverage upload
uses: codecov/codecov-action@v1
+6
View File
@@ -2,7 +2,13 @@
*.pyc
.tox
.cache
.venv
*.egg-info
/.project
/.pydevproject
/.coverage
build/
dist/
venv/
*.*~
*~
-18
View File
@@ -1,18 +0,0 @@
language: python
python:
- 2.7
- 3.4
- 3.5
- 3.6
install: pip install tox-travis codecov
script: tox
after_success:
- codecov
matrix:
allow_failures:
- python: 2.7
+109 -1
View File
@@ -1,9 +1,117 @@
Change Log
----------
**0.33** (2022-04-20)
- Test Python 3.10 in CI + legacy badge fix (`pull request #105 <https://github.com/ndokter/dsmr_parser/pull/105>`_).
- Update telegram_specifications.py (`pull request #106 <https://github.com/ndokter/dsmr_parser/pull/106>`_).
- Improve compatiblity with Belgian standard (`pull request #107 <https://github.com/ndokter/dsmr_parser/pull/107>`_).
- Improve documentation asyncio (`pull request #63 <https://github.com/ndokter/dsmr_parser/pull/63>`_).
**0.32** (2022-01-04)
- Support DSMR data read via RFXtrx with integrated P1 reader (`pull request #98 <https://github.com/ndokter/dsmr_parser/pull/98>`_).
**0.31** (2021-11-21)
- Support for (German) EasyMeter Q3D using COM-1 Ethernet Gateway (`pull request #92 <https://github.com/ndokter/dsmr_parser/pull/92>`_).
**0.30** (2021-08-18)
- Add support for Swedish smart meters (`pull request #86 <https://github.com/ndokter/dsmr_parser/pull/86>`_).
**0.29** (2021-04-18)
- Add value and unit properties to ProfileGenericObject to make sure that code like iterators that rely on that do not break (`pull request #71 <https://github.com/ndokter/dsmr_parser/pull/71>`_).
Remove deprecated asyncio coroutine decorator (`pull request #76 <https://github.com/ndokter/dsmr_parser/pull/76>`_).
**0.28** (2021-02-21)
- Optional keep alive monitoring for TCP/IP connections (`pull request #73 <https://github.com/ndokter/dsmr_parser/pull/73>`_).
- Catch parse errors in TelegramParser, ignore lines that can not be parsed (`pull request #74 <https://github.com/ndokter/dsmr_parser/pull/74>`_).
**0.27** (2020-12-24)
- fix for empty parentheses in ProfileGenericParser (redone) (`pull request #69 <https://github.com/ndokter/dsmr_parser/pull/69>`_).
**0.26** (2020-12-15)
- reverted fix for empty parentheses in ProfileGenericParser (`pull request #68 <https://github.com/ndokter/dsmr_parser/pull/68>`_).
**0.25** (2020-12-14)
- fix for empty parentheses in ProfileGenericParser (`pull request #57 <https://github.com/ndokter/dsmr_parser/pull/57>`_).
**0.24** (2020-11-27)
- Add Luxembourg equipment identifier (`pull request #62 <https://github.com/ndokter/dsmr_parser/pull/62>`_).
**0.23** (2020-11-07)
- Resolved issue with x-x:24.3.0 where it contains non-integer character (`pull request #61 <https://github.com/ndokter/dsmr_parser/pull/61>`_).
- Tests are not installed anymore (`pull request #59 <https://github.com/ndokter/dsmr_parser/pull/59>`_).
- Example telegram improvement (`pull request #58 <https://github.com/ndokter/dsmr_parser/pull/58>`_).
**0.22** (2020-08-23)
- CRC check speed is improved
- Exception info improvement
**0.21** (2020-05-25)
- All objects can produce a json serialization of their state.
**0.20** (2020-05-12)
- All objects can now print their values
- Add parser + object for generic profile
**0.19** (2020-05-03)
- Add following missing elements to telegram specification v4:
- SHORT_POWER_FAILURE_COUNT,
- INSTANTANEOUS_CURRENT_L1,
- INSTANTANEOUS_CURRENT_L2,
- INSTANTANEOUS_CURRENT_L3
- Add missing tests + fix small test bugs
- Complete telegram object v4 parse test
**0.18** (2020-01-28)
- PyCRC replacement (`pull request #48 <https://github.com/ndokter/dsmr_parser/pull/48>`_).
**0.17** (2019-12-21)
- Add a true telegram object (`pull request #40 <https://github.com/ndokter/dsmr_parser/pull/40>`_).
**0.16** (2019-12-21)
- Add support for Belgian and Smarty meters (`pull request #44 <https://github.com/ndokter/dsmr_parser/pull/44>`_).
**0.15** (2019-12-12)
- Fixed asyncio loop issue (`pull request #43 <https://github.com/ndokter/dsmr_parser/pull/43>`_).
**0.14** (2019-10-08)
- Changed serial reading to reduce CPU usage (`pull request #37 <https://github.com/ndokter/dsmr_parser/pull/37>`_).
**0.13** (2019-03-04)
- Fix DSMR v5.0 serial settings which were not used (`pull request #33 <https://github.com/ndokter/dsmr_parser/pull/33>`_).
**0.12** (2018-09-23)
- Add serial settings for DSMR v5.0 (`pull request #31 <https://github.com/ndokter/dsmr_parser/pull/31>`_).
- Lux-creos-obis-1.8.0 (`pull request #32 <https://github.com/ndokter/dsmr_parser/pull/32>`_).
**0.11** (2017-09-18)
- NULL value fix in checksum (`pull request #26 <https://github.com/ndokter/dsmr_parser/pull/26>`_)
**0.10** (2017-06-05)
- bugix: don't force full telegram signatures (`pull request #25 <https://github.com/ndokter/dsmr_parser/pull/25>`_)
- bugfix: don't force full telegram signatures (`pull request #25 <https://github.com/ndokter/dsmr_parser/pull/25>`_)
- removed unused code for automatic telegram detection as this needs reworking after the fix mentioned above
- InvalidChecksumError's are logged as warning instead of error
+273 -38
View File
@@ -4,8 +4,8 @@ DSMR Parser
.. image:: https://img.shields.io/pypi/v/dsmr-parser.svg
:target: https://pypi.python.org/pypi/dsmr-parser
.. image:: https://travis-ci.org/ndokter/dsmr_parser.svg?branch=master
:target: https://travis-ci.org/ndokter/dsmr_parser
.. image:: https://img.shields.io/github/workflow/status/ndokter/dsmr_parser/Tests/master
:target: https://github.com/ndokter/dsmr_parser/actions/workflows/tests.yml
A library for parsing Dutch Smart Meter Requirements (DSMR) telegram data. It
also includes client implementation to directly read and parse smart meter data.
@@ -14,7 +14,7 @@ also includes client implementation to directly read and parse smart meter data.
Features
--------
DSMR Parser supports DSMR versions 2, 3, 4 and 5. It has been tested with Python 3.4, 3.5 and 3.6.
DSMR Parser supports DSMR versions 2, 3, 4 and 5. See for the `currently supported/tested Python versions here <https://github.com/ndokter/dsmr_parser/blob/master/.github/workflows/tests.yml#L14>`_.
Client module usage
@@ -39,10 +39,136 @@ process because the code is blocking (not asynchronous):
for telegram in serial_reader.read():
print(telegram) # see 'Telegram object' docs below
**Socket client**
Read a remote serial port (for example using ser2net) and work with the parsed telegrams.
It should be run in a separate process because the code is blocking (not asynchronous):
.. code-block:: python
from dsmr_parser import telegram_specifications
from dsmr_parser.clients import SocketReader
socket_reader = SocketReader(
host='127.0.0.1',
port=2001,
telegram_specification=telegram_specifications.V4
)
for telegram in socket_reader.read():
print(telegram) # see 'Telegram object' docs below
**AsyncIO client**
To be documented.
For a test run using a tcp server (lasting 20 seconds) use the following example:
.. code-block:: python
import asyncio
import logging
from dsmr_parser import obis_references
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
logging.basicConfig(level=logging.INFO, format='%(message)s')
HOST = MY_HOST
PORT = MY_PORT
DSMR_VERSION = MY_DSMR_VERSION
logger = logging.getLogger('tcpclient')
logger.debug("Logger created")
def printTelegram(telegram):
logger.info(telegram)
async def main():
try:
logger.debug("Getting loop")
loop = asyncio.get_event_loop()
logger.debug("Creating reader")
await create_tcp_dsmr_reader(
HOST,
PORT,
DSMR_VERSION,
printTelegram,
loop
)
logger.debug("Reader created going to sleep now")
await asyncio.sleep(20)
logger.info('Finished run')
except Exception as e:
logger.error("Unexpected error: "+ e)
asyncio.run(main())
Note the creation of a callback function to call when a telegram is received. In this case `printTelegram`. Normally the used loop is the one running.
Currently the asyncio implementation does not support returning telegram objects directly as a `read_as_object()` for async tcp is currently not implemented.
Moreover, the telegram passed to `telegram_callback(telegram)` is already parsed. Therefore we can't feed it into the telegram constructor directly as that expects unparsed telegrams
However, if we construct a mock TelegramParser that just returns the already parsed object we can work around this. An example is below:
.. code-block:: python
import asyncio
import logging
#from dsmr_parser import obis_references
#from dsmr_parser import telegram_specifications
#from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
#from dsmr_parser.objects import Telegram
logging.basicConfig(level=logging.INFO, format='%(message)s')
HOST = MY_HOST
PORT = MY_PORT
DSMR_VERSION = MY_DSMR_VERSION
logger = logging.getLogger('tcpclient')
logger.debug("Logger created")
class mockTelegramParser(object):
def parse(self, telegram):
return telegram
telegram_parser = mockTelegramParser()
def printTelegram(telegram):
try:
logger.info(Telegram(telegram, telegram_parser, telegram_specifications.V4))
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
async def main():
try:
logger.debug("Getting loop")
loop = asyncio.get_event_loop()
logger.debug("Creating reader")
await create_tcp_dsmr_reader(
HOST,
PORT,
DSMR_VERSION,
printTelegram,
loop
)
logger.debug("Reader created going to sleep now")
while True:
await asyncio.sleep(1)
except Exception as e:
logger.error("Unexpected error: "+ e)
raise
if __name__ == '__main__':
try:
asyncio.run(main())
except (KeyboardInterrupt, SystemExit):
logger.info('Closing down...')
except Exception as e:
logger.error("Unexpected error: "+ e)
Parsing module usage
--------------------
@@ -51,42 +177,42 @@ into a dictionary.
.. code-block:: python
from dsmr_parser import telegram_specifications
from dsmr_parser.parsers import TelegramParser
from dsmr_parser import telegram_specifications
from dsmr_parser.parsers import TelegramParser
# String is formatted in separate lines for readability.
telegram_str = (
'/ISk5\2MT382-1000\r\n'
'\r\n'
'0-0:96.1.1(4B384547303034303436333935353037)\r\n'
'1-0:1.8.1(12345.678*kWh)\r\n'
'1-0:1.8.2(12345.678*kWh)\r\n'
'1-0:2.8.1(12345.678*kWh)\r\n'
'1-0:2.8.2(12345.678*kWh)\r\n'
'0-0:96.14.0(0002)\r\n'
'1-0:1.7.0(001.19*kW)\r\n'
'1-0:2.7.0(000.00*kW)\r\n'
'0-0:17.0.0(016*A)\r\n'
'0-0:96.3.10(1)\r\n'
'0-0:96.13.1(303132333435363738)\r\n'
'0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E'
'3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F30313233'
'3435363738393A3B3C3D3E3F)\r\n'
'0-1:96.1.0(3232323241424344313233343536373839)\r\n'
'0-1:24.1.0(03)\r\n'
'0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n'
'(00001.001)\r\n'
'0-1:24.4.0(1)\r\n'
'!\r\n'
)
# String is formatted in separate lines for readability.
telegram_str = (
'/ISk5\\2MT382-1000\r\n'
'\r\n'
'0-0:96.1.1(4B384547303034303436333935353037)\r\n'
'1-0:1.8.1(12345.678*kWh)\r\n'
'1-0:1.8.2(12345.678*kWh)\r\n'
'1-0:2.8.1(12345.678*kWh)\r\n'
'1-0:2.8.2(12345.678*kWh)\r\n'
'0-0:96.14.0(0002)\r\n'
'1-0:1.7.0(001.19*kW)\r\n'
'1-0:2.7.0(000.00*kW)\r\n'
'0-0:17.0.0(016*A)\r\n'
'0-0:96.3.10(1)\r\n'
'0-0:96.13.1(303132333435363738)\r\n'
'0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E'
'3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F30313233'
'3435363738393A3B3C3D3E3F)\r\n'
'0-1:96.1.0(3232323241424344313233343536373839)\r\n'
'0-1:24.1.0(03)\r\n'
'0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n'
'(00001.001)\r\n'
'0-1:24.4.0(1)\r\n'
'!\r\n'
)
parser = TelegramParser(telegram_specifications.V3)
telegram = parser.parse(telegram_str)
print(telegram) # see 'Telegram object' docs below
parser = TelegramParser(telegram_specifications.V3)
Telegram object
---------------
telegram = parser.parse(telegram_str)
print(telegram) # see 'Telegram object' docs below
Telegram dictionary
-------------------
A dictionary of which the key indicates the field type. These regex values
correspond to one of dsmr_parser.obis_reference constants.
@@ -136,7 +262,116 @@ Example to get some of the values:
gas_reading = telegram[obis_references.HOURLY_GAS_METER_READING]
# See dsmr_reader.obis_references for all readable telegram values.
# Note that the avilable values differ per DSMR version.
# Note that the available values differ per DSMR version.
Telegram as an Object
---------------------
An object version of the telegram is available as well.
.. code-block:: python
# DSMR v4.2 p1 using dsmr_parser and telegram objects
from dsmr_parser import telegram_specifications
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5
from dsmr_parser.objects import CosemObject, MBusObject, Telegram
from dsmr_parser.parsers import TelegramParser
import os
serial_reader = SerialReader(
device='/dev/ttyUSB0',
serial_settings=SERIAL_SETTINGS_V5,
telegram_specification=telegram_specifications.V4
)
# telegram = next(serial_reader.read_as_object())
# print(telegram)
for telegram in serial_reader.read_as_object():
os.system('clear')
print(telegram)
Example of output of print of the telegram object:
.. code-block:: console
P1_MESSAGE_HEADER: 42 [None]
P1_MESSAGE_TIMESTAMP: 2016-11-13 19:57:57+00:00 [None]
EQUIPMENT_IDENTIFIER: 3960221976967177082151037881335713 [None]
ELECTRICITY_USED_TARIFF_1: 1581.123 [kWh]
ELECTRICITY_USED_TARIFF_2: 1435.706 [kWh]
ELECTRICITY_DELIVERED_TARIFF_1: 0.000 [kWh]
ELECTRICITY_DELIVERED_TARIFF_2: 0.000 [kWh]
ELECTRICITY_ACTIVE_TARIFF: 0002 [None]
CURRENT_ELECTRICITY_USAGE: 2.027 [kW]
CURRENT_ELECTRICITY_DELIVERY: 0.000 [kW]
LONG_POWER_FAILURE_COUNT: 7 [None]
VOLTAGE_SAG_L1_COUNT: 0 [None]
VOLTAGE_SAG_L2_COUNT: 0 [None]
VOLTAGE_SAG_L3_COUNT: 0 [None]
VOLTAGE_SWELL_L1_COUNT: 0 [None]
VOLTAGE_SWELL_L2_COUNT: 0 [None]
VOLTAGE_SWELL_L3_COUNT: 0 [None]
TEXT_MESSAGE_CODE: None [None]
TEXT_MESSAGE: None [None]
DEVICE_TYPE: 3 [None]
INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 0.170 [kW]
INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 1.247 [kW]
INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 0.209 [kW]
INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 0.000 [kW]
INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 0.000 [kW]
INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 0.000 [kW]
EQUIPMENT_IDENTIFIER_GAS: 4819243993373755377509728609491464 [None]
HOURLY_GAS_METER_READING: 981.443 [m3]
Accessing the telegrams information as attributes directly:
.. code-block:: python
telegram
Out[3]: <dsmr_parser.objects.Telegram at 0x7f5e995d9898>
telegram.CURRENT_ELECTRICITY_USAGE
Out[4]: <dsmr_parser.objects.CosemObject at 0x7f5e98ae5ac8>
telegram.CURRENT_ELECTRICITY_USAGE.value
Out[5]: Decimal('2.027')
telegram.CURRENT_ELECTRICITY_USAGE.unit
Out[6]: 'kW'
The telegram object has an iterator, can be used to find all the information elements in the current telegram:
.. code-block:: python
[attr for attr, value in telegram]
Out[11]:
['P1_MESSAGE_HEADER',
'P1_MESSAGE_TIMESTAMP',
'EQUIPMENT_IDENTIFIER',
'ELECTRICITY_USED_TARIFF_1',
'ELECTRICITY_USED_TARIFF_2',
'ELECTRICITY_DELIVERED_TARIFF_1',
'ELECTRICITY_DELIVERED_TARIFF_2',
'ELECTRICITY_ACTIVE_TARIFF',
'CURRENT_ELECTRICITY_USAGE',
'CURRENT_ELECTRICITY_DELIVERY',
'LONG_POWER_FAILURE_COUNT',
'VOLTAGE_SAG_L1_COUNT',
'VOLTAGE_SAG_L2_COUNT',
'VOLTAGE_SAG_L3_COUNT',
'VOLTAGE_SWELL_L1_COUNT',
'VOLTAGE_SWELL_L2_COUNT',
'VOLTAGE_SWELL_L3_COUNT',
'TEXT_MESSAGE_CODE',
'TEXT_MESSAGE',
'DEVICE_TYPE',
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
'EQUIPMENT_IDENTIFIER_GAS',
'HOURLY_GAS_METER_READING']
Installation
+2 -2
View File
@@ -16,8 +16,8 @@ def console():
help='alternatively connect using TCP host.')
parser.add_argument('--port', default=None,
help='TCP port to use for connection')
parser.add_argument('--version', default='2.2', choices=['2.2', '4'],
help='DSMR version (2.2, 4)')
parser.add_argument('--version', default='2.2', choices=['2.2', '4', '5', '5B', '5L', '5S', 'Q3D'],
help='DSMR version (2.2, 4, 5, 5B, 5L, 5S, Q3D)')
parser.add_argument('--verbose', '-v', action='count')
args = parser.parse_args()
+2 -1
View File
@@ -1,5 +1,6 @@
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
SERIAL_SETTINGS_V4
SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5
from dsmr_parser.clients.serial_ import SerialReader, AsyncSerialReader
from dsmr_parser.clients.socket_ import SocketReader
from dsmr_parser.clients.protocol import create_dsmr_protocol, \
create_dsmr_reader, create_tcp_dsmr_reader
+174
View File
@@ -0,0 +1,174 @@
import logging
import fileinput
import tailer
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.objects import Telegram
from dsmr_parser.parsers import TelegramParser
logger = logging.getLogger(__name__)
class FileReader(object):
"""
Filereader to read and parse raw telegram strings from a file and instantiate Telegram objects
for each read telegram.
Usage:
from dsmr_parser import telegram_specifications
from dsmr_parser.clients.filereader import FileReader
if __name__== "__main__":
infile = '/data/smartmeter/readings.txt'
file_reader = FileReader(
file = infile,
telegram_specification = telegram_specifications.V4
)
for telegram in file_reader.read_as_object():
print(telegram)
The file can be created like:
from dsmr_parser import telegram_specifications
from dsmr_parser.clients import SerialReader, SERIAL_SETTINGS_V5
if __name__== "__main__":
outfile = '/data/smartmeter/readings.txt'
serial_reader = SerialReader(
device='/dev/ttyUSB0',
serial_settings=SERIAL_SETTINGS_V5,
telegram_specification=telegram_specifications.V4
)
for telegram in serial_reader.read_as_object():
f=open(outfile,"ab+")
f.write(telegram._telegram_data.encode())
f.close()
"""
def __init__(self, file, telegram_specification):
self._file = file
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read_as_object(self):
"""
Read complete DSMR telegram's from a file and return a Telegram object.
:rtype: generator
"""
with open(self._file, "rb") as file_handle:
while True:
data = file_handle.readline()
if not data:
break
self.telegram_buffer.append(data.decode())
for telegram in self.telegram_buffer.get_all():
try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
class FileInputReader(object):
"""
Filereader to read and parse raw telegram strings from stdin or files specified at the commandline
and instantiate Telegram objects for each read telegram.
Usage python script "syphon_smartmeter_readings_stdin.py":
from dsmr_parser import telegram_specifications
from dsmr_parser.clients.filereader import FileInputReader
if __name__== "__main__":
fileinput_reader = FileReader(
file = infile,
telegram_specification = telegram_specifications.V4
)
for telegram in fileinput_reader.read_as_object():
print(telegram)
Command line:
tail -f /data/smartmeter/readings.txt | python3 syphon_smartmeter_readings_stdin.py
"""
def __init__(self, telegram_specification):
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read_as_object(self):
"""
Read complete DSMR telegram's from stdin of filearguments specified on teh command line
and return a Telegram object.
:rtype: generator
"""
with fileinput.input(mode='rb') as file_handle:
while True:
data = file_handle.readline()
str = data.decode()
self.telegram_buffer.append(str)
for telegram in self.telegram_buffer.get_all():
try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
class FileTailReader(object):
"""
Filereader to read and parse raw telegram strings from the tail of a
given file and instantiate Telegram objects for each read telegram.
Usage python script "syphon_smartmeter_readings_stdin.py":
from dsmr_parser import telegram_specifications
from dsmr_parser.clients.filereader import FileTailReader
if __name__== "__main__":
infile = '/data/smartmeter/readings.txt'
filetail_reader = FileTailReader(
file = infile,
telegram_specification = telegram_specifications.V5
)
for telegram in filetail_reader.read_as_object():
print(telegram)
"""
def __init__(self, file, telegram_specification):
self._file = file
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read_as_object(self):
"""
Read complete DSMR telegram's from a files tail and return a Telegram object.
:rtype: generator
"""
with open(self._file, "rb") as file_handle:
for data in tailer.follow(file_handle):
str = data.decode()
self.telegram_buffer.append(str)
for telegram in self.telegram_buffer.get_all():
try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
+60 -13
View File
@@ -11,10 +11,17 @@ from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.parsers import TelegramParser
from dsmr_parser.clients.settings import SERIAL_SETTINGS_V2_2, \
SERIAL_SETTINGS_V4
SERIAL_SETTINGS_V4, SERIAL_SETTINGS_V5
def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None):
def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs):
"""Creates a DSMR asyncio protocol."""
protocol = _create_dsmr_protocol(dsmr_version, telegram_callback,
DSMRProtocol, loop, **kwargs)
return protocol
def _create_dsmr_protocol(dsmr_version, telegram_callback, protocol, loop=None, **kwargs):
"""Creates a DSMR asyncio protocol."""
if dsmr_version == '2.2':
@@ -23,15 +30,30 @@ def create_dsmr_protocol(dsmr_version, telegram_callback, loop=None):
elif dsmr_version == '4':
specification = telegram_specifications.V4
serial_settings = SERIAL_SETTINGS_V4
elif dsmr_version == '5':
elif dsmr_version == '4+':
specification = telegram_specifications.V5
serial_settings = SERIAL_SETTINGS_V4
elif dsmr_version == '5':
specification = telegram_specifications.V5
serial_settings = SERIAL_SETTINGS_V5
elif dsmr_version == '5B':
specification = telegram_specifications.BELGIUM_FLUVIUS
serial_settings = SERIAL_SETTINGS_V5
elif dsmr_version == "5L":
specification = telegram_specifications.LUXEMBOURG_SMARTY
serial_settings = SERIAL_SETTINGS_V5
elif dsmr_version == "5S":
specification = telegram_specifications.SWEDEN
serial_settings = SERIAL_SETTINGS_V5
elif dsmr_version == "Q3D":
specification = telegram_specifications.Q3D
serial_settings = SERIAL_SETTINGS_V5
else:
raise NotImplementedError("No telegram parser found for version: %s",
dsmr_version)
protocol = partial(DSMRProtocol, loop, TelegramParser(specification),
telegram_callback=telegram_callback)
protocol = partial(protocol, loop, TelegramParser(specification),
telegram_callback=telegram_callback, **kwargs)
return protocol, serial_settings
@@ -47,10 +69,14 @@ def create_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):
def create_tcp_dsmr_reader(host, port, dsmr_version,
telegram_callback, loop=None):
telegram_callback, loop=None,
keep_alive_interval=None):
"""Creates a DSMR asyncio protocol coroutine using TCP connection."""
if not loop:
loop = asyncio.get_event_loop()
protocol, _ = create_dsmr_protocol(
dsmr_version, telegram_callback, loop=None)
dsmr_version, telegram_callback, loop=loop,
keep_alive_interval=keep_alive_interval)
conn = loop.create_connection(protocol, host, port)
return conn
@@ -61,7 +87,8 @@ class DSMRProtocol(asyncio.Protocol):
transport = None
telegram_callback = None
def __init__(self, loop, telegram_parser, telegram_callback=None):
def __init__(self, loop, telegram_parser,
telegram_callback=None, keep_alive_interval=None):
"""Initialize class."""
self.loop = loop
self.log = logging.getLogger(__name__)
@@ -72,25 +99,46 @@ class DSMRProtocol(asyncio.Protocol):
self.telegram_buffer = TelegramBuffer()
# keep a lock until the connection is closed
self._closed = asyncio.Event()
self._keep_alive_interval = keep_alive_interval
self._active = True
def connection_made(self, transport):
"""Just logging for now."""
self.transport = transport
self.log.debug('connected')
self._active = False
if self.loop and self._keep_alive_interval:
self.loop.call_later(self._keep_alive_interval, self.keep_alive)
def data_received(self, data):
"""Add incoming data to buffer."""
data = data.decode('ascii')
# accept latin-1 (8-bit) on the line, to allow for non-ascii transport or padding
data = data.decode("latin1")
self._active = True
self.log.debug('received data: %s', data)
self.telegram_buffer.append(data)
for telegram in self.telegram_buffer.get_all():
# ensure actual telegram is ascii (7-bit) only (ISO 646:1991 IRV required in section 5.5 of IEC 62056-21)
telegram = telegram.encode("latin1").decode("ascii")
self.handle_telegram(telegram)
def keep_alive(self):
if self._active:
self.log.debug('keep-alive checked')
self._active = False
if self.loop:
self.loop.call_later(self._keep_alive_interval, self.keep_alive)
else:
self.log.warning('keep-alive check failed')
if self.transport:
self.transport.close()
def connection_lost(self, exc):
"""Stop when connection is lost."""
if exc:
self.log.exception('disconnected due to exception')
self.log.exception('disconnected due to exception', exc_info=exc)
else:
self.log.info('disconnected because of close/abort.')
self._closed.set()
@@ -108,7 +156,6 @@ class DSMRProtocol(asyncio.Protocol):
else:
self.telegram_callback(parsed_telegram)
@asyncio.coroutine
def wait_closed(self):
async def wait_closed(self):
"""Wait until connection is closed."""
yield from self._closed.wait()
await self._closed.wait()
+62
View File
@@ -0,0 +1,62 @@
"""Asyncio protocol implementation for handling telegrams over a RFXtrx connection ."""
import asyncio
from serial_asyncio import create_serial_connection
from .protocol import DSMRProtocol, _create_dsmr_protocol
def create_rfxtrx_dsmr_protocol(dsmr_version, telegram_callback, loop=None, **kwargs):
"""Creates a RFXtrxDSMR asyncio protocol."""
protocol = _create_dsmr_protocol(dsmr_version, telegram_callback,
RFXtrxDSMRProtocol, loop, **kwargs)
return protocol
def create_rfxtrx_dsmr_reader(port, dsmr_version, telegram_callback, loop=None):
"""Creates a DSMR asyncio protocol coroutine using a RFXtrx serial port."""
protocol, serial_settings = create_rfxtrx_dsmr_protocol(
dsmr_version, telegram_callback, loop=None)
serial_settings['url'] = port
conn = create_serial_connection(loop, protocol, **serial_settings)
return conn
def create_rfxtrx_tcp_dsmr_reader(host, port, dsmr_version,
telegram_callback, loop=None,
keep_alive_interval=None):
"""Creates a DSMR asyncio protocol coroutine using a RFXtrx TCP connection."""
if not loop:
loop = asyncio.get_event_loop()
protocol, _ = create_rfxtrx_dsmr_protocol(
dsmr_version, telegram_callback, loop=loop,
keep_alive_interval=keep_alive_interval)
conn = loop.create_connection(protocol, host, port)
return conn
PACKETTYPE_DSMR = 0x62
SUBTYPE_P1 = 0x01
class RFXtrxDSMRProtocol(DSMRProtocol):
remaining_data = b''
def data_received(self, data):
"""Add incoming data to buffer."""
data = self.remaining_data + data
packetlength = data[0] + 1 if len(data) > 0 else 1
while packetlength <= len(data):
packettype = data[1]
subtype = data[2]
if (packettype == PACKETTYPE_DSMR and subtype == SUBTYPE_P1):
dsmr_data = data[4:packetlength]
super().data_received(dsmr_data)
data = data[packetlength:]
packetlength = data[0] + 1 if len(data) > 0 else 1
self.remaining_data = data
+25 -6
View File
@@ -1,4 +1,3 @@
import asyncio
import logging
import serial
import serial_asyncio
@@ -6,6 +5,7 @@ import serial_asyncio
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.parsers import TelegramParser
from dsmr_parser.objects import Telegram
logger = logging.getLogger(__name__)
@@ -20,6 +20,7 @@ class SerialReader(object):
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read(self):
"""
@@ -30,7 +31,7 @@ class SerialReader(object):
"""
with serial.Serial(**self.serial_settings) as serial_handle:
while True:
data = serial_handle.readline()
data = serial_handle.read(max(1, min(1024, serial_handle.in_waiting)))
self.telegram_buffer.append(data.decode('ascii'))
for telegram in self.telegram_buffer.get_all():
@@ -41,14 +42,32 @@ class SerialReader(object):
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
def read_as_object(self):
"""
Read complete DSMR telegram's from the serial interface and return a Telegram object.
:rtype: generator
"""
with serial.Serial(**self.serial_settings) as serial_handle:
while True:
data = serial_handle.readline()
self.telegram_buffer.append(data.decode('ascii'))
for telegram in self.telegram_buffer.get_all():
try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
class AsyncSerialReader(SerialReader):
"""Serial reader using asyncio pyserial."""
PORT_KEY = 'url'
@asyncio.coroutine
def read(self, queue):
async def read(self, queue):
"""
Read complete DSMR telegram's from the serial interface and parse it
into CosemObject's and MbusObject's.
@@ -60,12 +79,12 @@ class AsyncSerialReader(SerialReader):
"""
# create Serial StreamReader
conn = serial_asyncio.open_serial_connection(**self.serial_settings)
reader, _ = yield from conn
reader, _ = await conn
while True:
# Read line if available or give control back to loop until new
# data has arrived.
data = yield from reader.readline()
data = await reader.readline()
self.telegram_buffer.append(data.decode('ascii'))
for telegram in self.telegram_buffer.get_all():
+10
View File
@@ -20,3 +20,13 @@ SERIAL_SETTINGS_V4 = {
'rtscts': 0,
'timeout': 20
}
SERIAL_SETTINGS_V5 = {
'baudrate': 115200,
'bytesize': serial.EIGHTBITS,
'parity': serial.PARITY_NONE,
'stopbits': serial.STOPBITS_ONE,
'xonxoff': 0,
'rtscts': 0,
'timeout': 20
}
+90
View File
@@ -0,0 +1,90 @@
import logging
import socket
from dsmr_parser.clients.telegram_buffer import TelegramBuffer
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
from dsmr_parser.parsers import TelegramParser
from dsmr_parser.objects import Telegram
logger = logging.getLogger(__name__)
class SocketReader(object):
BUFFER_SIZE = 256
def __init__(self, host, port, telegram_specification):
self.host = host
self.port = port
self.telegram_parser = TelegramParser(telegram_specification)
self.telegram_buffer = TelegramBuffer()
self.telegram_specification = telegram_specification
def read(self):
"""
Read complete DSMR telegram's from remote interface and parse it
into CosemObject's and MbusObject's
:rtype: generator
"""
buffer = b""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:
socket_handle.connect((self.host, self.port))
while True:
buffer += socket_handle.recv(self.BUFFER_SIZE)
lines = buffer.splitlines(keepends=True)
if len(lines) == 0:
continue
for data in lines:
self.telegram_buffer.append(data.decode('ascii'))
for telegram in self.telegram_buffer.get_all():
try:
yield self.telegram_parser.parse(telegram)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
buffer = b""
def read_as_object(self):
"""
Read complete DSMR telegram's from remote and return a Telegram object.
:rtype: generator
"""
buffer = b""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_handle:
socket_handle.connect((self.host, self.port))
while True:
buffer += socket_handle.recv(self.BUFFER_SIZE)
lines = buffer.splitlines(keepends=True)
if len(lines) == 0:
continue
for data in lines:
self.telegram_buffer.append(data.decode('ascii'))
for telegram in self.telegram_buffer.get_all():
try:
yield Telegram(telegram, self.telegram_parser, self.telegram_specification)
except InvalidChecksumError as e:
logger.warning(str(e))
except ParseError as e:
logger.error('Failed to parse telegram: %s', e)
buffer = b""
+62
View File
@@ -0,0 +1,62 @@
from dsmr_parser import obis_references as obis
"""
dsmr_parser.obis_name_mapping
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains a mapping of obis references to names.
"""
EN = {
obis.P1_MESSAGE_HEADER: 'P1_MESSAGE_HEADER',
obis.P1_MESSAGE_TIMESTAMP: 'P1_MESSAGE_TIMESTAMP',
obis.ELECTRICITY_IMPORTED_TOTAL: 'ELECTRICITY_IMPORTED_TOTAL',
obis.ELECTRICITY_USED_TARIFF_1: 'ELECTRICITY_USED_TARIFF_1',
obis.ELECTRICITY_USED_TARIFF_2: 'ELECTRICITY_USED_TARIFF_2',
obis.ELECTRICITY_EXPORTED_TOTAL: 'ELECTRICITY_EXPORTED_TOTAL',
obis.ELECTRICITY_DELIVERED_TARIFF_1: 'ELECTRICITY_DELIVERED_TARIFF_1',
obis.ELECTRICITY_DELIVERED_TARIFF_2: 'ELECTRICITY_DELIVERED_TARIFF_2',
obis.ELECTRICITY_ACTIVE_TARIFF: 'ELECTRICITY_ACTIVE_TARIFF',
obis.EQUIPMENT_IDENTIFIER: 'EQUIPMENT_IDENTIFIER',
obis.CURRENT_ELECTRICITY_USAGE: 'CURRENT_ELECTRICITY_USAGE',
obis.CURRENT_ELECTRICITY_DELIVERY: 'CURRENT_ELECTRICITY_DELIVERY',
obis.LONG_POWER_FAILURE_COUNT: 'LONG_POWER_FAILURE_COUNT',
obis.SHORT_POWER_FAILURE_COUNT: 'SHORT_POWER_FAILURE_COUNT',
obis.POWER_EVENT_FAILURE_LOG: 'POWER_EVENT_FAILURE_LOG',
obis.VOLTAGE_SAG_L1_COUNT: 'VOLTAGE_SAG_L1_COUNT',
obis.VOLTAGE_SAG_L2_COUNT: 'VOLTAGE_SAG_L2_COUNT',
obis.VOLTAGE_SAG_L3_COUNT: 'VOLTAGE_SAG_L3_COUNT',
obis.VOLTAGE_SWELL_L1_COUNT: 'VOLTAGE_SWELL_L1_COUNT',
obis.VOLTAGE_SWELL_L2_COUNT: 'VOLTAGE_SWELL_L2_COUNT',
obis.VOLTAGE_SWELL_L3_COUNT: 'VOLTAGE_SWELL_L3_COUNT',
obis.INSTANTANEOUS_VOLTAGE_L1: 'INSTANTANEOUS_VOLTAGE_L1',
obis.INSTANTANEOUS_VOLTAGE_L2: 'INSTANTANEOUS_VOLTAGE_L2',
obis.INSTANTANEOUS_VOLTAGE_L3: 'INSTANTANEOUS_VOLTAGE_L3',
obis.INSTANTANEOUS_CURRENT_L1: 'INSTANTANEOUS_CURRENT_L1',
obis.INSTANTANEOUS_CURRENT_L2: 'INSTANTANEOUS_CURRENT_L2',
obis.INSTANTANEOUS_CURRENT_L3: 'INSTANTANEOUS_CURRENT_L3',
obis.TEXT_MESSAGE_CODE: 'TEXT_MESSAGE_CODE',
obis.TEXT_MESSAGE: 'TEXT_MESSAGE',
obis.DEVICE_TYPE: 'DEVICE_TYPE',
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: 'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
obis.EQUIPMENT_IDENTIFIER_GAS: 'EQUIPMENT_IDENTIFIER_GAS',
obis.HOURLY_GAS_METER_READING: 'HOURLY_GAS_METER_READING',
obis.GAS_METER_READING: 'GAS_METER_READING',
obis.ACTUAL_TRESHOLD_ELECTRICITY: 'ACTUAL_TRESHOLD_ELECTRICITY',
obis.ACTUAL_SWITCH_POSITION: 'ACTUAL_SWITCH_POSITION',
obis.VALVE_POSITION_GAS: 'VALVE_POSITION_GAS',
obis.BELGIUM_5MIN_GAS_METER_READING: 'BELGIUM_5MIN_GAS_METER_READING',
obis.BELGIUM_MAX_POWER_PER_PHASE: 'BELGIUM_MAX_POWER_PER_PHASE',
obis.BELGIUM_MAX_CURRENT_PER_PHASE: 'BELGIUM_MAX_CURRENT_PER_PHASE',
obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: 'LUXEMBOURG_EQUIPMENT_IDENTIFIER',
obis.Q3D_EQUIPMENT_IDENTIFIER: 'Q3D_EQUIPMENT_IDENTIFIER',
obis.Q3D_EQUIPMENT_STATE: 'Q3D_EQUIPMENT_STATE',
obis.Q3D_EQUIPMENT_SERIALNUMBER: 'Q3D_EQUIPMENT_SERIALNUMBER',
}
REVERSE_EN = dict([(v, k) for k, v in EN.items()])
+20
View File
@@ -17,6 +17,7 @@ EQUIPMENT_IDENTIFIER = r'\d-\d:96\.1\.1.+?\r\n'
CURRENT_ELECTRICITY_USAGE = r'\d-\d:1\.7\.0.+?\r\n'
CURRENT_ELECTRICITY_DELIVERY = r'\d-\d:2\.7\.0.+?\r\n'
LONG_POWER_FAILURE_COUNT = r'96\.7\.9.+?\r\n'
SHORT_POWER_FAILURE_COUNT = r'96\.7\.21.+?\r\n'
POWER_EVENT_FAILURE_LOG = r'99\.97\.0.+?\r\n'
VOLTAGE_SAG_L1_COUNT = r'\d-\d:32\.32\.0.+?\r\n'
VOLTAGE_SAG_L2_COUNT = r'\d-\d:52\.32\.0.+?\r\n'
@@ -24,6 +25,12 @@ VOLTAGE_SAG_L3_COUNT = r'\d-\d:72\.32\.0.+?\r\n'
VOLTAGE_SWELL_L1_COUNT = r'\d-\d:32\.36\.0.+?\r\n'
VOLTAGE_SWELL_L2_COUNT = r'\d-\d:52\.36\.0.+?\r\n'
VOLTAGE_SWELL_L3_COUNT = r'\d-\d:72\.36\.0.+?\r\n'
INSTANTANEOUS_VOLTAGE_L1 = r'\d-\d:32\.7\.0.+?\r\n'
INSTANTANEOUS_VOLTAGE_L2 = r'\d-\d:52\.7\.0.+?\r\n'
INSTANTANEOUS_VOLTAGE_L3 = r'\d-\d:72\.7\.0.+?\r\n'
INSTANTANEOUS_CURRENT_L1 = r'\d-\d:31\.7\.0.+?\r\n'
INSTANTANEOUS_CURRENT_L2 = r'\d-\d:51\.7\.0.+?\r\n'
INSTANTANEOUS_CURRENT_L3 = r'\d-\d:71\.7\.0.+?\r\n'
TEXT_MESSAGE_CODE = r'\d-\d:96\.13\.1.+?\r\n'
TEXT_MESSAGE = r'\d-\d:96\.13\.0.+?\r\n'
DEVICE_TYPE = r'\d-\d:24\.1\.0.+?\r\n'
@@ -52,3 +59,16 @@ ELECTRICITY_DELIVERED_TARIFF_ALL = (
ELECTRICITY_DELIVERED_TARIFF_1,
ELECTRICITY_DELIVERED_TARIFF_2
)
# International generalized additions
ELECTRICITY_IMPORTED_TOTAL = r'\d-\d:1\.8\.0.+?\r\n' # Total imported energy register (P+)
ELECTRICITY_EXPORTED_TOTAL = r'\d-\d:2\.8\.0.+?\r\n' # Total exported energy register (P-)
# International non generalized additions (country specific) / risk for necessary refactoring
BELGIUM_5MIN_GAS_METER_READING = r'\d-\d:24\.2\.3.+?\r\n' # Different code, same format.
BELGIUM_MAX_POWER_PER_PHASE = r'\d-\d:17\.0\.0.+?\r\n' # Applicable when power limitation is active
BELGIUM_MAX_CURRENT_PER_PHASE = r'\d-\d:31\.4\.0.+?\r\n' # Applicable when current limitation is active
LUXEMBOURG_EQUIPMENT_IDENTIFIER = r'\d-\d:42\.0\.0.+?\r\n' # Logical device name
Q3D_EQUIPMENT_IDENTIFIER = r'\d-\d:0\.0\.0.+?\r\n' # Logical device name
Q3D_EQUIPMENT_STATE = r'\d-\d:96\.5\.5.+?\r\n' # Device state (hexadecimal)
Q3D_EQUIPMENT_SERIALNUMBER = r'\d-\d:96\.1\.255.+?\r\n' # Device Serialnumber
+172 -4
View File
@@ -1,3 +1,63 @@
import dsmr_parser.obis_name_mapping
import datetime
import json
from decimal import Decimal
class Telegram(object):
"""
Container for raw and parsed telegram data.
Initializing:
from dsmr_parser import telegram_specifications
from dsmr_parser.exceptions import InvalidChecksumError, ParseError
from dsmr_parser.objects import CosemObject, MBusObject, Telegram
from dsmr_parser.parsers import TelegramParser
from test.example_telegrams import TELEGRAM_V4_2
parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
Attributes can be accessed on a telegram object by addressing by their english name, for example:
telegram.ELECTRICITY_USED_TARIFF_1
All attributes in a telegram can be iterated over, for example:
[k for k,v in telegram]
yields:
['P1_MESSAGE_HEADER', 'P1_MESSAGE_TIMESTAMP', 'EQUIPMENT_IDENTIFIER', ...]
"""
def __init__(self, telegram_data, telegram_parser, telegram_specification):
self._telegram_data = telegram_data
self._telegram_specification = telegram_specification
self._telegram_parser = telegram_parser
self._obis_name_mapping = dsmr_parser.obis_name_mapping.EN
self._reverse_obis_name_mapping = dsmr_parser.obis_name_mapping.REVERSE_EN
self._dictionary = self._telegram_parser.parse(telegram_data)
self._item_names = self._get_item_names()
def __getattr__(self, name):
''' will only get called for undefined attributes '''
obis_reference = self._reverse_obis_name_mapping[name]
value = self._dictionary[obis_reference]
setattr(self, name, value)
return value
def _get_item_names(self):
return [self._obis_name_mapping[k] for k, v in self._dictionary.items()]
def __iter__(self):
for attr in self._item_names:
value = getattr(self, attr)
yield attr, value
def __str__(self):
output = ""
for attr, value in self:
output += "{}: \t {}\n".format(attr, str(value))
return output
def to_json(self):
return json.dumps(dict([[attr, json.loads(value.to_json())] for attr, value in self]))
class DSMRObject(object):
"""
Represents all data from a single telegram line.
@@ -19,7 +79,7 @@ class MBusObject(DSMRObject):
# TODO object, but let the parse set them differently? So don't use
# TODO hardcoded indexes here.
if len(self.values) != 2: # v2
return self.values[5]['value']
return self.values[6]['value']
else:
return self.values[1]['value']
@@ -29,10 +89,30 @@ class MBusObject(DSMRObject):
# TODO object, but let the parse set them differently? So don't use
# TODO hardcoded indexes here.
if len(self.values) != 2: # v2
return self.values[4]['value']
return self.values[5]['value']
else:
return self.values[1]['unit']
def __str__(self):
output = "{}\t[{}] at {}".format(str(self.value), str(self.unit), str(self.datetime.astimezone().isoformat()))
return output
def to_json(self):
timestamp = self.datetime
if isinstance(self.datetime, datetime.datetime):
timestamp = self.datetime.astimezone().isoformat()
value = self.value
if isinstance(self.value, datetime.datetime):
value = self.value.astimezone().isoformat()
if isinstance(self.value, Decimal):
value = float(self.value)
output = {
'datetime': timestamp,
'value': value,
'unit': self.unit
}
return json.dumps(output)
class CosemObject(DSMRObject):
@@ -44,6 +124,94 @@ class CosemObject(DSMRObject):
def unit(self):
return self.values[0]['unit']
def __str__(self):
print_value = self.value
if isinstance(self.value, datetime.datetime):
print_value = self.value.astimezone().isoformat()
output = "{}\t[{}]".format(str(print_value), str(self.unit))
return output
class ProfileGeneric(DSMRObject):
pass # TODO implement
def to_json(self):
json_value = self.value
if isinstance(self.value, datetime.datetime):
json_value = self.value.astimezone().isoformat()
if isinstance(self.value, Decimal):
json_value = float(self.value)
output = {
'value': json_value,
'unit': self.unit
}
return json.dumps(output)
class ProfileGenericObject(DSMRObject):
"""
Represents all data in a GenericProfile value.
All buffer values are returned as a list of MBusObjects,
containing the datetime (timestamp) and the value.
"""
def __init__(self, values):
super().__init__(values)
self._buffer_list = None
@property
def value(self):
# value is added to make sure the telegram iterator does not break
return self.values
@property
def unit(self):
# value is added to make sure all items have a unit so code that relies on that does not break
return None
@property
def buffer_length(self):
return self.values[0]['value']
@property
def buffer_type(self):
return self.values[1]['value']
@property
def buffer(self):
if self._buffer_list is None:
self._buffer_list = []
values_offset = 2
for i in range(self.buffer_length):
offset = values_offset + i * 2
self._buffer_list.append(MBusObject([self.values[offset], self.values[offset + 1]]))
return self._buffer_list
def __str__(self):
output = "\t buffer length: {}\n".format(self.buffer_length)
output += "\t buffer type: {}".format(self.buffer_type)
for buffer_value in self.buffer:
timestamp = buffer_value.datetime
if isinstance(timestamp, datetime.datetime):
timestamp = str(timestamp.astimezone().isoformat())
output += "\n\t event occured at: {}".format(timestamp)
output += "\t for: {} [{}]".format(buffer_value.value, buffer_value.unit)
return output
def to_json(self):
"""
:return: A json of all values in the GenericProfileObject , with the following structure
{'buffer_length': n,
'buffer_type': obis_ref,
'buffer': [{'datetime': d1,
'value': v1,
'unit': u1},
...
{'datetime': dn,
'value': vn,
'unit': un}
]
}
"""
list = [['buffer_length', self.buffer_length]]
list.append(['buffer_type', self.buffer_type])
buffer_repr = [json.loads(buffer_item.to_json()) for buffer_item in self.buffer]
list.append(['buffer', buffer_repr])
output = dict(list)
return json.dumps(output)
+85 -12
View File
@@ -1,15 +1,16 @@
import logging
import re
from PyCRC.CRC16 import CRC16
from ctypes import c_ushort
from dsmr_parser.objects import MBusObject, CosemObject
from dsmr_parser.objects import MBusObject, CosemObject, ProfileGenericObject
from dsmr_parser.exceptions import ParseError, InvalidChecksumError
logger = logging.getLogger(__name__)
class TelegramParser(object):
crc16_tab = []
def __init__(self, telegram_specification, apply_checksum_validation=True):
"""
@@ -54,7 +55,11 @@ class TelegramParser(object):
# Some signatures are optional and may not be present,
# so only parse lines that match
if match:
telegram[signature] = parser.parse(match.group(0))
try:
telegram[signature] = parser.parse(match.group(0))
except Exception:
logger.error("ignore line with signature {}, because parsing failed.".format(signature),
exc_info=True)
return telegram
@@ -79,7 +84,7 @@ class TelegramParser(object):
'incomplete. The checksum and/or content values are missing.'
)
calculated_crc = CRC16().calculate(checksum_contents.group(0))
calculated_crc = TelegramParser.crc16(checksum_contents.group(0))
expected_crc = int(checksum_hex.group(0), base=16)
if calculated_crc != expected_crc:
@@ -91,6 +96,33 @@ class TelegramParser(object):
)
)
@staticmethod
def crc16(telegram):
"""
Calculate the CRC16 value for the given telegram
:param str telegram:
"""
crcValue = 0x0000
if len(TelegramParser.crc16_tab) == 0:
for i in range(0, 256):
crc = c_ushort(i).value
for j in range(0, 8):
if (crc & 0x0001):
crc = c_ushort(crc >> 1).value ^ 0xA001
else:
crc = c_ushort(crc >> 1).value
TelegramParser.crc16_tab.append(hex(crc))
for c in telegram:
d = ord(c)
tmp = crcValue ^ d
rotated = c_ushort(crcValue >> 8).value
crcValue = rotated ^ int(TelegramParser.crc16_tab[(tmp & 0x00ff)], 0)
return crcValue
class DSMRObjectParser(object):
"""
@@ -100,19 +132,28 @@ class DSMRObjectParser(object):
def __init__(self, *value_formats):
self.value_formats = value_formats
def _is_line_wellformed(self, line, values):
# allows overriding by child class
return (values and (len(values) == len(self.value_formats)))
def _parse_values(self, values):
# allows overriding by child class
return [self.value_formats[i].parse(value)
for i, value in enumerate(values)]
def _parse(self, line):
# Match value groups, but exclude the parentheses
pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*]{0,}(?=\)))+')
pattern = re.compile(r'((?<=\()[0-9a-zA-Z\.\*\-\:]{0,}(?=\)))')
values = re.findall(pattern, line)
if not self._is_line_wellformed(line, values):
raise ParseError("Invalid '%s' line for '%s'", line, self)
# Convert empty value groups to None for clarity.
values = [None if value == '' else value for value in values]
if not values or len(values) != len(self.value_formats):
raise ParseError("Invalid '%s' line for '%s'", line, self)
return [self.value_formats[i].parse(value)
for i, value in enumerate(values)]
return self._parse_values(values)
class MBusParser(DSMRObjectParser):
@@ -182,8 +223,41 @@ class ProfileGenericParser(DSMRObjectParser):
9) Unit of buffer values (Unit of capture objects attribute)
"""
def __init__(self, buffer_types, head_parsers, parsers_for_unidentified):
self.value_formats = head_parsers
self.buffer_types = buffer_types
self.parsers_for_unidentified = parsers_for_unidentified
def _is_line_wellformed(self, line, values):
if values and (len(values) == 1) and (values[0] == ''):
# special case: single empty parentheses (indicated by empty string)
return True
if values and (len(values) >= 2) and (values[0].isdigit()):
buffer_length = int(values[0])
return (buffer_length <= 10) and (len(values) == (buffer_length * 2 + 2))
else:
return False
def _parse_values(self, values):
if values and (len(values) == 1) and (values[0] is None):
# special case: single empty parentheses; make sure empty ProfileGenericObject is created
values = [0, None] # buffer_length=0, buffer_value_obis_ID=None
buffer_length = int(values[0])
buffer_value_obis_ID = values[1]
if (buffer_length > 0):
if buffer_value_obis_ID in self.buffer_types:
bufferValueParsers = self.buffer_types[buffer_value_obis_ID]
else:
bufferValueParsers = self.parsers_for_unidentified
# add the parsers for the encountered value type z times
for _ in range(buffer_length):
self.value_formats.extend(bufferValueParsers)
return [self.value_formats[i].parse(value) for i, value in enumerate(values)]
def parse(self, line):
raise NotImplementedError()
return ProfileGenericObject(self._parse(line))
class ValueParser(object):
@@ -201,7 +275,6 @@ class ValueParser(object):
self.coerce_type = coerce_type
def parse(self, value):
unit_of_measurement = None
if value and '*' in value:
@@ -0,0 +1,10 @@
from dsmr_parser.parsers import ValueParser
from dsmr_parser.value_types import timestamp
PG_FAILURE_EVENT = r'0-0:96.7.19'
PG_HEAD_PARSERS = [ValueParser(int), ValueParser(str)]
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS = [ValueParser(str), ValueParser(str)]
BUFFER_TYPES = {
PG_FAILURE_EVENT: [ValueParser(timestamp), ValueParser(int)]
}
+88 -7
View File
@@ -1,9 +1,10 @@
from decimal import Decimal
from copy import deepcopy
from dsmr_parser import obis_references as obis
from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser
from dsmr_parser.parsers import CosemParser, ValueParser, MBusParser, ProfileGenericParser
from dsmr_parser.value_types import timestamp
from dsmr_parser.profile_generic_specifications import BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS
"""
dsmr_parser.telegram_specifications
@@ -33,11 +34,12 @@ V2_2 = {
obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)),
obis.GAS_METER_READING: MBusParser(
ValueParser(timestamp),
ValueParser(str), # changed to str see issue60
ValueParser(int),
ValueParser(int),
ValueParser(int),
ValueParser(str),
ValueParser(Decimal),
ValueParser(str), # obis ref
ValueParser(str), # unit, position 5
ValueParser(Decimal), # meter reading, position 6
),
}
}
@@ -57,8 +59,12 @@ V4 = {
obis.ELECTRICITY_ACTIVE_TARIFF: CosemParser(ValueParser(str)),
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
# POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO
obis.POWER_EVENT_FAILURE_LOG:
ProfileGenericParser(BUFFER_TYPES,
PG_HEAD_PARSERS,
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS),
obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)),
@@ -68,6 +74,9 @@ V4 = {
obis.TEXT_MESSAGE_CODE: CosemParser(ValueParser(int)),
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
obis.DEVICE_TYPE: CosemParser(ValueParser(int)),
obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)),
@@ -88,6 +97,7 @@ V5 = {
obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
obis.EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_USED_TARIFF_1: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_USED_TARIFF_2: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_DELIVERED_TARIFF_1: CosemParser(ValueParser(Decimal)),
@@ -96,13 +106,23 @@ V5 = {
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
obis.LONG_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
# POWER_EVENT_FAILURE_LOG: ProfileGenericParser(), TODO
obis.SHORT_POWER_FAILURE_COUNT: CosemParser(ValueParser(int)),
obis.POWER_EVENT_FAILURE_LOG:
ProfileGenericParser(BUFFER_TYPES,
PG_HEAD_PARSERS,
PG_UNIDENTIFIED_BUFFERTYPE_PARSERS),
obis.VOLTAGE_SAG_L1_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SAG_L2_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SAG_L3_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SWELL_L1_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SWELL_L2_COUNT: CosemParser(ValueParser(int)),
obis.VOLTAGE_SWELL_L3_COUNT: CosemParser(ValueParser(int)),
obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
obis.TEXT_MESSAGE: CosemParser(ValueParser(str)),
obis.DEVICE_TYPE: CosemParser(ValueParser(int)),
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
@@ -120,3 +140,64 @@ V5 = {
}
ALL = (V2_2, V3, V4, V5)
BELGIUM_FLUVIUS = deepcopy(V5)
BELGIUM_FLUVIUS['objects'].update({
obis.BELGIUM_5MIN_GAS_METER_READING: MBusParser(
ValueParser(timestamp),
ValueParser(Decimal)
),
obis.BELGIUM_MAX_POWER_PER_PHASE: CosemParser(ValueParser(Decimal)),
obis.BELGIUM_MAX_CURRENT_PER_PHASE: CosemParser(ValueParser(Decimal)),
obis.ACTUAL_SWITCH_POSITION: CosemParser(ValueParser(str)),
obis.VALVE_POSITION_GAS: CosemParser(ValueParser(str)),
})
LUXEMBOURG_SMARTY = deepcopy(V5)
LUXEMBOURG_SMARTY['objects'].update({
obis.LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
})
# Source: https://www.energiforetagen.se/globalassets/energiforetagen/det-erbjuder-vi/kurser-och-konferenser/elnat/
# branschrekommendation-lokalt-granssnitt-v2_0-201912.pdf
SWEDEN = {
'checksum_support': True,
'objects': {
obis.P1_MESSAGE_HEADER: CosemParser(ValueParser(str)),
obis.P1_MESSAGE_TIMESTAMP: CosemParser(ValueParser(timestamp)),
obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
obis.CURRENT_ELECTRICITY_DELIVERY: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_VOLTAGE_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_VOLTAGE_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_VOLTAGE_L3: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L1: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L2: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_CURRENT_L3: CosemParser(ValueParser(Decimal)),
}
}
Q3D = {
"checksum_support": False,
"objects": {
obis.Q3D_EQUIPMENT_IDENTIFIER: CosemParser(ValueParser(str)),
obis.ELECTRICITY_IMPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.ELECTRICITY_EXPORTED_TOTAL: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE: CosemParser(ValueParser(Decimal)),
obis.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE: CosemParser(ValueParser(Decimal)),
obis.CURRENT_ELECTRICITY_USAGE: CosemParser(ValueParser(Decimal)),
obis.Q3D_EQUIPMENT_STATE: CosemParser(ValueParser(str)),
obis.Q3D_EQUIPMENT_SERIALNUMBER: CosemParser(ValueParser(str)),
},
}
+6 -5
View File
@@ -3,16 +3,17 @@ from setuptools import setup, find_packages
setup(
name='dsmr-parser',
description='Library to parse Dutch Smart Meter Requirements (DSMR)',
author='Nigel Dokter',
author_email='nigeldokter@gmail.com',
author='Nigel Dokter and many others',
author_email='nigel@nldr.net',
license='MIT',
url='https://github.com/ndokter/dsmr_parser',
version='0.10',
packages=find_packages(),
version='0.33',
packages=find_packages(exclude=('test', 'test.*')),
install_requires=[
'pyserial>=3,<4',
'pyserial-asyncio<1',
'pytz',
'PyCRC>=1.2,<2'
'Tailer==0.4.1'
],
entry_points={
'console_scripts': ['dsmr_console=dsmr_parser.__main__:console']
+45 -4
View File
@@ -1,5 +1,5 @@
TELEGRAM_V2_2 = (
'/ISk5\2MT382-1004\r\n'
'/ISk5\\2MT382-1004\r\n'
'\r\n'
'0-0:96.1.1(00000000000000)\r\n'
'1-0:1.8.1(00001.001*kWh)\r\n'
@@ -22,7 +22,7 @@ TELEGRAM_V2_2 = (
)
TELEGRAM_V3 = (
'/ISk5\2MT382-1000\r\n'
'/ISk5\\2MT382-1000\r\n'
'\r\n'
'0-0:96.1.1(4B384547303034303436333935353037)\r\n'
'1-0:1.8.1(12345.678*kWh)\r\n'
@@ -87,7 +87,7 @@ TELEGRAM_V4_2 = (
)
TELEGRAM_V5 = (
'/ISk5\2MT382-1000\r\n'
'/ISk5\\2MT382-1000\r\n'
'\r\n'
'1-3:0.2.8(50)\r\n'
'0-0:1.0.0(170102192002W)\r\n'
@@ -126,5 +126,46 @@ TELEGRAM_V5 = (
'0-1:24.2.1(170102161005W)(00000.107*m3)\r\n'
'0-2:24.1.0(003)\r\n'
'0-2:96.1.0()\r\n'
'!87B3\r\n'
'!6EEE\r\n'
)
# EasyMeter via COM-1 Ethernet Gateway
# Q3D Manual (german) https://www.easymeter.com/downloads/products/zaehler/Q3D/Easymeter_Q3D_DE_2016-06-15.pdf
# - type code on page 8
# - D0-Specs on page 20
#
# last two lines are added by the COM-1 Ethernet Gateway
TELEGRAM_ESY5Q3DB1024_V304 = (
'/ESY5Q3DB1024 V3.04\r\n'
'\r\n'
'1-0:0.0.0*255(0272031312565)\r\n'
'1-0:1.8.0*255(00052185.7825309*kWh)\r\n'
'1-0:2.8.0*255(00019949.3221493*kWh)\r\n'
'1-0:21.7.0*255(000747.85*W)\r\n'
'1-0:41.7.0*255(000737.28*W)\r\n'
'1-0:61.7.0*255(000639.73*W)\r\n'
'1-0:1.7.0*255(002124.86*W)\r\n'
'1-0:96.5.5*255(80)\r\n'
'0-0:96.1.255*255(1ESY1313002565)\r\n'
'!\r\n'
' 25803103\r\n'
'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
'\xff\xff\xff\xff\xff\r\n'
)
TELEGRAM_ESY5Q3DA1004_V304 = (
'/ESY5Q3DA1004 V3.04\r\n'
'\r\n'
'1-0:0.0.0*255(1336001560)\r\n'
'1-0:1.8.0*255(00032549.5061662*kWh)\r\n'
'1-0:21.7.0*255(000557.29*W)\r\n'
'1-0:41.7.0*255(000521.62*W)\r\n'
'1-0:61.7.0*255(000609.30*W)\r\n'
'1-0:1.7.0*255(001688.21*W)\r\n'
'1-0:96.5.5*255(80)\r\n'
'0-0:96.1.255*255(1ESY1336001560)\r\n'
'!\r\n'
' 25818685\r\n'
'DE0000000000000000000000000000003\r\n'
)
+8
View File
@@ -0,0 +1,8 @@
from dsmr_parser import telegram_specifications
from dsmr_parser.objects import Telegram
from dsmr_parser.parsers import TelegramParser
from example_telegrams import TELEGRAM_V4_2
parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
print(telegram)
+21
View File
@@ -0,0 +1,21 @@
import unittest
import tempfile
from dsmr_parser.clients.filereader import FileReader
from dsmr_parser.telegram_specifications import V5
from test.example_telegrams import TELEGRAM_V5
class FileReaderTest(unittest.TestCase):
def test_read_as_object(self):
with tempfile.NamedTemporaryFile() as file:
with open(file.name, "w") as f:
f.write(TELEGRAM_V5)
telegrams = []
reader = FileReader(file=file.name, telegram_specification=V5)
# Call
for telegram in reader.read_as_object():
telegrams.append(telegram)
self.assertEqual(len(telegrams), 1)
+25 -1
View File
@@ -80,6 +80,12 @@ class TelegramParserV4_2Test(unittest.TestCase):
assert isinstance(result[obis.CURRENT_ELECTRICITY_DELIVERY].value, Decimal)
assert result[obis.CURRENT_ELECTRICITY_DELIVERY].value == Decimal('0')
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject)
assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int)
assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 15
# LONG_POWER_FAILURE_COUNT (96.7.9)
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT], CosemObject)
assert result[obis.LONG_POWER_FAILURE_COUNT].unit is None
@@ -132,8 +138,26 @@ class TelegramParserV4_2Test(unittest.TestCase):
assert result[obis.TEXT_MESSAGE].unit is None
assert result[obis.TEXT_MESSAGE].value is None
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0')
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('6')
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('2')
# DEVICE_TYPE (0-x:24.1.0)
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
assert isinstance(result[obis.DEVICE_TYPE], CosemObject)
assert result[obis.DEVICE_TYPE].unit is None
assert isinstance(result[obis.DEVICE_TYPE].value, int)
assert result[obis.DEVICE_TYPE].value == 3
+44 -3
View File
@@ -87,6 +87,12 @@ class TelegramParserV5Test(unittest.TestCase):
assert isinstance(result[obis.LONG_POWER_FAILURE_COUNT].value, int)
assert result[obis.LONG_POWER_FAILURE_COUNT].value == 0
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT], CosemObject)
assert result[obis.SHORT_POWER_FAILURE_COUNT].unit is None
assert isinstance(result[obis.SHORT_POWER_FAILURE_COUNT].value, int)
assert result[obis.SHORT_POWER_FAILURE_COUNT].value == 13
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
assert isinstance(result[obis.VOLTAGE_SAG_L1_COUNT], CosemObject)
assert result[obis.VOLTAGE_SAG_L1_COUNT].unit is None
@@ -123,13 +129,49 @@ class TelegramParserV5Test(unittest.TestCase):
assert isinstance(result[obis.VOLTAGE_SWELL_L3_COUNT].value, int)
assert result[obis.VOLTAGE_SWELL_L3_COUNT].value == 0
# INSTANTANEOUS_VOLTAGE_L1 (1-0:32.7.0)
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1], CosemObject)
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].unit == 'V'
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L1].value, Decimal)
assert result[obis.INSTANTANEOUS_VOLTAGE_L1].value == Decimal('230.0')
# INSTANTANEOUS_VOLTAGE_L2 (1-0:52.7.0)
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2], CosemObject)
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].unit == 'V'
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L2].value, Decimal)
assert result[obis.INSTANTANEOUS_VOLTAGE_L2].value == Decimal('230.0')
# INSTANTANEOUS_VOLTAGE_L3 (1-0:72.7.0)
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3], CosemObject)
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].unit == 'V'
assert isinstance(result[obis.INSTANTANEOUS_VOLTAGE_L3].value, Decimal)
assert result[obis.INSTANTANEOUS_VOLTAGE_L3].value == Decimal('229.0')
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L1].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L1].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L1].value == Decimal('0.48')
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L2].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L2].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L2].value == Decimal('0.44')
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3], CosemObject)
assert result[obis.INSTANTANEOUS_CURRENT_L3].unit == 'A'
assert isinstance(result[obis.INSTANTANEOUS_CURRENT_L3].value, Decimal)
assert result[obis.INSTANTANEOUS_CURRENT_L3].value == Decimal('0.86')
# TEXT_MESSAGE (0-0:96.13.0)
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
assert result[obis.TEXT_MESSAGE].unit is None
assert result[obis.TEXT_MESSAGE].value is None
# DEVICE_TYPE (0-x:24.1.0)
assert isinstance(result[obis.TEXT_MESSAGE], CosemObject)
assert isinstance(result[obis.DEVICE_TYPE], CosemObject)
assert result[obis.DEVICE_TYPE].unit is None
assert isinstance(result[obis.DEVICE_TYPE].value, int)
assert result[obis.DEVICE_TYPE].value == 3
@@ -199,7 +241,6 @@ class TelegramParserV5Test(unittest.TestCase):
def test_checksum_missing(self):
# Remove the checksum value causing a ParseError.
corrupted_telegram = TELEGRAM_V5.replace('!87B3\r\n', '')
corrupted_telegram = TELEGRAM_V5.replace('!6EEE\r\n', '')
with self.assertRaises(ParseError):
TelegramParser.validate_checksum(corrupted_telegram)
+89
View File
@@ -0,0 +1,89 @@
import unittest
from dsmr_parser import telegram_specifications
from dsmr_parser.objects import Telegram
from dsmr_parser.objects import ProfileGenericObject
from dsmr_parser.parsers import TelegramParser
from dsmr_parser.parsers import ProfileGenericParser
from dsmr_parser.profile_generic_specifications import BUFFER_TYPES
from dsmr_parser.profile_generic_specifications import PG_HEAD_PARSERS
from dsmr_parser.profile_generic_specifications import PG_UNIDENTIFIED_BUFFERTYPE_PARSERS
from test.example_telegrams import TELEGRAM_V5
class TestParserCornerCases(unittest.TestCase):
""" Test instantiation of Telegram object """
def test_power_event_log_empty_1(self):
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
parser = TelegramParser(telegram_specifications.V5)
telegram = Telegram(TELEGRAM_V5, parser, telegram_specifications.V5)
object_type = ProfileGenericObject
testitem = telegram.POWER_EVENT_FAILURE_LOG
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 0
assert testitem.buffer_type == '0-0:96.7.19'
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 0
def test_power_event_log_empty_2(self):
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
object_type = ProfileGenericObject
# Power Event Log with 0 items and no object type
pefl_line = r'1-0:99.97.0(0)()\r\n'
testitem = pef_parser.parse(pefl_line)
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 0
assert testitem.buffer_type is None
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 0
assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]
json = testitem.to_json()
assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}'
def test_power_event_log_null_values(self):
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
object_type = ProfileGenericObject
# Power Event Log with 1 item and no object type and nno values for the item
pefl_line = r'1-0:99.97.0(1)()()()\r\n'
testitem = pef_parser.parse(pefl_line)
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 1
assert testitem.buffer_type is None
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 1
assert testitem.values == [{'value': 1, 'unit': None}, {'value': None, 'unit': None},
{'value': None, 'unit': None}, {'value': None, 'unit': None}]
json = testitem.to_json()
assert json == \
'{"buffer_length": 1, "buffer_type": null, "buffer": [{"datetime": null, "value": null, "unit": null}]}'
def test_power_event_log_brackets_only(self):
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
# Issue 57
# Test of an ill formatted empty POWER_EVENT_FAILURE_LOG, observed on some smartmeters
# The idea is that instead of failing, the parser converts it to an empty POWER_EVENT_FAILURE_LOG
pef_parser = ProfileGenericParser(BUFFER_TYPES, PG_HEAD_PARSERS, PG_UNIDENTIFIED_BUFFERTYPE_PARSERS)
object_type = ProfileGenericObject
pefl_line = r'1-0:99.97.0()\r\n'
testitem = pef_parser.parse(pefl_line)
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 0
assert testitem.buffer_type is None
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 0
assert testitem.values == [{'value': 0, 'unit': None}, {'value': None, 'unit': None}]
json = testitem.to_json()
assert json == '{"buffer_length": 0, "buffer_type": null, "buffer": []}'
+25 -6
View File
@@ -3,9 +3,7 @@ from unittest.mock import Mock
import unittest
from dsmr_parser import obis_references as obis
from dsmr_parser import telegram_specifications
from dsmr_parser.parsers import TelegramParser
from dsmr_parser.clients.protocol import DSMRProtocol
from dsmr_parser.clients.protocol import create_dsmr_protocol
TELEGRAM_V2_2 = (
@@ -35,9 +33,10 @@ TELEGRAM_V2_2 = (
class ProtocolTest(unittest.TestCase):
def setUp(self):
telegram_parser = TelegramParser(telegram_specifications.V2_2)
self.protocol = DSMRProtocol(None, telegram_parser,
telegram_callback=Mock())
new_protocol, _ = create_dsmr_protocol('2.2',
telegram_callback=Mock(),
keep_alive_interval=1)
self.protocol = new_protocol()
def test_complete_packet(self):
"""Protocol should assemble incoming lines into complete packet."""
@@ -52,3 +51,23 @@ class ProtocolTest(unittest.TestCase):
assert float(telegram[obis.GAS_METER_READING].value) == 1.001
assert telegram[obis.GAS_METER_READING].unit == 'm3'
def test_receive_packet(self):
"""Protocol packet reception."""
mock_transport = Mock()
self.protocol.connection_made(mock_transport)
assert not self.protocol._active
self.protocol.data_received(TELEGRAM_V2_2.encode('ascii'))
assert self.protocol._active
# 1st call of keep_alive resets 'active' flag
self.protocol.keep_alive()
assert not self.protocol._active
# 2nd call of keep_alive should close the transport
self.protocol.keep_alive()
assert mock_transport.close.called_once()
self.protocol.connection_lost(None)
+77
View File
@@ -0,0 +1,77 @@
from unittest.mock import Mock
import unittest
from dsmr_parser import obis_references as obis
from dsmr_parser.clients.rfxtrx_protocol import create_rfxtrx_dsmr_protocol, PACKETTYPE_DSMR, SUBTYPE_P1
TELEGRAM_V2_2 = (
'/ISk5\2MT382-1004\r\n'
'\r\n'
'0-0:96.1.1(00000000000000)\r\n'
'1-0:1.8.1(00001.001*kWh)\r\n'
'1-0:1.8.2(00001.001*kWh)\r\n'
'1-0:2.8.1(00001.001*kWh)\r\n'
'1-0:2.8.2(00001.001*kWh)\r\n'
'0-0:96.14.0(0001)\r\n'
'1-0:1.7.0(0001.01*kW)\r\n'
'1-0:2.7.0(0000.00*kW)\r\n'
'0-0:17.0.0(0999.00*kW)\r\n'
'0-0:96.3.10(1)\r\n'
'0-0:96.13.1()\r\n'
'0-0:96.13.0()\r\n'
'0-1:24.1.0(3)\r\n'
'0-1:96.1.0(000000000000)\r\n'
'0-1:24.3.0(161107190000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n'
'(00001.001)\r\n'
'0-1:24.4.0(1)\r\n'
'!\r\n'
)
OTHER_RF_PACKET = b'\x03\x01\x02\x03'
def encode_telegram_as_RF_packets(telegram):
data = b''
for line in telegram.split('\n'):
packet_data = (line + '\n').encode('ascii')
packet_header = bytes(bytearray([
len(packet_data) + 3, # excluding length byte
PACKETTYPE_DSMR,
SUBTYPE_P1,
0 # seq num (ignored)
]))
data += packet_header + packet_data
# other RF packets can pass by on the line
data += OTHER_RF_PACKET
return data
class RFXtrxProtocolTest(unittest.TestCase):
def setUp(self):
new_protocol, _ = create_rfxtrx_dsmr_protocol('2.2',
telegram_callback=Mock(),
keep_alive_interval=1)
self.protocol = new_protocol()
def test_complete_packet(self):
"""Protocol should assemble incoming lines into complete packet."""
data = encode_telegram_as_RF_packets(TELEGRAM_V2_2)
# send data broken up in two parts
self.protocol.data_received(data[0:200])
self.protocol.data_received(data[200:])
telegram = self.protocol.telegram_callback.call_args_list[0][0][0]
assert isinstance(telegram, dict)
assert float(telegram[obis.CURRENT_ELECTRICITY_USAGE].value) == 1.01
assert telegram[obis.CURRENT_ELECTRICITY_USAGE].unit == 'kW'
assert float(telegram[obis.GAS_METER_READING].value) == 1.001
assert telegram[obis.GAS_METER_READING].unit == 'm3'
+322
View File
@@ -0,0 +1,322 @@
import unittest
import datetime
import pytz
from dsmr_parser import telegram_specifications
from dsmr_parser import obis_name_mapping
from dsmr_parser.objects import CosemObject
from dsmr_parser.objects import MBusObject
from dsmr_parser.objects import Telegram
from dsmr_parser.objects import ProfileGenericObject
from dsmr_parser.parsers import TelegramParser
from test.example_telegrams import TELEGRAM_V4_2
from decimal import Decimal
class TelegramTest(unittest.TestCase):
""" Test instantiation of Telegram object """
def __init__(self, *args, **kwargs):
self.item_names_tested = []
super(TelegramTest, self).__init__(*args, **kwargs)
def verify_telegram_item(self, telegram, testitem_name, object_type, unit_val, value_type, value_val):
testitem = eval("telegram.{}".format(testitem_name))
assert isinstance(testitem, object_type)
assert testitem.unit == unit_val
assert isinstance(testitem.value, value_type)
assert testitem.value == value_val
self.item_names_tested.append(testitem_name)
def test_instantiate(self):
parser = TelegramParser(telegram_specifications.V4)
telegram = Telegram(TELEGRAM_V4_2, parser, telegram_specifications.V4)
# P1_MESSAGE_HEADER (1-3:0.2.8)
self.verify_telegram_item(telegram,
'P1_MESSAGE_HEADER',
object_type=CosemObject,
unit_val=None,
value_type=str,
value_val='42')
# P1_MESSAGE_TIMESTAMP (0-0:1.0.0)
self.verify_telegram_item(telegram,
'P1_MESSAGE_TIMESTAMP',
CosemObject,
unit_val=None,
value_type=datetime.datetime,
value_val=datetime.datetime(2016, 11, 13, 19, 57, 57, tzinfo=pytz.UTC))
# ELECTRICITY_USED_TARIFF_1 (1-0:1.8.1)
self.verify_telegram_item(telegram,
'ELECTRICITY_USED_TARIFF_1',
object_type=CosemObject,
unit_val='kWh',
value_type=Decimal,
value_val=Decimal('1581.123'))
# ELECTRICITY_USED_TARIFF_2 (1-0:1.8.2)
self.verify_telegram_item(telegram,
'ELECTRICITY_USED_TARIFF_2',
object_type=CosemObject,
unit_val='kWh',
value_type=Decimal,
value_val=Decimal('1435.706'))
# ELECTRICITY_DELIVERED_TARIFF_1 (1-0:2.8.1)
self.verify_telegram_item(telegram,
'ELECTRICITY_DELIVERED_TARIFF_1',
object_type=CosemObject,
unit_val='kWh',
value_type=Decimal,
value_val=Decimal('0'))
# ELECTRICITY_DELIVERED_TARIFF_2 (1-0:2.8.2)
self.verify_telegram_item(telegram,
'ELECTRICITY_DELIVERED_TARIFF_2',
object_type=CosemObject,
unit_val='kWh',
value_type=Decimal,
value_val=Decimal('0'))
# ELECTRICITY_ACTIVE_TARIFF (0-0:96.14.0)
self.verify_telegram_item(telegram,
'ELECTRICITY_ACTIVE_TARIFF',
object_type=CosemObject,
unit_val=None,
value_type=str,
value_val='0002')
# EQUIPMENT_IDENTIFIER (0-0:96.1.1)
self.verify_telegram_item(telegram,
'EQUIPMENT_IDENTIFIER',
object_type=CosemObject,
unit_val=None,
value_type=str,
value_val='3960221976967177082151037881335713')
# CURRENT_ELECTRICITY_USAGE (1-0:1.7.0)
self.verify_telegram_item(telegram,
'CURRENT_ELECTRICITY_USAGE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('2.027'))
# CURRENT_ELECTRICITY_DELIVERY (1-0:2.7.0)
self.verify_telegram_item(telegram,
'CURRENT_ELECTRICITY_DELIVERY',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0'))
# SHORT_POWER_FAILURE_COUNT (1-0:96.7.21)
self.verify_telegram_item(telegram,
'SHORT_POWER_FAILURE_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=15)
# LONG_POWER_FAILURE_COUNT (96.7.9)
self.verify_telegram_item(telegram,
'LONG_POWER_FAILURE_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=7)
# VOLTAGE_SAG_L1_COUNT (1-0:32.32.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SAG_L1_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SAG_L2_COUNT (1-0:52.32.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SAG_L2_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SAG_L3_COUNT (1-0:72.32.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SAG_L3_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SWELL_L1_COUNT (1-0:32.36.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SWELL_L1_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SWELL_L2_COUNT (1-0:52.36.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SWELL_L2_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# VOLTAGE_SWELL_L3_COUNT (1-0:72.36.0)
self.verify_telegram_item(telegram,
'VOLTAGE_SWELL_L3_COUNT',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=0)
# TEXT_MESSAGE_CODE (0-0:96.13.1)
self.verify_telegram_item(telegram,
'TEXT_MESSAGE_CODE',
object_type=CosemObject,
unit_val=None,
value_type=type(None),
value_val=None)
# TEXT_MESSAGE (0-0:96.13.0)
self.verify_telegram_item(telegram,
'TEXT_MESSAGE',
object_type=CosemObject,
unit_val=None,
value_type=type(None),
value_val=None)
# INSTANTANEOUS_CURRENT_L1 (1-0:31.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_CURRENT_L1',
object_type=CosemObject,
unit_val='A',
value_type=Decimal,
value_val=Decimal('0'))
# INSTANTANEOUS_CURRENT_L2 (1-0:51.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_CURRENT_L2',
object_type=CosemObject,
unit_val='A',
value_type=Decimal,
value_val=Decimal('6'))
# INSTANTANEOUS_CURRENT_L3 (1-0:71.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_CURRENT_L3',
object_type=CosemObject,
unit_val='A',
value_type=Decimal,
value_val=Decimal('2'))
# DEVICE_TYPE (0-x:24.1.0)
self.verify_telegram_item(telegram,
'DEVICE_TYPE',
object_type=CosemObject,
unit_val=None,
value_type=int,
value_val=3)
# INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE (1-0:21.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0.170'))
# INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE (1-0:41.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('1.247'))
# INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE (1-0:61.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0.209'))
# INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE (1-0:22.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0'))
# INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE (1-0:42.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0'))
# INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE (1-0:62.7.0)
self.verify_telegram_item(telegram,
'INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE',
object_type=CosemObject,
unit_val='kW',
value_type=Decimal,
value_val=Decimal('0'))
# EQUIPMENT_IDENTIFIER_GAS (0-x:96.1.0)
self.verify_telegram_item(telegram,
'EQUIPMENT_IDENTIFIER_GAS',
object_type=CosemObject,
unit_val=None,
value_type=str,
value_val='4819243993373755377509728609491464')
# HOURLY_GAS_METER_READING (0-1:24.2.1)
self.verify_telegram_item(telegram,
'HOURLY_GAS_METER_READING',
object_type=MBusObject,
unit_val='m3',
value_type=Decimal,
value_val=Decimal('981.443'))
# POWER_EVENT_FAILURE_LOG (1-0:99.97.0)
testitem_name = 'POWER_EVENT_FAILURE_LOG'
object_type = ProfileGenericObject
testitem = eval("telegram.{}".format(testitem_name))
assert isinstance(testitem, object_type)
assert testitem.buffer_length == 3
assert testitem.buffer_type == '0-0:96.7.19'
buffer = testitem.buffer
assert isinstance(testitem.buffer, list)
assert len(buffer) == 3
assert all([isinstance(item, MBusObject) for item in buffer])
date0 = datetime.datetime(2000, 1, 4, 17, 3, 20, tzinfo=datetime.timezone.utc)
date1 = datetime.datetime(1999, 12, 31, 23, 0, 1, tzinfo=datetime.timezone.utc)
date2 = datetime.datetime(2000, 1, 1, 23, 0, 3, tzinfo=datetime.timezone.utc)
assert buffer[0].datetime == date0
assert buffer[1].datetime == date1
assert buffer[2].datetime == date2
assert buffer[0].value == 237126
assert buffer[1].value == 2147583646
assert buffer[2].value == 2317482647
assert all([isinstance(item.value, int) for item in buffer])
assert all([isinstance(item.unit, str) for item in buffer])
assert all([(item.unit == 's') for item in buffer])
self.item_names_tested.append(testitem_name)
# check if all items in telegram V4 specification are covered
V4_name_list = [obis_name_mapping.EN[signature] for signature, parser in
telegram_specifications.V4['objects'].items()]
V4_name_set = set(V4_name_list)
item_names_tested_set = set(self.item_names_tested)
assert item_names_tested_set == V4_name_set
+5 -7
View File
@@ -1,15 +1,10 @@
[tox]
envlist = py34,py35,p36
[testenv]
deps=
pytest
pytest-cov
pylama
pytest-asyncio
pytest-catchlog
pytest-mock
PyCRC
commands=
py.test --cov=dsmr_parser test {posargs}
pylama dsmr_parser test
@@ -17,8 +12,11 @@ commands=
[pylama:dsmr_parser/clients/__init__.py]
ignore = W0611
[pylama:dsmr_parser/parsers.py]
ignore = W605
[pylama:pylint]
max_line_length = 100
max_line_length = 120
[pylama:pycodestyle]
max_line_length = 100
max_line_length = 120