├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGES.txt ├── LICENSE ├── README.md ├── doc ├── Makefile ├── _static │ ├── ajax-loader.gif │ ├── basic.css │ ├── comment-bright.png │ ├── comment-close.png │ ├── comment.png │ ├── default.css │ ├── doctools.js │ ├── down-pressed.png │ ├── down.png │ ├── file.png │ ├── jquery.js │ ├── minus.png │ ├── plus.png │ ├── pygments.css │ ├── searchtools.js │ ├── sidebar.js │ ├── underscore.js │ ├── up-pressed.png │ ├── up.png │ └── websupport.js ├── conf.py ├── index.rst ├── make.bat ├── pysunspec.rst └── pysunspec_api.rst ├── scripts ├── pysunspec_coverage_report.py └── suns.py ├── setup.py └── sunspec ├── __init__.py └── core ├── __init__.py ├── client.py ├── data.py ├── device.py ├── modbus ├── __init__.py ├── client.py └── mbmap.py ├── pics.py ├── smdx.py ├── suns.py ├── test ├── __init__.py ├── devices │ ├── mbmap_test_device_1.xml │ ├── mbmap_test_device_1_a.xml │ ├── mbmap_test_device_1_b.xml │ ├── mbmap_test_device_1_c.xml │ ├── mbmap_test_device_1_d.xml │ ├── mbmap_test_device_1_processed.xml │ ├── mbmap_test_device_2.xml │ ├── mbmap_test_device_3.xml │ ├── mbmap_test_inverter_1.xml │ ├── mbmap_test_inverter_2.xml │ ├── mbmap_test_inverter_3.xml │ ├── pics_test_device_1.xml │ ├── pics_test_device_2.xml │ ├── pics_test_inverter_1.xml │ └── pics_test_inverter_2.xml ├── fake │ ├── __init__.py │ ├── serial.py │ └── socket.py ├── models │ └── smdx_65000.xml ├── test_all.py ├── test_client.py ├── test_data.py ├── test_device.py ├── test_modbus_client.py ├── test_modbus_mbmap.py └── test_util.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | # This file tells git what files to ignore (e.g., you won't see them as 2 | # untracked with "git status"). Add anything to it that can be cleared 3 | # without any worry (e.g., by "git clean -Xdf"), because it can be 4 | # regenerated. Lines beginning with # are comments. You can also ignore 5 | # files on a per-repository basis by modifying the core.excludesfile 6 | # configuration option (see "git help config"). If you need to make git 7 | # track a file that is ignored for some reason, you have to use 8 | # "git add -f". See "git help gitignore" for more information. 9 | 10 | 11 | # Regular Python bytecode file 12 | *.pyc 13 | 14 | # Optimized Python bytecode file 15 | *.pyo 16 | 17 | # Vim's swap files 18 | *.sw[op] 19 | 20 | # Generated by ctags (used to improve autocompletion in vim) 21 | tags 22 | 23 | my/ 24 | 25 | # Files generated by setup.py 26 | dist/ 27 | build/ 28 | 29 | # Built doc files (cd doc; make html) 30 | doc/_build/ 31 | doc/sphinx/ 32 | 33 | # Jetbrains IDE junk 34 | .idea/ 35 | 36 | # Mac OS X Junk 37 | .DS_Store 38 | *.AppleDouble 39 | 40 | # Backup files 41 | *~ 42 | 43 | # Coverage.py files 44 | .coverage 45 | htmlcov 46 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sunspec/models"] 2 | path = sunspec/models 3 | url = https://github.com/sunspec/models.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | dist: xenial 4 | 5 | python: 6 | - 2.7 7 | - 3.5 8 | - 3.6 9 | - 3.7 10 | - 3.8 11 | 12 | install: 13 | - git submodule update --init --recursive 14 | - pip install . 15 | 16 | script: python -m unittest discover -v sunspec 17 | 18 | notifications: 19 | email: false 20 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | ======================== 2 | pySunSpec Release Notes 3 | ======================== 4 | 5 | Version 1.0.4 21 Jun 2014 6 | --------------------------- 7 | - Updated to not read point values of all models on client device creation. Model point values need to be read explicitly after creation either at the device or model level. Speeds up device creation/scan significantly for devices with multiple models. 8 | - Updated connection usage for device scan for Modbus TCP devices. 9 | - Augmented mbmap functionality. 10 | - Augmented SunSpec data record functionality. 11 | - Corrected uninitialized point type variable when processing model types. 12 | - Added support for count and uint64 types. 13 | - Added unit test to read all smdx model files in the model directory. 14 | 15 | --- Include latest SMDX model definitions which have been updated with the following changes: 16 | - Common model documentation typo (fix the description for Opt and Vr to correct length of 16 chars) 17 | - Update SC and MPPT models to use "count". 18 | - Changed the 'N' attribute to be of type count. (This does not effect device implementations, clients may treat 'count' as uint16) 19 | - Corrected model name attributes in model 201 and model 306. 20 | 21 | Version 1.0.5 7 Apr 2015 22 | --------------------------- 23 | - Corrected curve example in documentation. 24 | - Added SMDX symbol support. 25 | - Updated setting of point base value based on type. 26 | - Updated client interface type strings. 27 | - Updated ModbusMap to support 'mapid' attribute. 28 | - Corrected suns integer and long conversion to handle non-string arguments. 29 | - Added support for eui48 (MAC) SunSpec type. 30 | 31 | --- Include latest SMDX model definitions which have been updated with the following changes: 32 | - Corrected MAC address point in model 11. 33 | - Added storage models 801, 802, 803. 34 | 35 | Version 1.0.6 4 Jun 2015 36 | --------------------------- 37 | - Corrected support for constant scale factor. 38 | - Updated to observe point address boundry on model read. 39 | 40 | --- Include latest SMDX model definitions which have been updated with the following changes: 41 | - Model 63001 - Added constant scale factor point and label info. 42 | - Model 63002 - Added label info. 43 | 44 | Version 1.0.7 8 Feb 2016 45 | ------------------------ 46 | - Correct client initialization error handling to close underlying device. 47 | 48 | --- Include latest SMDX model definitions which have been updated with the following changes: 49 | - Corrected acc32 to float32 in models 211, 212, 213, 214. 50 | - Removed scale factor definition from DeptRef definition in model 126. 51 | 52 | Version 1.0.8 20 Jul 2016 53 | ------------------------- 54 | - Corrected scan for Modbus address 0 (issue #11). 55 | 56 | --- Include latest SMDX model definitions which have been updated with the following changes: 57 | - Corrected model length to the convention of fixed block + one repeating block. 58 | - Corrected units for model 121 WGra to be per second rather per minute to be consistent with previous correction to IEC 61850-7-420. 59 | - Updated models 801-809 60 | 61 | Version 2.0.0 11 Feb 2018 62 | ------------------------- 63 | - Updated to support Python 3. Updated version officially supports 2.7, 3.3-3.6. 64 | - Include latest SMDX model definitions. 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (C) 2018 SunSpec Alliance 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a 5 | copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pySunSpec 2 | 3 | The pySunSpec package provides objects and applications that support 4 | interaction with SunSpec compliant devices and documents. It can be run in 5 | most environments that support Python and is tested on Windows 7, macOS, and 6 | Ubuntu. 7 | 8 | Copyright (c) 2018 SunSpec Alliance 9 | ([License](https://github.com/sunspec/pysunspec/blob/master/LICENSE)) 10 | 11 | [![Build Status](https://travis-ci.org/sunspec/pysunspec.svg?branch=master)](https://travis-ci.org/sunspec/pysunspec) 12 | 13 | 14 | # Features 15 | 16 | - Provides access to SunSpec Modbus RTU and TCP devices 17 | - High level object model allowing easy device scripting 18 | - Minimal dependencies for core package allowing it to run in more constrained 19 | Python environments 20 | - Runs on Windows, Mac, and Linux 21 | 22 | 23 | # Requirements 24 | 25 | - Python 2.7, 3.5-3.8 26 | - pySerial 27 | 28 | 29 | # Installation 30 | 31 | 32 | ## Python 33 | 34 | Since this is a Python library you will need Python installed both to run your 35 | code and this library. Supported Python versions are noted above. Unless 36 | there is a strong reason not to, it is recommended to use the current Python 3. 37 | The CPython interpreter from [python.org](python.org) is commonly used. 38 | The `#python` community support channel on the Freenode IRC network is usually 39 | quite active and has many knowledgeable and helpful users that can assist with 40 | general Python issues. 41 | 42 | Recent versions of the CPython Windows installer offer the option of adding 43 | the Python installation to the system path. If you are not familiar with this 44 | it is likely a good option to select. It allows you to run Python from a 45 | shell without having to type the full directory path to Python. If this option 46 | is not present you can add the paths manually as below. 47 | 48 | > Set the PATH environment variable to include directories that contain Python 49 | > packages and scripts. The environment variable settings are at: Computer > 50 | > System properties > Advanced system settings > Environment Variables. 51 | > 52 | > In the System variables section add the following to the 'Path' variable (if 53 | > using a version different than 2.7.x, adjust the string) 54 | > 55 | > ``` 56 | > C:\Python27;C:\Python27\Lib\site-packages\;C:\Python27\Scripts\; 57 | > ``` 58 | > 59 | > If you would like to be able to execute Python scripts without specifying the 60 | > '.py' in the script name, you can also add the '.PY' extension to the end of 61 | > the 'PATHEXT' variable. 62 | 63 | 64 | ## pySunSpec 65 | 66 | When installing from source you need to be sure to have the models available as 67 | well as the pySunSpec source. The models repository is referenced in the 68 | `.gitmodules` file which allows for it to be included with an initial clone. 69 | 70 | ``` 71 | git clone --recursive https://github.com/sunspec/pysunspec.git 72 | ``` 73 | 74 | If a non-recursive clone was done, the models can be retreived using the 75 | `git submodules` command from within your pySunSpec repository clone. 76 | 77 | ``` 78 | git submodule update --init 79 | ``` 80 | 81 | Python libraries should generally be installed before use. This reduces issues 82 | relating to paths and dependency on the value of the current working directory. 83 | It is strongly recommended that you work in an isolated environment such as 84 | can be created by `venv` or `virtualenv` (see [bit.ly/py-env](bit.ly/py-env)). 85 | pySunSpec can be installed per the 86 | [package installation tutorial](https://packaging.python.org/tutorials/installing-packages/) 87 | which also discusses virtual environments. 88 | 89 | 90 | ## Dependencies 91 | 92 | Depending on which installation method you follow you may need to separately 93 | install the pySerial library that pySunSpec depends on. This is the case if 94 | you get an error such as `ImportError: No module named 'serial'` when trying 95 | to use the pySunSpec code. pySerial is available as a Python package from 96 | [PyPi](https://pypi.python.org/pypi/pyserial) and as source from 97 | [GitHub](https://github.com/pyserial/pyserial) as both a Git repository and 98 | a .zip download. If you choose to not use an isolated environment as 99 | described above, on some systems (Debian based and likely other Linux 100 | flavors), `pyserial` is available via the system package manager package. 101 | Be aware of there possibly being separate versions for both Python 2 and 102 | Python 3. 103 | 104 | 105 | ## Verifying the pySunSpec Installation 106 | 107 | You can test the installation by opening a Command Prompt window and running 108 | the `unittest discover` command. You should see the results of the test execution 109 | with no test failures. Depending on how you chose to install Python and 110 | pySunSpec (adding Python to the path, using a virtualenv, etc) you may need 111 | to adjust the `python` portion of the command. 112 | 113 | Command: 114 | ``` 115 | python -m unittest discover -v sunspec 116 | ``` 117 | Expected result: 118 | ``` 119 | test_client_device (core.test.test_client.TestClientDevice) ... ok 120 | test_sunspec_client_device_1 (core.test.test_client.TestClientDevice) ... ok 121 | test_sunspec_client_device_3 (core.test.test_client.TestClientDevice) ... ok 122 | test_data (core.test.test_data.TestData) ... ok 123 | test_device_blocktype_not_equal (core.test.test_device.TestDevice) ... ok 124 | test_device_common_len_65 (core.test.test_device.TestDevice) ... ok 125 | test_device_constant_sf (core.test.test_device.TestDevice) ... ok 126 | test_device_from_pics (core.test.test_device.TestDevice) ... ok 127 | test_device_models_smdx (core.test.test_device.TestDevice) ... ok 128 | test_device_modeltype (core.test.test_device.TestDevice) ... ok 129 | test_device_modeltype_not_equal (core.test.test_device.TestDevice) ... ok 130 | test_device_pointtype (core.test.test_device.TestDevice) ... ok 131 | test_device_pointtype_not_equal (core.test.test_device.TestDevice) ... ok 132 | test_device_to_pics (core.test.test_device.TestDevice) ... ok 133 | test_device_value_get (core.test.test_device.TestDevice) ... ok 134 | test_device_value_set (core.test.test_device.TestDevice) ... ok 135 | test_modbus_client_device_rtu_read (core.test.test_modbus_client.TestModbusClient) ... ok 136 | test_modbus_client_device_rtu_write (core.test.test_modbus_client.TestModbusClient) ... ok 137 | test_modbus_client_device_tcp_read (core.test.test_modbus_client.TestModbusClient) ... ok 138 | test_modbus_client_device_tcp_write (core.test.test_modbus_client.TestModbusClient) ... ok 139 | test_modbus_mbmap_from_xml_element (core.test.test_modbus_mbmap.TestModbusMap) ... ok 140 | test_modbus_mbmap_from_xml_file (core.test.test_modbus_mbmap.TestModbusMap) ... ok 141 | 142 | ---------------------------------------------------------------------- 143 | Ran 22 tests in 0.634s 144 | 145 | OK 146 | ``` 147 | 148 | You should now be ready to use the pySunSpec package. 149 | 150 | 151 | # Documentation 152 | 153 | The documentation can be found on the [Read the 154 | Docs](https://pysunspec.readthedocs.io/en/latest/) site. 155 | 156 | 157 | # Questions, Bugs, Feature Requests 158 | 159 | If you have a question, think you've found a bug or have a feature request 160 | please open an [issue](https://github.com/sunspec/pysunspec/issues) on the 161 | Github Project Page. 162 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pySunSpec.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pySunSpec.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pySunSpec" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pySunSpec" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /doc/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/ajax-loader.gif -------------------------------------------------------------------------------- /doc/_static/basic.css: -------------------------------------------------------------------------------- 1 | /* 2 | * basic.css 3 | * ~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- basic theme. 6 | * 7 | * :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /* -- main layout ----------------------------------------------------------- */ 13 | 14 | div.clearer { 15 | clear: both; 16 | } 17 | 18 | /* -- relbar ---------------------------------------------------------------- */ 19 | 20 | div.related { 21 | width: 100%; 22 | font-size: 90%; 23 | } 24 | 25 | div.related h3 { 26 | display: none; 27 | } 28 | 29 | div.related ul { 30 | margin: 0; 31 | padding: 0 0 0 10px; 32 | list-style: none; 33 | } 34 | 35 | div.related li { 36 | display: inline; 37 | } 38 | 39 | div.related li.right { 40 | float: right; 41 | margin-right: 5px; 42 | } 43 | 44 | /* -- sidebar --------------------------------------------------------------- */ 45 | 46 | div.sphinxsidebarwrapper { 47 | padding: 10px 5px 0 10px; 48 | } 49 | 50 | div.sphinxsidebar { 51 | float: left; 52 | width: 230px; 53 | margin-left: -100%; 54 | font-size: 90%; 55 | } 56 | 57 | div.sphinxsidebar ul { 58 | list-style: none; 59 | } 60 | 61 | div.sphinxsidebar ul ul, 62 | div.sphinxsidebar ul.want-points { 63 | margin-left: 20px; 64 | list-style: square; 65 | } 66 | 67 | div.sphinxsidebar ul ul { 68 | margin-top: 0; 69 | margin-bottom: 0; 70 | } 71 | 72 | div.sphinxsidebar form { 73 | margin-top: 10px; 74 | } 75 | 76 | div.sphinxsidebar input { 77 | border: 1px solid #98dbcc; 78 | font-family: sans-serif; 79 | font-size: 1em; 80 | } 81 | 82 | div.sphinxsidebar #searchbox input[type="text"] { 83 | width: 170px; 84 | } 85 | 86 | div.sphinxsidebar #searchbox input[type="submit"] { 87 | width: 30px; 88 | } 89 | 90 | img { 91 | border: 0; 92 | } 93 | 94 | /* -- search page ----------------------------------------------------------- */ 95 | 96 | ul.search { 97 | margin: 10px 0 0 20px; 98 | padding: 0; 99 | } 100 | 101 | ul.search li { 102 | padding: 5px 0 5px 20px; 103 | background-image: url(file.png); 104 | background-repeat: no-repeat; 105 | background-position: 0 7px; 106 | } 107 | 108 | ul.search li a { 109 | font-weight: bold; 110 | } 111 | 112 | ul.search li div.context { 113 | color: #888; 114 | margin: 2px 0 0 30px; 115 | text-align: left; 116 | } 117 | 118 | ul.keywordmatches li.goodmatch a { 119 | font-weight: bold; 120 | } 121 | 122 | /* -- index page ------------------------------------------------------------ */ 123 | 124 | table.contentstable { 125 | width: 90%; 126 | } 127 | 128 | table.contentstable p.biglink { 129 | line-height: 150%; 130 | } 131 | 132 | a.biglink { 133 | font-size: 1.3em; 134 | } 135 | 136 | span.linkdescr { 137 | font-style: italic; 138 | padding-top: 5px; 139 | font-size: 90%; 140 | } 141 | 142 | /* -- general index --------------------------------------------------------- */ 143 | 144 | table.indextable { 145 | width: 100%; 146 | } 147 | 148 | table.indextable td { 149 | text-align: left; 150 | vertical-align: top; 151 | } 152 | 153 | table.indextable dl, table.indextable dd { 154 | margin-top: 0; 155 | margin-bottom: 0; 156 | } 157 | 158 | table.indextable tr.pcap { 159 | height: 10px; 160 | } 161 | 162 | table.indextable tr.cap { 163 | margin-top: 10px; 164 | background-color: #f2f2f2; 165 | } 166 | 167 | img.toggler { 168 | margin-right: 3px; 169 | margin-top: 3px; 170 | cursor: pointer; 171 | } 172 | 173 | div.modindex-jumpbox { 174 | border-top: 1px solid #ddd; 175 | border-bottom: 1px solid #ddd; 176 | margin: 1em 0 1em 0; 177 | padding: 0.4em; 178 | } 179 | 180 | div.genindex-jumpbox { 181 | border-top: 1px solid #ddd; 182 | border-bottom: 1px solid #ddd; 183 | margin: 1em 0 1em 0; 184 | padding: 0.4em; 185 | } 186 | 187 | /* -- general body styles --------------------------------------------------- */ 188 | 189 | a.headerlink { 190 | visibility: hidden; 191 | } 192 | 193 | h1:hover > a.headerlink, 194 | h2:hover > a.headerlink, 195 | h3:hover > a.headerlink, 196 | h4:hover > a.headerlink, 197 | h5:hover > a.headerlink, 198 | h6:hover > a.headerlink, 199 | dt:hover > a.headerlink { 200 | visibility: visible; 201 | } 202 | 203 | div.body p.caption { 204 | text-align: inherit; 205 | } 206 | 207 | div.body td { 208 | text-align: left; 209 | } 210 | 211 | .field-list ul { 212 | padding-left: 1em; 213 | } 214 | 215 | .first { 216 | margin-top: 0 !important; 217 | } 218 | 219 | p.rubric { 220 | margin-top: 30px; 221 | font-weight: bold; 222 | } 223 | 224 | img.align-left, .figure.align-left, object.align-left { 225 | clear: left; 226 | float: left; 227 | margin-right: 1em; 228 | } 229 | 230 | img.align-right, .figure.align-right, object.align-right { 231 | clear: right; 232 | float: right; 233 | margin-left: 1em; 234 | } 235 | 236 | img.align-center, .figure.align-center, object.align-center { 237 | display: block; 238 | margin-left: auto; 239 | margin-right: auto; 240 | } 241 | 242 | .align-left { 243 | text-align: left; 244 | } 245 | 246 | .align-center { 247 | text-align: center; 248 | } 249 | 250 | .align-right { 251 | text-align: right; 252 | } 253 | 254 | /* -- sidebars -------------------------------------------------------------- */ 255 | 256 | div.sidebar { 257 | margin: 0 0 0.5em 1em; 258 | border: 1px solid #ddb; 259 | padding: 7px 7px 0 7px; 260 | background-color: #ffe; 261 | width: 40%; 262 | float: right; 263 | } 264 | 265 | p.sidebar-title { 266 | font-weight: bold; 267 | } 268 | 269 | /* -- topics ---------------------------------------------------------------- */ 270 | 271 | div.topic { 272 | border: 1px solid #ccc; 273 | padding: 7px 7px 0 7px; 274 | margin: 10px 0 10px 0; 275 | } 276 | 277 | p.topic-title { 278 | font-size: 1.1em; 279 | font-weight: bold; 280 | margin-top: 10px; 281 | } 282 | 283 | /* -- admonitions ----------------------------------------------------------- */ 284 | 285 | div.admonition { 286 | margin-top: 10px; 287 | margin-bottom: 10px; 288 | padding: 7px; 289 | } 290 | 291 | div.admonition dt { 292 | font-weight: bold; 293 | } 294 | 295 | div.admonition dl { 296 | margin-bottom: 0; 297 | } 298 | 299 | p.admonition-title { 300 | margin: 0px 10px 5px 0px; 301 | font-weight: bold; 302 | } 303 | 304 | div.body p.centered { 305 | text-align: center; 306 | margin-top: 25px; 307 | } 308 | 309 | /* -- tables ---------------------------------------------------------------- */ 310 | 311 | table.docutils { 312 | border: 0; 313 | border-collapse: collapse; 314 | } 315 | 316 | table.docutils td, table.docutils th { 317 | padding: 1px 8px 1px 5px; 318 | border-top: 0; 319 | border-left: 0; 320 | border-right: 0; 321 | border-bottom: 1px solid #aaa; 322 | } 323 | 324 | table.field-list td, table.field-list th { 325 | border: 0 !important; 326 | } 327 | 328 | table.footnote td, table.footnote th { 329 | border: 0 !important; 330 | } 331 | 332 | th { 333 | text-align: left; 334 | padding-right: 5px; 335 | } 336 | 337 | table.citation { 338 | border-left: solid 1px gray; 339 | margin-left: 1px; 340 | } 341 | 342 | table.citation td { 343 | border-bottom: none; 344 | } 345 | 346 | /* -- other body styles ----------------------------------------------------- */ 347 | 348 | ol.arabic { 349 | list-style: decimal; 350 | } 351 | 352 | ol.loweralpha { 353 | list-style: lower-alpha; 354 | } 355 | 356 | ol.upperalpha { 357 | list-style: upper-alpha; 358 | } 359 | 360 | ol.lowerroman { 361 | list-style: lower-roman; 362 | } 363 | 364 | ol.upperroman { 365 | list-style: upper-roman; 366 | } 367 | 368 | dl { 369 | margin-bottom: 15px; 370 | } 371 | 372 | dd p { 373 | margin-top: 0px; 374 | } 375 | 376 | dd ul, dd table { 377 | margin-bottom: 10px; 378 | } 379 | 380 | dd { 381 | margin-top: 3px; 382 | margin-bottom: 10px; 383 | margin-left: 30px; 384 | } 385 | 386 | dt:target, .highlighted { 387 | background-color: #fbe54e; 388 | } 389 | 390 | dl.glossary dt { 391 | font-weight: bold; 392 | font-size: 1.1em; 393 | } 394 | 395 | .field-list ul { 396 | margin: 0; 397 | padding-left: 1em; 398 | } 399 | 400 | .field-list p { 401 | margin: 0; 402 | } 403 | 404 | .refcount { 405 | color: #060; 406 | } 407 | 408 | .optional { 409 | font-size: 1.3em; 410 | } 411 | 412 | .versionmodified { 413 | font-style: italic; 414 | } 415 | 416 | .system-message { 417 | background-color: #fda; 418 | padding: 5px; 419 | border: 3px solid red; 420 | } 421 | 422 | .footnote:target { 423 | background-color: #ffa; 424 | } 425 | 426 | .line-block { 427 | display: block; 428 | margin-top: 1em; 429 | margin-bottom: 1em; 430 | } 431 | 432 | .line-block .line-block { 433 | margin-top: 0; 434 | margin-bottom: 0; 435 | margin-left: 1.5em; 436 | } 437 | 438 | .guilabel, .menuselection { 439 | font-family: sans-serif; 440 | } 441 | 442 | .accelerator { 443 | text-decoration: underline; 444 | } 445 | 446 | .classifier { 447 | font-style: oblique; 448 | } 449 | 450 | abbr, acronym { 451 | border-bottom: dotted 1px; 452 | cursor: help; 453 | } 454 | 455 | /* -- code displays --------------------------------------------------------- */ 456 | 457 | pre { 458 | overflow: auto; 459 | overflow-y: hidden; /* fixes display issues on Chrome browsers */ 460 | } 461 | 462 | td.linenos pre { 463 | padding: 5px 0px; 464 | border: 0; 465 | background-color: transparent; 466 | color: #aaa; 467 | } 468 | 469 | table.highlighttable { 470 | margin-left: 0.5em; 471 | } 472 | 473 | table.highlighttable td { 474 | padding: 0 0.5em 0 0.5em; 475 | } 476 | 477 | tt.descname { 478 | background-color: transparent; 479 | font-weight: bold; 480 | font-size: 1.2em; 481 | } 482 | 483 | tt.descclassname { 484 | background-color: transparent; 485 | } 486 | 487 | tt.xref, a tt { 488 | background-color: transparent; 489 | font-weight: bold; 490 | } 491 | 492 | h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { 493 | background-color: transparent; 494 | } 495 | 496 | .viewcode-link { 497 | float: right; 498 | } 499 | 500 | .viewcode-back { 501 | float: right; 502 | font-family: sans-serif; 503 | } 504 | 505 | div.viewcode-block:target { 506 | margin: -1px -10px; 507 | padding: 0 10px; 508 | } 509 | 510 | /* -- math display ---------------------------------------------------------- */ 511 | 512 | img.math { 513 | vertical-align: middle; 514 | } 515 | 516 | div.body div.math p { 517 | text-align: center; 518 | } 519 | 520 | span.eqno { 521 | float: right; 522 | } 523 | 524 | /* -- printout stylesheet --------------------------------------------------- */ 525 | 526 | @media print { 527 | div.document, 528 | div.documentwrapper, 529 | div.bodywrapper { 530 | margin: 0 !important; 531 | width: 100%; 532 | } 533 | 534 | div.sphinxsidebar, 535 | div.related, 536 | div.footer, 537 | #top-link { 538 | display: none; 539 | } 540 | } -------------------------------------------------------------------------------- /doc/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/comment-bright.png -------------------------------------------------------------------------------- /doc/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/comment-close.png -------------------------------------------------------------------------------- /doc/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/comment.png -------------------------------------------------------------------------------- /doc/_static/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | * default.css_t 3 | * ~~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- default theme. 6 | * 7 | * :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: sans-serif; 18 | font-size: 100%; 19 | background-color: #11303d; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | background-color: #1c4e63; 27 | } 28 | 29 | div.documentwrapper { 30 | float: left; 31 | width: 100%; 32 | } 33 | 34 | div.bodywrapper { 35 | margin: 0 0 0 230px; 36 | } 37 | 38 | div.body { 39 | background-color: #ffffff; 40 | color: #000000; 41 | padding: 0 20px 30px 20px; 42 | } 43 | 44 | div.footer { 45 | color: #ffffff; 46 | width: 100%; 47 | padding: 9px 0 9px 0; 48 | text-align: center; 49 | font-size: 75%; 50 | } 51 | 52 | div.footer a { 53 | color: #ffffff; 54 | text-decoration: underline; 55 | } 56 | 57 | div.related { 58 | background-color: #133f52; 59 | line-height: 30px; 60 | color: #ffffff; 61 | } 62 | 63 | div.related a { 64 | color: #ffffff; 65 | } 66 | 67 | div.sphinxsidebar { 68 | } 69 | 70 | div.sphinxsidebar h3 { 71 | font-family: 'Trebuchet MS', sans-serif; 72 | color: #ffffff; 73 | font-size: 1.4em; 74 | font-weight: normal; 75 | margin: 0; 76 | padding: 0; 77 | } 78 | 79 | div.sphinxsidebar h3 a { 80 | color: #ffffff; 81 | } 82 | 83 | div.sphinxsidebar h4 { 84 | font-family: 'Trebuchet MS', sans-serif; 85 | color: #ffffff; 86 | font-size: 1.3em; 87 | font-weight: normal; 88 | margin: 5px 0 0 0; 89 | padding: 0; 90 | } 91 | 92 | div.sphinxsidebar p { 93 | color: #ffffff; 94 | } 95 | 96 | div.sphinxsidebar p.topless { 97 | margin: 5px 10px 10px 10px; 98 | } 99 | 100 | div.sphinxsidebar ul { 101 | margin: 10px; 102 | padding: 0; 103 | color: #ffffff; 104 | } 105 | 106 | div.sphinxsidebar a { 107 | color: #98dbcc; 108 | } 109 | 110 | div.sphinxsidebar input { 111 | border: 1px solid #98dbcc; 112 | font-family: sans-serif; 113 | font-size: 1em; 114 | } 115 | 116 | 117 | 118 | /* -- hyperlink styles ------------------------------------------------------ */ 119 | 120 | a { 121 | color: #355f7c; 122 | text-decoration: none; 123 | } 124 | 125 | a:visited { 126 | color: #355f7c; 127 | text-decoration: none; 128 | } 129 | 130 | a:hover { 131 | text-decoration: underline; 132 | } 133 | 134 | 135 | 136 | /* -- body styles ----------------------------------------------------------- */ 137 | 138 | div.body h1, 139 | div.body h2, 140 | div.body h3, 141 | div.body h4, 142 | div.body h5, 143 | div.body h6 { 144 | font-family: 'Trebuchet MS', sans-serif; 145 | background-color: #f2f2f2; 146 | font-weight: normal; 147 | color: #20435c; 148 | border-bottom: 1px solid #ccc; 149 | margin: 20px -20px 10px -20px; 150 | padding: 3px 0 3px 10px; 151 | } 152 | 153 | div.body h1 { margin-top: 0; font-size: 200%; } 154 | div.body h2 { font-size: 160%; } 155 | div.body h3 { font-size: 140%; } 156 | div.body h4 { font-size: 120%; } 157 | div.body h5 { font-size: 110%; } 158 | div.body h6 { font-size: 100%; } 159 | 160 | a.headerlink { 161 | color: #c60f0f; 162 | font-size: 0.8em; 163 | padding: 0 4px 0 4px; 164 | text-decoration: none; 165 | } 166 | 167 | a.headerlink:hover { 168 | background-color: #c60f0f; 169 | color: white; 170 | } 171 | 172 | div.body p, div.body dd, div.body li { 173 | text-align: justify; 174 | line-height: 130%; 175 | } 176 | 177 | div.admonition p.admonition-title + p { 178 | display: inline; 179 | } 180 | 181 | div.admonition p { 182 | margin-bottom: 5px; 183 | } 184 | 185 | div.admonition pre { 186 | margin-bottom: 5px; 187 | } 188 | 189 | div.admonition ul, div.admonition ol { 190 | margin-bottom: 5px; 191 | } 192 | 193 | div.note { 194 | background-color: #eee; 195 | border: 1px solid #ccc; 196 | } 197 | 198 | div.seealso { 199 | background-color: #ffc; 200 | border: 1px solid #ff6; 201 | } 202 | 203 | div.topic { 204 | background-color: #eee; 205 | } 206 | 207 | div.warning { 208 | background-color: #ffe4e4; 209 | border: 1px solid #f66; 210 | } 211 | 212 | p.admonition-title { 213 | display: inline; 214 | } 215 | 216 | p.admonition-title:after { 217 | content: ":"; 218 | } 219 | 220 | pre { 221 | padding: 5px; 222 | background-color: #eeffcc; 223 | color: #333333; 224 | line-height: 120%; 225 | border: 1px solid #ac9; 226 | border-left: none; 227 | border-right: none; 228 | } 229 | 230 | tt { 231 | background-color: #ecf0f3; 232 | padding: 0 1px 0 1px; 233 | font-size: 0.95em; 234 | } 235 | 236 | th { 237 | background-color: #ede; 238 | } 239 | 240 | .warning tt { 241 | background: #efc2c2; 242 | } 243 | 244 | .note tt { 245 | background: #d6d6d6; 246 | } 247 | 248 | .viewcode-back { 249 | font-family: sans-serif; 250 | } 251 | 252 | div.viewcode-block:target { 253 | background-color: #f4debf; 254 | border-top: 1px solid #ac9; 255 | border-bottom: 1px solid #ac9; 256 | } -------------------------------------------------------------------------------- /doc/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | */ 33 | jQuery.urldecode = function(x) { 34 | return decodeURIComponent(x).replace(/\+/g, ' '); 35 | }; 36 | 37 | /** 38 | * small helper function to urlencode strings 39 | */ 40 | jQuery.urlencode = encodeURIComponent; 41 | 42 | /** 43 | * This function returns the parsed url parameters of the 44 | * current request. Multiple values per key are supported, 45 | * it will always return arrays of strings for the value parts. 46 | */ 47 | jQuery.getQueryParameters = function(s) { 48 | if (typeof s == 'undefined') 49 | s = document.location.search; 50 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 51 | var result = {}; 52 | for (var i = 0; i < parts.length; i++) { 53 | var tmp = parts[i].split('=', 2); 54 | var key = jQuery.urldecode(tmp[0]); 55 | var value = jQuery.urldecode(tmp[1]); 56 | if (key in result) 57 | result[key].push(value); 58 | else 59 | result[key] = [value]; 60 | } 61 | return result; 62 | }; 63 | 64 | /** 65 | * highlight a given string on a jquery object by wrapping it in 66 | * span elements with the given class name. 67 | */ 68 | jQuery.fn.highlightText = function(text, className) { 69 | function highlight(node) { 70 | if (node.nodeType == 3) { 71 | var val = node.nodeValue; 72 | var pos = val.toLowerCase().indexOf(text); 73 | if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { 74 | var span = document.createElement("span"); 75 | span.className = className; 76 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 77 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 78 | document.createTextNode(val.substr(pos + text.length)), 79 | node.nextSibling)); 80 | node.nodeValue = val.substr(0, pos); 81 | } 82 | } 83 | else if (!jQuery(node).is("button, select, textarea")) { 84 | jQuery.each(node.childNodes, function() { 85 | highlight(this); 86 | }); 87 | } 88 | } 89 | return this.each(function() { 90 | highlight(this); 91 | }); 92 | }; 93 | 94 | /** 95 | * Small JavaScript module for the documentation. 96 | */ 97 | var Documentation = { 98 | 99 | init : function() { 100 | this.fixFirefoxAnchorBug(); 101 | this.highlightSearchWords(); 102 | this.initIndexTable(); 103 | }, 104 | 105 | /** 106 | * i18n support 107 | */ 108 | TRANSLATIONS : {}, 109 | PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, 110 | LOCALE : 'unknown', 111 | 112 | // gettext and ngettext don't access this so that the functions 113 | // can safely bound to a different name (_ = Documentation.gettext) 114 | gettext : function(string) { 115 | var translated = Documentation.TRANSLATIONS[string]; 116 | if (typeof translated == 'undefined') 117 | return string; 118 | return (typeof translated == 'string') ? translated : translated[0]; 119 | }, 120 | 121 | ngettext : function(singular, plural, n) { 122 | var translated = Documentation.TRANSLATIONS[singular]; 123 | if (typeof translated == 'undefined') 124 | return (n == 1) ? singular : plural; 125 | return translated[Documentation.PLURALEXPR(n)]; 126 | }, 127 | 128 | addTranslations : function(catalog) { 129 | for (var key in catalog.messages) 130 | this.TRANSLATIONS[key] = catalog.messages[key]; 131 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 132 | this.LOCALE = catalog.locale; 133 | }, 134 | 135 | /** 136 | * add context elements like header anchor links 137 | */ 138 | addContextElements : function() { 139 | $('div[id] > :header:first').each(function() { 140 | $('\u00B6'). 141 | attr('href', '#' + this.id). 142 | attr('title', _('Permalink to this headline')). 143 | appendTo(this); 144 | }); 145 | $('dt[id]').each(function() { 146 | $('\u00B6'). 147 | attr('href', '#' + this.id). 148 | attr('title', _('Permalink to this definition')). 149 | appendTo(this); 150 | }); 151 | }, 152 | 153 | /** 154 | * workaround a firefox stupidity 155 | */ 156 | fixFirefoxAnchorBug : function() { 157 | if (document.location.hash && $.browser.mozilla) 158 | window.setTimeout(function() { 159 | document.location.href += ''; 160 | }, 10); 161 | }, 162 | 163 | /** 164 | * highlight the search words provided in the url in the text 165 | */ 166 | highlightSearchWords : function() { 167 | var params = $.getQueryParameters(); 168 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 169 | if (terms.length) { 170 | var body = $('div.body'); 171 | window.setTimeout(function() { 172 | $.each(terms, function() { 173 | body.highlightText(this.toLowerCase(), 'highlighted'); 174 | }); 175 | }, 10); 176 | $('') 178 | .appendTo($('#searchbox')); 179 | } 180 | }, 181 | 182 | /** 183 | * init the domain index toggle buttons 184 | */ 185 | initIndexTable : function() { 186 | var togglers = $('img.toggler').click(function() { 187 | var src = $(this).attr('src'); 188 | var idnum = $(this).attr('id').substr(7); 189 | $('tr.cg-' + idnum).toggle(); 190 | if (src.substr(-9) == 'minus.png') 191 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 192 | else 193 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 194 | }).css('display', ''); 195 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 196 | togglers.click(); 197 | } 198 | }, 199 | 200 | /** 201 | * helper function to hide the search marks again 202 | */ 203 | hideSearchWords : function() { 204 | $('#searchbox .highlight-link').fadeOut(300); 205 | $('span.highlighted').removeClass('highlighted'); 206 | }, 207 | 208 | /** 209 | * make the url absolute 210 | */ 211 | makeURL : function(relativeURL) { 212 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 213 | }, 214 | 215 | /** 216 | * get the current relative url 217 | */ 218 | getCurrentURL : function() { 219 | var path = document.location.pathname; 220 | var parts = path.split(/\//); 221 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 222 | if (this == '..') 223 | parts.pop(); 224 | }); 225 | var url = parts.join('/'); 226 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 227 | } 228 | }; 229 | 230 | // quick alias for translations 231 | _ = Documentation.gettext; 232 | 233 | $(document).ready(function() { 234 | Documentation.init(); 235 | }); 236 | -------------------------------------------------------------------------------- /doc/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/down-pressed.png -------------------------------------------------------------------------------- /doc/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/down.png -------------------------------------------------------------------------------- /doc/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/file.png -------------------------------------------------------------------------------- /doc/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/minus.png -------------------------------------------------------------------------------- /doc/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/plus.png -------------------------------------------------------------------------------- /doc/_static/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #eeffcc; } 3 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 4 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 5 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #666666 } /* Operator */ 7 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 8 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 9 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 10 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 11 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 12 | .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 14 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 15 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 16 | .highlight .go { color: #333333 } /* Generic.Output */ 17 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 18 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 19 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 20 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 21 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 22 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 23 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 24 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 25 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 26 | .highlight .kt { color: #902000 } /* Keyword.Type */ 27 | .highlight .m { color: #208050 } /* Literal.Number */ 28 | .highlight .s { color: #4070a0 } /* Literal.String */ 29 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 30 | .highlight .nb { color: #007020 } /* Name.Builtin */ 31 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 32 | .highlight .no { color: #60add5 } /* Name.Constant */ 33 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 34 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 35 | .highlight .ne { color: #007020 } /* Name.Exception */ 36 | .highlight .nf { color: #06287e } /* Name.Function */ 37 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 38 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 39 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 40 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 41 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 42 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 44 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 45 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 46 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 47 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 48 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 49 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 50 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 51 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 52 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 53 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 54 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 55 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 56 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 57 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 58 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 59 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 60 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 61 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 62 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /doc/_static/sidebar.js: -------------------------------------------------------------------------------- 1 | /* 2 | * sidebar.js 3 | * ~~~~~~~~~~ 4 | * 5 | * This script makes the Sphinx sidebar collapsible. 6 | * 7 | * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds 8 | * in .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton 9 | * used to collapse and expand the sidebar. 10 | * 11 | * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden 12 | * and the width of the sidebar and the margin-left of the document 13 | * are decreased. When the sidebar is expanded the opposite happens. 14 | * This script saves a per-browser/per-session cookie used to 15 | * remember the position of the sidebar among the pages. 16 | * Once the browser is closed the cookie is deleted and the position 17 | * reset to the default (expanded). 18 | * 19 | * :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. 20 | * :license: BSD, see LICENSE for details. 21 | * 22 | */ 23 | 24 | $(function() { 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | // global elements used by the functions. 34 | // the 'sidebarbutton' element is defined as global after its 35 | // creation, in the add_sidebar_button function 36 | var bodywrapper = $('.bodywrapper'); 37 | var sidebar = $('.sphinxsidebar'); 38 | var sidebarwrapper = $('.sphinxsidebarwrapper'); 39 | 40 | // for some reason, the document has no sidebar; do not run into errors 41 | if (!sidebar.length) return; 42 | 43 | // original margin-left of the bodywrapper and width of the sidebar 44 | // with the sidebar expanded 45 | var bw_margin_expanded = bodywrapper.css('margin-left'); 46 | var ssb_width_expanded = sidebar.width(); 47 | 48 | // margin-left of the bodywrapper and width of the sidebar 49 | // with the sidebar collapsed 50 | var bw_margin_collapsed = '.8em'; 51 | var ssb_width_collapsed = '.8em'; 52 | 53 | // colors used by the current theme 54 | var dark_color = $('.related').css('background-color'); 55 | var light_color = $('.document').css('background-color'); 56 | 57 | function sidebar_is_collapsed() { 58 | return sidebarwrapper.is(':not(:visible)'); 59 | } 60 | 61 | function toggle_sidebar() { 62 | if (sidebar_is_collapsed()) 63 | expand_sidebar(); 64 | else 65 | collapse_sidebar(); 66 | } 67 | 68 | function collapse_sidebar() { 69 | sidebarwrapper.hide(); 70 | sidebar.css('width', ssb_width_collapsed); 71 | bodywrapper.css('margin-left', bw_margin_collapsed); 72 | sidebarbutton.css({ 73 | 'margin-left': '0', 74 | 'height': bodywrapper.height() 75 | }); 76 | sidebarbutton.find('span').text('»'); 77 | sidebarbutton.attr('title', _('Expand sidebar')); 78 | document.cookie = 'sidebar=collapsed'; 79 | } 80 | 81 | function expand_sidebar() { 82 | bodywrapper.css('margin-left', bw_margin_expanded); 83 | sidebar.css('width', ssb_width_expanded); 84 | sidebarwrapper.show(); 85 | sidebarbutton.css({ 86 | 'margin-left': ssb_width_expanded-12, 87 | 'height': bodywrapper.height() 88 | }); 89 | sidebarbutton.find('span').text('«'); 90 | sidebarbutton.attr('title', _('Collapse sidebar')); 91 | document.cookie = 'sidebar=expanded'; 92 | } 93 | 94 | function add_sidebar_button() { 95 | sidebarwrapper.css({ 96 | 'float': 'left', 97 | 'margin-right': '0', 98 | 'width': ssb_width_expanded - 28 99 | }); 100 | // create the button 101 | sidebar.append( 102 | '
«
' 103 | ); 104 | var sidebarbutton = $('#sidebarbutton'); 105 | light_color = sidebarbutton.css('background-color'); 106 | // find the height of the viewport to center the '<<' in the page 107 | var viewport_height; 108 | if (window.innerHeight) 109 | viewport_height = window.innerHeight; 110 | else 111 | viewport_height = $(window).height(); 112 | sidebarbutton.find('span').css({ 113 | 'display': 'block', 114 | 'margin-top': (viewport_height - sidebar.position().top - 20) / 2 115 | }); 116 | 117 | sidebarbutton.click(toggle_sidebar); 118 | sidebarbutton.attr('title', _('Collapse sidebar')); 119 | sidebarbutton.css({ 120 | 'color': '#FFFFFF', 121 | 'border-left': '1px solid ' + dark_color, 122 | 'font-size': '1.2em', 123 | 'cursor': 'pointer', 124 | 'height': bodywrapper.height(), 125 | 'padding-top': '1px', 126 | 'margin-left': ssb_width_expanded - 12 127 | }); 128 | 129 | sidebarbutton.hover( 130 | function () { 131 | $(this).css('background-color', dark_color); 132 | }, 133 | function () { 134 | $(this).css('background-color', light_color); 135 | } 136 | ); 137 | } 138 | 139 | function set_position_from_cookie() { 140 | if (!document.cookie) 141 | return; 142 | var items = document.cookie.split(';'); 143 | for(var k=0; k2;a== 12 | null&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect= 13 | function(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e= 14 | e&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= 15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a, 17 | c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}}; 24 | b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments, 25 | 1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)}; 26 | b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"}; 27 | b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a), 28 | function(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\/g,"\\").replace(/\\'/g,"'")};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape||t,function(a,b){return"',_.escape("+ 29 | u(b)+"),'"}).replace(d.interpolate||t,function(a,b){return"',"+u(b)+",'"}).replace(d.evaluate||t,function(a,b){return"');"+u(b).replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]= 30 | function(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return v(d,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain= 31 | true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); 32 | -------------------------------------------------------------------------------- /doc/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/up-pressed.png -------------------------------------------------------------------------------- /doc/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/doc/_static/up.png -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pySunSpec documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Dec 02 12:56:18 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('..')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.autosummary', 34 | 'sphinx.ext.napoleon' 35 | ] 36 | 37 | autodoc_member_order = 'bysource' 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix of source filenames. 43 | source_suffix = '.rst' 44 | 45 | # The encoding of source files. 46 | #source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = u'pySunSpec' 53 | copyright = u'2014, SunSpec Alliance' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '1.0' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '1.0.3' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | #language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | #today_fmt = '%B %d, %Y' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ['_build'] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all 79 | # documents. 80 | #default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | #add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | #add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | #show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = 'sphinx' 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | #modindex_common_prefix = [] 98 | 99 | # If true, keep warnings as "system message" paragraphs in the built documents. 100 | #keep_warnings = False 101 | 102 | 103 | # -- Options for HTML output ---------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. See the documentation for 106 | # a list of builtin themes. 107 | html_theme = 'default' 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | #html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | #html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | #html_title = None 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | #html_short_title = None 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | #html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | #html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | html_static_path = ['_static'] 137 | 138 | # Add any extra paths that contain custom files (such as robots.txt or 139 | # .htaccess) here, relative to this directory. These files are copied 140 | # directly to the root of the documentation. 141 | #html_extra_path = [] 142 | 143 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 144 | # using the given strftime format. 145 | #html_last_updated_fmt = '%b %d, %Y' 146 | 147 | # If true, SmartyPants will be used to convert quotes and dashes to 148 | # typographically correct entities. 149 | #html_use_smartypants = True 150 | 151 | # Custom sidebar templates, maps document names to template names. 152 | #html_sidebars = {} 153 | 154 | # Additional templates that should be rendered to pages, maps page names to 155 | # template names. 156 | #html_additional_pages = {} 157 | 158 | # If false, no module index is generated. 159 | #html_domain_indices = True 160 | 161 | # If false, no index is generated. 162 | #html_use_index = True 163 | 164 | # If true, the index is split into individual pages for each letter. 165 | #html_split_index = False 166 | 167 | # If true, links to the reST sources are added to the pages. 168 | #html_show_sourcelink = True 169 | 170 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 171 | #html_show_sphinx = True 172 | 173 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 174 | #html_show_copyright = True 175 | 176 | # If true, an OpenSearch description file will be output, and all pages will 177 | # contain a tag referring to it. The value of this option must be the 178 | # base URL from which the finished HTML is served. 179 | #html_use_opensearch = '' 180 | 181 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 182 | #html_file_suffix = None 183 | 184 | # Output file base name for HTML help builder. 185 | htmlhelp_basename = 'pySunSpecdoc' 186 | 187 | 188 | # -- Options for LaTeX output --------------------------------------------- 189 | 190 | latex_elements = { 191 | # The paper size ('letterpaper' or 'a4paper'). 192 | #'papersize': 'letterpaper', 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #'pointsize': '10pt', 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | #'preamble': '', 199 | } 200 | 201 | # Grouping the document tree into LaTeX files. List of tuples 202 | # (source start file, target name, title, 203 | # author, documentclass [howto, manual, or own class]). 204 | latex_documents = [ 205 | ('index', 'pySunSpec.tex', u'pySunSpec Documentation', 206 | u'SunSpec Alliance', 'manual'), 207 | ] 208 | 209 | # The name of an image file (relative to this directory) to place at the top of 210 | # the title page. 211 | #latex_logo = None 212 | 213 | # For "manual" documents, if this is true, then toplevel headings are parts, 214 | # not chapters. 215 | #latex_use_parts = False 216 | 217 | # If true, show page references after internal links. 218 | #latex_show_pagerefs = False 219 | 220 | # If true, show URL addresses after external links. 221 | #latex_show_urls = False 222 | 223 | # Documents to append as an appendix to all manuals. 224 | #latex_appendices = [] 225 | 226 | # If false, no module index is generated. 227 | #latex_domain_indices = True 228 | 229 | 230 | # -- Options for manual page output --------------------------------------- 231 | 232 | # One entry per manual page. List of tuples 233 | # (source start file, name, description, authors, manual section). 234 | man_pages = [ 235 | ('index', 'pysunspec', u'pySunSpec Documentation', 236 | [u'SunSpec Alliance'], 1) 237 | ] 238 | 239 | # If true, show URL addresses after external links. 240 | #man_show_urls = False 241 | 242 | 243 | # -- Options for Texinfo output ------------------------------------------- 244 | 245 | # Grouping the document tree into Texinfo files. List of tuples 246 | # (source start file, target name, title, author, 247 | # dir menu entry, description, category) 248 | texinfo_documents = [ 249 | ('index', 'pySunSpec', u'pySunSpec Documentation', 250 | u'SunSpec Alliance', 'pySunSpec', 'One line description of project.', 251 | 'Miscellaneous'), 252 | ] 253 | 254 | # Documents to append as an appendix to all manuals. 255 | #texinfo_appendices = [] 256 | 257 | # If false, no module index is generated. 258 | #texinfo_domain_indices = True 259 | 260 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 261 | #texinfo_show_urls = 'footnote' 262 | 263 | # If true, do not generate a @detailmenu in the "Top" node's menu. 264 | #texinfo_no_detailmenu = False 265 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. pySunSpec documentation master file, created by 2 | sphinx-quickstart on Mon Dec 02 12:56:18 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pySunSpec's documentation 7 | ==================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | 14 | pysunspec 15 | pysunspec_api 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pySunSpec.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pySunSpec.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /doc/pysunspec_api.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | API Reference 3 | ============== 4 | 5 | :mod:`sunspec.core` --- SunSpec Core Functionality 6 | ================================================== 7 | 8 | The core package contains modules that provide the ability to interact with 9 | SunSpec devices and encode/decode documents in SunSpec standard formats. The 10 | modules in the core package used to interact with SunSpec devices are organized 11 | into the following main functional areas: base SunSpec device/model support, 12 | Modbus protocol support, and client device support. 13 | 14 | The device module provides objects that map to the SunSpec model definitions 15 | within the SunSpec standards. A device is a collection of SunSpec model 16 | definitions that are implemented by a SunSpec compliant device. The device 17 | module provides objects for device, model, block, and point instances as well 18 | as objects that support the model, block, and point type definitions. 19 | 20 | The modbus package provides protocol support for Modbus TCP, Modbus RTU, and 21 | locally defined Modbus maps that can be accessed directly. 22 | 23 | The client module extends the basic objects in the device module to provide 24 | objects that can be used to access devices using the Modbus protocol objects 25 | supplied in the modbus package. 26 | 27 | :mod:`sunspec.core.client` --- SunSpec Client classes 28 | ===================================================== 29 | 30 | .. module:: sunspec.core.client 31 | 32 | Classes 33 | ------- 34 | 35 | .. autoclass:: sunspec.core.client.SunSpecClientDevice 36 | :members: close, read 37 | 38 | .. autoclass:: sunspec.core.client.SunSpecClientModelBase 39 | :members: read, write 40 | 41 | .. autoclass:: sunspec.core.client.SunSpecClientBlockBase 42 | :members: 43 | 44 | .. autoclass:: sunspec.core.client.ClientDevice 45 | :members: read, write, read_points, scan 46 | 47 | .. autoclass:: sunspec.core.client.ClientModel 48 | :members: load, read_points, write_points 49 | 50 | .. autoclass:: sunspec.core.client.ClientBlock 51 | :members: 52 | 53 | .. autoclass:: sunspec.core.client.ClientPoint 54 | :members: write 55 | 56 | Exceptions 57 | ---------- 58 | 59 | .. exception:: SunSpecClientError 60 | 61 | Derived from :const:`sunspec.core.device.SunSpecError`. Raised for any 62 | sunspec module error in sunspec.core.client classes. 63 | 64 | Constants 65 | --------- 66 | 67 | *Device Types* 68 | 69 | .. data:: RTU 70 | .. data:: TCP 71 | .. data:: MAPPED 72 | 73 | *RTU Device Parity* 74 | 75 | .. data:: PARITY_NONE 76 | .. data:: PARITY_EVEN 77 | 78 | :mod:`sunspec.core.device` --- SunSpec Device classes 79 | ===================================================== 80 | 81 | .. module:: sunspec.core.device 82 | 83 | The device module provides the base SunSpec device functionality. A SunSpec 84 | device consists of a set of SunSpec models which contain blocks and points as 85 | specified in the respective SunSpec model definitions. 86 | 87 | Classes 88 | ------- 89 | 90 | .. autoclass:: sunspec.core.device.Device 91 | :members: add_model, from_pics, to_pics, not_equal 92 | 93 | .. autoclass:: sunspec.core.device.Model 94 | :members: load, from_pics, to_pics, not_equal 95 | 96 | .. autoclass:: sunspec.core.device.Block 97 | :members: from_pics, to_pics, not_equal 98 | 99 | .. autoclass:: sunspec.core.device.Point 100 | :members: from_pics, to_pics, not_equal 101 | 102 | .. autoclass:: sunspec.core.device.ModelType 103 | :members: from_smdx, not_equal 104 | 105 | .. autoclass:: sunspec.core.device.BlockType 106 | :members: from_smdx, not_equal 107 | 108 | .. autoclass:: sunspec.core.device.PointType 109 | :members: from_smdx, not_equal 110 | 111 | Exceptions 112 | ---------- 113 | 114 | .. exception:: SunSpecError 115 | 116 | Raised for sunspec.core.device errors. 117 | 118 | :mod:`sunspec.core.modbus` --- SunSpec Modbus Package 119 | ======================================================== 120 | 121 | The Modbus package provides standard Modbus support for Modbus RTU, Modbus TCP, and Modbus file mapped client devices. 122 | 123 | :mod:`sunspec.core.modbus.client` --- Modbus Client classes 124 | =========================================================== 125 | 126 | .. module:: sunspec.core.modbus.client 127 | 128 | Classes 129 | ------- 130 | 131 | .. autoclass:: sunspec.core.modbus.client.ModbusClientDeviceRTU 132 | :members: close, read, write 133 | 134 | .. autoclass:: sunspec.core.modbus.client.ModbusClientDeviceTCP 135 | :members: connect, disconnect, close, read, write 136 | 137 | .. autoclass:: sunspec.core.modbus.client.ModbusClientDeviceMapped 138 | :members: close, read, write 139 | 140 | .. autoclass:: sunspec.core.modbus.client.ModbusClientRTU 141 | :members: open, close, add_device, remove_device, read, write 142 | 143 | Exceptions 144 | ---------- 145 | 146 | .. exception:: ModbusClientError 147 | 148 | Raised for general errors in sunspec.core.modbus.client modules. 149 | 150 | .. exception:: ModbusClientTimeout 151 | 152 | Raised for Modbus timeout errors. Derived from :const:`ModbusClientError`. 153 | 154 | .. exception:: ModbusClientException 155 | 156 | Raised for Modbus protocol exceptions. Derived from :const:`ModbusClientError`. 157 | 158 | Constants 159 | --------- 160 | 161 | *Parity* 162 | 163 | .. data:: PARITY_NONE 164 | .. data:: PARITY_EVEN 165 | 166 | *Read Modbus Functions* 167 | 168 | .. data:: FUNC_READ_HOLDING 169 | .. data:: FUNC_READ_INPUT 170 | 171 | :mod:`sunspec.core.modbus.mbmap` --- Modbus Map classes 172 | ======================================================== 173 | 174 | The mbmap module implements a local Modbus map image. The map supports read and write operations and can be used by Modbus clients 175 | in place of an actual modbus device. The module supports an xml encoding for representing modbus maps as a document. 176 | 177 | The xml representation has a root element of *mbmap* that contains a set of *regs* elements representing one or more registers in the map. 178 | Currently only big endian is supported. 179 | 180 | Attributes for *mbmap* element: 181 | 182 | +-----------+--------------------------------------------+-------------------------------+----------------------+ 183 | | Attribute | Description | Valid values | Default value | 184 | +===========+============================================+===============================+======================+ 185 | | addr | Base Modbus address | Valid modbus address | 40000 | 186 | +-----------+--------------------------------------------+-------------------------------+----------------------+ 187 | | func | Modbus function associated with the map | holding, input | holding | 188 | +-----------+--------------------------------------------+-------------------------------+----------------------+ 189 | 190 | Attributes for *regs* element: 191 | 192 | +-----------+--------------------------------------------+-------------------------------+----------------------+ 193 | | Attribute | Description | Valid values | Default value | 194 | +===========+============================================+===============================+======================+ 195 | | offset | Register offset | Value with map length | Next offset | 196 | +-----------+--------------------------------------------+-------------------------------+----------------------+ 197 | | type | Register(s) type | s16, u16, s32, u32, s64, u64, | hexstr | 198 | | | | f32, f64, string, hexstr | | 199 | +-----------+--------------------------------------------+-------------------------------+----------------------+ 200 | | len | String length for string type | Value within map length | Length of the *regs* | 201 | | | | | element value | 202 | +-----------+--------------------------------------------+-------------------------------+----------------------+ 203 | 204 | If the registers are contiguous, offset is optional. 205 | 206 | The *hexstr* type is a series of ascii hex characters. Spaces are removed from the string before processing and 207 | can be used to increase readability. 208 | 209 | Example:: 210 | 211 | 212 | 213 | SunS 214 | 1 215 | 66 216 | SunSpecTest 217 | TestInverter-1 218 | opt_a_b_c 219 | 1.2.3 220 | sn-123456789 221 | 1 222 | 0 223 | 224 | 225 | 63010 226 | 4 227 | 228 | 229 | 00 79 00 1E 230 | 0079 001E 231 | 232 | 233 | 0xffff 234 | 0 235 | 236 | 237 | .. module:: sunspec.core.modbus.mbmap 238 | 239 | Classes 240 | ------- 241 | 242 | .. autoclass:: sunspec.core.modbus.mbmap.ModbusMap 243 | :members: from_xml, read, write, not_equal 244 | 245 | .. autoclass:: sunspec.core.modbus.mbmap.ModbusMapRegs 246 | :members: read, write, append, not_equal 247 | 248 | Exceptions 249 | ---------- 250 | 251 | .. exception:: ModbusMapError 252 | 253 | Raised for errors in sunspec.core.modbus.mbmap modules. 254 | 255 | Constants 256 | --------- 257 | 258 | *Modbus Map Functions* 259 | 260 | .. attribute:: MBMAP_FUNC_INPUT 261 | .. attribute:: MBMAP_FUNC_HOLDING 262 | -------------------------------------------------------------------------------- /scripts/pysunspec_coverage_report.py: -------------------------------------------------------------------------------- 1 | import coverage 2 | import os 3 | import unittest 4 | 5 | # Start the coverage.py tracking 6 | cov = coverage.Coverage() 7 | cov.start() 8 | 9 | # Find the top_level sunspec path 10 | scripts_dir = os.path.abspath(os.path.dirname(__file__)) # scripts/ 11 | pysunspec_top = os.path.split(scripts_dir)[0] # ../ 12 | sunspec_dir = os.path.join(pysunspec_top, 'sunspec') 13 | 14 | # Find and run all of the tests in the repository 15 | loader = unittest.TestLoader() 16 | package_tests = loader.discover(sunspec_dir) 17 | runner = unittest.TextTestRunner() 18 | for test in package_tests: 19 | runner.run(test) 20 | 21 | # End the coverage.py tracking and make an html report. The html report can be 22 | # viewed in the folder this code was run by opening htmlcov/index.html 23 | cov.stop() 24 | cov.save() 25 | cov.html_report() 26 | -------------------------------------------------------------------------------- /scripts/suns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2018, SunSpec Alliance 5 | All Rights Reserved 6 | 7 | """ 8 | 9 | import sys 10 | import time 11 | import sunspec.core.client as client 12 | import sunspec.core.suns as suns 13 | from optparse import OptionParser 14 | 15 | """ 16 | Original suns options: 17 | 18 | -o: output mode for data (text, xml) 19 | -x: export model description (slang, xml) 20 | -t: transport type: tcp or rtu (default: tcp) 21 | -a: modbus slave address (default: 1) 22 | -i: ip address to use for modbus tcp (default: localhost) 23 | -P: port number for modbus tcp (default: 502) 24 | -p: serial port for modbus rtu (default: /dev/ttyUSB0) 25 | -b: baud rate for modbus rtu (default: 9600) 26 | -T: timeout, in seconds (can be fractional, such as 1.5; default: 2.0) 27 | -r: number of retries attempted for each modbus read 28 | -m: specify model file 29 | -M: specify directory containing model files 30 | -s: run as a test server 31 | -I: logger id (for sunspec logger xml output) 32 | -N: logger id namespace (for sunspec logger xml output, defaults to 'mac') 33 | -l: limit number of registers requested in a single read (max is 125) 34 | -c: check models for internal consistency then exit 35 | -v: verbose level (up to -vvvv for most verbose) 36 | -V: print current release number and exit 37 | """ 38 | 39 | if __name__ == "__main__": 40 | 41 | usage = 'usage: %prog [options]' 42 | parser = OptionParser(usage=usage) 43 | parser.add_option('-t', metavar=' ', 44 | default='tcp', 45 | help='transport type: rtu, tcp, mapped [default: tcp]') 46 | parser.add_option('-a', metavar=' ', type='int', 47 | default=1, 48 | help='modbus slave address [default: 1]') 49 | parser.add_option('-i', metavar=' ', 50 | default='localhost', 51 | help='ip address to use for modbus tcp [default: localhost]') 52 | parser.add_option('-P', metavar=' ', type='int', 53 | default=502, 54 | help='port number for modbus tcp [default: 502]') 55 | parser.add_option('-p', metavar=' ', 56 | default='/dev/ttyUSB0', 57 | help='serial port for modbus rtu [default: /dev/ttyUSB0]') 58 | parser.add_option('-b', metavar=' ', 59 | default=9600, 60 | help='baud rate for modbus rtu [default: 9600]') 61 | parser.add_option('-T', metavar=' ', type='float', 62 | default=2.0, 63 | help='timeout, in seconds (can be fractional, such as 1.5) [default: 2.0]') 64 | parser.add_option('-m', metavar=' ', 65 | help='modbus map file') 66 | 67 | options, args = parser.parse_args() 68 | 69 | try: 70 | if options.t == 'tcp': 71 | sd = client.SunSpecClientDevice(client.TCP, options.a, ipaddr=options.i, ipport=options.P, timeout=options.T) 72 | elif options.t == 'rtu': 73 | sd = client.SunSpecClientDevice(client.RTU, options.a, name=options.p, baudrate=options.b, timeout=options.T) 74 | elif options.t == 'mapped': 75 | sd = client.SunSpecClientDevice(client.MAPPED, options.a, name=options.m) 76 | else: 77 | print('Unknown -t option: %s' % (options.t)) 78 | sys.exit(1) 79 | 80 | except client.SunSpecClientError as e: 81 | print('Error: %s' % (e)) 82 | sys.exit(1) 83 | 84 | if sd is not None: 85 | print( '\nTimestamp: %s' % (time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))) 86 | 87 | # read all models in the device 88 | sd.read() 89 | 90 | for model in sd.device.models_list: 91 | if model.model_type.label: 92 | label = '%s (%s)' % (model.model_type.label, str(model.id)) 93 | else: 94 | label = '(%s)' % (str(model.id)) 95 | print('\nmodel: %s\n' % (label)) 96 | for block in model.blocks: 97 | if block.index > 0: 98 | index = '%02d:' % (block.index) 99 | else: 100 | index = ' ' 101 | for point in block.points_list: 102 | if point.value is not None: 103 | if point.point_type.label: 104 | label = ' %s%s (%s):' % (index, point.point_type.label, point.point_type.id) 105 | else: 106 | label = ' %s(%s):' % (index, point.point_type.id) 107 | units = point.point_type.units 108 | if units is None: 109 | units = '' 110 | if point.point_type.type == suns.SUNS_TYPE_BITFIELD16: 111 | value = '0x%04x' % (point.value) 112 | elif point.point_type.type == suns.SUNS_TYPE_BITFIELD32: 113 | value = '0x%08x' % (point.value) 114 | else: 115 | value = str(point.value).rstrip('\0') 116 | print('%-40s %20s %-10s' % (label, value, str(units))) 117 | 118 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2018, SunSpec Alliance 5 | All Rights Reserved 6 | 7 | """ 8 | from distutils.core import setup 9 | 10 | setup(name = 'pysunspec', 11 | version = '2.1.1', 12 | description = 'Python SunSpec Tools', 13 | author = 'Bob Fox', 14 | author_email = 'bob@sunspec.org', 15 | classifiers = [ 16 | 'Operating System :: OS Independent', 17 | 'Operating System :: POSIX :: Linux', 18 | 'Operating System :: MacOS :: MacOS X', 19 | 'Operating System :: Microsoft :: Windows', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 2', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3.5', 25 | 'Programming Language :: Python :: 3.6', 26 | 'Programming Language :: Python :: 3.7', 27 | 'Programming Language :: Python :: 3.8', 28 | ], 29 | packages = ['sunspec', 'sunspec.core', 'sunspec.core.modbus', 'sunspec.core.test', 'sunspec.core.test.fake'], 30 | package_data = {'sunspec': ['models/smdx/*'], 'sunspec.core.test': ['devices/*']}, 31 | scripts = ['scripts/suns.py'], 32 | python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', 33 | install_requires = ['pyserial'], 34 | ) 35 | -------------------------------------------------------------------------------- /sunspec/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # pysunspec version 3 | version = '2.0.0' 4 | 5 | 6 | -------------------------------------------------------------------------------- /sunspec/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/sunspec/core/__init__.py -------------------------------------------------------------------------------- /sunspec/core/data.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import time 25 | import sys 26 | 27 | 28 | try: 29 | import xml.etree.ElementTree as ET 30 | except: 31 | import elementtree.ElementTree as ET 32 | 33 | import sunspec.core.util as util 34 | 35 | SDX_VERSION = '1' 36 | SDX_SUNSPEC_DATA = 'sunSpecData' 37 | SDX_SUNSPEC_DATA_VERSION = 'v' 38 | SDX_DEVICE = 'd' 39 | SDX_DEVICE_LOGGER_ID = 'lid' 40 | SDX_DEVICE_NAMESPACE = 'ns' 41 | SDX_DEVICE_ID = 'id' 42 | SDX_DEVICE_IFC = 'if' 43 | SDX_DEVICE_MAN = 'man' 44 | SDX_DEVICE_MOD = 'mod' 45 | SDX_DEVICE_SN = 'sn' 46 | SDX_DEVICE_TIME = 't' 47 | SDX_DEVICE_CORRELATION_ID = 'cid' 48 | SDX_MODEL = 'm' 49 | SDX_MODEL_ID = 'id' 50 | SDX_MODEL_NAMESPACE = 'ns' 51 | SDX_MODEL_INDEX = 'x' 52 | SDX_POINT = 'p' 53 | SDX_POINT_ID = 'id' 54 | SDX_POINT_INDEX = 'index' 55 | SDX_POINT_SF = 'sf' 56 | SDX_POINT_UNITS = 'u' 57 | SDX_POINT_DESC = 'd' 58 | SDX_POINT_TIME = 't' 59 | 60 | class SunSpecDataError(Exception): 61 | pass 62 | 63 | class SunSpecData(object): 64 | 65 | def device_add(self, logger_id=None, man=None, mod=None, sn=None, timestamp=None, cid=None, device_id=None, ifc=None, namespace=None): 66 | 67 | d = DeviceData(logger_id, man, mod, sn, timestamp, cid, device_id, ifc, namespace) 68 | self.device_data.append(d) 69 | return d 70 | 71 | def from_xml(self, element=None, data_record=None): 72 | 73 | if data_record is not None: 74 | self.root = ET.fromstring(data_record) 75 | elif element is not None: 76 | self.root = element 77 | 78 | if self.root is not None: 79 | if self.root.tag != SDX_SUNSPEC_DATA: 80 | raise SunSpecDataError("Unexpected root element: %s" % (self.root.tag)) 81 | 82 | self.version = self.root.attrib.get(SDX_SUNSPEC_DATA_VERSION) 83 | 84 | for d in self.root.findall('*'): 85 | if d.tag != SDX_DEVICE: 86 | raise SunSpecDataError("Unexpected '{}' element in '{}' element".format(d.tag, self.root.tag)) 87 | dd = DeviceData() 88 | dd.from_xml(d) 89 | self.device_data.append(dd) 90 | 91 | def to_xml(self, parent=None): 92 | 93 | attr = {} 94 | 95 | if self.version: 96 | attr[SDX_SUNSPEC_DATA_VERSION] = self.version 97 | 98 | if parent is None: 99 | self.root = ET.Element(SDX_SUNSPEC_DATA, attrib=attr) 100 | else: 101 | self.root = ET.SubElement(parent, SDX_SUNSPEC_DATA, attrib=attr) 102 | 103 | for d in self.device_data: 104 | d.to_xml(self.root) 105 | 106 | def to_xml_str(self, pretty_print=False): 107 | 108 | attr = {} 109 | 110 | if self.version: 111 | attr[SDX_SUNSPEC_DATA_VERSION] = self.version 112 | 113 | self.root = ET.Element(SDX_SUNSPEC_DATA, attrib=attr) 114 | for d in self.device_data: 115 | d.to_xml(self.root) 116 | 117 | if pretty_print: 118 | util.indent(self.root) 119 | 120 | out = ET.tostring(self.root) 121 | if sys.version_info > (3,): 122 | temp = "" 123 | for i in out: 124 | temp += chr(i) 125 | out = temp 126 | 127 | return out 128 | 129 | def __init__(self, element=None, data_record=None): 130 | 131 | self.root = None 132 | self.version = None 133 | self.device_data = [] 134 | 135 | self.from_xml(element, data_record) 136 | 137 | class DeviceData(object): 138 | 139 | def model_add(self, model_id=None, index=None, namespace=None): 140 | 141 | m = ModelData(model_id, namespace, index) 142 | self.model_data.append(m) 143 | return m 144 | 145 | def timestamp_add(self, timestamp=None): 146 | 147 | if timestamp is None: 148 | timestamp = time.time() 149 | self.timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(timestamp)) 150 | 151 | def from_xml(self, element): 152 | 153 | self.logger_id = element.attrib.get(SDX_DEVICE_LOGGER_ID) 154 | self.namespace = element.attrib.get(SDX_DEVICE_NAMESPACE) 155 | self.device_id = element.attrib.get(SDX_DEVICE_ID) 156 | self.ifc = element.attrib.get(SDX_DEVICE_IFC) 157 | self.man = element.attrib.get(SDX_DEVICE_MAN) 158 | self.mod = element.attrib.get(SDX_DEVICE_MOD) 159 | self.sn = element.attrib.get(SDX_DEVICE_SN) 160 | self.time = element.attrib.get(SDX_DEVICE_TIME) 161 | self.cid = element.attrib.get(SDX_DEVICE_CORRELATION_ID) 162 | 163 | for m in element.findall('*'): 164 | if m.tag != SDX_MODEL: 165 | raise SunSpecDataError("Unexpected '{}' element in '{}' element".format(m.tag, element.tag)) 166 | md = ModelData() 167 | md.from_xml(m) 168 | self.model_data.append(md) 169 | 170 | def to_xml(self, parent): 171 | 172 | attr = {} 173 | 174 | if self.logger_id: 175 | attr[SDX_DEVICE_LOGGER_ID] = self.logger_id 176 | if self.namespace: 177 | attr[SDX_DEVICE_NAMESPACE] = self.namespace 178 | if self.device_id: 179 | attr[SDX_DEVICE_ID] = self.device_id 180 | if self.ifc: 181 | attr[SDX_DEVICE_IFC] = self.ifc 182 | if self.man: 183 | attr[SDX_DEVICE_MAN] = self.man 184 | if self.mod: 185 | attr[SDX_DEVICE_MOD] = self.mod 186 | if self.sn: 187 | attr[SDX_DEVICE_SN] = self.sn 188 | if self.timestamp: 189 | attr[SDX_DEVICE_TIME] = self.timestamp 190 | if self.cid: 191 | attr[SDX_DEVICE_CORRELATION_ID] = self.cid 192 | 193 | e = ET.SubElement(parent, SDX_DEVICE, attrib=attr) 194 | for m in self.model_data: 195 | m.to_xml(e) 196 | 197 | def __init__(self, logger_id=None, man=None, mod=None, sn=None, timestamp=None, cid=None, device_id=None, ifc=None, namespace=None): 198 | 199 | self.logger_id = logger_id 200 | self.namespace = namespace 201 | self.device_id = device_id 202 | self.ifc = ifc 203 | self.man = man 204 | self.mod = mod 205 | self.sn = sn 206 | self.timestamp = timestamp 207 | self.cid = cid 208 | self.model_data = [] 209 | 210 | if timestamp is not None: 211 | self.timestamp_add(timestamp) 212 | 213 | class ModelData(object): 214 | 215 | def __init__(self, model_id=None, index=None, namespace=None): 216 | 217 | self.model_id = str(model_id) 218 | self.namespace = namespace 219 | self.index = index 220 | self.point_data = [] 221 | 222 | def point_add(self, point_id=None, value=None, index=None, sf=None, units=None, desc=None, time=None): 223 | 224 | p = PointData(point_id, value, index, sf, units, desc, time) 225 | self.point_data.append(p) 226 | return p 227 | 228 | def from_xml(self, element): 229 | 230 | self.model_id = element.attrib.get(SDX_MODEL_ID) 231 | self.namespace = element.attrib.get(SDX_MODEL_NAMESPACE) 232 | index = element.attrib.get(SDX_MODEL_INDEX) 233 | if index is not None: 234 | index = int(index) 235 | self.index = index 236 | for p in element.findall('*'): 237 | if p.tag != SDX_POINT: 238 | raise SunSpecDataError("Unexpected '{}' element in '{}' element".format(p.tag, element.tag)) 239 | pd = PointData() 240 | pd.from_xml(p) 241 | self.point_data.append(pd) 242 | 243 | def to_xml(self, parent): 244 | 245 | attr = {SDX_MODEL_ID: str(self.model_id)} 246 | 247 | if self.index is None and sys.version_info > (3,): 248 | self.index = -233 249 | if self.index > 1: 250 | attr[SDX_MODEL_INDEX] = self.index 251 | if self.index is -233 and sys.version_info > (3,): 252 | self.index = None 253 | if self.namespace: 254 | attr[SDX_MODEL_NAMESPACE] = self.namespace 255 | 256 | e = ET.SubElement(parent, SDX_MODEL, attrib=attr) 257 | for p in self.point_data: 258 | p.to_xml(e) 259 | 260 | class PointData(object): 261 | 262 | def __init__(self, point_id=None, value=None, index=None, sf=None, units=None, desc=None, time=None): 263 | 264 | self.point_id = point_id 265 | self.value = value 266 | self.index = index 267 | self.sf = sf 268 | self.units = units 269 | self.desc = desc 270 | self.time = time 271 | 272 | # skip units and description for now 273 | def from_xml(self, element): 274 | 275 | self.point_id = element.attrib.get(SDX_POINT_ID) 276 | index = element.attrib.get(SDX_POINT_INDEX) 277 | if index is not None: 278 | index = int(index) 279 | self.index = index 280 | sf = element.attrib.get(SDX_POINT_SF) 281 | if sf is not None: 282 | sf = int(sf) 283 | self.sf = sf 284 | # self.units = element.attrib.get(SDX_POINT_UNITS) 285 | # self.desc = element.attrib.get(SDX_POINT_DESC) 286 | self.time = element.attrib.get(SDX_POINT_TIME) 287 | self.value = element.text 288 | 289 | def to_xml(self, parent): 290 | 291 | attr = {SDX_POINT_ID: str(self.point_id)} 292 | 293 | if self.index: 294 | attr[SDX_POINT_INDEX] = str(self.index) 295 | if self.sf is not None: 296 | attr[SDX_POINT_SF] = str(self.sf) 297 | if self.time: 298 | attr[SDX_POINT_TIME] = self.time 299 | 300 | e = ET.SubElement(parent, SDX_POINT, attrib=attr) 301 | if self.value is not None: 302 | e.text = str(self.value) 303 | 304 | 305 | -------------------------------------------------------------------------------- /sunspec/core/modbus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/sunspec/core/modbus/__init__.py -------------------------------------------------------------------------------- /sunspec/core/pics.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import sunspec.core.suns as suns 25 | 26 | PICS_ROOT = 'sunSpecPics' 27 | PICS_ATTR_VERSION = 'v' 28 | PICS_VERSION = '1' 29 | PICS_DATE = 'date' 30 | PICS_MANUFACTURER = 'manufacturer' 31 | PICS_SUBMITTER = 'submitter' 32 | PICS_EMAIL = 'email' 33 | PICS_PHONE = 'phone' 34 | PICS_SLAVE_ID = 'slaveid' 35 | PICS_IP_ADDR = 'ipaddr' 36 | PICS_IP_PORT ='ipport' 37 | PICS_DEVICE = 'device' 38 | PICS_ATTR_BASE_ADDR = 'addr' 39 | PICS_BASE_ADDR_DEFAULT = suns.SUNS_BASE_ADDR_DEFAULT 40 | PICS_MODEL = 'model' 41 | PICS_BLOCK = 'block' 42 | PICS_POINT = 'point' 43 | PICS_ATTR_ID = 'id' 44 | PICS_ATTR_LEN = 'len' 45 | PICS_ATTR_INDEX = 'index' 46 | PICS_ATTR_TYPE = 'type' 47 | PICS_TYPE_FIXED = 'fixed' 48 | PICS_TYPE_REPEATING = 'repeating' 49 | PICS_ATTR_ACCESS = 'access' 50 | PICS_ACCESS_R = 'r' 51 | PICS_ACCESS_RW = 'rw' 52 | PICS_ATTR_IMPLEMENTED = 'impl' 53 | PICS_IMPLEMENTED_FALSE = 'false' 54 | PICS_IMPLEMENTED_TRUE = 'true' 55 | 56 | pics_access_types = { 57 | PICS_ACCESS_R: suns.SUNS_ACCESS_R, 58 | PICS_ACCESS_RW: suns.SUNS_ACCESS_RW 59 | } 60 | 61 | # map PICS block types to SunSpec block types 62 | pics_block_types = { 63 | PICS_TYPE_FIXED: suns.SUNS_BLOCK_FIXED, 64 | PICS_TYPE_REPEATING: suns.SUNS_BLOCK_REPEATING 65 | } 66 | -------------------------------------------------------------------------------- /sunspec/core/smdx.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import sunspec.core.suns as suns 25 | 26 | SMDX_ROOT = 'sunSpecModels' 27 | SMDX_MODEL = 'model' 28 | SMDX_BLOCK = 'block' 29 | SMDX_POINT = 'point' 30 | SMDX_ATTR_ID = 'id' 31 | SMDX_ATTR_LEN = 'len' 32 | SMDX_ATTR_NAME = 'name' 33 | SMDX_ATTR_TYPE = 'type' 34 | SMDX_ATTR_TYPE_FIXED = 'fixed' 35 | SMDX_ATTR_TYPE_REPEATING = 'repeating' 36 | SMDX_ATTR_OFFSET = 'offset' 37 | SMDX_ATTR_MANDATORY = 'mandatory' 38 | SMDX_ATTR_ACCESS = 'access' 39 | SMDX_ATTR_SF = 'sf' 40 | SMDX_ATTR_UNITS = 'units' 41 | 42 | SMDX_SYMBOL = 'symbol' 43 | 44 | SMDX_STRINGS = 'strings' 45 | SMDX_LABEL = 'label' 46 | SMDX_DESCRIPTION = 'description' 47 | SMDX_NOTES = 'notes' 48 | 49 | SMDX_TYPE_INT16 = 'int16' 50 | SMDX_TYPE_UINT16 = 'uint16' 51 | SMDX_TYPE_COUNT = 'count' 52 | SMDX_TYPE_ACC16 = 'acc16' 53 | SMDX_TYPE_ENUM16 = 'enum16' 54 | SMDX_TYPE_BITFIELD16 = 'bitfield16' 55 | SMDX_TYPE_PAD = 'pad' 56 | SMDX_TYPE_INT32 = 'int32' 57 | SMDX_TYPE_UINT32 = 'uint32' 58 | SMDX_TYPE_ACC32 = 'acc32' 59 | SMDX_TYPE_ENUM32 = 'enum32' 60 | SMDX_TYPE_BITFIELD32 = 'bitfield32' 61 | SMDX_TYPE_IPADDR = 'ipaddr' 62 | SMDX_TYPE_INT64 = 'int64' 63 | SMDX_TYPE_UINT64 = 'uint64' 64 | SMDX_TYPE_ACC64 = 'acc64' 65 | SMDX_TYPE_IPV6ADDR = 'ipv6addr' 66 | SMDX_TYPE_FLOAT32 = 'float32' 67 | SMDX_TYPE_STRING = 'string' 68 | SMDX_TYPE_SUNSSF = 'sunssf' 69 | SMDX_TYPE_EUI48 = 'eui48' 70 | 71 | SMDX_ACCESS_R = 'r' 72 | SMDX_ACCESS_RW = 'rw' 73 | 74 | SMDX_MANDATORY_FALSE = 'false' 75 | SMDX_MANDATORY_TRUE = 'true' 76 | 77 | smdx_access_types = { 78 | SMDX_ACCESS_R: suns.SUNS_ACCESS_R, 79 | SMDX_ACCESS_RW: suns.SUNS_ACCESS_RW 80 | } 81 | 82 | smdx_mandatory_types = { 83 | SMDX_MANDATORY_FALSE: suns.SUNS_MANDATORY_FALSE, 84 | SMDX_MANDATORY_TRUE: suns.SUNS_MANDATORY_TRUE 85 | } 86 | 87 | # map SMDX block types to SunSpec block types 88 | smdx_block_types = { 89 | SMDX_ATTR_TYPE_FIXED: suns.SUNS_BLOCK_FIXED, 90 | SMDX_ATTR_TYPE_REPEATING: suns.SUNS_BLOCK_REPEATING 91 | } 92 | 93 | # map SMDX point types to SunSpec point types 94 | smdx_point_types = { 95 | SMDX_TYPE_INT16: suns.SUNS_TYPE_INT16, 96 | SMDX_TYPE_UINT16: suns.SUNS_TYPE_UINT16, 97 | SMDX_TYPE_COUNT: suns.SUNS_TYPE_COUNT, 98 | SMDX_TYPE_ACC16: suns.SUNS_TYPE_ACC16, 99 | SMDX_TYPE_ENUM16: suns.SUNS_TYPE_ENUM16, 100 | SMDX_TYPE_BITFIELD16: suns.SUNS_TYPE_BITFIELD16, 101 | SMDX_TYPE_PAD: suns.SUNS_TYPE_PAD, 102 | SMDX_TYPE_INT32: suns.SUNS_TYPE_INT32, 103 | SMDX_TYPE_UINT32: suns.SUNS_TYPE_UINT32, 104 | SMDX_TYPE_ACC32: suns.SUNS_TYPE_ACC32, 105 | SMDX_TYPE_ENUM32: suns.SUNS_TYPE_ENUM32, 106 | SMDX_TYPE_BITFIELD32: suns.SUNS_TYPE_BITFIELD32, 107 | SMDX_TYPE_IPADDR: suns.SUNS_TYPE_IPADDR, 108 | SMDX_TYPE_INT64: suns.SUNS_TYPE_INT64, 109 | SMDX_TYPE_UINT64: suns.SUNS_TYPE_UINT64, 110 | SMDX_TYPE_ACC64: suns.SUNS_TYPE_ACC64, 111 | SMDX_TYPE_IPV6ADDR: suns.SUNS_TYPE_IPV6ADDR, 112 | SMDX_TYPE_FLOAT32: suns.SUNS_TYPE_FLOAT32, 113 | SMDX_TYPE_STRING: suns.SUNS_TYPE_STRING, 114 | SMDX_TYPE_SUNSSF: suns.SUNS_TYPE_SUNSSF, 115 | SMDX_TYPE_EUI48: suns.SUNS_TYPE_EUI48 116 | } 117 | 118 | def model_id_to_filename(model_id): 119 | 120 | return 'smdx_%05d.xml' % (int(model_id)) 121 | 122 | def model_filename_to_id(filename): 123 | 124 | model_id = None 125 | 126 | if filename[0:5] == 'smdx_' and filename[-4:] == '.xml': 127 | try: 128 | model_id = int(filename[5:-4]) 129 | except Exception as e: 130 | pass 131 | 132 | return model_id 133 | -------------------------------------------------------------------------------- /sunspec/core/suns.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import sunspec.core.util as util 25 | import sys 26 | 27 | # Python 3 compatibility for long() 28 | if sys.version_info > (3,): 29 | long = int 30 | 31 | SUNS_BASE_ADDR_DEFAULT = 40000 32 | SUNS_SUNS_LEN = 2 33 | 34 | SUNS_TYPE_INT16 = 'int16' 35 | SUNS_TYPE_UINT16 = 'uint16' 36 | SUNS_TYPE_COUNT = 'count' 37 | SUNS_TYPE_ACC16 = 'acc16' 38 | SUNS_TYPE_ENUM16 = 'enum16' 39 | SUNS_TYPE_BITFIELD16 = 'bitfield16' 40 | SUNS_TYPE_PAD = 'pad' 41 | SUNS_TYPE_INT32 = 'int32' 42 | SUNS_TYPE_UINT32 = 'uint32' 43 | SUNS_TYPE_ACC32 = 'acc32' 44 | SUNS_TYPE_ENUM32 = 'enum32' 45 | SUNS_TYPE_BITFIELD32 = 'bitfield32' 46 | SUNS_TYPE_IPADDR = 'ipaddr' 47 | SUNS_TYPE_INT64 = 'int64' 48 | SUNS_TYPE_UINT64 = 'uint64' 49 | SUNS_TYPE_ACC64 = 'acc64' 50 | SUNS_TYPE_IPV6ADDR = 'ipv6addr' 51 | SUNS_TYPE_FLOAT32 = 'float32' 52 | SUNS_TYPE_STRING = 'string' 53 | SUNS_TYPE_SUNSSF = 'sunssf' 54 | SUNS_TYPE_EUI48 = 'eui48' 55 | 56 | SUNS_ACCESS_R = 'r' 57 | SUNS_ACCESS_RW = 'rw' 58 | 59 | SUNS_MANDATORY_FALSE = 'false' 60 | SUNS_MANDATORY_TRUE = 'true' 61 | 62 | SUNS_UNIMPL_INT16 = -32768 63 | SUNS_UNIMPL_UINT16 = 0xffff 64 | SUNS_UNIMPL_ACC16 = 0 65 | SUNS_UNIMPL_ENUM16 = 0xffff 66 | SUNS_UNIMPL_BITFIELD16 = 0xffff 67 | SUNS_UNIMPL_INT32 = -2147483648 68 | SUNS_UNIMPL_UINT32 = 0xffffffff 69 | SUNS_UNIMPL_ACC32 = 0 70 | SUNS_UNIMPL_ENUM32 = 0xffffffff 71 | SUNS_UNIMPL_BITFIELD32 = 0xffffffff 72 | SUNS_UNIMPL_IPADDR = 0 73 | SUNS_UNIMPL_INT64 = -9223372036854775808 74 | SUNS_UNIMPL_UINT64 = 0xffffffffffffffff 75 | SUNS_UNIMPL_ACC64 = 0 76 | SUNS_UNIMPL_IPV6ADDR = 0 77 | SUNS_UNIMPL_FLOAT32 = 0x7fc00000 78 | SUNS_UNIMPL_STRING = 0 79 | SUNS_UNIMPL_SUNSSF = -32768 80 | SUNS_UNIMPL_EUI48 = 'FF:FF:FF:FF:FF:FF' 81 | 82 | SUNS_BLOCK_FIXED = 'fixed' 83 | SUNS_BLOCK_REPEATING = 'repeating' 84 | 85 | SUNS_END_MODEL_ID = 0xffff 86 | 87 | def suns_to_int(x): 88 | try: 89 | return int(x, 0) 90 | except TypeError: 91 | return int(x) 92 | 93 | def suns_to_long(x): 94 | try: 95 | return long(x, 0) 96 | except TypeError: 97 | return long(x) 98 | 99 | def suns_to_str(s): 100 | return str(s) 101 | 102 | def suns_to_float(f): 103 | try: 104 | return float(f) 105 | except ValueError: 106 | return None 107 | 108 | def suns_is_impl_int16(value): 109 | return not value == SUNS_UNIMPL_INT16 110 | 111 | def suns_is_impl_uint16(value): 112 | return not value == SUNS_UNIMPL_UINT16 113 | 114 | def suns_is_impl_acc16(value): 115 | return not value == SUNS_UNIMPL_ACC16 116 | 117 | def suns_is_impl_enum16(value): 118 | return not value == SUNS_UNIMPL_ENUM16 119 | 120 | def suns_is_impl_bitfield16(value): 121 | return not value == SUNS_UNIMPL_BITFIELD16 122 | 123 | def suns_is_impl_int32(value): 124 | return not value == SUNS_UNIMPL_INT32 125 | 126 | def suns_is_impl_uint32(value): 127 | return not value == SUNS_UNIMPL_UINT32 128 | 129 | def suns_is_impl_acc32(value): 130 | return not value == SUNS_UNIMPL_ACC32 131 | 132 | def suns_is_impl_enum32(value): 133 | return not value == SUNS_UNIMPL_ENUM32 134 | 135 | def suns_is_impl_bitfield32(value): 136 | return not value == SUNS_UNIMPL_BITFIELD32 137 | 138 | def suns_is_impl_ipaddr(value): 139 | return not value == SUNS_UNIMPL_IPADDR 140 | 141 | def suns_is_impl_int64(value): 142 | return not value == SUNS_UNIMPL_INT64 143 | 144 | def suns_is_impl_uint64(value): 145 | return not value == SUNS_UNIMPL_UINT64 146 | 147 | def suns_is_impl_acc64(value): 148 | return not value == SUNS_UNIMPL_ACC64 149 | 150 | def suns_is_impl_ipv6addr(value): 151 | if value: 152 | return not value[0] == '\0' 153 | return False 154 | 155 | def suns_is_impl_float32(value): 156 | return (value == value) and (value != None) 157 | 158 | def suns_is_impl_string(value): 159 | if value: 160 | return not value[0] == '\0' 161 | return False 162 | 163 | def suns_is_impl_sunssf(value): 164 | return not value == SUNS_UNIMPL_SUNSSF 165 | 166 | def suns_is_impl_eui48(value): 167 | return not value == SUNS_UNIMPL_EUI48 168 | 169 | # each entry contains: (len in registers, uniplemented value, data to value function, value to data function, to value function, default value) 170 | suns_point_type_info = { 171 | SUNS_TYPE_INT16: (1, suns_is_impl_int16, util.data_to_s16, util.s16_to_data, suns_to_int, 0), 172 | SUNS_TYPE_UINT16: (1, suns_is_impl_uint16, util.data_to_u16, util.u16_to_data, suns_to_int, 0), 173 | SUNS_TYPE_COUNT: (1, suns_is_impl_uint16, util.data_to_u16, util.u16_to_data, suns_to_int, 0), 174 | SUNS_TYPE_ACC16: (1, suns_is_impl_acc16, util.data_to_u16, util.u16_to_data, suns_to_int, 0), 175 | SUNS_TYPE_ENUM16: (1, suns_is_impl_enum16, util.data_to_u16, util.u16_to_data, suns_to_int, 0), 176 | SUNS_TYPE_BITFIELD16: (1, suns_is_impl_bitfield16, util.data_to_u16, util.u16_to_data, suns_to_int, 0), 177 | SUNS_TYPE_PAD: (1, suns_is_impl_int16, util.data_to_s16, util.s16_to_data, suns_to_int, 0), 178 | SUNS_TYPE_INT32: (2, suns_is_impl_int32, util.data_to_s32, util.s32_to_data, suns_to_int, 0), 179 | SUNS_TYPE_UINT32: (2, suns_is_impl_uint32, util.data_to_u32, util.u32_to_data, suns_to_long, 0), 180 | SUNS_TYPE_ACC32: (2, suns_is_impl_acc32, util.data_to_u32, util.u32_to_data, suns_to_long, 0), 181 | SUNS_TYPE_ENUM32: (2, suns_is_impl_enum32, util.data_to_u32, util.u32_to_data, suns_to_long, 0), 182 | SUNS_TYPE_BITFIELD32: (2, suns_is_impl_bitfield32, util.data_to_u32, util.u32_to_data, suns_to_long, 0), 183 | SUNS_TYPE_IPADDR: (2, suns_is_impl_ipaddr, util.data_to_u32, util.u32_to_data, suns_to_long, 0), 184 | SUNS_TYPE_INT64: (4, suns_is_impl_int64, util.data_to_s64, util.s64_to_data, suns_to_long, 0), 185 | SUNS_TYPE_UINT64: (4, suns_is_impl_uint64, util.data_to_s64, util.s64_to_data, suns_to_long, 0), 186 | SUNS_TYPE_ACC64: (4, suns_is_impl_acc64, util.data_to_s64, util.s64_to_data, suns_to_long, 0), 187 | SUNS_TYPE_IPV6ADDR: (8, suns_is_impl_ipv6addr, util.data_to_ipv6addr, util.ipv6addr_to_data, suns_to_str, 0), 188 | SUNS_TYPE_FLOAT32: (2, suns_is_impl_float32, util.data_to_float, util.float_to_data32, suns_to_float, 0), 189 | SUNS_TYPE_STRING: (None, suns_is_impl_string, util.data_to_str, util.str_to_data, suns_to_str, ''), 190 | SUNS_TYPE_SUNSSF: (1, suns_is_impl_sunssf, util.data_to_s16, util.s16_to_data, suns_to_int, 0), 191 | SUNS_TYPE_EUI48: (4, suns_is_impl_eui48, util.data_to_eui48, util.eui48_to_data, suns_to_str, 0) 192 | } 193 | -------------------------------------------------------------------------------- /sunspec/core/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/sunspec/core/test/__init__.py -------------------------------------------------------------------------------- /sunspec/core/test/devices/mbmap_test_device_1_c.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SunS 4 | 1 5 | 66 6 | SunSpecTest 7 | TestDevice-1 8 | opt_a_b_c 9 | 1.2.3 10 | sn-123456789 11 | 1 12 | 0 13 | 14 | 15 | 63001 16 | 188 17 | fff6 000a 0000 0001 0001 ffff 0002 fffe 0003 8000 0004 0005 fff4 0006 0007 ffff 18 | 0008 0000 0009 ffff 000a ffff 0000 000b 0000 000c 0000 000d 0000 000e 0000 000f 19 | 80000000 20 | 00000010 21 | 00000011 22 | 00000012 23 | 00000013 24 | 00000014 25 | ffffffff 26 | 00000015 27 | 00000000 28 | 00000016 29 | ffffffff 30 | 00000017 31 | ffffffff 32 | 01020304 33 | 00000000 34 | 0000000000000018 35 | 8000000000000000 36 | 0000000000000019 37 | 0000000000000000 38 | 0000000000000000 39 | 0000000000000000 40 | 0000000000000000 41 | 0000000000000000 42 | 41d00000 43 | 7fc00000 44 | 3132333435363738000000000000000000000000000000000000000000000000 45 | 46 | 47 | 48 | 49 | 0000000000000000000000000000000000000000000000000000000000000000 50 | 51 | 52 | 53 | 2 54 | 3 55 | 4 56 | -32768 57 | 58 | 59 | -2 60 | 30 61 | 31 62 | -32768 63 | 32 64 | 33 65 | 34 66 | 0xffff 67 | 35 68 | -2147483648 69 | 36 70 | 0xffffffff 71 | -3 72 | 0x8000 73 | 74 | 75 | -4 76 | 40 77 | 41 78 | -32768 79 | 42 80 | 43 81 | 44 82 | 0xffff 83 | 45 84 | -2147483648 85 | 46 86 | 0xffffffff 87 | -5 88 | 0x8000 89 | 90 | 91 | 2 92 | 50 93 | 51 94 | -32768 95 | 52 96 | 53 97 | 54 98 | 0xffff 99 | 55 100 | -2147483648 101 | 56 102 | 0xffffffff 103 | 3 104 | 0xffff 105 | 106 | 0xffff 107 | 0 108 | 109 | 110 | -------------------------------------------------------------------------------- /sunspec/core/test/devices/mbmap_test_device_1_processed.xml: -------------------------------------------------------------------------------- 1 | 53756e530001004253756e5370656354657374000000000000000000000000000000000000000000546573744465766963652d3100000000000000000000000000000000000000006f70745f615f625f6300000000000000312e322e330000000000000000000000736e2d313233343536373839000000000000000000000000000000000000000000010000f61900bcfff6000a000000010001ffff0002fffe0003800000040005fff400060007ffff000800000009ffff000affff0000000b0000000c0000000d0000000e0000000f800000000000001000000011000000120000001300000014ffffffff000000150000000000000016ffffffff00000017ffffffff01020304000000000000000000000018800000000000000000000000000000190000000000000000000000000000000000000000000000000000000000000000000000000000000041d000007fc00000313233343536373800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000300048000fffe001e001f8000002000210022ffff000000238000000000000024fffffffffffd8000fffc002800298000002a002b002cffff0000002d800000000000002efffffffffffb80000002003200338000003400350036ffff000000378000000000000038ffffffff0003ffffffff0000 -------------------------------------------------------------------------------- /sunspec/core/test/devices/mbmap_test_device_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SunS 4 | 1 5 | 65 6 | SunSpecTest 7 | TestDevice-2 8 | opt_a_b_c 9 | 1.2.3 10 | sn-123456789 11 | 1 12 | 13 | 0xffff 14 | 0 15 | 16 | 17 | -------------------------------------------------------------------------------- /sunspec/core/test/devices/mbmap_test_device_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SunS 4 | 1 5 | 66 6 | SunSpecTest 7 | TestDevice-3 8 | opt_a_b_c 9 | 1.2.3 10 | sn-123456789 11 | 1 12 | 0 13 | 14 | 63002 15 | 4 16 | 17 | -1 18 | 1111 19 | 2222 20 | -2 21 | 22 | 0xffff 23 | 0 24 | 25 | 26 | -------------------------------------------------------------------------------- /sunspec/core/test/devices/mbmap_test_inverter_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SunS 4 | 1 5 | 66 6 | SunSpecTest 7 | TestInverter-1 8 | opt_a_b_c 9 | 1.2.3 10 | sn-123456789 11 | 1 12 | 0 13 | 14 | 15 | 103 16 | 50 17 | 124 18 | 41 19 | 42 20 | 43 21 | -1 22 | 0xffff 23 | 0xffff 24 | 0xffff 25 | 2401 26 | 2402 27 | 2403 28 | -1 29 | 2970 30 | 0 31 | 5999 32 | -2 33 | 2978 34 | 0 35 | 1 36 | -1 37 | 995 38 | -3 39 | 1234567 40 | 0 41 | 100 42 | -1 43 | 3001 44 | -1 45 | 3001 46 | 0 47 | 401 48 | 402 49 | 403 50 | 404 51 | -1 52 | 1 53 | 2 54 | 3 55 | 4 56 | 5 57 | 6 58 | 7 59 | 8 60 | 61 | 0xffff 62 | 0 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /sunspec/core/test/devices/mbmap_test_inverter_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SunS 4 | 1 5 | 66 6 | SunSpecTest 7 | TestInverter-2 8 | opt_a_b_c 9 | 1.2.3 10 | sn-123456789 11 | 1 12 | 0 13 | 14 | 15 | 103 16 | 50 17 | 124 18 | 41 19 | 42 20 | 43 21 | -1 22 | 0xffff 23 | 0xffff 24 | 0xffff 25 | 2401 26 | 2402 27 | 2403 28 | -1 29 | 2970 30 | 0 31 | 5999 32 | -2 33 | 2978 34 | 0 35 | 1 36 | -1 37 | 995 38 | -3 39 | 1234567 40 | 0 41 | 100 42 | -1 43 | 3001 44 | -1 45 | 3001 46 | 0 47 | 401 48 | 402 49 | 403 50 | 404 51 | -1 52 | 1 53 | 2 54 | 3 55 | 4 56 | 5 57 | 6 58 | 7 59 | 8 60 | 61 | 62 | 160 63 | 68 64 | -1 65 | -2 66 | 0 67 | 0 68 | 1 69 | 3 70 | 300 71 | 72 | 1 73 | 1:1 74 | 11 75 | 30011 76 | 331 77 | 2111 78 | 0 79 | 55 80 | 1 81 | 2 82 | 83 | 2 84 | 1:2 85 | 12 86 | 30012 87 | 332 88 | 2112 89 | 0 90 | 55 91 | 1 92 | 2 93 | 94 | 3 95 | 1:3 96 | 13 97 | 30013 98 | 333 99 | 2113 100 | 0 101 | 55 102 | 1 103 | 2 104 | 105 | 0xffff 106 | 0 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /sunspec/core/test/devices/pics_test_device_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SunSpecTest 6 | TestDevice-1 7 | opt_a_b_c 8 | 1.2.3 9 | sn-123456789 10 | 1 11 | 12 | 13 | 14 | 15 | -10 16 | 10 17 | 0 18 | 1 19 | 1 20 | -1 21 | 2 22 | -2 23 | 3 24 | 25 | 4 26 | 5 27 | 65524 28 | 6 29 | 7 30 | 0xffff 31 | 8 32 | 0 33 | 9 34 | 0xffff 35 | 10 36 | 0xffff 37 | 11 38 | 12 39 | 13 40 | 14 41 | 15 42 | -2147483648 43 | 16 44 | 17 45 | 18 46 | 19 47 | 20 48 | 0xffffffff 49 | 21 50 | 0 51 | 22 52 | 0xffffffff 53 | 23 54 | 0xffffffff 55 | 0x01020304 56 | 0 57 | 24 58 | -9223372036854775808 59 | 25 60 | 0 61 | 62 | 63 | 26 64 | 0x7fc00000 65 | 12345678 66 | 67 | 2 68 | 3 69 | 4 70 | -32768 71 | 72 | 73 | -2 74 | 30 75 | 31 76 | -32768 77 | 32 78 | 33 79 | 34 80 | 0xffff 81 | 35 82 | -2147483648 83 | 36 84 | 0xffffffff 85 | -3 86 | 0x8000 87 | 88 | 89 | -4 90 | 40 91 | 41 92 | -32768 93 | 42 94 | 43 95 | 44 96 | 0xffff 97 | 45 98 | -2147483648 99 | 46 100 | 0xffffffff 101 | -5 102 | 0x8000 103 | 104 | 105 | 2 106 | 50 107 | 51 108 | -32768 109 | 52 110 | 53 111 | 54 112 | 0xffff 113 | 55 114 | -2147483648 115 | 56 116 | 0xffffffff 117 | 3 118 | 0x8000 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /sunspec/core/test/devices/pics_test_device_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SunSpecTest 6 | TestDevice-2 7 | opt_a_b_c 8 | 1.2.3 9 | sn-123456789 10 | 1 11 | 12 | 13 | 14 | 15 | -10 16 | 10 17 | 0 18 | 1 19 | 1 20 | -1 21 | 2 22 | -2 23 | 3 24 | 25 | 4 26 | 5 27 | 65524 28 | 6 29 | 7 30 | 0xffff 31 | 8 32 | 0 33 | 9 34 | 0xffff 35 | 10 36 | 0xffff 37 | 11 38 | 12 39 | 13 40 | 14 41 | 15 42 | -2147483648 43 | 16 44 | 17 45 | 18 46 | 19 47 | 20 48 | 0xffffffff 49 | 21 50 | 0 51 | 22 52 | 0xffffffff 53 | 23 54 | 0xffffffff 55 | 0x01020304 56 | 0 57 | 24 58 | -9223372036854775808 59 | 25 60 | 0 61 | 62 | 63 | 26 64 | 0x7fc00000 65 | 12345678 66 | 67 | 2 68 | 3 69 | 4 70 | -32768 71 | 72 | 73 | -2 74 | 30 75 | 31 76 | -32768 77 | 32 78 | 33 79 | 34 80 | 0xffff 81 | 35 82 | -2147483648 83 | 36 84 | 0xffffffff 85 | -3 86 | 0x8000 87 | 88 | 89 | -4 90 | 40 91 | 41 92 | -32768 93 | 42 94 | 43 95 | 44 96 | 0xffff 97 | 45 98 | -2147483648 99 | 46 100 | 0xffffffff 101 | -5 102 | 0x8000 103 | 104 | 105 | 2 106 | 50 107 | 51 108 | -32768 109 | 52 110 | 53 111 | 54 112 | 0xffff 113 | 55 114 | -2147483648 115 | 56 116 | 0xffffffff 117 | 3 118 | 0x8000 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /sunspec/core/test/devices/pics_test_inverter_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SunSpec 6 | TestInverter-1 7 | Option-a-b-c 8 | 1.2.3 9 | 987654321 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 0 26 | -1 27 | -2 28 | -2 29 | 0 30 | 0 31 | -1 32 | 0 33 | 0 34 | 35 | 0 36 | -2 37 | 38 | 39 | 40 | 41 | 42 | 4 43 | 4 44 | 0 45 | 0 46 | 0 47 | 48 | 49 | 4 50 | 2 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /sunspec/core/test/devices/pics_test_inverter_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SunSpecTest 6 | TestInverter-2 7 | Option-a-b-c 8 | 1.2.3 9 | 987654321 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 0 26 | -1 27 | -2 28 | -2 29 | 0 30 | 0 31 | -1 32 | 0 33 | 0 34 | 35 | 0 36 | -2 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /sunspec/core/test/fake/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunspec/pysunspec/119b17600c8f20243dfdb694aacdaffefec2521b/sunspec/core/test/fake/__init__.py -------------------------------------------------------------------------------- /sunspec/core/test/fake/serial.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | EIGHTBITS = 8 25 | PARITY_NONE = 'N' 26 | STOPBITS_ONE = 1 27 | 28 | class Serial(object): 29 | 30 | def __init__(self, port=None, baudrate=9600, bytesize=EIGHTBITS, parity=PARITY_NONE, stopbits=STOPBITS_ONE, 31 | timeout=None, xonxoff=False, rtscts=False, writeTimeout=None, dsrdtr=False, interCharTimeout=None): 32 | 33 | self.test_serial = True 34 | self.port = port 35 | self.baudrate = baudrate 36 | self.bytesize = bytesize 37 | self.parity = parity 38 | self.stopbits = stopbits 39 | self.timeout = timeout 40 | self.xonoff = xonxoff 41 | self.rtscts = rtscts 42 | self.writeTimeout = writeTimeout 43 | self.dsrdtr = dsrdtr 44 | self.interCharTimeout = interCharTimeout 45 | self.is_open = False 46 | self.in_buf = b'' 47 | self.out_buf = b'' 48 | 49 | self.open() 50 | 51 | 52 | def open(self): 53 | self.is_open = True 54 | 55 | def close(self): 56 | self.is_open = False 57 | 58 | def read(self, size=1): 59 | data = '' 60 | read_len = size 61 | 62 | data_len = len(self.in_buf) 63 | if data_len < read_len: 64 | read_len = data_len 65 | 66 | if read_len > 0: 67 | data = self.in_buf[:read_len] 68 | self.in_buf = self.in_buf[read_len:] 69 | return data 70 | 71 | def write(self, data): 72 | self.out_buf += data 73 | 74 | def flushInput(self): 75 | pass 76 | 77 | def flushOutput(self): 78 | pass 79 | -------------------------------------------------------------------------------- /sunspec/core/test/fake/socket.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | class socket(object): 25 | 26 | def __init__(self, family=None, stype=None, proto=None): 27 | 28 | self.test_socket = True 29 | self.family = family 30 | self.stype = stype 31 | self.proto = proto 32 | self.connected = False 33 | self.timeout = 0 34 | self.in_buf = b'' 35 | self.out_buf = b'' 36 | 37 | def connect(self, addr_port): 38 | self.connected = True 39 | 40 | def settimeout(self, timeout): 41 | self.timeout = timeout 42 | 43 | def close(self): 44 | self.connected = False 45 | 46 | def recv(self, size): 47 | data = '' 48 | read_len = size 49 | 50 | data_len = len(self.in_buf) 51 | if data_len < read_len: 52 | read_len = data_len 53 | 54 | if read_len > 0: 55 | data = self.in_buf[:read_len] 56 | self.in_buf = self.in_buf[read_len:] 57 | return data 58 | 59 | def send(self, data): 60 | self.out_buf += data 61 | 62 | def sendall(self, data): 63 | self.out_buf += data 64 | -------------------------------------------------------------------------------- /sunspec/core/test/models/smdx_65000.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /sunspec/core/test/test_all.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import sys 25 | import os 26 | import sunspec 27 | 28 | test_modules = [ 29 | 'test_device', 30 | 'test_client', 31 | 'test_modbus_client', 32 | 'test_modbus_mbmap', 33 | 'test_data' 34 | ] 35 | 36 | def test_all(pathlist=None, stop_on_failure=False, local=False): 37 | 38 | total_count_run = 0 39 | total_count_passed = 0 40 | total_count_failed = 0 41 | 42 | current_path = os.path.dirname(os.path.realpath(__file__)) 43 | sys.path.append(current_path) 44 | 45 | # add current directory path if local is true 46 | if local: 47 | local_path = os.path.join(current_path, '..', '..', '..') 48 | if sys.path[1] != local_path: 49 | print('Adding local path: ', local_path) 50 | sys.path.insert(1, local_path) 51 | else: 52 | print('Using local path: ', local_path) 53 | 54 | print('pySunSpec version: {}\nTest device path: {}\n'.format(sunspec.version, os.path.join(current_path, 'devices'))) 55 | 56 | for m in test_modules: 57 | module = __import__(m) 58 | (count_run, count_passed, count_failed) = module.test_all(pathlist, stop_on_failure) 59 | total_count_run += count_run 60 | total_count_passed += count_passed 61 | total_count_failed += count_failed 62 | 63 | print('\nTotal tests run: %d Total tests passed: %d Total tests failed: %d' % (total_count_run, total_count_passed, total_count_failed)) 64 | return (total_count_run, total_count_passed, total_count_failed) 65 | 66 | if __name__ == "__main__": 67 | 68 | local = False 69 | 70 | if len(sys.argv) > 1 and sys.argv[1] == 'local': 71 | local = True 72 | 73 | (count_run, count_passed, count_failed) = test_all(local=local) 74 | sys.exit(count_failed) 75 | -------------------------------------------------------------------------------- /sunspec/core/test/test_client.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import sys 25 | import os 26 | import unittest 27 | 28 | import sunspec.core.client as client 29 | import sunspec.core.device as device 30 | import sunspec.core.util as util 31 | 32 | 33 | class TestClientDevice(unittest.TestCase): 34 | def setUp(self): 35 | path = os.path.abspath(__file__) 36 | self.pathlist = util.PathList(['.', 37 | os.path.join(os.path.dirname(path), 38 | 'devices')]) 39 | 40 | device.check_for_models(pathlist=self.pathlist) 41 | 42 | def test_client_device(self): 43 | d = client.ClientDevice(client.MAPPED, slave_id=1, 44 | name='mbmap_test_device_1.xml', 45 | pathlist=self.pathlist) 46 | d.scan() 47 | d.read_points() 48 | 49 | dp = device.Device() 50 | dp.from_pics(filename='pics_test_device_1.xml', 51 | pathlist=self.pathlist) 52 | not_equal = dp.not_equal(d) 53 | if not_equal: 54 | raise Exception(not_equal) 55 | 56 | 57 | def test_sunspec_client_device_1(self): 58 | d = client.SunSpecClientDevice(client.MAPPED, slave_id=1, 59 | name='mbmap_test_device_1.xml', 60 | pathlist=self.pathlist) 61 | d.read() 62 | 63 | dp = device.Device() 64 | dp.from_pics(filename='pics_test_device_1.xml', 65 | pathlist=self.pathlist) 66 | not_equal = dp.not_equal(d.device) 67 | if not_equal: 68 | raise Exception(not_equal) 69 | 70 | expected = 'SunSpecTest' 71 | if d.common.Mn != expected: 72 | raise Exception("'common.Mn' point mismatch: {} {}".format(d.common.Mn, expected)) 73 | 74 | expected = 'sn-123456789' 75 | if d.common.SN != expected: 76 | raise Exception("'common.SN' point mismatch: {} {}".format(d.common.SN, expected)) 77 | 78 | 79 | # int16 read and write 80 | d.model_63001.read() 81 | expected = -20 82 | if d.model_63001.int16_4 != expected: 83 | raise Exception("'model_63001.int16_4' point mismatch: {} {}".format(d.model_63001.int16_4, expected)) 84 | value = 330 85 | d.model_63001.int16_4 = value 86 | d.model_63001.write() 87 | d.model_63001.read() 88 | value = d.model_63001.int16_4 89 | if d.model_63001.int16_4 != value: 90 | raise Exception("'model_63001.int16_4' write failure: {} {}".format(d.model_63001.int16_4, value)) 91 | 92 | # string read and write 93 | expected = '12345678' 94 | if d.model_63001.string != expected: 95 | raise Exception("'model_63001.string' point mismatch: {} {}".format(d.model_63001.string, expected)) 96 | 97 | value = 'abcdefg' 98 | d.model_63001.string = value 99 | d.model_63001.write() 100 | d.model_63001.read() 101 | if d.model_63001.string != value: 102 | raise Exception("'model_63001.string' write failure: {} {}".format(d.model_63001.string, value)) 103 | 104 | # write multiple 105 | d.model_63001.read() 106 | expected = 65524 107 | if d.model_63001.uint16_3 != expected: 108 | raise Exception("'model_63001.uint16_3' point mismatch: {} {}".format(d.model_63001.uint16_3, expected)) 109 | expected = 60 110 | if d.model_63001.uint16_4 != expected: 111 | raise Exception("'model_63001.uint16_4' point mismatch: {} {}".format(d.model_63001.uint16_4, expected)) 112 | expected = 7 113 | if d.model_63001.uint16_5 != expected: 114 | raise Exception("'model_63001.uint16_5' point mismatch: {} {}".format(d.model_63001.uint16_5, expected)) 115 | value_3 = 65525 116 | value_4 = 70 117 | value_5 = 8 118 | d.model_63001.uint16_3 = value_3 119 | d.model_63001.uint16_4 = value_4 120 | d.model_63001.uint16_5 = value_5 121 | d.model_63001.write() 122 | d.model_63001.read() 123 | value = d.model_63001.uint16_3 124 | if d.model_63001.uint16_3 != value_3: 125 | raise Exception("'model_63001.int16_3' write failure: {} {}".format(d.model_63001.uint16_3, value_3)) 126 | value = d.model_63001.uint16_4 127 | if d.model_63001.uint16_4 != value_4: 128 | raise Exception("'model_63001.int16_4' write failure: {} {}".format(d.model_63001.uint16_4, value_4)) 129 | value = d.model_63001.uint16_5 130 | if d.model_63001.uint16_5 != value_5: 131 | raise Exception("'model_63001.int16_5' write failure: {} {}".format(d.model_63001.uint16_5, value_5)) 132 | 133 | # write multiple 134 | d.model_63001.read() 135 | expected = value_3 136 | if d.model_63001.uint16_3 != expected: 137 | raise Exception("'model_63001.uint16_3' point mismatch: {} {}".format(d.model_63001.uint16_3, expected)) 138 | expected = value_4 139 | if d.model_63001.uint16_4 != expected: 140 | raise Exception("'model_63001.uint16_4' point mismatch: {} {}".format(d.model_63001.uint16_4, expected)) 141 | expected = value_5 142 | if d.model_63001.uint16_5 != expected: 143 | raise Exception("'model_63001.uint16_5' point mismatch: {} {}".format(d.model_63001.uint16_5, expected)) 144 | value_3 = 65524 145 | value_5 = 7 146 | d.model_63001.uint16_3 = value_3 147 | d.model_63001.uint16_5 = value_5 148 | d.model_63001.write() 149 | d.model_63001.read() 150 | value = d.model_63001.uint16_3 151 | if d.model_63001.uint16_3 != value_3: 152 | raise Exception("'model_63001.int16_3' write failure: {} {}".format(d.model_63001.uint16_3, value_3)) 153 | value = d.model_63001.uint16_4 154 | if d.model_63001.uint16_4 != value_4: 155 | raise Exception("'model_63001.int16_4' write failure: {} {}".format(d.model_63001.uint16_4, value_4)) 156 | value = d.model_63001.uint16_5 157 | if d.model_63001.uint16_5 != value_5: 158 | raise Exception("'model_63001.int16_5' write failure: {} {}".format(d.model_63001.uint16_5, value_5)) 159 | 160 | d.close() 161 | 162 | 163 | def test_sunspec_client_device_3(self): 164 | d = client.SunSpecClientDevice(client.MAPPED, slave_id=1, name='mbmap_test_device_3.xml', pathlist=self.pathlist) 165 | 166 | # int16 read and write 167 | d.model_63002.read() 168 | expected = 1111 169 | value = int(d.model_63002.repeating[1].int16_1 * 10) 170 | if value != expected: 171 | raise Exception("'model_63002.int16_1' point mismatch: {} {}".format(value, expected)) 172 | 173 | d.model_63002.repeating[1].int16_1 = 333.3 174 | d.model_63002.write() 175 | d.model_63002.read() 176 | expected = 3333 177 | value = int(d.model_63002.repeating[1].int16_1 * 10) 178 | if value != expected: 179 | raise Exception("'model_63002.int16_2' write failure: {} {}".format(value, expected)) 180 | 181 | expected = 2222 182 | value = int(d.model_63002.repeating[1].int16_2 * 100) 183 | if value != expected: 184 | raise Exception("'model_63002.int16_1' point mismatch: {} {}".format(value, expected)) 185 | 186 | d.close() 187 | 188 | 189 | if __name__ == "__main__": 190 | 191 | unittest.main() 192 | -------------------------------------------------------------------------------- /sunspec/core/test/test_data.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import sys 25 | import os 26 | import time 27 | import unittest 28 | 29 | import sunspec.core.data as data 30 | import sunspec.core.util as util 31 | 32 | class TestData(unittest.TestCase): 33 | def test_data(self): 34 | t = 1387560564.48 35 | expected_xml = '

23

24

' 36 | 37 | sd = data.SunSpecData() 38 | # def __init__(self, logger_id=None, namespace=None, device_id=None, ifc=None, man=None, mod=None, sn=None, time=None, cid=None): 39 | d = sd.device_add(logger_id='00:00:00:00:00:01', man='Man', mod='Mod', sn='SN', timestamp=t) 40 | m = d.model_add(1) 41 | m.point_add("P1", 23) 42 | m.point_add("P2", 24) 43 | 44 | # print(sd.to_xml(pretty_print=True)) 45 | xml = sd.to_xml_str() 46 | if xml != expected_xml: 47 | raise Exception('XML mismatch: {} {}'.format(xml, expected_xml)) 48 | 49 | 50 | if __name__ == "__main__": 51 | 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /sunspec/core/test/test_modbus_client.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import sys 25 | import os 26 | import unittest 27 | 28 | import sunspec.core.device as device 29 | import sunspec.core.modbus.client as modbus 30 | 31 | 32 | class TestModbusClient(unittest.TestCase): 33 | def test_modbus_client_device_rtu_read(self): 34 | """ 35 | -> 01 03 9C 40 00 02 EB 8F 36 | <- 01 03 04 53 75 6E 53 96 F0 37 | """ 38 | 39 | d = modbus.ModbusClientDeviceRTU(1, modbus.TEST_NAME, trace_func=None) 40 | 41 | d.client.serial.in_buf = b'\x01\x03\x04\x53\x75\x6E\x53\x96\xF0' 42 | d.client.serial.out_buf = b'' 43 | 44 | data = d.read(40000, 2) 45 | 46 | if d.client.serial.out_buf != b'\x01\x03\x9C\x40\x00\x02\xEB\x8F': 47 | raise Exception("Modbus request mismatch") 48 | 49 | if data != b'SunS': 50 | raise Exception("Read data mismatch - expected: 'SunS' received: %s") % (data) 51 | 52 | d.close() 53 | 54 | 55 | def test_modbus_client_device_rtu_write(self): 56 | """ 57 | -> 01 10 9C 40 00 02 04 41 42 43 44 8B B2 58 | <- 01 10 9C 40 00 02 6E 4C 59 | """ 60 | 61 | d = modbus.ModbusClientDeviceRTU(1, modbus.TEST_NAME, trace_func=None) 62 | 63 | d.client.serial.in_buf = b'\x01\x10\x9C\x40\x00\x02\x6E\x4C' 64 | d.client.serial.out_buf = b'' 65 | 66 | d.write(40000, 'ABCD') 67 | 68 | if d.client.serial.out_buf != b'\x01\x10\x9C\x40\x00\x02\x04\x41\x42\x43\x44\x8B\xB2': 69 | raise Exception("Modbus request mismatch") 70 | 71 | d.close() 72 | 73 | 74 | def test_modbus_client_device_tcp_read(self): 75 | """ 76 | -> 00 00 00 00 00 06 01 03 9C 40 00 02 77 | <- 00 00 00 00 00 07 01 03 04 53 75 6E 53 78 | """ 79 | 80 | d = modbus.ModbusClientDeviceTCP(1, ipaddr="127.0.0.1", trace_func=None, test=True) 81 | 82 | d.socket.in_buf = b'\x00\x00\x00\x00\x00\x07\x01\x03\x04\x53\x75\x6E\x53' 83 | d.socket.out_buf = b'' 84 | 85 | data = d.read(40000, 2) 86 | 87 | if d.socket.out_buf != b'\x00\x00\x00\x00\x00\x06\x01\x03\x9C\x40\x00\x02': 88 | raise Exception("Modbus request mismatch") 89 | 90 | if data != b'SunS': 91 | raise Exception("Read data mismatch - expected: 'SunS' received: %s") % (data) 92 | 93 | d.close() 94 | 95 | 96 | def test_modbus_client_device_tcp_write(self): 97 | """ 98 | -> 00 00 00 00 00 0B 01 10 9C 40 00 02 04 41 42 43 44 99 | <- 00 00 00 00 00 06 01 10 9C 40 00 02 100 | """ 101 | 102 | d = modbus.ModbusClientDeviceTCP(1, ipaddr="127.0.0.1", trace_func=None, test=True) 103 | 104 | d.socket.in_buf = b'\x00\x00\x00\x00\x00\x06\x01\x10\x9C\x40\x00\x02' 105 | d.socket.out_buf = b'' 106 | 107 | d.write(40000, 'ABCD') 108 | 109 | if d.socket.out_buf != b'\x00\x00\x00\x00\x00\x0B\x01\x10\x9C\x40\x00\x02\x04\x41\x42\x43\x44': 110 | raise Exception("Modbus request mismatch") 111 | 112 | d.close() 113 | 114 | 115 | if __name__ == "__main__": 116 | 117 | unittest.main() 118 | -------------------------------------------------------------------------------- /sunspec/core/test/test_modbus_mbmap.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import sys 25 | import os 26 | import unittest 27 | 28 | try: 29 | import xml.etree.ElementTree as ET 30 | except: 31 | import elementtree.ElementTree as ET 32 | 33 | import sunspec.core.device as device 34 | import sunspec.core.util as util 35 | import sunspec.core.modbus.mbmap as mbmap 36 | 37 | 38 | class TestModbusMap(unittest.TestCase): 39 | def setUp(self): 40 | path = os.path.abspath(__file__) 41 | self.pathlist = util.PathList(['.', 42 | os.path.join(os.path.dirname(path), 43 | 'devices')]) 44 | 45 | def test_modbus_mbmap_from_xml_file(self): 46 | m1 = mbmap.ModbusMap() 47 | m1.from_xml('mbmap_test_device_1.xml', self.pathlist) 48 | 49 | m2 = mbmap.ModbusMap() 50 | m2.from_xml('mbmap_test_device_1_a.xml', self.pathlist) 51 | not_equal = m1.not_equal(m2) 52 | if not_equal: 53 | raise Exception(not_equal) 54 | 55 | m3 = mbmap.ModbusMap() 56 | m3.from_xml('mbmap_test_device_1_b.xml', self.pathlist) 57 | not_equal = m1.not_equal(m3) 58 | if not_equal: 59 | raise Exception(not_equal) 60 | 61 | m4 = mbmap.ModbusMap() 62 | m4.from_xml('mbmap_test_device_1_c.xml', self.pathlist) 63 | not_equal = m1.not_equal(m4) 64 | if not_equal: 65 | raise Exception(not_equal) 66 | 67 | m5 = mbmap.ModbusMap() 68 | m5.from_xml('mbmap_test_device_1_d.xml', self.pathlist) 69 | not_equal = m1.not_equal(m5) 70 | if not_equal: 71 | raise Exception(not_equal) 72 | 73 | 74 | def test_modbus_mbmap_from_xml_element(self): 75 | filename = os.path.join(self.pathlist.path[1], 76 | 'mbmap_test_device_1.xml') 77 | 78 | f = open(filename, 'r') 79 | map_data = f.read() 80 | f.close() 81 | root = ET.fromstring(map_data) 82 | 83 | m1 = mbmap.ModbusMap() 84 | m1.from_xml(element=root) 85 | 86 | m2 = mbmap.ModbusMap() 87 | m2.from_xml('mbmap_test_device_1_a.xml', self.pathlist) 88 | not_equal = m1.not_equal(m2) 89 | if not_equal: 90 | raise Exception(not_equal) 91 | 92 | m3 = mbmap.ModbusMap() 93 | m3.from_xml('mbmap_test_device_1_b.xml', self.pathlist) 94 | not_equal = m1.not_equal(m3) 95 | if not_equal: 96 | raise Exception(not_equal) 97 | 98 | m4 = mbmap.ModbusMap() 99 | m4.from_xml('mbmap_test_device_1_c.xml', self.pathlist) 100 | not_equal = m1.not_equal(m4) 101 | if not_equal: 102 | raise Exception(not_equal) 103 | 104 | def test_modbus_mbmap_regs_add(self): 105 | m1 = mbmap.ModbusMap(base_addr=999, func='holding', mapid=12345) 106 | m1.regs_add(offset=40072, count=1) 107 | 108 | def test_modbus_mbmap_to_xml(self): 109 | filename = os.path.join(self.pathlist.path[1], 'mbmap_test_device_1.xml') 110 | 111 | f = open(filename, 'r') 112 | map_data = f.read() 113 | f.close() 114 | root = ET.fromstring(map_data) 115 | 116 | expected_output_filename = os.path.join(self.pathlist.path[1], 'mbmap_test_device_1_processed.xml') 117 | f = open(expected_output_filename, 'r') 118 | expected_output = f.read() 119 | f.close() 120 | expected_root = ET.fromstring(expected_output) 121 | 122 | # Convert from xml to ModbusMap and back to xml to verify to_xml() is working properly 123 | m1 = mbmap.ModbusMap() 124 | m1.from_xml(element=root) 125 | 126 | assert m1.to_xml().find('regs').text == expected_root.find('regs').text 127 | 128 | 129 | if __name__ == "__main__": 130 | 131 | unittest.main() 132 | -------------------------------------------------------------------------------- /sunspec/core/test/test_util.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import sys 25 | import os 26 | import time 27 | import unittest 28 | 29 | import sunspec.core.util as util 30 | 31 | class TestUtil(unittest.TestCase): 32 | 33 | def test_data_to_s16(self): 34 | self.assertEqual(util.data_to_s16(b'\x12\x34'), int(4660)) 35 | self.assertEqual(util.data_to_s16(b'\x92\x34'), int(-28108)) 36 | 37 | def test_data_to_u16(self): 38 | self.assertEqual(util.data_to_u16(b'\x12\x34'), int(4660)) 39 | self.assertEqual(util.data_to_u16(b'\x92\x34'), int(37428)) 40 | 41 | def test_data_to_s32(self): 42 | self.assertEqual(util.data_to_s32(b'\x12\x34\x56\x78'), int(305419896)) 43 | self.assertEqual(util.data_to_s32(b'\x92\x34\x56\x78'), int(-1842063752)) 44 | 45 | def test_data_to_u32(self): 46 | self.assertEqual(util.data_to_u32(b'\x12\x34\x56\x78'), int(305419896)) 47 | self.assertEqual(util.data_to_u32(b'\x92\x34\x56\x78'), int(2452903544)) 48 | 49 | def test_data_to_s64(self): 50 | self.assertEqual(util.data_to_s64(b'\x12\x34\x56\x78\x12\x34\x56\x78'), int(1311768465173141112)) 51 | self.assertEqual(util.data_to_s64(b'\x92\x34\x56\x78\x12\x34\x56\x78'), int(-7911603571681634696)) 52 | 53 | def test_data_to_u64(self): 54 | self.assertEqual(util.data_to_u64(b'\x12\x34\x56\x78\x12\x34\x56\x78'), int(1311768465173141112)) 55 | self.assertEqual(util.data_to_u64(b'\x92\x34\x56\x78\x12\x34\x56\x78'), int(10535140502027916920)) 56 | 57 | def test_data_to_ipv6addr(self): 58 | self.assertIsNone(util.data_to_ipv6addr(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')) 59 | self.assertEqual(util.data_to_ipv6addr(b'\x12\x34\x56\x78\x9A\xBC\xDE\xF0\x12\x34\x56\x78\x9A\xBC\xDE\xF0'), '12345678:9ABCDEF0:12345678:9ABCDEF0') 60 | self.assertIsNone(util.data_to_ipv6addr(b'\x01\x00\x00\x00\x00\x00\x00\x00')) 61 | 62 | def test_data_to_eui48(self): 63 | self.assertIsNone(util.data_to_eui48(b'\x00\x00\x00\x00\x00\x00\x00\x00')) 64 | self.assertEqual(util.data_to_eui48(b'\x00\x00\x12\x34\x56\x78\x9A\xBC'), '12:34:56:78:9A:BC') 65 | 66 | def test_data_to_float(self): 67 | self.assertIsNone(util.data_to_float(b'\x7f\xc0\x00\x00')) 68 | self.assertEqual(util.data_to_float(b'\x44\x7a\x00\x00'), float(1000)) 69 | self.assertEqual(util.data_to_float(b'\xc4\x7a\x00\x00'), float(-1000)) 70 | 71 | def test_data_to_double(self): 72 | self.assertIsNone(util.data_to_double(b'\x7F\xF8\x00\x00\x00\x00\x00\x00')) 73 | self.assertEqual(util.data_to_double(b'\x40\x8f\x40\x00\x00\x00\x00\x00'), float(1000)) 74 | self.assertEqual(util.data_to_double(b'\xc0\x8f\x40\x00\x00\x00\x00\x00'), float(-1000)) 75 | 76 | def test_data_to_str(self): 77 | self.assertEqual(util.data_to_str(b'\x53\x75\x6e\x53\x70\x65\x63\x20\x54\x65\x73\x74\x00'), 'SunSpec Test') 78 | 79 | 80 | def test_s16_to_data(self): 81 | self.assertEqual(util.s16_to_data(int(4660)), b'\x12\x34') 82 | self.assertEqual(util.s16_to_data(int(-28108)), b'\x92\x34') 83 | 84 | def test_u16_to_data(self): 85 | self.assertEqual(util.u16_to_data(int(4660)), b'\x12\x34') 86 | self.assertEqual(util.u16_to_data(int(37428)), b'\x92\x34') 87 | 88 | def test_s32_to_data(self): 89 | self.assertEqual(util.s32_to_data(int(305419896)), b'\x12\x34\x56\x78') 90 | self.assertEqual(util.s32_to_data(int(-1842063752)), b'\x92\x34\x56\x78') 91 | 92 | def test_u32_to_data(self): 93 | self.assertEqual(util.u32_to_data(int(305419896)), b'\x12\x34\x56\x78') 94 | self.assertEqual(util.u32_to_data(int(2452903544)), b'\x92\x34\x56\x78') 95 | 96 | def test_s64_to_data(self): 97 | self.assertEqual(util.s64_to_data(int(1311768465173141112)), b'\x12\x34\x56\x78\x12\x34\x56\x78') 98 | self.assertEqual(util.s64_to_data(int(-7911603571681634696)), b'\x92\x34\x56\x78\x12\x34\x56\x78') 99 | 100 | def test_u64_to_data(self): 101 | self.assertEqual(util.u64_to_data(int(1311768465173141112)), b'\x12\x34\x56\x78\x12\x34\x56\x78') 102 | self.assertEqual(util.u64_to_data(int(10535140502027916920)), b'\x92\x34\x56\x78\x12\x34\x56\x78') 103 | 104 | def test_ipv6addr_to_data(self): 105 | self.assertEqual(util.ipv6addr_to_data('12345678:9ABCDEF0:12345678:9ABCDEF0'), b'\x12\x34\x56\x78\x9A\xBC\xDE\xF0\x12\x34\x56\x78\x9A\xBC\xDE\xF0') 106 | 107 | def test_float_to_data32(self): 108 | self.assertEqual(util.float_to_data32(float(1000)), b'\x44\x7a\x00\x00') 109 | self.assertEqual(util.float_to_data32(float(-1000)), b'\xc4\x7a\x00\x00') 110 | 111 | def test_float32_to_data(self): 112 | self.assertEqual(util.float32_to_data(float(1000)), b'\x44\x7a\x00\x00') 113 | self.assertEqual(util.float32_to_data(float(-1000)), b'\xc4\x7a\x00\x00') 114 | 115 | def test_float_to_data(self): 116 | self.assertEqual(util.float_to_data(float(1000)), b'\x40\x8f\x40\x00\x00\x00\x00\x00') 117 | self.assertEqual(util.float_to_data(float(-1000)), b'\xc0\x8f\x40\x00\x00\x00\x00\x00') 118 | 119 | def test_str_to_data(self): 120 | self.assertEqual(util.str_to_data('SunSpec Test'), b'\x53\x75\x6e\x53\x70\x65\x63\x20\x54\x65\x73\x74\x00') 121 | 122 | def test_eui48_to_data(self): 123 | self.assertEqual(util.eui48_to_data('12:34:56:78:9A:BC'), b'\x00\x00\x12\x34\x56\x78\x9A\xBC') 124 | 125 | 126 | if __name__ == "__main__": 127 | 128 | unittest.main() 129 | -------------------------------------------------------------------------------- /sunspec/core/util.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Copyright (C) 2018 SunSpec Alliance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | """ 23 | 24 | import os 25 | import struct 26 | import sys 27 | import zipfile 28 | import array 29 | import base64 30 | 31 | class SunSpecError(Exception): 32 | pass 33 | 34 | """ Functions to pack and unpack data string values 35 | 36 | """ 37 | def data_to_s16(data): 38 | s16 = struct.unpack('>h', data) 39 | return s16[0] 40 | 41 | def data_to_u16(data): 42 | u16 = struct.unpack('>H', data) 43 | return u16[0] 44 | 45 | def data_to_s32(data): 46 | s32 = struct.unpack('>l', data) 47 | return s32[0] 48 | 49 | def data_to_u32(data): 50 | u32 = struct.unpack('>L', data) 51 | return u32[0] 52 | 53 | def data_to_s64(data): 54 | s64 = struct.unpack('>q', data) 55 | return s64[0] 56 | 57 | def data_to_u64(data): 58 | u64 = struct.unpack('>Q', data) 59 | return u64[0] 60 | 61 | def data_to_ipv6addr(data): 62 | if sys.version_info < (3,): 63 | data = [ord(x) for x in data] 64 | 65 | value = False 66 | for i in data: 67 | if i != 0: 68 | value = True 69 | break 70 | if value and len(data) == 16: 71 | return '{:02X}{:02X}{:02X}{:02X}:{:02X}{:02X}{:02X}{:02X}:{:02X}{:02X}{:02X}{:02X}:{:02X}{:02X}{:02X}{:02X}'.format( 72 | data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], 73 | data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]) 74 | 75 | def data_to_eui48(data): 76 | if sys.version_info < (3,): 77 | data = [ord(x) for x in data] 78 | 79 | value = False 80 | for i in data: 81 | if i != 0: 82 | value = True 83 | break 84 | if value and len(data) == 8: 85 | return '{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}'.format( 86 | data[2], data[3], data[4], data[5], data[6], data[7]) 87 | 88 | def data_to_float(data): 89 | f = struct.unpack('>f', data) 90 | if str(f[0]) != str(float('nan')): 91 | return f[0] 92 | 93 | def data_to_double(data): 94 | d = struct.unpack('>d', data) 95 | if str(d[0]) != str(float('nan')): 96 | return d[0] 97 | 98 | def data_to_str(data): 99 | 100 | # Change the data from bytes string to regular string for python 3 101 | # compatibility 102 | if sys.version_info > (3,): 103 | data = str(data, 'latin-1') 104 | 105 | if len(data) > 1: 106 | data = data[0] + data[1:].rstrip('\0') 107 | return data 108 | 109 | def s16_to_data(s16, len=None): 110 | return struct.pack('>h', s16) 111 | 112 | def u16_to_data(u16, len=None): 113 | return struct.pack('>H', u16) 114 | 115 | def s32_to_data(s32, len=None): 116 | return struct.pack('>l', s32) 117 | 118 | def u32_to_data(u32, len=None): 119 | return struct.pack('>L', u32) 120 | 121 | def s64_to_data(s64, len=None): 122 | return struct.pack('>q', s64) 123 | 124 | def u64_to_data(u64, len=None): 125 | return struct.pack('>Q', u64) 126 | 127 | def ipv6addr_to_data(addr, slen=None): 128 | s = base64.b16decode(addr.replace(':', '')) 129 | if slen is None: 130 | slen = len(s) 131 | return struct.pack(str(slen) + 's', s) 132 | 133 | def float_to_data32(f, len=None): 134 | return struct.pack('>f', f) 135 | 136 | def float32_to_data(f, len=None): 137 | return struct.pack('>f', f) 138 | 139 | def float_to_data(f, len=None): 140 | # python float is really a double 141 | return struct.pack('>d', f) 142 | 143 | def str_to_data(s, slen=None): 144 | if slen is None: 145 | slen = len(s) 146 | if sys.version_info > (3,): 147 | s = bytes(s, 'latin-1') 148 | if slen < 16: 149 | s += b'\x00' 150 | slen += 1 151 | return struct.pack(str(slen) + 's', s) 152 | 153 | def eui48_to_data(eui48): 154 | return (b'\x00\x00' + base64.b16decode(eui48.replace(':', ''))) 155 | 156 | 157 | """ Simple XML pretty print support function 158 | 159 | """ 160 | def indent(elem, level=0): 161 | i = "\n" + level*" " 162 | if len(elem): 163 | if not elem.text or not elem.text.strip(): 164 | elem.text = i + " " 165 | if not elem.tail or not elem.tail.strip(): 166 | elem.tail = i 167 | for elem in elem: 168 | indent(elem, level+1) 169 | if not elem.tail or not elem.tail.strip(): 170 | elem.tail = i 171 | else: 172 | if level and (not elem.tail or not elem.tail.strip()): 173 | elem.tail = i 174 | 175 | """ File path list 176 | 177 | """ 178 | class PathList(object): 179 | 180 | def __init__(self, path_list=None): 181 | self.path = [] 182 | 183 | if path_list is not None: 184 | self.path = path_list 185 | 186 | """ Add path to path list 187 | 188 | Provides a list of file system paths to search for non-python files similar to sys.path for python 189 | modules. Zipfiles can be included in a path name and the contents of the zipfile will be searched 190 | based on the remaining path content. Zip file support has the following restrictions: only one zip file 191 | in a path, zi pfiles must have a .zip extension in the name, directories can not end in .zip. 192 | Paths are searched in the order they were added to the path list. 193 | 194 | """ 195 | def add(self, path): 196 | 197 | self.path.append(path) 198 | 199 | """ Read first instance of specified file found in path list 200 | 201 | Traverses the path list and returns the contents of the first instance of the specified file. 202 | Supports zip files in path. 203 | 204 | """ 205 | def read(self, filename): 206 | 207 | file_path = '' 208 | zip_file_path = '' 209 | 210 | # traverse path list until first instance of file found 211 | for p in self.path: 212 | file_path = '' 213 | element_list = p.split(os.sep) 214 | sep = os.sep 215 | 216 | for e in element_list: 217 | if e == '': 218 | file_path += sep 219 | elif file_path and file_path != sep: 220 | file_path += sep 221 | file_path += e 222 | if e.endswith('.zip'): 223 | zip_file_path = file_path 224 | if os.path.exists(zip_file_path): 225 | file_path = '' 226 | # zip file separator is always '/' 227 | sep = '/' 228 | else: 229 | # continue with next path list element if zip file does not exist 230 | zip_file_path = '' 231 | break 232 | 233 | if file_path and file_path != os.sep: 234 | file_path += sep 235 | file_path += filename 236 | if zip_file_path: 237 | zip_file = zipfile.ZipFile(zip_file_path) 238 | try: 239 | zip_file.getinfo(file_path) 240 | except Exception as e: 241 | continue 242 | return zip_file.read(file_path) 243 | else: 244 | if os.path.exists(file_path): 245 | with open(file_path, 'rb') as f: 246 | return f.read() 247 | else: 248 | continue 249 | 250 | # file not found 251 | raise NameError(filename) 252 | 253 | def __str__(self): 254 | 255 | paths = [] 256 | 257 | for p in self.path: 258 | paths.append(p) 259 | 260 | return str(paths) 261 | 262 | 263 | 264 | 265 | --------------------------------------------------------------------------------