pax_global_header00006660000000000000000000000064132517476740014532gustar00rootroot0000000000000052 comment=66e3c200a404e089c7c0294a53015554758c15be termineter-1.0.4/000077500000000000000000000000001325174767400137125ustar00rootroot00000000000000termineter-1.0.4/.gitignore000066400000000000000000000005701325174767400157040ustar00rootroot00000000000000*.csv *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Geany *.geany # Sphinx Documentation docs/html table_diff.html todo.txt termineter-1.0.4/INSTALL.md000066400000000000000000000022201325174767400153360ustar00rootroot00000000000000# Termineter Install Guide ## Requirements Termineter supports Python 2.7 and 3.4+. It is recommended that users use Python3. The requirements are listed in the requirements.txt file. They can be installed with `python3 -m pip install -r requirements.txt`. ## How To Install Termineter No installation or modification is necessary. Start a command prompt, navigate to the termineter directory and use python to run termineter. If using a USB optical probe with an FTDI chip, you may need to load and configure the appropriate serial to USB driver in order to use the device. The following command will configure the hardware on Linux. **Kernel version < 3.12** ``` modprobe ftdi-sio vendor=0xVVVV product=0xPPPP ``` **Kernel version >= 3.12** ``` modprobe ftdi-sio echo VVVV PPPP > /sys/bus/usb-serial/drivers/ftdi_sio/new_id ``` Where VVVV is the vendor ID and PPPP is the product ID. These values can be obtained from the lsusb command. # How To Update (All) Updates can be obtained from the projects home page, either in source archives for major revisions or from the trunk. Git must be installed and used to update to the latest revision from the trunk. termineter-1.0.4/LICENSE000066400000000000000000000027311325174767400147220ustar00rootroot00000000000000Copyright (c) 2012-2018, SecureState LLC All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. termineter-1.0.4/README.md000066400000000000000000000035511325174767400151750ustar00rootroot00000000000000``` ______ _ __ /_ __/__ _________ ___ (_)___ ___ / /____ _____ / / / _ \/ ___/ __ `__ \/ / __ \/ _ \/ __/ _ \/ ___/ / / / __/ / / / / / / / / / / / __/ /_/ __/ / /_/ \___/_/ /_/ /_/ /_/_/_/ /_/\___/\__/\___/_/ ``` # Summary Termineter is a Python framework which provides a platform for the security testing of smart meters. It implements the C1218 and C1219 protocols for communication over an optical interface. Currently supported are Meters using C1219-2007 with 7-bit character sets. This is the most common configuration found in North America. Termineter communicates with Smart Meters via a connection using an ANSI type-2 optical probe with a serial interface. [![asciicast](https://asciinema.org/a/154407.png)][1] # License Termineter is released under the BSD 3-clause license, for more details see the [LICENSE](https://github.com/securestate/termineter/blob/master/LICENSE) file. # Credits Special Thanks To: * Caroline Aronoff (Alpha testing and fixing older PySerial compatibility) * Chris Murrey - f8lerror (Alpha testing) * Jake Garlie - jagar (Alpha testing) * Scott Turner - fantomgoat (Bug report and fix) * Kevin Underwood (Bug report and fix) * Don Weber - cutaway (Developer of InGuardians' OptiGuard) Termineter Development Team: * Spencer McIntyre of the SecureState Research and Innovation Team # About Author: Spencer McIntyre - zeroSteiner ([\@zeroSteiner][2]) Author Home Page: http://www.securestate.com/ Project Home Page: https://github.com/securestate/termineter Project Documentation: http://termineter.readthedocs.org/en/latest # Install Termineter can be installed from the Python Package Index using pip. Simply run `sudo pip install termineter`. For additional install information please see the INSTALL.md file. [1]: https://asciinema.org/a/154407 [2]: https://twitter.com/zeroSteiner termineter-1.0.4/docs/000077500000000000000000000000001325174767400146425ustar00rootroot00000000000000termineter-1.0.4/docs/conf.py000066400000000000000000000201171325174767400161420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. GITHUB_BRANCH = 'dev' GITHUB_REPO = 'securestate/termineter' import sys import os _prj_root = os.path.dirname(__file__) _prj_root = os.path.relpath(os.path.join('..', '..'), _prj_root) _prj_root = os.path.abspath(_prj_root) sys.path.insert(1, _prj_root) del _prj_root, _pkg on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # -- General configuration ------------------------------------------------ needs_sphinx = '1.3' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', ] extlinks = { 'release': ("https://github.com/{0}/releases/tag/v%s".format(GITHUB_REPO), 'v') } def linkcode_resolve(domain, info): if domain != 'py': return None if not info['module']: return None file_name = info['module'].replace('.', '/') + '.py' return "https://github.com/{0}/blob/{1}/{2}".format(GITHUB_REPO, GITHUB_BRANCH, file_name) intersphinx_mapping = {'smokezephyr': ('https://smoke-zephyr.readthedocs.org/en/latest/', None)} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Termineter' copyright = '2011-2015, SecureState LLC' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = version.version.split('-')[0] # The full version, including alpha/beta/rc tags. release = version.distutils_version language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'termineter_doc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'Termineter.tex', u'Termineter Documentation', u'Spencer McIntyre', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'termineter', u'Termineter Documentation', [u'Spencer McIntyre'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Termineter', u'Termineter Documentation', u'Spencer McIntyre', 'Termineter', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False termineter-1.0.4/docs/requirements.txt000066400000000000000000000000341325174767400201230ustar00rootroot00000000000000pyasn1>=0.1.7 pyserial>=2.6 termineter-1.0.4/docs/source/000077500000000000000000000000001325174767400161425ustar00rootroot00000000000000termineter-1.0.4/docs/source/c1218/000077500000000000000000000000001325174767400167005ustar00rootroot00000000000000termineter-1.0.4/docs/source/c1218/connection.rst000066400000000000000000000005051325174767400215710ustar00rootroot00000000000000:mod:`c1218.connection` ======================= .. module:: c1218.connection :synopsis: Classes ------- .. autoclass:: c1218.connection.Connection :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.connection.ConnectionBase :members: :special-members: __init__ :undoc-members: termineter-1.0.4/docs/source/c1218/data.rst000066400000000000000000000023631325174767400203470ustar00rootroot00000000000000:mod:`c1218.data` ================= .. module:: c1218.data :synopsis: Classes ------- .. autoclass:: c1218.data.C1218Request :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218LogonRequest :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218SecurityRequest :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218LogoffRequest :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218NegotiateRequest :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218WaitRequest :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218IdentRequest :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218TerminateRequest :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218ReadRequest :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218WriteRequest :members: :special-members: __init__ :undoc-members: .. autoclass:: c1218.data.C1218Packet :members: :special-members: __init__ :undoc-members: termineter-1.0.4/docs/source/c1218/errors.rst000066400000000000000000000005411325174767400207460ustar00rootroot00000000000000:mod:`c1218.errors` =================== .. module:: c1218.errors :synopsis: Exceptions ---------- .. autoexception:: c1218.errors.C1218Error .. autoexception:: c1218.errors.C1218IOError .. autoexception:: c1218.errors.C1218NegotiateError .. autoexception:: c1218.errors.C1218ReadTableError .. autoexception:: c1218.errors.C1218WriteTableError termineter-1.0.4/docs/source/c1218/index.rst000066400000000000000000000002571325174767400205450ustar00rootroot00000000000000:mod:`c1218` ============ .. module:: c1218 :synopsis: .. toctree:: :maxdepth: 2 :titlesonly: urlhandler/index.rst connection.rst data.rst errors.rst termineter-1.0.4/docs/source/c1218/urlhandler/000077500000000000000000000000001325174767400210405ustar00rootroot00000000000000termineter-1.0.4/docs/source/c1218/urlhandler/index.rst000066400000000000000000000002401325174767400226750ustar00rootroot00000000000000:mod:`c1218.urlhandler` ======================= .. module:: c1218.urlhandler :synopsis: .. toctree:: :maxdepth: 2 :titlesonly: protocol_unix.rst termineter-1.0.4/docs/source/c1218/urlhandler/protocol_unix.rst000066400000000000000000000004171325174767400245000ustar00rootroot00000000000000:mod:`c1218.urlhandler.protocol_unix` ===================================== .. module:: c1218.urlhandler.protocol_unix :synopsis: Classes ------- .. autoclass:: c1218.urlhandler.protocol_unix.UnixSerial :members: :special-members: __init__ :undoc-members: termineter-1.0.4/docs/source/c1219/000077500000000000000000000000001325174767400167015ustar00rootroot00000000000000termineter-1.0.4/docs/source/c1219/access/000077500000000000000000000000001325174767400201425ustar00rootroot00000000000000termineter-1.0.4/docs/source/c1219/access/general.rst000066400000000000000000000003571325174767400223160ustar00rootroot00000000000000:mod:`c1219.access.general` =========================== .. module:: c1219.access.general :synopsis: Classes ------- .. autoclass:: c1219.access.general.C1219GeneralAccess :members: :special-members: __init__ :undoc-members: termineter-1.0.4/docs/source/c1219/access/index.rst000066400000000000000000000002721325174767400220040ustar00rootroot00000000000000:mod:`c1219.access` =================== .. module:: c1219.access :synopsis: .. toctree:: :maxdepth: 2 :titlesonly: general.rst log.rst security.rst telephone.rst termineter-1.0.4/docs/source/c1219/access/log.rst000066400000000000000000000003331325174767400214540ustar00rootroot00000000000000:mod:`c1219.access.log` ======================= .. module:: c1219.access.log :synopsis: Classes ------- .. autoclass:: c1219.access.log.C1219LogAccess :members: :special-members: __init__ :undoc-members: termineter-1.0.4/docs/source/c1219/access/security.rst000066400000000000000000000003641325174767400225460ustar00rootroot00000000000000:mod:`c1219.access.security` ============================ .. module:: c1219.access.security :synopsis: Classes ------- .. autoclass:: c1219.access.security.C1219SecurityAccess :members: :special-members: __init__ :undoc-members: termineter-1.0.4/docs/source/c1219/access/telephone.rst000066400000000000000000000003711325174767400226600ustar00rootroot00000000000000:mod:`c1219.access.telephone` ============================= .. module:: c1219.access.telephone :synopsis: Classes ------- .. autoclass:: c1219.access.telephone.C1219TelephoneAccess :members: :special-members: __init__ :undoc-members: termineter-1.0.4/docs/source/c1219/data.rst000066400000000000000000000003071325174767400203440ustar00rootroot00000000000000:mod:`c1219.data` ================= .. module:: c1219.data :synopsis: Classes ------- .. autoclass:: c1219.data.C1219ProcedureInit :members: :special-members: __init__ :undoc-members: termineter-1.0.4/docs/source/c1219/errors.rst000066400000000000000000000003151325174767400207460ustar00rootroot00000000000000:mod:`c1219.errors` =================== .. module:: c1219.errors :synopsis: Exceptions ---------- .. autoexception:: c1219.errors.C1219ProcedureError .. autoexception:: c1219.errors.C1219ParseError termineter-1.0.4/docs/source/c1219/index.rst000066400000000000000000000002311325174767400205360ustar00rootroot00000000000000:mod:`c1219` ============ .. module:: c1219 :synopsis: .. toctree:: :maxdepth: 2 :titlesonly: access/index.rst data.rst errors.rst termineter-1.0.4/docs/source/conf.py000066400000000000000000000200461325174767400174430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. GITHUB_BRANCH = 'new-docs' GITHUB_REPO = 'securestate/termineter' import sys import os _prj_root = os.path.dirname(__file__) _prj_root = os.path.relpath(os.path.join('..', '..'), _prj_root) _prj_root = os.path.abspath(_prj_root) sys.path.insert(1, _prj_root) del _prj_root on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # -- General configuration ------------------------------------------------ needs_sphinx = '1.3' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', ] extlinks = { 'release': ("https://github.com/{0}/releases/tag/v%s".format(GITHUB_REPO), 'v') } def linkcode_resolve(domain, info): if domain != 'py': return None if not info['module']: return None file_name = info['module'].replace('.', '/') + '.py' return "https://github.com/{0}/blob/{1}/{2}".format(GITHUB_REPO, GITHUB_BRANCH, file_name) intersphinx_mapping = {'smokezephyr': ('https://smoke-zephyr.readthedocs.org/en/latest/', None)} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Termineter' copyright = '2011-2015, SecureState LLC' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.1.0' # The full version, including alpha/beta/rc tags. release = version language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'termineter_doc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'Termineter.tex', u'Termineter Documentation', u'Spencer McIntyre', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'termineter', u'Termineter Documentation', [u'Spencer McIntyre'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Termineter', u'Termineter Documentation', u'Spencer McIntyre', 'Termineter', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False termineter-1.0.4/docs/source/index.rst000066400000000000000000000013621325174767400200050ustar00rootroot00000000000000Termineter Documentation ======================== Termineter is a framework written in python to provide a platform for the security testing of smart meters. It implements the C12.18 and C12.19 protocols for communication. Currently supported are Meters using C12.19 with 7-bit character sets. Termineter communicates with Smart Meters via a connection using an ANSI type-2 optical probe with a serial interface. The source code is available on the `GitHub homepage`_. .. _GitHub Homepage: https://github.com/securestate/termineter .. _technical-docs: .. toctree:: :caption: Technical Documentation :maxdepth: 1 c1218/index.rst c1219/index.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` termineter-1.0.4/lib/000077500000000000000000000000001325174767400144605ustar00rootroot00000000000000termineter-1.0.4/lib/c1218/000077500000000000000000000000001325174767400152165ustar00rootroot00000000000000termineter-1.0.4/lib/c1218/__init__.py000066400000000000000000000000001325174767400173150ustar00rootroot00000000000000termineter-1.0.4/lib/c1218/connection.py000066400000000000000000000416141325174767400177350ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1218/connection.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import logging import random import sys import time from c1218.data import * from c1218.errors import C1218NegotiateError, C1218IOError, C1218ReadTableError, C1218WriteTableError from c1218.utilities import check_data_checksum, packet_checksum from c1219.data import C1219ProcedureInit from c1219.errors import C1219ProcedureError import serial if hasattr(serial, 'protocol_handler_packages') and not 'c1218.urlhandler' in serial.protocol_handler_packages: serial.protocol_handler_packages.append('c1218.urlhandler') if hasattr(logging, 'NullHandler'): logging.getLogger('c1218').addHandler(logging.NullHandler()) class ConnectionBase(object): def __init__(self, device, c1218_settings={}, serial_settings=None, toggle_control=True, **kwargs): """ This is a C12.18 driver for serial connections. It relies on PySerial to communicate with an ANSI Type-2 Optical probe to communicate with a device (presumably a smart meter). :param str device: A connection string to be passed to the PySerial library. If PySerial is new enough, the serial_for_url function will be used to allow the user to use a rfc2217 bridge. :param dict c1218_settings: A settings dictionary to configure the C1218 parameters of 'nbrpkts' and 'pktsize' If not provided the default settings of 2 (nbrpkts) and 512 (pktsize) will be used. :param dict serial_settings: A PySerial settings dictionary to be applied to the serial connection instance. :param bool toggle_control: Enables or diables automatically settings the toggle bit in C12.18 frames. """ self.logger = logging.getLogger('c1218.connection') self.loggerio = logging.getLogger('c1218.connection.io') self.toggle_control = toggle_control self._toggle_bit = False if hasattr(serial, 'serial_for_url'): self.serial_h = serial.serial_for_url(device) else: self.logger.warning('serial library does not have serial_for_url functionality, it\'s not the latest version') self.serial_h = serial.Serial(device) self.logger.debug('successfully opened serial device: ' + device) self.device = device self.c1218_pktsize = (c1218_settings.get('pktsize') or 512) self.c1218_nbrpkts = (c1218_settings.get('nbrpkts') or 2) if serial_settings: self.logger.debug('applying pySerial settings dictionary') self.serial_h.parity = serial_settings['parity'] self.serial_h.baudrate = serial_settings['baudrate'] self.serial_h.bytesize = serial_settings['bytesize'] self.serial_h.xonxoff = serial_settings['xonxoff'] self.serial_h.interCharTimeout = serial_settings['interCharTimeout'] self.serial_h.rtscts = serial_settings['rtscts'] self.serial_h.timeout = serial_settings['timeout'] self.serial_h.stopbits = serial_settings['stopbits'] self.serial_h.dsrdtr = serial_settings['dsrdtr'] self.serial_h.writeTimeout = serial_settings['writeTimeout'] try: self.serial_h.setRTS(True) except IOError: self.logger.warning('could not set RTS to True') else: self.logger.debug('set RTS to True') try: self.serial_h.setDTR(False) except IOError: self.logger.warning('could not set DTR to False') else: self.logger.debug('set DTR to False') self.logged_in = False self._initialized = False self.c1219_endian = '<' def __repr__(self): return '<' + self.__class__.__name__ + ' Device: ' + self.device + ' >' def send(self, data): """ This sends a raw C12.18 frame and waits checks for an ACK response. In the event that a NACK is received, this function will attempt to resend the frame up to 3 times. :param data: the data to be transmitted :type data: str, :py:class:`~c1218.data.C1218Packet` """ if not isinstance(data, C1218Packet): data = C1218Packet(data) if self.toggle_control: # bit wise, fuck yeah if self._toggle_bit: data.set_control(ord(data.control) | 0x20) self._toggle_bit = False elif not self._toggle_bit: if ord(data.control) & 0x20: data.set_control(ord(data.control) ^ 0x20) self._toggle_bit = True elif self.toggle_control and not isinstance(data, C1218Packet): self.loggerio.warning('toggle bit is on but the data is not a C1218Packet instance') data = data.build() self.loggerio.debug("sending frame, length: {0:<3} data: {1}".format(len(data), binascii.b2a_hex(data).decode('utf-8'))) for pktcount in range(0, 3): self.write(data) response = self.serial_h.read(1) if response == NACK: self.loggerio.warning('received a NACK after writing data') time.sleep(0.10) elif len(response) == 0: self.loggerio.error('received empty response after writing data') time.sleep(0.10) elif response != ACK: self.loggerio.error('received unknown response: ' + hex(ord(response)) + ' after writing data') else: return self.loggerio.critical('failed 3 times to correctly send a frame') raise C1218IOError('failed 3 times to correctly send a frame') def recv(self, full_frame=False): """ Receive a C1218Packet, the payload data is returned. :param bool full_frame: If set to True, the entire C1218 frame is returned instead of just the payload. """ payloadbuffer = b'' tries = 3 while tries: tmpbuffer = self.serial_h.read(1) if tmpbuffer != b'\xee': self.loggerio.error('did not receive \\xee as the first byte of the frame') self.loggerio.debug('received \\x' + binascii.b2a_hex(tmpbuffer).decode('utf-8') + ' instead') tries -= 1 continue tmpbuffer += self.serial_h.read(5) sequence, length = struct.unpack('>xxxBH', tmpbuffer) payload = self.serial_h.read(length) tmpbuffer += payload chksum = self.serial_h.read(2) if chksum == packet_checksum(tmpbuffer): self.serial_h.write(ACK) data = tmpbuffer + chksum self.loggerio.debug("received frame, length: {0:<3} data: {1}".format(len(data), binascii.b2a_hex(data).decode('utf-8'))) payloadbuffer += payload if sequence == 0: if full_frame: payloadbuffer = data if sys.version_info[0] == 2: payloadbuffer = bytearray(payloadbuffer) return payloadbuffer else: tries = 3 else: self.serial_h.write(NACK) self.loggerio.warning('crc does not match on received frame') tries -= 1 self.loggerio.critical('failed 3 times to correctly receive a frame') raise C1218IOError('failed 3 times to correctly receive a frame') def write(self, data): """ Write raw data to the serial connection. The CRC must already be included at the end. This function is not meant to be called directly. :param str data: The raw data to write to the serial connection. """ return self.serial_h.write(data) def read(self, size): """ Read raw data from the serial connection. This function is not meant to be called directly. :param int size: The number of bytes to read from the serial connection. """ data = self.serial_h.read(size) self.logger.debug('read data, length: ' + str(len(data)) + ' data: ' + binascii.b2a_hex(data).decode('utf-8')) self.serial_h.write(ACK) if sys.version_info[0] == 2: data = bytearray(data) return data def close(self): """ Send a terminate request and then disconnect from the serial device. """ if self._initialized: self.stop() self.logged_in = False return self.serial_h.close() class Connection(ConnectionBase): def __init__(self, *args, **kwargs): """ This is a C12.18 driver for serial connections. It relies on PySerial to communicate with an ANSI Type-2 Optical probe to communicate with a device (presumably a smart meter). :param str device: A connection string to be passed to the PySerial library. If PySerial is new enough, the serial_for_url function will be used to allow the user to use a rfc2217 bridge. :param dict c1218_settings: A settings dictionary to configure the C1218 parameters of 'nbrpkts' and 'pktsize' If not provided the default settings of 2 (nbrpkts) and 512 (pktsize) will be used. :param dict serial_settings: A PySerial settings dictionary to be applied to the serial connection instance. :param bool toggle_control: Enables or disables automatically settings the toggle bit in C12.18 frames. :param bool enable_cache: Cache specific, read only tables in memory, the first time the table is read it will be stored for retreival on subsequent requests. This is enabled only for specific tables (currently only 0 and 1). """ enable_cache = kwargs.pop('enable_cache', True) super(Connection, self).__init__(*args, **kwargs) self.caching_enabled = enable_cache self._cacheable_tables = [0, 1] self._table_cache = {} if enable_cache: self.logger.info('selective table caching has been enabled') def flush_table_cache(self): self.logger.info('flushing all cached tables') self._table_cache = {} def set_table_cache_policy(self, cache_policy): if self.caching_enabled == cache_policy: return self.caching_enabled = cache_policy if cache_policy: self.logger.info('selective table caching has been enabled') else: self.flush_table_cache() self.logger.info('selective table caching has been disabled') return def start(self): """ Send an identity request and then a negotiation request. """ self.serial_h.flushOutput() self.serial_h.flushInput() self.send(C1218IdentRequest()) data = self.recv() if data[0] != 0x00: self.logger.error('received incorrect response to identification service request') return False self._initialized = True self.send(C1218NegotiateRequest(self.c1218_pktsize, self.c1218_nbrpkts, baudrate=9600)) data = self.recv() if data[0] != 0x00: self.logger.error('received incorrect response to negotiate service request') self.stop() raise C1218NegotiateError('received incorrect response to negotiate service request', data[0]) return True def stop(self, force=False): """ Send a terminate request. :param bool force: ignore the remote devices response """ if self._initialized: self.send(C1218TerminateRequest()) data = self.recv() if data == b'\x00' or force: self._initialized = False self._toggle_bit = False return True return False def login(self, username='0000', userid=0, password=None): """ Log into the connected device. :param str username: the username to log in with (len(username) <= 10) :param int userid: the userid to log in with (0x0000 <= userid <= 0xffff) :param str password: password to log in with (len(password) <= 20) :rtype: bool """ if password and len(password) > 20: self.logger.error('password longer than 20 characters received') raise Exception('password longer than 20 characters, login failed') self.send(C1218LogonRequest(username, userid)) data = self.recv() if data != b'\x00': self.logger.warning('login failed, username and user id rejected') return False if password is not None: self.send(C1218SecurityRequest(password)) data = self.recv() if data != b'\x00': self.logger.warning('login failed, password rejected') return False self.logged_in = True return True def logoff(self): """ Send a logoff request. :rtype: bool """ self.send(C1218LogoffRequest()) data = self.recv() if data == b'\x00': self._initialized = False return True return False def get_table_data(self, tableid, octetcount=None, offset=None): """ Read data from a table. If successful, all of the data from the requested table will be returned. :param int tableid: The table number to read from (0x0000 <= tableid <= 0xffff) :param int octetcount: Limit the amount of data read, only works if the meter supports this type of reading. :param int offset: The offset at which to start to read the data from. """ if self.caching_enabled and tableid in self._cacheable_tables and tableid in self._table_cache.keys(): self.logger.info('returning cached table #' + str(tableid)) return self._table_cache[tableid] self.send(C1218ReadRequest(tableid, offset, octetcount)) data = self.recv() status = data[0] if status != 0x00: status = status details = (C1218_RESPONSE_CODES.get(status) or 'unknown response code') self.logger.error('could not read table id: ' + str(tableid) + ', error: ' + details) raise C1218ReadTableError('could not read table id: ' + str(tableid) + ', error: ' + details, status) if len(data) < 4: if len(data) == 0: self.logger.error('could not read table id: ' + str(tableid) + ', error: no data was returned') raise C1218ReadTableError('could not read table id: ' + str(tableid) + ', error: no data was returned') self.logger.error('could not read table id: ' + str(tableid) + ', error: data read was corrupt, invalid length (less than 4)') raise C1218ReadTableError('could not read table id: ' + str(tableid) + ', error: data read was corrupt, invalid length (less than 4)') length = struct.unpack('>H', data[1:3])[0] chksum = data[-1] data = data[3:-1] if len(data) != length: self.logger.error('could not read table id: ' + str(tableid) + ', error: data read was corrupt, invalid length') raise C1218ReadTableError('could not read table id: ' + str(tableid) + ', error: data read was corrupt, invalid length') if not check_data_checksum(data, chksum): self.logger.error('could not read table id: ' + str(tableid) + ', error: data read was corrupt, invalid check sum') raise C1218ReadTableError('could not read table id: ' + str(tableid) + ', error: data read was corrupt, invalid checksum') if self.caching_enabled and tableid in self._cacheable_tables and not tableid in self._table_cache.keys(): self.logger.info('caching table #' + str(tableid)) self._table_cache[tableid] = data return data def set_table_data(self, tableid, data, offset=None): """ Write data to a table. :param int tableid: The table number to write to (0x0000 <= tableid <= 0xffff) :param str data: The data to write into the table. :param int offset: The offset at which to start to write the data (0x000000 <= octetcount <= 0xffffff). """ self.send(C1218WriteRequest(tableid, data, offset)) data = self.recv() if data[0] != 0x00: status = data[0] details = (C1218_RESPONSE_CODES.get(status) or 'unknown response code') self.logger.error('could not write data to the table, error: ' + details) raise C1218WriteTableError('could not write data to the table, error: ' + details, status) return def run_procedure(self, process_number, std_vs_mfg, params=''): """ Initiate a C1219 procedure, the request is written to table 7 and the response is read from table 8. :param int process_number: The numeric procedure identifier (0 <= process_number <= 2047). :param bool std_vs_mfg: Whether the procedure is manufacturer specified or not. True is manufacturer specified. :param bytes params: The parameters to pass to the procedure initiation request. :return: A tuple of the result code and the response data. :rtype: tuple """ seqnum = random.randint(2, 254) self.logger.info('starting procedure: ' + str(process_number) + ' (' + hex(process_number) + ') sequence number: ' + str(seqnum) + ' (' + hex(seqnum) + ')') procedure_request = C1219ProcedureInit(self.c1219_endian, process_number, std_vs_mfg, 0, seqnum, params).build() self.set_table_data(7, procedure_request) response = self.get_table_data(8) if response[:3] == procedure_request[:3]: return response[3], response[4:] else: self.logger.error('invalid response from procedure response table (table #8)') raise C1219ProcedureError('invalid response from procedure response table (table #8)') termineter-1.0.4/lib/c1218/data.py000066400000000000000000000335741325174767400165150ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1218/data.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import binascii import struct from c1218.utilities import check_data_checksum, data_checksum, packet_checksum ACK = b'\x06' NACK = b'\x15' C1218_RESPONSE_CODES = { 0: 'ok (Acknowledge)', 1: 'err (Error)', 2: 'sns (Service Not Supported)', 3: 'isc (Insufficient Security Clearance)', 4: 'onp (Operation Not Possible)', 5: 'iar (Inappropriate Action Requested)', 6: 'bsy (Device Busy)', 7: 'dnr (Data Not Ready)', 8: 'dlk (Data Locked)', 9: 'rno (Renegotiate Request)', 10: 'isss (Invalid Service Sequence State)', 'ok': 0, 'err': 1, 'sns': 2, 'isc': 3, 'onp': 4, 'iar': 5, 'bsy': 6, 'dnr': 7, 'dlk': 8, 'rno': 9, 'isss': 10, } class C1218Request(object): def __repr__(self): return '<' + self.__class__.__name__ + ' >' def __str__(self): return self.build() def __len__(self): return len(self.build()) def build(self): raise NotImplementedError('no build method defined') @classmethod def from_bytes(cls, data): raise NotImplementedError('no parse method defined') @classmethod def from_hex(cls, data): return cls.from_bytes(binascii.a2b_hex(data)) @property def name(self): name = self.__class__.__name__ if not name.startswith('C1218'): raise Exception('class name does not start with \'C1218\'') if not name.endswith('Request'): raise Exception('class name does not end with \'Request\'') return name[5:-7] class C1218LogonRequest(C1218Request): logon = b'\x50' def __init__(self, username='', userid=0): self._userid = b'\x00\x00' self._username = b'\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20' self.set_username(username) self.set_userid(userid) def build(self): return self.logon + self._userid + self._username @classmethod def from_bytes(cls, data): if len(data) != 13: raise Exception('invalid data (size)') if data[0] != 0x50: raise Exception('invalid start byte') userid = struct.unpack('>H', data[1:3])[0] username = data[3:] return cls(username, userid) def set_userid(self, userid): if isinstance(userid, str) and userid.isdigit(): userid = int(userid) elif not isinstance(userid, int): ValueError('userid must be between 0x0000 and 0xffff') if not 0x0000 <= userid <= 0xffff: raise ValueError('userid must be between 0x0000 and 0xffff') self._userid = struct.pack('>H', userid) @property def userid(self): return struct.unpack('>H', self._userid)[0] def set_username(self, value): if len(value) > 10: raise ValueError('username must be 10 characters or less') if not isinstance(value, bytes): value = value.encode('utf-8') self._username = value + (b'\x20' * (10 - len(value))) @property def username(self): return self._username class C1218SecurityRequest(C1218Request): security = b'\x51' def __init__(self, password=''): self._password = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' self.set_password(password) def build(self): return self.security + self._password @classmethod def from_bytes(cls, data): if len(data) != 21: raise Exception('invalid data (size)') if data[0] != 0x51: raise Exception('invalid start byte') password = data[1:21] return cls(password) def set_password(self, value): if len(value) > 20: raise ValueError('password must be 20 byte or less') if not isinstance(value, bytes): value = value.encode('utf-8') self._password = value + (b'\x00' * (20 - len(value))) @property def password(self): return self._password class C1218LogoffRequest(C1218Request): logoff = b'\x52' def build(self): return self.logoff @classmethod def from_bytes(cls, data): if len(data) != 1: raise Exception('invalid data (size)') if data[0] != 0x52: raise Exception('invalid start byte') return cls() class C1218NegotiateRequest(C1218Request): negotiate = b'\x60' def __init__(self, pktsize, nbrpkt, baudrate=None): self._pktsize = b'\x01\x00' self._nbrpkt = 1 self._baudrate = b'' self.set_pktsize(pktsize) self.set_nbrpkt(nbrpkt) if baudrate: self.set_baudrate(baudrate) def build(self): pktsize = struct.pack('>H', self._pktsize) nbrpkt = struct.pack('B', self._nbrpkt) return self.negotiate + pktsize + nbrpkt + self._baudrate @classmethod def from_bytes(cls, data): if data[0] == 0x60: baud_included = False if len(data) != 4: raise Exception('invalid data (size)') elif data[0] < 0x6c and data[0] > 0x60: baud_included = True if len(data) != 5: raise Exception('invalid data (size)') else: raise Exception('invalid start byte') pktsize = struct.unpack('>H', data[1:3])[0] nbrpkt = data[3] baudrate = None if baud_included: baudrate = data[4] if baudrate == 0 or baudrate > 10: raise Exception('invalid data (invalid baudrate)') request = cls(pktsize, nbrpkt, baudrate) request.negotiate = struct.pack('B', data[0]) return request def set_pktsize(self, pktsize): self._pktsize = pktsize def set_nbrpkt(self, nbrpkt): self._nbrpkt = nbrpkt def set_baudrate(self, baudrate): c1218_baudrate_codes = {300: 1, 600: 2, 1200: 3, 2400: 4, 4800: 5, 9600: 6, 14400: 7, 19200: 8, 28800: 9, 57600: 10} if baudrate in c1218_baudrate_codes: self._baudrate = struct.pack('B', c1218_baudrate_codes[baudrate]) elif 0 < baudrate < 11: self._baudrate = struct.pack('B', baudrate) else: raise Exception('invalid data (invalid baudrate)') self.negotiate = b'\x61' class C1218WaitRequest(C1218Request): wait = b'\x70' def __init__(self, time=1): self._time = b'\x01' self.set_time(time) def build(self): return self.wait + self._time @classmethod def from_bytes(cls, data): if len(data) != 2: raise Exception('invalid data (size)') if data[0] != 0x70: raise Exception('invalid start byte') return cls(data[1]) def set_time(self, time): self._time = struct.pack('B', time) class C1218IdentRequest(C1218Request): ident = b'\x20' def build(self): return self.ident @classmethod def from_bytes(cls, data): if len(data) != 1: raise Exception('invalid data (size)') if data[0] != 0x20: raise Exception('invalid start byte') return cls() class C1218TerminateRequest(C1218Request): terminate = b'\x21' def build(self): return self.terminate @classmethod def from_bytes(cls, data): if len(data) != 1: raise Exception('invalid data (size)') if data[0] != 0x21: raise Exception('invalid start byte') return cls() class C1218ReadRequest(C1218Request): read = b'\x30' def __init__(self, tableid, offset=None, octetcount=None): self._tableid = b'\x00\x01' self._offset = b'' self._octetcount = b'' self.set_tableid(tableid) if offset is not None or octetcount is not None: self.read = b'\x3f' self.set_offset(offset or 0) self.set_octetcount(octetcount or 0) def build(self): return self.read + self._tableid + self._offset + self._octetcount @classmethod def from_bytes(cls, data): if (data[0] == 0x30 and len(data) < 3) or (data[0] == 0x3f and len(data) < 8): raise Exception('invalid data (size)') if data[0] != 0x30 and data[0] != 0x3f: raise Exception('invalid start byte') tableid = struct.unpack('>H', data[1:3])[0] if data[0] == 0x30: offset = None octetcount = None elif data[0] == 0x3f: offset = struct.unpack('>I', b'\x00' + data[3:6])[0] octetcount = struct.unpack('>H', data[6:8])[0] request = cls(tableid, offset, octetcount) request.read = struct.pack('B', data[0]) return request def set_tableid(self, tableid): self._tableid = struct.pack('>H', tableid) @property def tableid(self): return struct.unpack('>H', self._tableid)[0] def set_offset(self, offset): if self._octetcount and self._offset: self.read = b'\x3f' self._offset = struct.pack('>I', (offset & 0xffffff))[1:] @property def offset(self): if self._offset == b'': return None return struct.unpack('>I', b'\x00' + self._offset)[0] def set_octetcount(self, octetcount): if self._octetcount and self._offset: self.read = b'\x3f' self._octetcount = struct.pack('>H', octetcount) @property def octetcount(self): if self._octetcount == b'': return None return struct.unpack('>H', self._octetcount)[0] class C1218WriteRequest(C1218Request): write = b'\x40' def __init__(self, tableid, data, offset=None): self._tableid = b'\x00\x01' self._offset = b'' self._datalen = b'\x00\x00' self._data = b'' self._crc8 = b'' self.set_tableid(tableid) self.set_data(data) if offset is not None and offset != 0: self.write = b'\x4f' self.set_offset(offset) def build(self): packet = self.write packet += self._tableid packet += self._offset packet += self._datalen packet += self._data packet += data_checksum(self._data) return packet @classmethod def from_bytes(cls, data): if len(data) < 3: raise Exception('invalid data (size)') if data[0] != 0x40 and data[0] != 0x4f: raise Exception('invalid start byte') tableid = struct.unpack('>H', data[1:3])[0] chksum = data[-1] if data[0] == 0x40: table_data = data[5:-1] offset = None elif data[0] == 0x4f: table_data = data[8:-1] offset = struct.unpack('>I', b'\x00' + data[3:6])[0] if check_data_checksum(table_data, chksum): raise Exception('invalid check sum') request = cls(tableid, table_data, offset=offset) request.write = data[0] return request def set_tableid(self, tableid): self._tableid = struct.pack('>H', tableid) @property def tableid(self): return struct.unpack('>H', self._tableid)[0] def set_offset(self, offset): self._offset = struct.pack('>I', (offset & 0xffffff))[1:] @property def offset(self): if self._offset == b'': return None return struct.unpack('>I', b'\x00' + self._offset)[0] def set_data(self, data): self._data = data self._datalen = struct.pack('>H', len(data)) @property def data(self): return self._data class C1218Packet(C1218Request): start = b'\xee' identity = b'\x00' control = b'\x00' sequence = b'\x00' def __init__(self, data=None, control=None, length=None): self._length = b'\x00\x00' # can never exceed 8183 self._data = b'' if data: self.set_data(data) if length: self.set_length(length) if control: self.set_control(control) def __repr__(self): if isinstance(self._data, C1218Request): repr_data = repr(self._data) else: repr_data = '0x' + binascii.b2a_hex(self._data).decode('utf-8') crc = binascii.b2a_hex(packet_checksum(self.start + self.identity + self.control + self.sequence + self._length + self._data)).decode('utf-8') return '' @property def data(self): return self._data @data.setter def data(self, value): self.set_data(value) @classmethod def from_bytes(cls, data): if len(data) < 8: raise Exception('invalid data (size)') if data[0] != 0xee: raise Exception('invalid start byte') identity = data[1] control = data[2] sequence = data[3] length = struct.unpack('>H', data[4:6])[0] chksum = data[-2:] if packet_checksum(data[:-2]) != chksum: raise Exception('invalid check sum') data = data[6:-2] frame = C1218Packet(data, control, length) frame.identity = struct.pack('B', identity) frame.sequence = struct.pack('B', sequence) return frame def set_control(self, control): if isinstance(control, int): if not (0x00 < control < 0xff): raise ValueError('control must be between 0x00 and 0xff') control = struct.pack('B', control) if not isinstance(control, bytes): raise ValueError('control must be an int or bytes instance') self.control = control def set_data(self, data): if isinstance(data, C1218Request): data = data.build() elif not isinstance(data, bytes): data = data.encode('utf-8') self._data = data self.set_length(len(self._data)) def set_length(self, length): if length > 8183: raise ValueError('length can not exceed 8183') self._length = struct.pack('>H', length) def build(self): packet = self.start packet += self.identity packet += self.control packet += self.sequence packet += self._length packet += self._data packet += packet_checksum(packet) return packet C1218_REQUEST_IDS = { 0x20: C1218IdentRequest, 0x21: C1218TerminateRequest, 0x30: C1218ReadRequest, 0x3f: C1218ReadRequest, 0x40: C1218WriteRequest, 0x4f: C1218WriteRequest, 0x50: C1218LogonRequest, 0x51: C1218SecurityRequest, 0x52: C1218LogoffRequest, 0x60: C1218NegotiateRequest, 0x61: C1218NegotiateRequest, 0x70: C1218WaitRequest, } termineter-1.0.4/lib/c1218/errors.py000066400000000000000000000046111325174767400171060ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1218/errors.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals class C1218Error(Exception): """ This is a generic C1218 Error. """ def __init__(self, msg, code=None): self.message = msg self.code = code def __str__(self): return repr(self.message) class C1218IOError(C1218Error): """ Raised when there is a problem sending or receiving data. """ def __init__(self, msg): self.message = msg class C1218NegotiateError(C1218Error): """ Raised in response to an invalid reply to a Negotiate request. """ pass class C1218ReadTableError(C1218Error): """ Raised when a table is not successfully read. :param int errcode: The error that was returned while reading the table. """ pass class C1218WriteTableError(C1218Error): """ Raised when a table is not successfully written to. :param int errcode: The error that was returned while writing to the table. """ pass termineter-1.0.4/lib/c1218/urlhandler/000077500000000000000000000000001325174767400173565ustar00rootroot00000000000000termineter-1.0.4/lib/c1218/urlhandler/__init__.py000066400000000000000000000000001325174767400214550ustar00rootroot00000000000000termineter-1.0.4/lib/c1218/urlhandler/protocol_unix.py000066400000000000000000000110371325174767400226360ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1218/urlhandler/protocol_unix.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import logging import os import socket import time import urlparse from serial.serialutil import * from serial.urlhandler.protocol_socket import SocketSerial class UnixSerial(SocketSerial): """ Serial port implementation for unix sockets. """ def open(self): self.logger = None if self._port is None: raise SerialException('Port must be configured before it can be used.') if self._isOpen: raise SerialException('Port is already open.') try: self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) details = self.from_url(self.portstr) socket_path = details['path'] if details['mode'] == 'server': if os.path.exists(socket_path): os.unlink(socket_path) self._socket.bind(socket_path) self._socket.listen(1) self._server_socket = self._socket self._socket = self._server_socket.accept()[0] else: self._socket.connect(socket_path) except Exception as error: self._socket = None raise SerialException("Could not open port {0}: {1}".format(self.portstr, repr(error))) self._socket.settimeout(2) self._isOpen = True def close(self): if not self._isOpen: return if self._socket: try: self._socket.shutdown(socket.SHUT_RDWR) self._socket.close() except: pass self._socket = None if hasattr(self, '_server_socket'): socket_file = self._server_socket.getsockname() try: self._server_socket.shutdown(socket.SHUT_RDWR) self._server_socket.close() except: pass delattr(self, '_server_socket') os.unlink(socket_file) self._isOpen = False def from_url(self, url): details = {} url = urlparse.urlparse(url) options = urlparse.parse_qs(url.query) options_get = lambda key, default: options.get(key, [default])[0] details['path'] = url.path details['mode'] = options_get('mode', 'client') assert(details['mode'] in ('server', 'client')) log_level = options_get('logging', 'ERROR').upper() assert(log_level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')) self.logger = logging.getLogger('c1218.connection.socket.unix') self.logger.setLevel(getattr(logging, log_level)) return details def read(self, size=1): if not self._isOpen: raise portNotOpenError data = bytearray() if self._timeout != None: timeout = time.time() + self._timeout else: timeout = float('inf') while len(data) < size and time.time() < timeout: try: data = self._socket.recv(size - len(data)) except socket.timeout: continue except socket.error as error: raise SerialException('connection failed (' + str(error) + ')') return bytes(data) # assemble Serial class with the platform specific implementation and the base # for file-like behavior. for Python 2.6 and newer, that provide the new I/O # library, derive from io.RawIOBase try: import io except ImportError: # classic version with our own file-like emulation class Serial(UnixSerial, FileLike): pass else: # io library present class Serial(UnixSerial, io.RawIOBase): pass termineter-1.0.4/lib/c1218/utilities.py000066400000000000000000000040271325174767400176060ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1218/utilities.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import struct import crcelk def check_data_checksum(data, checksum): if isinstance(checksum, int): checksum = struct.pack('B', checksum) return data_checksum(data) == checksum def data_checksum(data): chksum = 0 for i in struct.unpack('B' * len(data), data): chksum += i chksum = ((chksum - 1) & 0xff) ^ 0xff return struct.pack('B', chksum) def packet_checksum(data): chksum = crcelk.CRC_HDLC.calc_bytes(data) return struct.pack('> 1) or 'Unknown' encoding = self.encoding self._nameplate_type = {0: 'Gas', 1: 'Water', 2: 'Electric'}.get(general_config_table[7]) or 'Unknown' self._id_form = general_config_table[1] & 32 self._std_version_no = general_config_table[11] self._std_revision_no = general_config_table[12] self._dim_std_tables_used = general_config_table[13] self._dim_mfg_tables_used = general_config_table[14] self._dim_std_proc_used = general_config_table[15] self._dim_mfg_proc_used = general_config_table[16] self._std_tables_used = [] tmp_data = general_config_table[19:] for p in range(self._dim_std_tables_used): for i in range(7): if tmp_data[p] & (2 ** i): self._std_tables_used.append(i + (p * 8)) self._mfg_tables_used = [] tmp_data = tmp_data[self._dim_std_tables_used:] for p in range(self._dim_mfg_tables_used): for i in range(7): if tmp_data[p] & (2 ** i): self._mfg_tables_used.append(i + (p * 8)) self._std_proc_used = [] tmp_data = tmp_data[self._dim_mfg_tables_used:] for p in range(self._dim_std_proc_used): for i in range(7): if tmp_data[p] & (2 ** i): self._std_proc_used.append(i + (p * 8)) self._mfg_proc_used = [] tmp_data = tmp_data[self._dim_std_proc_used:] for p in range(self._dim_mfg_proc_used): for i in range(7): if tmp_data[p] & (2 ** i): self._mfg_proc_used.append(i + (p * 8)) ### Parse GENERAL_MFG_ID_TBL ### self._manufacturer = general_mfg_table[0:4].rstrip().decode(encoding) self._ed_model = general_mfg_table[4:12].rstrip().decode(encoding) self._hw_version_no = general_mfg_table[12] self._hw_revision_no = general_mfg_table[13] self._fw_version_no = general_mfg_table[14] self._fw_revision_no = general_mfg_table[15] if self._id_form == 0: self._mfg_serial_no = general_mfg_table[16:32].strip() else: self._mfg_serial_no = general_mfg_table[16:24] self._mfg_serial_no = self._mfg_serial_no.decode(encoding) ### Parse ED_MODE_STATUS_TBL ### if mode_status_table: self._ed_mode = mode_status_table[0] self._std_status = struct.unpack(conn.c1219_endian + 'H', mode_status_table[1:3])[0] ### Parse DEVICE_IDENT_TBL ### if ident_table: if self._id_form == 0 and len(ident_table) != 20: raise C1219ParseError('expected to read more data from DEVICE_IDENT_TBL', DEVICE_IDENT_TBL) elif self._id_form != 0 and len(ident_table) != 10: raise C1219ParseError('expected to read more data from DEVICE_IDENT_TBL', DEVICE_IDENT_TBL) self._device_id = ident_table.strip().decode(encoding) def set_device_id(self, newid): if self._id_form == 0: newid += ' ' * (20 - len(newid)) else: newid += ' ' * (10 - len(newid)) newid = newid.encode(self.encoding) self.conn.set_table_data(DEVICE_IDENT_TBL, newid) self.conn.send(C1218WriteRequest(PROC_INITIATE_TBL, b'\x46\x08\x1c\x03\x0b\x0c\x09\x0f\x12')) data = self.conn.recv() if data != b'\x00': pass try: ident_table = self.conn.get_table_data(DEVICE_IDENT_TBL) except C1218ReadTableError: return 1 if self._id_form == 0 and len(ident_table) != 20: raise C1219ParseError('expected to read more data from DEVICE_IDENT_TBL', DEVICE_IDENT_TBL) elif self._id_form != 0 and len(ident_table) != 10: raise C1219ParseError('expected to read more data from DEVICE_IDENT_TBL', DEVICE_IDENT_TBL) if not ident_table.startswith(newid): return 2 self._device_id = newid return 0 @property def encoding(self): return {2: 'iso-8859-1', 4: 'utf-16', 5: 'utf-32'}.get(self._char_format, 'utf-8') @property def char_format(self): return self._char_format @property def nameplate_type(self): return self._nameplate_type @property def id_form(self): return self._id_form @property def std_version_no(self): return self._std_version_no @property def std_revision_no(self): return self._std_revision_no @property def std_tbls_used(self): return self._std_tables_used @property def mfg_tbls_used(self): return self._mfg_tables_used @property def std_proc_used(self): return self._std_proc_used @property def mfg_proc_used(self): return self._mfg_proc_used @property def manufacturer(self): return self._manufacturer @property def ed_model(self): return self._ed_model @property def hw_version_no(self): return self._hw_version_no @property def hw_revision_no(self): return self._hw_revision_no @property def fw_version_no(self): return self._fw_version_no @property def fw_revision_no(self): return self._fw_revision_no @property def mfg_serial_no(self): return self._mfg_serial_no @property def ed_mode(self): return self._ed_mode @property def std_status(self): return self._std_status @property def device_id(self): return self._device_id termineter-1.0.4/lib/c1219/access/local_display.py000066400000000000000000000074121325174767400216550ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1219/access/local_display.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # This library contains classes to facilitate retreiving complex C1219 # tables from a target device. Each parser expects to be passed a # connection object. Right now the connection object is a # c1218.connection.Connection instance, but anythin implementing the basic # methods should work. from __future__ import unicode_literals import collections import struct from c1219.access import BaseC1219TableAccess from c1219.constants import * DispListDescRcd = collections.namedtuple('DispListDescRcd', ('on_time', 'off_time', 'hold_time', 'default_list', 'nbr_items')) class C1219LocalDisplayAccess(BaseC1219TableAccess): # Corresponds To Decade 3x _tbl_props = ( 'on_time_flag', 'off_time_flag', 'hold_time_flag', 'nbr_disp_sources', 'width_disp_sources', 'nbr_pri_disp_list_items', 'nbr_pri_disp_lists', 'nbr_sec_disp_list_items', 'nbr_sec_disp_lists' ) def __init__(self, conn): """ Initializes a new instance of the class and reads tables from the corresponding decades to populate information. @type conn: c1218.connection.Connection @param conn: The driver to be used for interacting with the necessary tables. """ self.conn = conn act_disp = conn.get_table_data(ACT_DISP_TBL) unpacked = struct.unpack('> 4) & 0b1111 bfld = pri_disp_list[1] hold_time = bfld & 0b1111 default_list = (bfld >> 4) & 0b1111 nbr_items = pri_disp_list[2] self.pri_disp_list.append(DispListDescRcd(on_time, off_time, hold_time, default_list, nbr_items)) pri_disp_list = pri_disp_list[3:] self.pri_disp_sources = struct.unpack("{0}H".format(self._nbr_pri_disp_list_items), pri_disp_list) termineter-1.0.4/lib/c1219/access/log.py000066400000000000000000000125431325174767400176200ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1219/access/log.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # This library contains classes to facilitate retreiving complex C1219 # tables from a target device. Each parser expects to be passed a # connection object. Right now the connection object is a # c1218.connection.Connection instance, but anythin implementing the basic # methods should work. from __future__ import unicode_literals import struct from c1219.constants import * from c1219.data import get_history_entry_record from c1219.errors import C1219ParseError class C1219LogAccess(object): # Corresponds To Decade 7x """ This class provides generic access to the log data tables that are stored in the decade 7x tables. """ def __init__(self, conn): """ Initializes a new instance of the class and reads tables from the corresponding decades to populate information. @type conn: c1218.connection.Connection @param conn: The driver to be used for interacting with the necessary tables. """ self.conn = conn general_config_table = self.conn.get_table_data(GEN_CONFIG_TBL) actual_log_table = self.conn.get_table_data(ACT_LOG_TBL) history_log_data_table = self.conn.get_table_data(HISTORY_LOG_DATA_TBL) if len(general_config_table) < 19: raise C1219ParseError('expected to read more data from GEN_CONFIG_TBL', GEN_CONFIG_TBL) if len(actual_log_table) < 9: raise C1219ParseError('expected to read more data from ACT_LOG_TBL', ACT_LOG_TBL) if len(history_log_data_table) < 11: raise C1219ParseError('expected to read more data from HISTORY_LOG_DATA_TBL', HISTORY_LOG_DATA_TBL) ### Parse GEN_CONFIG_TBL ### tm_format = general_config_table[1] & 7 std_version_no = general_config_table[11] std_revision_no = general_config_table[12] ### Parse ACT_LOG_TBL ### log_flags = actual_log_table[0] event_number_flag = bool(log_flags & 1) hist_date_time_flag = bool(log_flags & 2) hist_seq_nbr_flag = bool(log_flags & 4) hist_inhibit_ovf_flag = bool(log_flags & 8) event_inhibit_ovf_flag = bool(log_flags & 16) nbr_std_events = actual_log_table[1] nbr_mfg_events = actual_log_table[2] hist_data_length = actual_log_table[3] event_data_length = actual_log_table[4] self.__nbr_history_entries__, self._nbr_event_entries = struct.unpack(self.conn.c1219_endian + 'HH', actual_log_table[5:9]) if std_version_no > 1: ext_log_flags = actual_log_table[9] nbr_program_tables = struct.unpack(self.conn.c1219_endian + 'H', actual_log_table[10:12]) else: ext_log_flags = None nbr_program_tables = None ### Parse HISTORY_LOG_DATA_TBL ### order_flag = history_log_data_table[0] & 1 overflow_flag = history_log_data_table[0] & 2 list_type_flag = history_log_data_table[0] & 4 inhibit_overflow_flag = history_log_data_table[0] & 8 nbr_valid_entries, last_entry_element, last_entry_seq_num, nbr_unread_entries = struct.unpack(self.conn.c1219_endian + 'HHIH', history_log_data_table[1:11]) log_data = history_log_data_table[11:] size_of_log_rcd = hist_data_length + 4 # hist_data_length + (SIZEOF(USER_ID) + SIZEOF(TABLE_IDB_BFLD)) if hist_date_time_flag: size_of_log_rcd += LTIME_LENGTH[tm_format] if event_number_flag: size_of_log_rcd += 2 if hist_seq_nbr_flag: size_of_log_rcd += 2 if len(log_data) != (size_of_log_rcd * self.nbr_history_entries): raise C1219ParseError('log data size does not align with expected record size, possibly corrupt', HISTORY_LOG_DATA_TBL) entry_idx = 0 self._logs = [] while entry_idx < self.nbr_history_entries: self._logs.append(get_history_entry_record(self.conn.c1219_endian, hist_date_time_flag, tm_format, event_number_flag, hist_seq_nbr_flag, log_data[:size_of_log_rcd])) log_data = log_data[size_of_log_rcd:] entry_idx += 1 @property def nbr_event_entries(self): return self._nbr_event_entries @property def nbr_history_entries(self): return self.__nbr_history_entries__ @property def logs(self): return self._logs termineter-1.0.4/lib/c1219/access/security.py000066400000000000000000000127221325174767400207050ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1219/access/security.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # This library contains classes to facilitate retreiving complex C1219 # tables from a target device. Each parser expects to be passed a # connection object. Right now the connection object is a # c1218.connection.Connection instance, but anythin implementing the basic # methods should work. from __future__ import unicode_literals import struct from c1218.errors import C1218ReadTableError from c1219.constants import * from c1219.data import get_table_idcb_field from c1219.errors import C1219ParseError class C1219SecurityAccess(object): # Corresponds To Decade 4x """ This class provides generic access to the security configuration tables that are stored in the decade 4x tables. """ def __init__(self, conn): """ Initializes a new instance of the class and reads tables from the corresponding decades to populate information. @type conn: c1218.connection.Connection @param conn: The driver to be used for interacting with the necessary tables. """ self.conn = conn act_security_table = conn.get_table_data(ACT_SECURITY_LIMITING_TBL) security_table = conn.get_table_data(SECURITY_TBL) access_ctl_table = conn.get_table_data(ACCESS_CONTROL_TBL) try: key_table = conn.get_table_data(KEY_TBL) except C1218ReadTableError: key_table = None if len(act_security_table) < 6: raise C1219ParseError('expected to read more data from ACT_SECURITY_LIMITING_TBL', ACT_SECURITY_LIMITING_TBL) ### Parse ACT_SECURITY_LIMITING_TBL ### self._nbr_passwords = act_security_table[0] self._password_len = act_security_table[1] self._nbr_keys = act_security_table[2] self._key_len = act_security_table[3] self._nbr_perm_used = struct.unpack(self.conn.c1219_endian + 'H', act_security_table[4:6])[0] ### Parse SECURITY_TBL ### if len(security_table) != ((self.nbr_passwords * self.password_len) + self.nbr_passwords): raise C1219ParseError('expected to read more data from SECURITY_TBL', SECURITY_TBL) self._passwords = {} tmp = 0 while tmp < self.nbr_passwords: self._passwords[tmp] = {'idx': tmp, 'password': security_table[:self.password_len], 'groups': security_table[self.password_len]} security_table = security_table[self.password_len + 1:] tmp += 1 ### Parse ACCESS_CONTROL_TBL ### if len(access_ctl_table) != (self.nbr_perm_used * 4): raise C1219ParseError('expected to read more data from ACCESS_CONTROL_TBL', ACCESS_CONTROL_TBL) self._table_permissions = {} self._procedure_permissions = {} tmp = 0 while tmp < self.nbr_perm_used: (proc_nbr, std_vs_mfg, proc_flag, flag1, flag2, flag3) = get_table_idcb_field(self.conn.c1219_endian, access_ctl_table) if proc_flag: self._procedure_permissions[proc_nbr] = {'idx': proc_nbr, 'mfg': std_vs_mfg, 'anyread': flag1, 'anywrite': flag2, 'read': access_ctl_table[2], 'write': access_ctl_table[3]} else: self._table_permissions[proc_nbr] = {'idx': proc_nbr, 'mfg': std_vs_mfg, 'anyread': flag1, 'anywrite': flag2, 'read': access_ctl_table[2], 'write': access_ctl_table[3]} access_ctl_table = access_ctl_table[4:] tmp += 1 ### Parse KEY_TBL ### self._keys = {} if key_table is not None: if len(key_table) != (self.nbr_keys * self.key_len): raise C1219ParseError('expected to read more data from KEY_TBL', KEY_TBL) tmp = 0 while tmp < self.nbr_keys: self._keys[tmp] = key_table[:self.key_len] key_table = key_table[self.key_len:] tmp += 1 @property def nbr_passwords(self): return self._nbr_passwords @property def password_len(self): return self._password_len @property def nbr_keys(self): return self._nbr_keys @property def key_len(self): return self._key_len @property def nbr_perm_used(self): return self._nbr_perm_used @property def passwords(self): return self._passwords @property def table_permissions(self): return self._table_permissions @property def procedure_permissions(self): return self._procedure_permissions @property def keys(self): return self._keys termineter-1.0.4/lib/c1219/access/telephone.py000066400000000000000000000166311325174767400210240ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1219/access/telephone.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # This library contains classes to facilitate retreiving complex C1219 # tables from a target device. Each parser expects to be passed a # connection object. Right now the connection object is a # c1218.connection.Connection instance, but anythin implementing the basic # methods should work. from __future__ import unicode_literals import struct from c1219.constants import * from c1219.errors import C1219ParseError, C1219ProcedureError class C1219TelephoneAccess(object): # Corresponds To Decade 9x """ This class provides generic access to the telephone/modem configuration tables that are stored in the decade 9x tables. """ def __init__(self, conn): """ Initializes a new instance of the class and reads tables from the corresponding decades to populate information. @type conn: c1218.connection.Connection @param conn: The driver to be used for interacting with the necessary tables. """ self._global_bit_rate = None self._originate_bit_rate = None self._answer_bit_rate = None self._prefix_number = '' self._primary_phone_number_idx = None self._secondary_phone_number_idx = None self.conn = conn actual_telephone_table = self.conn.get_table_data(ACT_TELEPHONE_TBL) global_parameters_table = self.conn.get_table_data(GLOBAL_PARAMETERS_TBL) originate_parameters_table = self.conn.get_table_data(ORIGINATE_PARAMETERS_TBL) originate_schedule_table = self.conn.get_table_data(ORIGINATE_SCHEDULE_TBL) answer_parameters_table = self.conn.get_table_data(ANSWER_PARAMETERS_TBL) if (actual_telephone_table) < 14: raise C1219ParseError('expected to read more data from ACT_TELEPHONE_TBL', ACT_TELEPHONE_TBL) ### Parse ACT_TELEPHONE_TBL ### use_extended_status = bool(actual_telephone_table[0] & 128) prefix_length = actual_telephone_table[4] nbr_originate_numbers = actual_telephone_table[5] phone_number_length = actual_telephone_table[6] bit_rate_settings = (actual_telephone_table[1] >> 3) & 3 # not the actual settings but rather where they are defined self._can_answer = bool(actual_telephone_table[0] & 1) self._use_extended_status = use_extended_status self._nbr_originate_numbers = nbr_originate_numbers ### Parse GLOBAL_PARAMETERS_TBL ### self._psem_identity = global_parameters_table[0] if bit_rate_settings == 1: if len(global_parameters_table) < 5: raise C1219ParseError('expected to read more data from GLOBAL_PARAMETERS_TBL', GLOBAL_PARAMETERS_TBL) self._global_bit_rate = struct.unpack(conn.c1219_endian + 'I', global_parameters_table[1:5])[0] ### Parse ORIGINATE_PARAMETERS_TBL ### if bit_rate_settings == 2: self._originate_bit_rate = struct.unpack(conn.c1219_endian + 'I', originate_parameters_table[0:4])[0] originate_parameters_table = originate_parameters_table[4:] self._dial_delay = originate_parameters_table[0] originate_parameters_table = originate_parameters_table[1:] if prefix_length != 0: self._prefix_number = originate_parameters_table[:prefix_length] originate_parameters_table = originate_parameters_table[prefix_length:] self._originating_numbers = {} tmp = 0 while tmp < self._nbr_originate_numbers: self._originating_numbers[tmp] = {'idx': tmp, 'number': originate_parameters_table[:phone_number_length], 'status': None} originate_parameters_table = originate_parameters_table[phone_number_length:] tmp += 1 ### Parse ORIGINATE_SHCEDULE_TBL ### primary_phone_number_idx = originate_schedule_table[0] & 7 secondary_phone_number_idx = (originate_schedule_table[0] >> 4) & 7 if primary_phone_number_idx < 7: self._primary_phone_number_idx = primary_phone_number_idx if secondary_phone_number_idx < 7: self._secondary_phone_number_idx = secondary_phone_number_idx ### Prase ANSWER_PARAMETERS_TBL ### if bit_rate_settings == 2: self._answer_bit_rate = struct.unpack(conn.c1219_endian + 'I', answer_parameters_table[0:4])[0] self.update_last_call_statuses() def initiate_call(self, number=None, idx=None): if number: idx = None for tmpidx in self._originating_numbers.keys(): if self._originating_numbers[tmpidx]['number'] == number: idx = tmpidx if idx is None: raise C1219ProcedureError('target phone number not found in originating numbers') if not idx in self._originating_numbers.keys(): raise C1219ProcedureError('phone number index not within originating numbers range') return self.initiate_call_ex(self.conn, idx) @staticmethod def initiate_call_ex(conn, idx): return conn.run_procedure(20, False, struct.pack('B', idx)) def update_last_call_statuses(self): tmp = 0 call_status_table = self.conn.get_table_data(CALL_STATUS_TBL) if (len(call_status_table) % self.nbr_originate_numbers) != 0: raise C1219ParseError('expected to read more data from CALL_STATUS_TBL', CALL_STATUS_TBL) call_status_rcd_length = (len(call_status_table) / self.nbr_originate_numbers) while tmp < self.nbr_originate_numbers: self._originating_numbers[tmp]['status'] = call_status_table[0] call_status_table = call_status_table[call_status_rcd_length:] tmp += 1 @property def answer_bit_rate(self): return self._answer_bit_rate @property def can_answer(self): return self._can_answer @property def dial_delay(self): return self._dial_delay @property def global_bit_rate(self): return self._global_bit_rate @property def nbr_originate_numbers(self): return self._nbr_originate_numbers @property def originate_bit_rate(self): return self._originate_bit_rate @property def originating_numbers(self): return self._originating_numbers @property def prefix_number(self): return self._prefix_number @property def primary_phone_number_idx(self): return self._primary_phone_number_idx @property def psem_identity(self): return self._psem_identity @property def secondary_phone_number_idx(self): return self._secondary_phone_number_idx @property def use_extended_status(self): return self._use_extended_status termineter-1.0.4/lib/c1219/constants.py000066400000000000000000000265021325174767400176120ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1219/constants.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals GEN_CONFIG_TBL = 0 GENERAL_MFG_ID_TBL = 1 ED_MODE_STATUS_TBL = 3 DEVICE_IDENT_TBL = 5 PROC_INITIATE_TBL = 7 PROC_RESPONSE_TBL = 8 DIM_REGS_TBL = 20 ACT_REGS_TBL = 21 DATA_SELECTION_TBL = 22 CURRENT_REG_DATA_TBL = 23 PREVIOUS_SEASON_DATA_TBL = 24 PREVIOUS_DEMAND_RESET_DATA_TBL = 25 SELF_READ_DATA_TBL = 26 PRESENT_REGISTER_SELECT_TBL = 27 PRESENT_REGISTER_DATA_TBL = 28 DIM_DISP_TBL = 30 ACT_DISP_TBL = 31 DISP_SOURCE_TBL = 32 PRI_DISP_LIST_TBL = 33 SEC_DISP_LIST_TBL = 34 DIM_SECURITY_LIMITING_TBL = 40 ACT_SECURITY_LIMITING_TBL = 41 SECURITY_TBL = 42 DEFAULT_ACCESS_CONTROL_TBL = 43 ACCESS_CONTROL_TBL = 44 KEY_TBL = 45 ACT_LOG_TBL = 71 HISTORY_LOG_DATA_TBL = 74 ACT_TELEPHONE_TBL = 91 GLOBAL_PARAMETERS_TBL = 92 ORIGINATE_PARAMETERS_TBL = 93 ORIGINATE_SCHEDULE_TBL = 94 ANSWER_PARAMETERS_TBL = 95 CALL_PURPOSE_TBL = 96 CALL_STATUS_TBL = 97 ORIGINATE_STATUS_TBL = 98 C1219_TABLES = { 0: 'General Configuration Table', 1: 'General Manufacturer Identification Table', 2: 'Device Nameplate Table', 3: 'ED_MODE Status Table', 4: 'Pending Status Table', 5: 'Device Identification Table', 6: 'Utility Information Table', 7: 'Procedure Initiate Table', 8: 'Procedure Response Table', 10: 'Dimension Sources Limiting Table', 11: 'Actual Sources Limiting Table', 12: 'Unit of Measure Entry Table', 13: 'Demand Control Table', 14: 'Data Control Table', 15: 'Constants Table', 16: 'Source Definition Table', 17: 'Transformer Loss Compensation Table', 20: 'Dimension Register Table', 21: 'Actual Register Table', 22: 'Data Selection Table', 23: 'Current Register Data Table', 24: 'Previous Season Data Table', 25: 'Previous Demand Reset Data Table', 26: 'Self Read Data Table', 27: 'Present Register Selection Table', 28: 'Present Register Data Table', 30: 'Dimension Display Limits Table', 31: 'Actual Display Limiting Table', 32: 'Display Source Table', 33: 'Primary Display List Table', 34: 'Secondary Display List Table', 40: 'Dimension Security Limiting Table', 41: 'Actual Security Limiting Table', 42: 'Security Table', 43: 'Default Access Control Table', 44: 'Access Control Table', 45: 'Key Table', 50: 'Dimension Time and TOU Table', 51: 'Actual Time and TOU Table', 52: 'Clock Table', 53: 'Time Offset Table', 54: 'Calender Table', 55: 'Clock State Table', 56: 'Time Remaining Table', 57: 'Precision Clock State Table', 60: 'Dimension Load Profile Table', 61: 'Actual Load Profile Table', 62: 'Load Profile Control Table', 63: 'Load Profile Status Table', 64: 'Load Profile Data Set 1 Table', 65: 'Load Profile Data Set 2 Table', 66: 'Load Profile Data Set 3 Table', 67: 'Load Profile Data Set 4 Table', 70: 'Dimension Log Table', 71: 'Actual Log Table', 72: 'Events Identification Table', 73: 'History Log Control Table', 74: 'History Log Data Table', 75: 'Event Log Control Table', 76: 'Event Log Data Table', 77: 'Event Log and Signatures Enable Table', 78: 'End Device Program State Table', 79: 'Event Counters Table', 80: 'Dimension User Defined Tables Function Limiting Table', 81: 'Actual User Defined Tables Function Limiting Table', 82: 'User Defined Tables List Table', 83: 'User Defined Tables Selection Table', 84: 'User Defined Table 0 Table', 85: 'User Defined Table 1 Table', 86: 'User Defined Table 2 Table', 87: 'User Defined Table 3 Table', 88: 'User Defined Table 4 Table', 89: 'User Defined Table 5 Table', 90: 'Dimension Telephone Table', 91: 'Actual Telephone Table', 92: 'Global Parameters Table', 93: 'Originate Parameters Table', 94: 'Originate Schedule Table', 95: 'Answer Parameters', 96: 'Call Purpose', 97: 'Call Status', 98: 'Originate Status', 100: 'Dimension Extended Source Limiting Table', 101: 'Actual Extending Source Limiting Table', 102: 'Source Information Table', 103: 'External Scaling Table', 104: 'Demand Control Table', 105: 'Transformer Loss Compensation', 106: 'Gas Constant AGA3', 107: 'Gas Constant AGA7', 110: 'Dimension Load Control', 111: 'Actual Load Control Limiting Table', 112: 'Load Control Status', 113: 'Load Control Configuration', 114: 'Load Control Schedule', 115: 'Load Control Conditions', 116: 'Prepayment Status', 117: 'Prepayment Control', 118: 'Billing Control', 140: 'Extended User-defined Tables Function Limiting Table', 141: 'Extended User-defined Tables Actual Limits Table', 142: 'Extended User-defined Selections Table', 143: 'Extended User-defined Constants Table', 150: 'Quality of Service Dimension Limits Table', 151: 'Actual Quality of Service Limiting Table', 152: 'Quality of Service Control Table', 153: 'Quality of Service Incidents Table', 154: 'Quality of Servce Log Table', 155: 'Asynchronous Time-Domain Waveforms Table', 156: 'Asynchronous Frequency-Domain Spectrum Table', 157: 'Periodic Time Domain Waveforms Table', 158: 'Periodic Frequency-Domain Spectrum Table', 160: 'Dimension One-Way', 161: 'Actual One-Way', 162: 'One-Way Control Table', 163: 'One-Way Data Table', 164: 'One-Way Commands/Responses/Extended User-defined Tables Table', } C1219_EVENT_CODES = { 0: 'No Event', 1: 'Primary Power Down', 2: 'Primary Power Up', 3: 'Time Changed (Time-stamp is old time)', 4: 'Time Changed (Time-stamp is new time)', 5: 'Time Changed (Time-stamp is old time in STIME_DATE format)', 6: 'Time Changed (Time-stamp new time in STIME_DATE format)', 7: 'End Device Accessed for Read', 8: 'End Device Accessed for Write', 9: 'Procedure Invoked', 10: 'Table Written To', 11: 'End Device Programmed', 12: 'Communication Terminated Normally', 13: 'Communication Terminated Abnormally', 14: 'Reset List Pointers', 15: 'Updated List Pointers', 16: 'History Log Cleared', 17: 'History Log Pointers Updated', 18: 'Event Log Cleared', 19: 'Event Log Pointers Updated', 20: 'Demand Reset Occurred', 21: 'Self Read Occurred', 22: 'Daylight Saving Time On', 23: 'Daylight Savings Time Off', 24: 'Season Changed', 25: 'Rate Change', 26: 'Special Schedule Activated', 27: 'Tier Switch / Change', 28: 'Pending Table Activated', 29: 'Pending Table Activation Cleared', 30: 'Metering mode started', 31: 'Metering mode stopped', 32: 'Test mode started', 33: 'Test mode stopped', 34: 'Meter shop mode started', 35: 'Meter shop mode stopped', 36: 'End Device reprogrammed', 37: 'Configuration error detected', 38: 'Self check error detected', 39: 'RAM failure detected', 40: 'ROM failure detected', 41: 'Nonvolatile memory failure detected', 42: 'Clock error detected', 43: 'Measurement error detected', 44: 'Low battery detected', 45: 'Low loss potential detected', 46: 'Demand overload detected', 47: 'Tamper attempt detected', 48: 'Reverse rotation detected', 49: 'Control point changed by a command', 50: 'Control point changed by the schedule', 51: 'Control point changed by a condition', 52: 'Control point changed for the prepayment', 53: 'Added to remaining credit', 54: 'Subtracted from remaining credit', 55: 'Adjusted the remaining credit', 56: 'End Device sealed', 57: 'End Device unsealed', 58: 'Procedure Invoked (with values)', 59: 'Table Written To (with values)', 60: 'End Device Programmed (with values)', 61: 'End Device sealed (with values)', 62: 'End Device unsealed (with values)', 63: 'Procedure Invoked (with signature)', 64: 'Table Written To (with signature)', 65: 'End Device Programmed (with signature)', 66: 'End Device sealed (with signature)', 67: 'End Device unsealed (with signature)', 68: 'Procedure Invoked (with signature and values)', 69: 'Table Written To(with signature and values)', 70: 'End Device Programmed (with signature and values)', 71: 'End Device sealed(with signature and values)', 72: 'End Device unsealed(with signature and values)', 73: 'Read Secured Table', 74: 'Read Secured Register', 75: 'Read Secured Table (with values)', 76: 'Read Secured Register (with values)' } C1219_PROCEDURE_NAMES = { 0: 'Cold Start', 1: 'Warm Start', 2: 'Save Configuration', 3: 'Clear Data', 4: 'Reset List Pointers', 5: 'Update Last Read Entry', 6: 'Change End Device Mode', 7: 'Clear Standard Status Flags', 8: 'Clear Manufacturer Flags', 9: 'Remote Reset', 10: 'Set Date and/or Time', 11: 'Execute Diagnostics Procedure', 12: 'Activate All Pending Tables', 13: 'Activate Specific Pending Table(s)', 14: 'Clear All Pending Tables', 15: 'Clear Specific Pending Table(s)', 16: 'Start Load Profile', 17: 'Stop Load Profile', 18: 'Log In', 19: 'Log Out', 20: 'Initiate an Immediate Call', 21: 'Direct Load Control', 22: 'Modify Credit', 27: 'Clear Pending Call Status', 28: 'Start Quality of Service Monitors', 29: 'Stop Quality of Service Monitors', 30: 'Start Secured Registers', 31: 'Stop Secured Registers', 32: 'Set Precision Date and/or Time' } C1219_PROC_RESULT_CODES = { 0: 'Procedure completed', 1: 'Procedure accepted but not fully completed', 2: 'Invalid parameter for known procedure, procedure was ignored', 3: 'Procedure conflicts with current device setup, procedure was ignored', 4: 'Timing constraint, procedure was ignored', 5: 'No authorization for requested procedure, procedure was ignored', 6: 'Unrecognized procedure, procedure was ignored' } C1219_CALL_STATUS_FLAGS = { 0: 'No phone call made', 1: 'Phone call in progress', 2: 'Dialing', 3: 'Waiting for a connection', 4: 'Communicating', 5: 'Completed normally', 6: 'Not completed', 7: 'Not completed, Line busy', 8: 'Not completed, No dial tone', 9: 'Not completed, Line cut', 10: 'Not completed, No Connection', 11: 'Not completed, No modem response' } C1219_METER_MODE_FLAGS = { 1: 'METERING', 2: 'TEST', 4: 'METERSHOP', 8: 'FACTORY' } C1219_METER_MODE_NAMES = { 'METERING': 1, 'TEST': 2, 'METERSHOP': 4, 'FACTORY': 8 } LTIME_LENGTH = {0: 0, 1: 6, 2: 6, 3: 5, 4: 4} MONTHS = { 1: 'JAN', 2: 'FEB', 3: 'MAR', 4: 'APR', 5: 'MAY', 6: 'JUN', 7: 'JUL', 8: 'AUG', 9: 'SEP', 10: 'OCT', 11: 'NOV', 12: 'DEC' } termineter-1.0.4/lib/c1219/data.py000066400000000000000000000166421325174767400165130ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1219/data.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import struct import time from c1219.constants import * def format_ltime(endianess, tm_format, data): """ Return data formatted into a human readable time stamp. :param str endianess: The endianess to use when packing values ('>' or '<') :param int tm_format: The format that the data is packed in, this typically corresponds with the value in the GEN_CONFIG_TBL (table #0) (1 <= tm_format <= 4) :param bytes data: The packed and machine-formatted data to parse :rtype: str """ if tm_format == 0: return '' elif tm_format == 1 or tm_format == 2: # I can't find solid documentation on the BCD data-type y = data[0] year = '????' if 90 <= y <= 99: year = '19' + str(y) elif 0 <= y <= 9: year = '200' + str(y) elif 10 <= y <= 89: year = '20' + str(y) month = data[1] day = data[2] hour = data[3] minute = data[4] second = data[5] elif tm_format == 3 or tm_format == 4: if tm_format == 3: u_time = float(struct.unpack(endianess + 'I', data[0:4])[0]) second = float(data[4]) final_time = time.gmtime((u_time * 60) + second) elif tm_format == 4: final_time = time.gmtime(float(struct.unpack(endianess + 'I', data[0:4])[0])) year = str(final_time.tm_year) month = str(final_time.tm_mon) day = str(final_time.tm_mday) hour = str(final_time.tm_hour) minute = str(final_time.tm_min) second = str(final_time.tm_sec) return "{0} {1} {2} {3}:{4}:{5}".format((MONTHS.get(month) or 'UNKNOWN'), day, year, hour, minute, second) def get_history_entry_record(endianess, hist_date_time_flag, tm_format, event_number_flag, hist_seq_nbr_flag, data): """ Return data formatted into a log entry. :param str endianess: The endianess to use when packing values ('>' or '<') :param bool hist_date_time_flag: Whether or not a time stamp is included. :param int tm_format: The format that the data is packed in, this typically corresponds with the value in the GEN_CONFIG_TBL (table #0) (1 <= tm_format <= 4) :param bool event_number_flag: Whether or not an event number is included. :param bool hist_seq_nbr_flag: Whether or not an history sequence number is included. :param str data: The packed and machine-formatted data to parse :rtype: dict """ rcd = {} if hist_date_time_flag: tmstmp = format_ltime(endianess, tm_format, data[0:LTIME_LENGTH.get(tm_format)]) if tmstmp: rcd['Time'] = tmstmp data = data[LTIME_LENGTH.get(tm_format):] if event_number_flag: rcd['Event Number'] = struct.unpack(endianess + 'H', data[:2])[0] data = data[2:] if hist_seq_nbr_flag: rcd['History Sequence Number'] = struct.unpack(endianess + 'H', data[:2])[0] data = data[2:] rcd['User ID'] = struct.unpack(endianess + 'H', data[:2])[0] rcd['Procedure Number'], rcd['Std vs Mfg'] = get_table_idbb_field(endianess, data[2:4])[:2] rcd['Arguments'] = data[4:] return rcd def get_table_idbb_field(endianess, data): """ Return data from a packed TABLE_IDB_BFLD bit-field. :param str endianess: The endianess to use when packing values ('>' or '<') :param str data: The packed and machine-formatted data to parse :rtype: tuple :return: Tuple of (proc_nbr, std_vs_mfg) """ bfld = struct.unpack(endianess + 'H', data[:2])[0] proc_nbr = bfld & 0x7ff std_vs_mfg = bool(bfld & 0x800) selector = (bfld & 0xf000) >> 12 return (proc_nbr, std_vs_mfg, selector) def get_table_idcb_field(endianess, data): """ Return data from a packed TABLE_IDC_BFLD bit-field. :param str endianess: The endianess to use when packing values ('>' or '<') :param str data: The packed and machine-formatted data to parse :rtype: tuple :return: Tuple of (proc_nbr, std_vs_mfg, proc_flag, flag1, flag2, flag3) """ bfld = struct.unpack(endianess + 'H', data[:2])[0] proc_nbr = bfld & 2047 std_vs_mfg = bool(bfld & 2048) proc_flag = bool(bfld & 4096) flag1 = bool(bfld & 8192) flag2 = bool(bfld & 16384) flag3 = bool(bfld & 32768) return (proc_nbr, std_vs_mfg, proc_flag, flag1, flag2, flag3) class C1219ProcedureInit(object): """ A C1219 Procedure Request, this data is written to table 7 in order to start a procedure. :param str endianess: The endianess to use when packing values ('>' or '<') :param int table_proc_nbr: The numeric procedure identifier (0 <= table_proc_nbr <= 2047). :param bool std_vs_mfg: Whether the procedure is manufacturer specified or not. True is manufacturer specified. :param int selector: Controls how data is returned (0 <= selector <= 15). 0: Post response in PROC_RESPONSE_TBL (#8) on completion. 1: Post response in PROC_RESPONSE_TBL (#8) on exception. 2: Do not post response in PROC_RESPONSE_TBL (#8). 3: Post response in PROC_RESPONSE_TBL (#8) immediately and another response in PROC_RESPONSE_TBL (#8) on completion. 4-15: Reserved. :param int seqnum: The identifier for this procedure to be used for coordination (0x00 <= seqnum <= 0xff). :param str params: The parameters to pass to the procedure initiation request. """ def __init__(self, endianess, table_proc_nbr, std_vs_mfg, selector, seqnum, params=b''): mfg_defined = 0 if std_vs_mfg: mfg_defined = 1 self.mfg_defined = bool(mfg_defined) self.selector = selector mfg_defined <<= 11 selector <<= 12 self.table_idb_bfld = struct.pack(endianess + 'H', (table_proc_nbr | mfg_defined | selector)) self.endianess = endianess self.proc_nbr = table_proc_nbr self.seqnum = seqnum self.params = params def __repr__(self): return "<{0} mfg_defined={1} proc_nbr={2} >".format(self.__class__.__name__, self.mfg_defined, self.proc_nbr) def __str__(self): return self.build() def build(self): return self.table_idb_bfld + struct.pack('B', self.seqnum) + self.params @classmethod def from_bytes(cls, endianess, data): if len(data) < 3: raise Exception('invalid data (size)') proc_nbr, std_vs_mfg, selector = get_table_idbb_field(endianess, data[0:2]) seqnum = data[2] return cls(endianess, proc_nbr, std_vs_mfg, selector, seqnum, data[3:]) termineter-1.0.4/lib/c1219/errors.py000066400000000000000000000041131325174767400171040ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1219/errors.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals class C1219ProcedureError(Exception): """ Raised when a procedure can not be executed. """ def __init__(self, msg): self.message = msg def __str__(self): return repr(self.message) class C1219ParseError(Exception): """ Raised when there is an error parsing data. :param int tableid: If the data originated from a table, the faulty table can be specified here. """ def __init__(self, msg, tableid=None): self.message = msg self.tableid = tableid def __str__(self): return repr(self.message) termineter-1.0.4/lib/c1222/000077500000000000000000000000001325174767400152115ustar00rootroot00000000000000termineter-1.0.4/lib/c1222/__init__.py000066400000000000000000000000001325174767400173100ustar00rootroot00000000000000termineter-1.0.4/lib/c1222/connection.py000066400000000000000000000117741325174767400177340ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1222/connection.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import logging import random import select import socket from c1222.data import * from c1222.errors import C1222IOError if hasattr(logging, 'NullHandler'): logging.getLogger('c1222').addHandler(logging.NullHandler()) def sock_read_ready(socket, timeout): readys = select.select([socket.fileno()], [], [], timeout) return len(readys[0]) == 1 class Connection(object): def __init__(self, host, called_ap, calling_ap, enable_cache=True, bind_host=('', 1153)): self.logger = logging.getLogger('c1222.connection') self.loggerio = logging.getLogger('c1222.connection.io') self.read_timeout = 3.0 self.server_sock_h = None self.read_sock_h = None self.bind_host = bind_host self.start_listener() self.host = host self.sock_h = socket.create_connection(host) self.logger.debug('successfully connected to: ' + host[0] + ':' + str(host[1])) if not isinstance(called_ap, C1222CalledAPTitle): called_ap = C1222CalledAPTitle(called_ap) self.called_ap = called_ap if not isinstance(calling_ap, C1222CallingAPTitle): calling_ap = C1222CallingAPTitle(calling_ap) self.calling_ap = calling_ap self.logged_in = False self._initialized = False self.c1219_endian = '<' self.caching_enabled = enable_cache self._cacheable_tables = [0, 1] self._table_cache = {} if enable_cache: self.logger.info('selective table caching has been enabled') def start_listener(self): if self.server_sock_h is not None: raise Exception('server socket already created') self.server_sock_h = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_sock_h.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_sock_h.bind(self.bind_host) self.server_sock_h.listen(1) def stop_listener(self): if self.server_sock_h is None: return self.server_sock_h.close() self.server_sock_h = None def recv(self): if self.read_sock_h is None: readable = select.select([self.sock_h.fileno(), self.server_sock_h.fileno()], [], [], self.read_timeout) readable = readable[0] if len(readable) > 1: raise C1222IOError('too many file handles available for reading') if len(readable) < 1: raise C1222IOError('not enough file handles available for reading') readable = readable[0] if readable == self.server_sock_h.fileno(): (self.read_sock_h, addr) = self.server_sock_h.accept() self.logger.info("received connection from {0}:{1}".format(addr[0], addr[1])) self.stop_listener() elif readable == self.sock_h.fileno(): self.read_sock_h = self.sock_h self.stop_listener() else: raise C1222IOError('unknown file handle is available for reading') data = b'' while sock_read_ready(self.read_sock_h, self.read_timeout): tmp_data = self.read_sock_h.recv(8192) data += tmp_data if len(tmp_data) != 8192: break pkt = C1222Packet.from_bytes(data) if not isinstance(pkt.data, C1222UserInformation): return pkt.data return C1222EPSEM.from_bytes(pkt.data.data) def send(self, data): pkt = C1222Packet(self.called_ap, self.calling_ap, random.randint(0, 999999), data=C1222UserInformation(C1222EPSEM(data))) self.sock_h.send(pkt.build()) def start(self): self.send(C1222IdentRequest()) try: self.recv() except C1222IOError: self.logger.error('received incorrect response to identification service request') else: return True return False def close(self): self.sock_h.close() if self.read_sock_h is not None: self.read_sock_h.close() self.stop_listener() termineter-1.0.4/lib/c1222/data.py000066400000000000000000000311011325174767400164700ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1222/data.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import binascii import struct from c1222.utilities import data_checksum from pyasn1.codec.ber import encoder as ber_encoder from pyasn1.codec.ber import decoder as ber_decoder from pyasn1.type import tag from pyasn1.type import univ class C1222CallingAPTitle(univ.ObjectIdentifier): tagSet = univ.ObjectIdentifier.tagSet.tagExplicitly(tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 6)) def encode(self): return ber_encoder.encode(self) class C1222CallingAPInvocationID(univ.Integer): tagSet = univ.Integer.tagSet.tagExplicitly(tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 8)) def encode(self): return ber_encoder.encode(self) class C1222CalledAPTitle(univ.ObjectIdentifier): tagSet = univ.ObjectIdentifier.tagSet.tagExplicitly(tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 2)) def encode(self): return ber_encoder.encode(self) class C1222Data(object): """ This class provides basic methods for constructable data fragments of the C12.22 protocol. """ def __repr__(self): return '<' + self.__class__.__name__ + ' >' def __str__(self): return self.build() def __len__(self): return len(self.build()) def build(self): return b'' class C1222EPSEM(C1222Data): def __init__(self, data, ed_class=b''): self.data = data # flags self.reserved = False self.recovery = False self.proxy_service = False self.ed_class = ed_class self.security_mode = 0 self.response_mode = 0 def __repr__(self): return '' @classmethod def from_bytes(cls, data): if len(data) < 2: raise Exception('invalid data (size)') flags = data[0] reserved = bool(flags & (1 << 7)) recovery = bool(flags & (1 << 6)) proxy_service = bool(flags & (1 << 5)) ed_class = bool(flags & (1 << 4)) security_mode = ((flags & (3 << 2)) >> 2) response_mode = (flags & 3) if ed_class: ed_class = data[1:5] length = data[5] data = data[6:] else: ed_class = b'' length = data[1] data = data[2:] if length != len(data): raise Exception('invalid data (size)') epsem = cls(data, ed_class) epsem.reserved = reserved epsem.recovery = recovery epsem.proxy_service = proxy_service epsem.security_mode = security_mode epsem.response_mode = response_mode return epsem def build(self): flags = 0 flags |= (int(self.reserved) << 7) flags |= (int(self.recovery) << 6) flags |= (int(self.proxy_service) << 5) flags |= (int(bool(self.ed_class)) << 4) flags |= (self.security_mode << 2) flags |= self.response_mode flags = struct.pack('B', flags) data = str(self.data) return flags + self.ed_class + struct.pack('B', len(data)) + data class C1222UserInformation(C1222Data): def __init__(self, data): self.data = data @classmethod def from_bytes(cls, data): if len(data) < 6: raise Exception('invalid data (size)') if data[0] != 0xbe: raise Exception('invalid start byte') if data[1] != len(data[2:]): raise Exception('invalid data (size)') if data[2] != 0x28: raise Exception('invalid start byte') if ord(data[3]) != len(data[4:]): raise Exception('invalid data (size)') if data[4] != 0x81: raise Exception('invalid start byte') if data[5] != len(data[6:]): raise Exception('invalid data (size)') return cls(data[6:]) def build(self): data = self.data data = b'\x81' + struct.pack('B', len(data)) + data data = b'\x28' + struct.pack('B', len(data)) + data data = b'\xbe' + struct.pack('B', len(data)) + data return data class C1222Request(C1222Data): @property def name(self): name = self.__class__.__name__ if not name.startswith('C1222'): raise Exception('class name does not start with \'C1222\'') if not name.endswith('Request'): raise Exception('class name does not end with \'Request\'') return name[5:-7] def set_ap_title(self, ap_title): if not hasattr(self, '_ap_title'): raise Exception(self.__class__.__name__ + ' does not support the ap_title element') if isinstance(ap_title, univ.ObjectIdentifier): self._ap_title = ber_encoder.encode(ap_title) if isinstance(ap_title, str): self._ap_title = ap_title else: self._ap_title = ber_encoder.encode(univ.ObjectIdentifier(ap_title)) def set_userid(self, userid): if not hasattr(self, '_userid'): raise Exception(self.__class__.__name__ + ' does not support the userid element') if not isinstance(userid, int): ValueError('userid must be between 0x0000 and 0xffff') if not 0x0000 <= userid <= 0xffff: raise ValueError('userid must be between 0x0000 and 0xffff') self._userid = struct.pack('>H', userid) class C1222DisconnectRequest(C1222Request): disconnect = b'\x22' def build(self): return self.disconnect class C1222IdentRequest(C1222Request): ident = b'\x20' def build(self): return self.ident class C1222LogoffRequest(C1222Request): logoff = b'\x52' def build(self): return self.logoff class C1222LogonRequest(C1222Request): logon = b'\x50' def __init__(self, username='', userid=0, session_idle_timeout=0): self._userid = b'\x00\x00' self._username = b'\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20' self._session_idle_timeout = b'\x00\x00' self.set_username(username) self.set_userid(userid) self.set_session_idle_timeout(session_idle_timeout) def build(self): return self.logon + self._userid + self._username + self._session_idle_timeout def set_username(self, value): if len(value) > 10: raise ValueError('username must be 10 characters or less') self._username = value.encode('utf-8') + (b'\x20' * (10 - len(value))) def set_session_idle_timeout(self, session_idle_timeout): if not isinstance(session_idle_timeout, int): ValueError('session_idle_timeout must be between 0x0000 and 0xffff') if not 0x0000 <= session_idle_timeout <= 0xffff: raise ValueError('session_idle_timeout must be between 0x0000 and 0xffff') self._session_idle_timeout = struct.pack('>H', session_idle_timeout) class C1222ReadRequest(C1222Request): read = b'\x30' def __init__(self, tableid, offset=None, octetcount=None): self._tableid = b'\x00\x01' self._offset = b'' self._octetcount = b'' self.set_tableid(tableid) if (offset is not None and offset != 0) and (octetcount is not None and octetcount != 0): self.read = b'\x3f' self.set_offset(offset) self.set_octetcount(octetcount) def build(self): return self.read + self._tableid + self._offset + self._octetcount def set_tableid(self, tableid): self._tableid = struct.pack('>H', tableid) def set_offset(self, offset): self._offset = struct.pack('>I', (offset & 0xffffff))[1:] def set_octetcount(self, octetcount): self._octetcount = struct.pack('>H', octetcount) class C1222ResolveRequest(C1222Request): resolve = b'\x25' def __init__(self, ap_title): self._ap_title = b'' self.set_ap_title(ap_title) def build(self): return self.resolve + self._ap_title class C1222SecurityRequest(C1222Request): security = b'\x51' def __init__(self, password='', userid=0): self._password = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' self._userid = b'\x00\x00' self.set_password(password) self.set_userid(userid) def build(self): return self.security + self._password + self._userid def set_password(self, value): if len(value) > 20: raise ValueError('password must be 20 characters or less') self._password = value.encode('utf-8') + (b'\x20' * (20 - len(value))) class C1222TerminateRequest(C1222Request): terminate = b'\x21' def build(self): return self.terminate class C1222TraceRequest(C1222Request): trace = b'\x26' def __init__(self, ap_title): self._ap_title = b'' self.set_ap_title(ap_title) def build(self): return self.trace + self._ap_title class C1222WaitRequest(C1222Request): wait = b'\x70' def __init__(self, time=0): self._time = b'\x00' self.set_time(time) def build(self): return self.wait + self._time def set_time(self, time): self._time = struct.pack('B', time) class C1222WriteRequest(C1222Request): write = b'\x40' def __init__(self, tableid, data, offset=None): self._tableid = b'\x00\x01' self._offset = b'' self._datalen = b'\x00\x00' self._data = b'' self.set_tableid(tableid) self.set_data(data) if offset is not None and offset != 0: self.write = b'\x4f' self.set_offset(offset) def build(self): packet = self.write packet += self._tableid packet += self._offset packet += self._datalen packet += self._data packet += data_checksum(self._data) return packet def set_tableid(self, tableid): self._tableid = struct.pack('>H', tableid) def set_offset(self, offset): self._offset = struct.pack('>I', (offset & 0xffffff))[1:] def set_data(self, data): self._data = data self._datalen = struct.pack('>H', len(data)) class C1222Packet(C1222Request): start = b'\x60' def __init__(self, called_ap, calling_ap, calling_ap_invocation_id, data=None, length=None): self._length = b'\x00' self._data = b'' if not isinstance(called_ap, C1222CalledAPTitle): called_ap = C1222CalledAPTitle(called_ap) self.called_ap = called_ap if not isinstance(calling_ap, C1222CallingAPTitle): calling_ap = C1222CallingAPTitle(calling_ap) self.calling_ap = calling_ap if not isinstance(calling_ap_invocation_id, C1222CallingAPInvocationID): calling_ap_invocation_id = C1222CallingAPInvocationID(calling_ap_invocation_id) self.calling_ap_invocation_id = calling_ap_invocation_id if data: self.set_data(data) else: self.set_data(b'') if length: self.set_length(length) def __repr__(self): if isinstance(self._data, C1222Data): return '' else: return '' @classmethod def from_bytes(cls, data): if data[0] != 0x60: raise Exception('invalid start byte') (called_ap, data) = ber_decoder.decode(data) (called_ap_invocation_id, data) = ber_decoder.decode(data) (calling_ap, data) = ber_decoder.decode(data) (calling_ap_invocation_id, data) = ber_decoder.decode(data) if data[0] == 0xbe: data = C1222UserInformation.from_bytes(data) called_ap = C1222CalledAPTitle(called_ap) calling_ap = C1222CallingAPTitle(calling_ap) calling_ap_invocation_id = C1222CallingAPInvocationID(calling_ap_invocation_id) frame = cls(called_ap, calling_ap, calling_ap_invocation_id, data) return frame @property def data(self): return self._data @data.setter def data(self, value): self.set_data(value) def set_data(self, value): self._data = value length = len(self.called_ap.encode()) length += len(self.calling_ap.encode()) length += len(self.calling_ap_invocation_id.encode()) length += len(self._data) self.set_length(length) def set_length(self, length): self._length = struct.pack('B', length) def build(self): packet = self.start packet += self._length packet += self.called_ap.encode() packet += self.calling_ap.encode() packet += self.calling_ap_invocation_id.encode() packet += self._data return packet termineter-1.0.4/lib/c1222/errors.py000066400000000000000000000047011325174767400171010ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1222/errors.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals class C1222Error(Exception): """ This is a generic C1222 Error. """ def __init__(self, msg, error_code=None): self.message = msg self.err_code = error_code def __str__(self): return repr(self.message) class C1222IOError(C1222Error): """ Raised when there is a problem sending or receiving data. """ def __init__(self, msg): self.message = msg class C1222NegotiateError(C1222Error): """ Raised in response to an invalid reply to a Negotiate request. """ pass class C1222ReadTableError(C1222Error): """ Raised when a table is not successfully read. @type errcode: Integer @param errcode: The error that was returned while reading the table. """ pass class C1222WriteTableError(C1222Error): """ Raised when a table is not successfully written to. @type errcode: Integer @param errcode: The error that was returned while writing to the table. """ pass termineter-1.0.4/lib/c1222/utilities.py000066400000000000000000000035761325174767400176110ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # c1222/utilities.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import struct import crcelk def data_checksum(data): chksum = 0 for i in struct.unpack('B' * len(data), data): chksum += i chksum = ((chksum - 1) & 0xff) ^ 0xff return struct.pack('B', chksum) def packet_checksum(data): chksum = crcelk.CRC_HDLC.calc_bytes(data) return struct.pack('' def _opt_callback_set_cache_tables(self, policy, _): if self.is_serial_connected(): self.serial_connection.set_table_cache_policy(policy) return True def _opt_callback_set_table_format(self, table_format, _): if table_format not in tabulate.tabulate_formats: self.print_error('TABLE_FORMAT must be one of: ' + ', '.join(tabulate.tabulate_formats)) return False return True def _run_optical(self, module): if not self._serial_connected: self.print_error('The serial interface has not been connected') return False try: self.serial_get() except Exception as error: self.print_exception(error) return False ConnectionState = termineter.module.ConnectionState if not self.advanced_options['AUTO_CONNECT']: return True if module.connection_state == ConnectionState.none: return True try: self.serial_connect() except Exception as error: self.print_exception(error) return self.print_good('Successfully connected and the device is responding') if module.connection_state == ConnectionState.connected: return True if not self.serial_login(): self.logger.warning('meter login failed, some tables may not be accessible') if module.connection_state == ConnectionState.authenticated: return True self.logger.warning('unknown optical connection state: ' + module.connection_state.name) return True def reload_module(self, module_path=None): """ Reloads a module into the framework. If module_path is not specified, then the current_module variable is used. Returns True on success, False on error. @type module_path: String @param module_path: The name of the module to reload """ if module_path is None: if self.current_module is not None: module_path = self.current_module.name else: self.logger.warning('must specify module if not module is currently being used') return False if module_path not in self.module: self.logger.error('invalid module requested for reload') raise termineter.errors.FrameworkRuntimeError('invalid module requested for reload') self.logger.info('reloading module: ' + module_path) module_instance = self.import_module(module_path, reload_module=True) if not isinstance(module_instance, termineter.module.TermineterModule): self.logger.error('module: ' + module_path + ' is not derived from the TermineterModule class') raise termineter.errors.FrameworkRuntimeError('module: ' + module_path + ' is not derived from the TermineterModule class') if not hasattr(module_instance, 'run'): self.logger.error('module: ' + module_path + ' has no run() method') raise termineter.errors.FrameworkRuntimeError('module: ' + module_path + ' has no run() method') if not isinstance(module_instance.options, termineter.options.Options) or not isinstance(module_instance.advanced_options, termineter.options.Options): self.logger.error('module: ' + module_path + ' options and advanced_options must be termineter.options.Options instances') raise termineter.errors.FrameworkRuntimeError('options and advanced_options must be termineter.options.Options instances') module_instance.name = module_path.split('/')[-1] module_instance.path = module_path self.modules[module_path] = module_instance if self.current_module is not None: if self.current_module.path == module_instance.path: self.current_module = module_instance return True def run(self, module=None): if not isinstance(module, termineter.module.TermineterModule) and not isinstance(self.current_module, termineter.module.TermineterModule): raise termineter.errors.FrameworkRuntimeError('either the module or the current_module must be sent') if module is None: module = self.current_module if isinstance(module, termineter.module.TermineterModuleOptical) and not self._run_optical(module): return self.logger.info('running module: ' + module.path) try: result = module.run() finally: if isinstance(module, termineter.module.TermineterModuleOptical) and self.serial_connection and self.advanced_options['AUTO_CONNECT']: self.serial_connection.stop() return result @property def use_colors(self): return self.options['USE_COLOR'] @use_colors.setter def use_colors(self, value): self.options.set_option_value('USE_COLOR', str(value)) def get_module_logger(self, name): """ This returns a logger for individual modules to allow them to be inherited from the framework and thus be named appropriately. @type name: String @param name: The name of the module requesting the logger """ return logging.getLogger('termineter.module.' + name) def import_module(self, module_path, reload_module=False): module = self.__package__ + '.modules.' + module_path.replace('/', '.') try: module = importlib.import_module(module) if reload_module: importlib.reload(module) module_instance = module.Module(self) except Exception: self.logger.error('failed to load module: ' + module_path, exc_info=True) raise termineter.errors.FrameworkRuntimeError('failed to load module: ' + module_path) return module_instance def print_exception(self, error): message = 'Caught ' + error.__class__.__name__ + ': ' + str(error) self.logger.error(message, exc_info=True) self.print_error(message) def print_error(self, message): prefix = '[-] ' if self.options['USE_COLOR']: prefix = termcolor.colored(prefix, 'red', attrs=('bold',)) self.stdout.write(prefix + (os.linesep + prefix).join(message.split(os.linesep)) + os.linesep) self.stdout.flush() def print_good(self, message): prefix = '[+] ' if self.options['USE_COLOR']: prefix = termcolor.colored(prefix, 'green', attrs=('bold',)) self.stdout.write(prefix + (os.linesep + prefix).join(message.split(os.linesep)) + os.linesep) self.stdout.flush() def print_hexdump(self, data): data_len = len(data) i = 0 while i < data_len: self.stdout.write("{0:04x} ".format(i)) for j in range(16): if i + j < data_len: self.stdout.write("{0:02x} ".format(data[i + j])) else: self.stdout.write(' ') if j % 16 == 7: self.stdout.write(' ') self.stdout.write(' ') r = '' for j in data[i:i + 16]: if 32 < j < 128: r += chr(j) else: r += '.' self.stdout.write(r + os.linesep) i += 16 self.stdout.flush() def print_line(self, message): self.stdout.write(message + os.linesep) self.stdout.flush() def print_status(self, message): prefix = '[*] ' if self.options['USE_COLOR']: prefix = termcolor.colored(prefix, 'blue', attrs=('bold',)) self.stdout.write(prefix + (os.linesep + prefix).join(message.split(os.linesep)) + os.linesep) self.stdout.flush() def print_table(self, table, headers=(), line_prefix=None, tablefmt=None): tablefmt = tablefmt or self.advanced_options['TABLE_FORMAT'] text = tabulate.tabulate(table, headers=headers, tablefmt=tablefmt) if line_prefix: text = '\n'.join(line_prefix + line for line in text.split('\n')) self.print_line(text) def print_warning(self, message): prefix = '[!] ' if self.options['USE_COLOR']: prefix = termcolor.colored(prefix, '', attrs=('bold',)) self.stdout.write(prefix + (os.linesep + prefix).join(message.split(os.linesep)) + os.linesep) self.stdout.flush() def is_serial_connected(self): """ Returns True if the serial interface is connected. """ return self._serial_connected def serial_disconnect(self): """ Closes the serial connection to the meter and disconnects from the device. """ if self._serial_connected: try: self.serial_connection.close() except c1218.errors.C1218IOError as error: self.logger.error('caught C1218IOError: ' + str(error)) except serial.serialutil.SerialException as error: self.logger.error('caught SerialException: ' + str(error)) self._serial_connected = False self.logger.warning('the serial interface has been disconnected') return True def serial_get(self): """ Create the serial connection from the framework settings and return it, setting the framework instance in the process. """ frmwk_c1218_settings = { 'nbrpkts': self.advanced_options['C1218_MAX_PACKETS'], 'pktsize': self.advanced_options['C1218_PACKET_SIZE'] } frmwk_serial_settings = termineter.utilities.get_default_serial_settings() frmwk_serial_settings['baudrate'] = self.advanced_options['SERIAL_BAUD_RATE'] frmwk_serial_settings['bytesize'] = self.advanced_options['SERIAL_BYTE_SIZE'] frmwk_serial_settings['stopbits'] = self.advanced_options['SERIAL_STOP_BITS'] self.logger.info('opening serial device: ' + self.options['SERIAL_CONNECTION']) try: self.serial_connection = c1218.connection.Connection(self.options['SERIAL_CONNECTION'], c1218_settings=frmwk_c1218_settings, serial_settings=frmwk_serial_settings, enable_cache=self.advanced_options['CACHE_TABLES']) except Exception as error: self.logger.error('could not open the serial device') raise error return self.serial_connection def serial_connect(self): """ Connect to the serial device. """ self.serial_get() try: self.serial_connection.start() except c1218.errors.C1218IOError as error: self.logger.error('serial connection has been opened but the meter is unresponsive') raise error self._serial_connected = True return True def serial_login(self): """ Attempt to log into the meter over the C12.18 protocol. Returns True on success, False on a failure. This can be called by modules in order to login with a username and password configured within the framework instance. """ if not self._serial_connected: raise termineter.errors.FrameworkRuntimeError('the serial interface is disconnected') username = self.options['USERNAME'] user_id = self.options['USER_ID'] password = self.options['PASSWORD'] if self.options['PASSWORD_HEX']: hex_regex = re.compile('^([0-9a-fA-F]{2})+$') if hex_regex.match(password) is None: self.print_error('Invalid characters in password') raise termineter.errors.FrameworkConfigurationError('invalid characters in password') password = binascii.a2b_hex(password) if len(username) > 10: self.print_error('Username cannot be longer than 10 characters') raise termineter.errors.FrameworkConfigurationError('username cannot be longer than 10 characters') if not (0 <= user_id <= 0xffff): self.print_error('User id must be between 0 and 0xffff') raise termineter.errors.FrameworkConfigurationError('user id must be between 0 and 0xffff') if len(password) > 20: self.print_error('Password cannot be longer than 20 characters') raise termineter.errors.FrameworkConfigurationError('password cannot be longer than 20 characters') if not self.serial_connection.login(username, user_id, password): return False return True def test_serial_connection(self): """ Connect to the serial device and then verifies that the meter is responding. Once the serial device is open, this function attempts to retrieve the contents of table #0 (GEN_CONFIG_TBL) to configure the endianess it will use. Returns True on success. """ self.serial_connect() username = self.options['USERNAME'] user_id = self.options['USER_ID'] if len(username) > 10: self.logger.error('username cannot be longer than 10 characters') raise termineter.errors.FrameworkConfigurationError('username cannot be longer than 10 characters') if not (0 <= user_id <= 0xffff): self.logger.error('user id must be between 0 and 0xffff') raise termineter.errors.FrameworkConfigurationError('user id must be between 0 and 0xffff') try: if not self.serial_connection.login(username, user_id): self.logger.error('the meter has rejected the username and user id') raise termineter.errors.FrameworkConfigurationError('the meter has rejected the username and user id') except c1218.errors.C1218IOError as error: self.logger.error('serial connection has been opened but the meter is unresponsive') raise error try: general_config_table = self.serial_connection.get_table_data(0) except c1218.errors.C1218ReadTableError as error: self.logger.error('serial connection as been opened but the general configuration table (table #0) could not be read') raise error if general_config_table[0] & 1: self.logger.info('setting the connection to use big-endian for C12.19 data') self.serial_connection.c1219_endian = '>' else: self.logger.info('setting the connection to use little-endian for C12.19 data') self.serial_connection.c1219_endian = '<' try: self.serial_connection.stop() except c1218.errors.C1218IOError as error: self.logger.error('serial connection has been opened but the meter is unresponsive') raise error self.logger.warning('the serial interface has been connected') return True termineter-1.0.4/lib/termineter/data/000077500000000000000000000000001325174767400175475ustar00rootroot00000000000000termineter-1.0.4/lib/termineter/data/smeter_passwords.txt000066400000000000000000000761611325174767400237270ustar00rootroot00000000000000000102030405060708090a0b0c0d0e0f10111213 000102030405060708090a0b0c0d0e0f10111213 0102030405060708090a0b0c0d0f101112131415 00112233445566778899aabbccddeeff00112233 112233445566778899aabbccddeeff0011223344 4d41535445525f30313200000000000000000000 5245414445525f30313200000000000000000000 435553544f4d45525f3000000000000000000000 aabbccddeeff0011223320202020202020202020 0000000000000000000000000000000000000000 0101010101010101010101010101010101010101 0202020202020202020202020202020202020202 0303030303030303030303030303030303030303 0404040404040404040404040404040404040404 0505050505050505050505050505050505050505 0606060606060606060606060606060606060606 0707070707070707070707070707070707070707 0808080808080808080808080808080808080808 0909090909090909090909090909090909090909 0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c 0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d 0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f 1010101010101010101010101010101010101010 1111111111111111111111111111111111111111 1212121212121212121212121212121212121212 1313131313131313131313131313131313131313 1414141414141414141414141414141414141414 1515151515151515151515151515151515151515 1616161616161616161616161616161616161616 1717171717171717171717171717171717171717 1818181818181818181818181818181818181818 1919191919191919191919191919191919191919 1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a 1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b 1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c 1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d 1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e 1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f 2020202020202020202020202020202020202020 2121212121212121212121212121212121212121 2222222222222222222222222222222222222222 2323232323232323232323232323232323232323 2424242424242424242424242424242424242424 2525252525252525252525252525252525252525 2626262626262626262626262626262626262626 2727272727272727272727272727272727272727 2828282828282828282828282828282828282828 2929292929292929292929292929292929292929 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b 2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c 2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d 2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e 2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f 3030303030303030303030303030303030303030 3131313131313131313131313131313131313131 3232323232323232323232323232323232323232 3333333333333333333333333333333333333333 3434343434343434343434343434343434343434 3535353535353535353535353535353535353535 3636363636363636363636363636363636363636 3737373737373737373737373737373737373737 3838383838383838383838383838383838383838 3939393939393939393939393939393939393939 3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a 3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b 3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c 3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d 3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e 3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f 4040404040404040404040404040404040404040 4141414141414141414141414141414141414141 4242424242424242424242424242424242424242 4343434343434343434343434343434343434343 4444444444444444444444444444444444444444 4545454545454545454545454545454545454545 4646464646464646464646464646464646464646 4747474747474747474747474747474747474747 4848484848484848484848484848484848484848 4949494949494949494949494949494949494949 4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a 4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b 4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c 4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d 4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e 4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f 5050505050505050505050505050505050505050 5151515151515151515151515151515151515151 5252525252525252525252525252525252525252 5353535353535353535353535353535353535353 5454545454545454545454545454545454545454 5555555555555555555555555555555555555555 5656565656565656565656565656565656565656 5757575757575757575757575757575757575757 5858585858585858585858585858585858585858 5959595959595959595959595959595959595959 5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a 5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b 5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c 5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d 5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e 5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f 6060606060606060606060606060606060606060 6161616161616161616161616161616161616161 6262626262626262626262626262626262626262 6363636363636363636363636363636363636363 6464646464646464646464646464646464646464 6565656565656565656565656565656565656565 6666666666666666666666666666666666666666 6767676767676767676767676767676767676767 6868686868686868686868686868686868686868 6969696969696969696969696969696969696969 6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a 6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b 6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c 6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d 6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e 6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f 7070707070707070707070707070707070707070 7171717171717171717171717171717171717171 7272727272727272727272727272727272727272 7373737373737373737373737373737373737373 7474747474747474747474747474747474747474 7575757575757575757575757575757575757575 7676767676767676767676767676767676767676 7777777777777777777777777777777777777777 7878787878787878787878787878787878787878 7979797979797979797979797979797979797979 7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a 7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b 7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c 7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d 7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e 7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f 8080808080808080808080808080808080808080 8181818181818181818181818181818181818181 8282828282828282828282828282828282828282 8383838383838383838383838383838383838383 8484848484848484848484848484848484848484 8585858585858585858585858585858585858585 8686868686868686868686868686868686868686 8787878787878787878787878787878787878787 8888888888888888888888888888888888888888 8989898989898989898989898989898989898989 8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a 8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b 8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c 8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d 8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e 8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f 9090909090909090909090909090909090909090 9191919191919191919191919191919191919191 9292929292929292929292929292929292929292 9393939393939393939393939393939393939393 9494949494949494949494949494949494949494 9595959595959595959595959595959595959595 9696969696969696969696969696969696969696 9797979797979797979797979797979797979797 9898989898989898989898989898989898989898 9999999999999999999999999999999999999999 9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a 9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b 9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c 9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d 9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e 9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1 a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2 a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3 a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4 a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5 a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6 a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7 a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8 a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa abababababababababababababababababababab acacacacacacacacacacacacacacacacacacacac adadadadadadadadadadadadadadadadadadadad aeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeae afafafafafafafafafafafafafafafafafafafaf b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0 b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1 b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2 b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3 b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4 b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5 b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6 b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7 b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8 b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9 babababababababababababababababababababa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc bdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbd bebebebebebebebebebebebebebebebebebebebe bfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbf c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0 c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1 c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2 c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3 c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4 c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5 c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6c6 c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7 c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8 c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9c9 cacacacacacacacacacacacacacacacacacacaca cbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcb cccccccccccccccccccccccccccccccccccccccc cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd cececececececececececececececececececece cfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0 d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1 d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2 d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3 d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4 d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5 d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6 d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7 d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8 d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9d9 dadadadadadadadadadadadadadadadadadadada dbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdbdb dcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdc dddddddddddddddddddddddddddddddddddddddd dededededededededededededededededededede dfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdf e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1 e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2 e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3 e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4 e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5 e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6 e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7 e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8e8 e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9 eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea ebebebebebebebebebebebebebebebebebebebeb ecececececececececececececececececececec edededededededededededededededededededed eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee efefefefefefefefefefefefefefefefefefefef f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0 f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1 f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2 f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3 f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4 f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5 f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6 f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7 f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8 f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9f9 fafafafafafafafafafafafafafafafafafafafa fbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfb fcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfc fdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfdfd fefefefefefefefefefefefefefefefefefefefe ffffffffffffffffffffffffffffffffffffffff 0000000000000000000020202020202020202020 0101010101010101010120202020202020202020 0202020202020202020220202020202020202020 0303030303030303030320202020202020202020 0404040404040404040420202020202020202020 0505050505050505050520202020202020202020 0606060606060606060620202020202020202020 0707070707070707070720202020202020202020 0808080808080808080820202020202020202020 0909090909090909090920202020202020202020 0a0a0a0a0a0a0a0a0a0a20202020202020202020 0b0b0b0b0b0b0b0b0b0b20202020202020202020 0c0c0c0c0c0c0c0c0c0c20202020202020202020 0d0d0d0d0d0d0d0d0d0d20202020202020202020 0e0e0e0e0e0e0e0e0e0e20202020202020202020 0f0f0f0f0f0f0f0f0f0f20202020202020202020 1010101010101010101020202020202020202020 1111111111111111111120202020202020202020 1212121212121212121220202020202020202020 1313131313131313131320202020202020202020 1414141414141414141420202020202020202020 1515151515151515151520202020202020202020 1616161616161616161620202020202020202020 1717171717171717171720202020202020202020 1818181818181818181820202020202020202020 1919191919191919191920202020202020202020 1a1a1a1a1a1a1a1a1a1a20202020202020202020 1b1b1b1b1b1b1b1b1b1b20202020202020202020 1c1c1c1c1c1c1c1c1c1c20202020202020202020 1d1d1d1d1d1d1d1d1d1d20202020202020202020 1e1e1e1e1e1e1e1e1e1e20202020202020202020 1f1f1f1f1f1f1f1f1f1f20202020202020202020 2020202020202020202020202020202020202020 2121212121212121212120202020202020202020 2222222222222222222220202020202020202020 2323232323232323232320202020202020202020 2424242424242424242420202020202020202020 2525252525252525252520202020202020202020 2626262626262626262620202020202020202020 2727272727272727272720202020202020202020 2828282828282828282820202020202020202020 2929292929292929292920202020202020202020 2a2a2a2a2a2a2a2a2a2a20202020202020202020 2b2b2b2b2b2b2b2b2b2b20202020202020202020 2c2c2c2c2c2c2c2c2c2c20202020202020202020 2d2d2d2d2d2d2d2d2d2d20202020202020202020 2e2e2e2e2e2e2e2e2e2e20202020202020202020 2f2f2f2f2f2f2f2f2f2f20202020202020202020 3030303030303030303020202020202020202020 3131313131313131313120202020202020202020 3232323232323232323220202020202020202020 3333333333333333333320202020202020202020 3434343434343434343420202020202020202020 3535353535353535353520202020202020202020 3636363636363636363620202020202020202020 3737373737373737373720202020202020202020 3838383838383838383820202020202020202020 3939393939393939393920202020202020202020 3a3a3a3a3a3a3a3a3a3a20202020202020202020 3b3b3b3b3b3b3b3b3b3b20202020202020202020 3c3c3c3c3c3c3c3c3c3c20202020202020202020 3d3d3d3d3d3d3d3d3d3d20202020202020202020 3e3e3e3e3e3e3e3e3e3e20202020202020202020 3f3f3f3f3f3f3f3f3f3f20202020202020202020 4040404040404040404020202020202020202020 4141414141414141414120202020202020202020 4242424242424242424220202020202020202020 4343434343434343434320202020202020202020 4444444444444444444420202020202020202020 4545454545454545454520202020202020202020 4646464646464646464620202020202020202020 4747474747474747474720202020202020202020 4848484848484848484820202020202020202020 4949494949494949494920202020202020202020 4a4a4a4a4a4a4a4a4a4a20202020202020202020 4b4b4b4b4b4b4b4b4b4b20202020202020202020 4c4c4c4c4c4c4c4c4c4c20202020202020202020 4d4d4d4d4d4d4d4d4d4d20202020202020202020 4e4e4e4e4e4e4e4e4e4e20202020202020202020 4f4f4f4f4f4f4f4f4f4f20202020202020202020 5050505050505050505020202020202020202020 5151515151515151515120202020202020202020 5252525252525252525220202020202020202020 5353535353535353535320202020202020202020 5454545454545454545420202020202020202020 5555555555555555555520202020202020202020 5656565656565656565620202020202020202020 5757575757575757575720202020202020202020 5858585858585858585820202020202020202020 5959595959595959595920202020202020202020 5a5a5a5a5a5a5a5a5a5a20202020202020202020 5b5b5b5b5b5b5b5b5b5b20202020202020202020 5c5c5c5c5c5c5c5c5c5c20202020202020202020 5d5d5d5d5d5d5d5d5d5d20202020202020202020 5e5e5e5e5e5e5e5e5e5e20202020202020202020 5f5f5f5f5f5f5f5f5f5f20202020202020202020 6060606060606060606020202020202020202020 6161616161616161616120202020202020202020 6262626262626262626220202020202020202020 6363636363636363636320202020202020202020 6464646464646464646420202020202020202020 6565656565656565656520202020202020202020 6666666666666666666620202020202020202020 6767676767676767676720202020202020202020 6868686868686868686820202020202020202020 6969696969696969696920202020202020202020 6a6a6a6a6a6a6a6a6a6a20202020202020202020 6b6b6b6b6b6b6b6b6b6b20202020202020202020 6c6c6c6c6c6c6c6c6c6c20202020202020202020 6d6d6d6d6d6d6d6d6d6d20202020202020202020 6e6e6e6e6e6e6e6e6e6e20202020202020202020 6f6f6f6f6f6f6f6f6f6f20202020202020202020 7070707070707070707020202020202020202020 7171717171717171717120202020202020202020 7272727272727272727220202020202020202020 7373737373737373737320202020202020202020 7474747474747474747420202020202020202020 7575757575757575757520202020202020202020 7676767676767676767620202020202020202020 7777777777777777777720202020202020202020 7878787878787878787820202020202020202020 7979797979797979797920202020202020202020 7a7a7a7a7a7a7a7a7a7a20202020202020202020 7b7b7b7b7b7b7b7b7b7b20202020202020202020 7c7c7c7c7c7c7c7c7c7c20202020202020202020 7d7d7d7d7d7d7d7d7d7d20202020202020202020 7e7e7e7e7e7e7e7e7e7e20202020202020202020 7f7f7f7f7f7f7f7f7f7f20202020202020202020 8080808080808080808020202020202020202020 8181818181818181818120202020202020202020 8282828282828282828220202020202020202020 8383838383838383838320202020202020202020 8484848484848484848420202020202020202020 8585858585858585858520202020202020202020 8686868686868686868620202020202020202020 8787878787878787878720202020202020202020 8888888888888888888820202020202020202020 8989898989898989898920202020202020202020 8a8a8a8a8a8a8a8a8a8a20202020202020202020 8b8b8b8b8b8b8b8b8b8b20202020202020202020 8c8c8c8c8c8c8c8c8c8c20202020202020202020 8d8d8d8d8d8d8d8d8d8d20202020202020202020 8e8e8e8e8e8e8e8e8e8e20202020202020202020 8f8f8f8f8f8f8f8f8f8f20202020202020202020 9090909090909090909020202020202020202020 9191919191919191919120202020202020202020 9292929292929292929220202020202020202020 9393939393939393939320202020202020202020 9494949494949494949420202020202020202020 9595959595959595959520202020202020202020 9696969696969696969620202020202020202020 9797979797979797979720202020202020202020 9898989898989898989820202020202020202020 9999999999999999999920202020202020202020 9a9a9a9a9a9a9a9a9a9a20202020202020202020 9b9b9b9b9b9b9b9b9b9b20202020202020202020 9c9c9c9c9c9c9c9c9c9c20202020202020202020 9d9d9d9d9d9d9d9d9d9d20202020202020202020 9e9e9e9e9e9e9e9e9e9e20202020202020202020 9f9f9f9f9f9f9f9f9f9f20202020202020202020 a0a0a0a0a0a0a0a0a0a020202020202020202020 a1a1a1a1a1a1a1a1a1a120202020202020202020 a2a2a2a2a2a2a2a2a2a220202020202020202020 a3a3a3a3a3a3a3a3a3a320202020202020202020 a4a4a4a4a4a4a4a4a4a420202020202020202020 a5a5a5a5a5a5a5a5a5a520202020202020202020 a6a6a6a6a6a6a6a6a6a620202020202020202020 a7a7a7a7a7a7a7a7a7a720202020202020202020 a8a8a8a8a8a8a8a8a8a820202020202020202020 a9a9a9a9a9a9a9a9a9a920202020202020202020 aaaaaaaaaaaaaaaaaaaa20202020202020202020 abababababababababab20202020202020202020 acacacacacacacacacac20202020202020202020 adadadadadadadadadad20202020202020202020 aeaeaeaeaeaeaeaeaeae20202020202020202020 afafafafafafafafafaf20202020202020202020 b0b0b0b0b0b0b0b0b0b020202020202020202020 b1b1b1b1b1b1b1b1b1b120202020202020202020 b2b2b2b2b2b2b2b2b2b220202020202020202020 b3b3b3b3b3b3b3b3b3b320202020202020202020 b4b4b4b4b4b4b4b4b4b420202020202020202020 b5b5b5b5b5b5b5b5b5b520202020202020202020 b6b6b6b6b6b6b6b6b6b620202020202020202020 b7b7b7b7b7b7b7b7b7b720202020202020202020 b8b8b8b8b8b8b8b8b8b820202020202020202020 b9b9b9b9b9b9b9b9b9b920202020202020202020 babababababababababa20202020202020202020 bbbbbbbbbbbbbbbbbbbb20202020202020202020 bcbcbcbcbcbcbcbcbcbc20202020202020202020 bdbdbdbdbdbdbdbdbdbd20202020202020202020 bebebebebebebebebebe20202020202020202020 bfbfbfbfbfbfbfbfbfbf20202020202020202020 c0c0c0c0c0c0c0c0c0c020202020202020202020 c1c1c1c1c1c1c1c1c1c120202020202020202020 c2c2c2c2c2c2c2c2c2c220202020202020202020 c3c3c3c3c3c3c3c3c3c320202020202020202020 c4c4c4c4c4c4c4c4c4c420202020202020202020 c5c5c5c5c5c5c5c5c5c520202020202020202020 c6c6c6c6c6c6c6c6c6c620202020202020202020 c7c7c7c7c7c7c7c7c7c720202020202020202020 c8c8c8c8c8c8c8c8c8c820202020202020202020 c9c9c9c9c9c9c9c9c9c920202020202020202020 cacacacacacacacacaca20202020202020202020 cbcbcbcbcbcbcbcbcbcb20202020202020202020 cccccccccccccccccccc20202020202020202020 cdcdcdcdcdcdcdcdcdcd20202020202020202020 cececececececececece20202020202020202020 cfcfcfcfcfcfcfcfcfcf20202020202020202020 d0d0d0d0d0d0d0d0d0d020202020202020202020 d1d1d1d1d1d1d1d1d1d120202020202020202020 d2d2d2d2d2d2d2d2d2d220202020202020202020 d3d3d3d3d3d3d3d3d3d320202020202020202020 d4d4d4d4d4d4d4d4d4d420202020202020202020 d5d5d5d5d5d5d5d5d5d520202020202020202020 d6d6d6d6d6d6d6d6d6d620202020202020202020 d7d7d7d7d7d7d7d7d7d720202020202020202020 d8d8d8d8d8d8d8d8d8d820202020202020202020 d9d9d9d9d9d9d9d9d9d920202020202020202020 dadadadadadadadadada20202020202020202020 dbdbdbdbdbdbdbdbdbdb20202020202020202020 dcdcdcdcdcdcdcdcdcdc20202020202020202020 dddddddddddddddddddd20202020202020202020 dededededededededede20202020202020202020 dfdfdfdfdfdfdfdfdfdf20202020202020202020 e0e0e0e0e0e0e0e0e0e020202020202020202020 e1e1e1e1e1e1e1e1e1e120202020202020202020 e2e2e2e2e2e2e2e2e2e220202020202020202020 e3e3e3e3e3e3e3e3e3e320202020202020202020 e4e4e4e4e4e4e4e4e4e420202020202020202020 e5e5e5e5e5e5e5e5e5e520202020202020202020 e6e6e6e6e6e6e6e6e6e620202020202020202020 e7e7e7e7e7e7e7e7e7e720202020202020202020 e8e8e8e8e8e8e8e8e8e820202020202020202020 e9e9e9e9e9e9e9e9e9e920202020202020202020 eaeaeaeaeaeaeaeaeaea20202020202020202020 ebebebebebebebebebeb20202020202020202020 ecececececececececec20202020202020202020 edededededededededed20202020202020202020 eeeeeeeeeeeeeeeeeeee20202020202020202020 efefefefefefefefefef20202020202020202020 f0f0f0f0f0f0f0f0f0f020202020202020202020 f1f1f1f1f1f1f1f1f1f120202020202020202020 f2f2f2f2f2f2f2f2f2f220202020202020202020 f3f3f3f3f3f3f3f3f3f320202020202020202020 f4f4f4f4f4f4f4f4f4f420202020202020202020 f5f5f5f5f5f5f5f5f5f520202020202020202020 f6f6f6f6f6f6f6f6f6f620202020202020202020 f7f7f7f7f7f7f7f7f7f720202020202020202020 f8f8f8f8f8f8f8f8f8f820202020202020202020 f9f9f9f9f9f9f9f9f9f920202020202020202020 fafafafafafafafafafa20202020202020202020 fbfbfbfbfbfbfbfbfbfb20202020202020202020 fcfcfcfcfcfcfcfcfcfc20202020202020202020 fdfdfdfdfdfdfdfdfdfd20202020202020202020 fefefefefefefefefefe20202020202020202020 ffffffffffffffffffff20202020202020202020 0000000000000000000000000000000000000000 0101010101010101010100000000000000000000 0202020202020202020200000000000000000000 0303030303030303030300000000000000000000 0404040404040404040400000000000000000000 0505050505050505050500000000000000000000 0606060606060606060600000000000000000000 0707070707070707070700000000000000000000 0808080808080808080800000000000000000000 0909090909090909090900000000000000000000 0a0a0a0a0a0a0a0a0a0a00000000000000000000 0b0b0b0b0b0b0b0b0b0b00000000000000000000 0c0c0c0c0c0c0c0c0c0c00000000000000000000 0d0d0d0d0d0d0d0d0d0d00000000000000000000 0e0e0e0e0e0e0e0e0e0e00000000000000000000 0f0f0f0f0f0f0f0f0f0f00000000000000000000 1010101010101010101000000000000000000000 1111111111111111111100000000000000000000 1212121212121212121200000000000000000000 1313131313131313131300000000000000000000 1414141414141414141400000000000000000000 1515151515151515151500000000000000000000 1616161616161616161600000000000000000000 1717171717171717171700000000000000000000 1818181818181818181800000000000000000000 1919191919191919191900000000000000000000 1a1a1a1a1a1a1a1a1a1a00000000000000000000 1b1b1b1b1b1b1b1b1b1b00000000000000000000 1c1c1c1c1c1c1c1c1c1c00000000000000000000 1d1d1d1d1d1d1d1d1d1d00000000000000000000 1e1e1e1e1e1e1e1e1e1e00000000000000000000 1f1f1f1f1f1f1f1f1f1f00000000000000000000 2020202020202020202000000000000000000000 2121212121212121212100000000000000000000 2222222222222222222200000000000000000000 2323232323232323232300000000000000000000 2424242424242424242400000000000000000000 2525252525252525252500000000000000000000 2626262626262626262600000000000000000000 2727272727272727272700000000000000000000 2828282828282828282800000000000000000000 2929292929292929292900000000000000000000 2a2a2a2a2a2a2a2a2a2a00000000000000000000 2b2b2b2b2b2b2b2b2b2b00000000000000000000 2c2c2c2c2c2c2c2c2c2c00000000000000000000 2d2d2d2d2d2d2d2d2d2d00000000000000000000 2e2e2e2e2e2e2e2e2e2e00000000000000000000 2f2f2f2f2f2f2f2f2f2f00000000000000000000 3030303030303030303000000000000000000000 3131313131313131313100000000000000000000 3232323232323232323200000000000000000000 3333333333333333333300000000000000000000 3434343434343434343400000000000000000000 3535353535353535353500000000000000000000 3636363636363636363600000000000000000000 3737373737373737373700000000000000000000 3838383838383838383800000000000000000000 3939393939393939393900000000000000000000 3a3a3a3a3a3a3a3a3a3a00000000000000000000 3b3b3b3b3b3b3b3b3b3b00000000000000000000 3c3c3c3c3c3c3c3c3c3c00000000000000000000 3d3d3d3d3d3d3d3d3d3d00000000000000000000 3e3e3e3e3e3e3e3e3e3e00000000000000000000 3f3f3f3f3f3f3f3f3f3f00000000000000000000 4040404040404040404000000000000000000000 4141414141414141414100000000000000000000 4242424242424242424200000000000000000000 4343434343434343434300000000000000000000 4444444444444444444400000000000000000000 4545454545454545454500000000000000000000 4646464646464646464600000000000000000000 4747474747474747474700000000000000000000 4848484848484848484800000000000000000000 4949494949494949494900000000000000000000 4a4a4a4a4a4a4a4a4a4a00000000000000000000 4b4b4b4b4b4b4b4b4b4b00000000000000000000 4c4c4c4c4c4c4c4c4c4c00000000000000000000 4d4d4d4d4d4d4d4d4d4d00000000000000000000 4e4e4e4e4e4e4e4e4e4e00000000000000000000 4f4f4f4f4f4f4f4f4f4f00000000000000000000 5050505050505050505000000000000000000000 5151515151515151515100000000000000000000 5252525252525252525200000000000000000000 5353535353535353535300000000000000000000 5454545454545454545400000000000000000000 5555555555555555555500000000000000000000 5656565656565656565600000000000000000000 5757575757575757575700000000000000000000 5858585858585858585800000000000000000000 5959595959595959595900000000000000000000 5a5a5a5a5a5a5a5a5a5a00000000000000000000 5b5b5b5b5b5b5b5b5b5b00000000000000000000 5c5c5c5c5c5c5c5c5c5c00000000000000000000 5d5d5d5d5d5d5d5d5d5d00000000000000000000 5e5e5e5e5e5e5e5e5e5e00000000000000000000 5f5f5f5f5f5f5f5f5f5f00000000000000000000 6060606060606060606000000000000000000000 6161616161616161616100000000000000000000 6262626262626262626200000000000000000000 6363636363636363636300000000000000000000 6464646464646464646400000000000000000000 6565656565656565656500000000000000000000 6666666666666666666600000000000000000000 6767676767676767676700000000000000000000 6868686868686868686800000000000000000000 6969696969696969696900000000000000000000 6a6a6a6a6a6a6a6a6a6a00000000000000000000 6b6b6b6b6b6b6b6b6b6b00000000000000000000 6c6c6c6c6c6c6c6c6c6c00000000000000000000 6d6d6d6d6d6d6d6d6d6d00000000000000000000 6e6e6e6e6e6e6e6e6e6e00000000000000000000 6f6f6f6f6f6f6f6f6f6f00000000000000000000 7070707070707070707000000000000000000000 7171717171717171717100000000000000000000 7272727272727272727200000000000000000000 7373737373737373737300000000000000000000 7474747474747474747400000000000000000000 7575757575757575757500000000000000000000 7676767676767676767600000000000000000000 7777777777777777777700000000000000000000 7878787878787878787800000000000000000000 7979797979797979797900000000000000000000 7a7a7a7a7a7a7a7a7a7a00000000000000000000 7b7b7b7b7b7b7b7b7b7b00000000000000000000 7c7c7c7c7c7c7c7c7c7c00000000000000000000 7d7d7d7d7d7d7d7d7d7d00000000000000000000 7e7e7e7e7e7e7e7e7e7e00000000000000000000 7f7f7f7f7f7f7f7f7f7f00000000000000000000 8080808080808080808000000000000000000000 8181818181818181818100000000000000000000 8282828282828282828200000000000000000000 8383838383838383838300000000000000000000 8484848484848484848400000000000000000000 8585858585858585858500000000000000000000 8686868686868686868600000000000000000000 8787878787878787878700000000000000000000 8888888888888888888800000000000000000000 8989898989898989898900000000000000000000 8a8a8a8a8a8a8a8a8a8a00000000000000000000 8b8b8b8b8b8b8b8b8b8b00000000000000000000 8c8c8c8c8c8c8c8c8c8c00000000000000000000 8d8d8d8d8d8d8d8d8d8d00000000000000000000 8e8e8e8e8e8e8e8e8e8e00000000000000000000 8f8f8f8f8f8f8f8f8f8f00000000000000000000 9090909090909090909000000000000000000000 9191919191919191919100000000000000000000 9292929292929292929200000000000000000000 9393939393939393939300000000000000000000 9494949494949494949400000000000000000000 9595959595959595959500000000000000000000 9696969696969696969600000000000000000000 9797979797979797979700000000000000000000 9898989898989898989800000000000000000000 9999999999999999999900000000000000000000 9a9a9a9a9a9a9a9a9a9a00000000000000000000 9b9b9b9b9b9b9b9b9b9b00000000000000000000 9c9c9c9c9c9c9c9c9c9c00000000000000000000 9d9d9d9d9d9d9d9d9d9d00000000000000000000 9e9e9e9e9e9e9e9e9e9e00000000000000000000 9f9f9f9f9f9f9f9f9f9f00000000000000000000 a0a0a0a0a0a0a0a0a0a000000000000000000000 a1a1a1a1a1a1a1a1a1a100000000000000000000 a2a2a2a2a2a2a2a2a2a200000000000000000000 a3a3a3a3a3a3a3a3a3a300000000000000000000 a4a4a4a4a4a4a4a4a4a400000000000000000000 a5a5a5a5a5a5a5a5a5a500000000000000000000 a6a6a6a6a6a6a6a6a6a600000000000000000000 a7a7a7a7a7a7a7a7a7a700000000000000000000 a8a8a8a8a8a8a8a8a8a800000000000000000000 a9a9a9a9a9a9a9a9a9a900000000000000000000 aaaaaaaaaaaaaaaaaaaa00000000000000000000 abababababababababab00000000000000000000 acacacacacacacacacac00000000000000000000 adadadadadadadadadad00000000000000000000 aeaeaeaeaeaeaeaeaeae00000000000000000000 afafafafafafafafafaf00000000000000000000 b0b0b0b0b0b0b0b0b0b000000000000000000000 b1b1b1b1b1b1b1b1b1b100000000000000000000 b2b2b2b2b2b2b2b2b2b200000000000000000000 b3b3b3b3b3b3b3b3b3b300000000000000000000 b4b4b4b4b4b4b4b4b4b400000000000000000000 b5b5b5b5b5b5b5b5b5b500000000000000000000 b6b6b6b6b6b6b6b6b6b600000000000000000000 b7b7b7b7b7b7b7b7b7b700000000000000000000 b8b8b8b8b8b8b8b8b8b800000000000000000000 b9b9b9b9b9b9b9b9b9b900000000000000000000 babababababababababa00000000000000000000 bbbbbbbbbbbbbbbbbbbb00000000000000000000 bcbcbcbcbcbcbcbcbcbc00000000000000000000 bdbdbdbdbdbdbdbdbdbd00000000000000000000 bebebebebebebebebebe00000000000000000000 bfbfbfbfbfbfbfbfbfbf00000000000000000000 c0c0c0c0c0c0c0c0c0c000000000000000000000 c1c1c1c1c1c1c1c1c1c100000000000000000000 c2c2c2c2c2c2c2c2c2c200000000000000000000 c3c3c3c3c3c3c3c3c3c300000000000000000000 c4c4c4c4c4c4c4c4c4c400000000000000000000 c5c5c5c5c5c5c5c5c5c500000000000000000000 c6c6c6c6c6c6c6c6c6c600000000000000000000 c7c7c7c7c7c7c7c7c7c700000000000000000000 c8c8c8c8c8c8c8c8c8c800000000000000000000 c9c9c9c9c9c9c9c9c9c900000000000000000000 cacacacacacacacacaca00000000000000000000 cbcbcbcbcbcbcbcbcbcb00000000000000000000 cccccccccccccccccccc00000000000000000000 cdcdcdcdcdcdcdcdcdcd00000000000000000000 cececececececececece00000000000000000000 cfcfcfcfcfcfcfcfcfcf00000000000000000000 d0d0d0d0d0d0d0d0d0d000000000000000000000 d1d1d1d1d1d1d1d1d1d100000000000000000000 d2d2d2d2d2d2d2d2d2d200000000000000000000 d3d3d3d3d3d3d3d3d3d300000000000000000000 d4d4d4d4d4d4d4d4d4d400000000000000000000 d5d5d5d5d5d5d5d5d5d500000000000000000000 d6d6d6d6d6d6d6d6d6d600000000000000000000 d7d7d7d7d7d7d7d7d7d700000000000000000000 d8d8d8d8d8d8d8d8d8d800000000000000000000 d9d9d9d9d9d9d9d9d9d900000000000000000000 dadadadadadadadadada00000000000000000000 dbdbdbdbdbdbdbdbdbdb00000000000000000000 dcdcdcdcdcdcdcdcdcdc00000000000000000000 dddddddddddddddddddd00000000000000000000 dededededededededede00000000000000000000 dfdfdfdfdfdfdfdfdfdf00000000000000000000 e0e0e0e0e0e0e0e0e0e000000000000000000000 e1e1e1e1e1e1e1e1e1e100000000000000000000 e2e2e2e2e2e2e2e2e2e200000000000000000000 e3e3e3e3e3e3e3e3e3e300000000000000000000 e4e4e4e4e4e4e4e4e4e400000000000000000000 e5e5e5e5e5e5e5e5e5e500000000000000000000 e6e6e6e6e6e6e6e6e6e600000000000000000000 e7e7e7e7e7e7e7e7e7e700000000000000000000 e8e8e8e8e8e8e8e8e8e800000000000000000000 e9e9e9e9e9e9e9e9e9e900000000000000000000 eaeaeaeaeaeaeaeaeaea00000000000000000000 ebebebebebebebebebeb00000000000000000000 ecececececececececec00000000000000000000 edededededededededed00000000000000000000 eeeeeeeeeeeeeeeeeeee00000000000000000000 efefefefefefefefefef00000000000000000000 f0f0f0f0f0f0f0f0f0f000000000000000000000 f1f1f1f1f1f1f1f1f1f100000000000000000000 f2f2f2f2f2f2f2f2f2f200000000000000000000 f3f3f3f3f3f3f3f3f3f300000000000000000000 f4f4f4f4f4f4f4f4f4f400000000000000000000 f5f5f5f5f5f5f5f5f5f500000000000000000000 f6f6f6f6f6f6f6f6f6f600000000000000000000 f7f7f7f7f7f7f7f7f7f700000000000000000000 f8f8f8f8f8f8f8f8f8f800000000000000000000 f9f9f9f9f9f9f9f9f9f900000000000000000000 fafafafafafafafafafa00000000000000000000 fbfbfbfbfbfbfbfbfbfb00000000000000000000 fcfcfcfcfcfcfcfcfcfc00000000000000000000 fdfdfdfdfdfdfdfdfdfd00000000000000000000 fefefefefefefefefefe00000000000000000000 ffffffffffffffffffff00000000000000000000 termineter-1.0.4/lib/termineter/errors.py000066400000000000000000000034651325174767400205340ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/errors.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals class FrameworkError(Exception): def __init__(self, msg): self.msg = msg def __str__(self): return repr(self.msg) class FrameworkConfigurationError(FrameworkError): pass class FrameworkRuntimeError(FrameworkError): pass termineter-1.0.4/lib/termineter/interface.py000066400000000000000000000552111325174767400211540ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/interface.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import absolute_import from __future__ import unicode_literals import code import logging import os import platform import random import subprocess import sys import textwrap import termineter import termineter.cmd import termineter.core import termineter.errors import termineter.its import termcolor codename = 'T-1000' def complete_all_paths(path): if not path: return [(p + os.sep if os.path.isdir(p) else p) for p in os.listdir('.')] if path[-1] == os.sep and not os.path.isdir(path): return [] if os.path.isdir(path): file_prefix = '' else: path, file_prefix = os.path.split(path) path = path or '.' + os.sep if not path[-1] == os.sep: path += os.sep return [path + (p + os.sep if os.path.isdir(os.path.join(path, p)) else p) for p in os.listdir(path) if p.startswith(file_prefix)] def complete_path(path, allow_files=False): paths = complete_all_paths(path) if not allow_files: paths = [p for p in paths if not os.path.isfile(p)] return paths # the core interpreter for the console class InteractiveInterpreter(termineter.cmd.Cmd): """The core interpreter for the CLI interface.""" __name__ = 'termineter' prompt = __name__ + ' > ' ruler = '+' doc_header = 'Type help For Information\nList Of Available Commands:' def __init__(self, check_rc_file=True, stdin=None, stdout=None, log_handler=None): super(InteractiveInterpreter, self).__init__(stdin=stdin, stdout=stdout) if not self.use_rawinput: # No 'use_rawinput' will cause problems with the ipy command so disable it for now self._disabled_commands.append('ipy') if not termineter.its.on_linux: self._hidden_commands.append('prep_driver') self._hidden_commands.append('cd') self._hidden_commands.append('exploit') self._hidden_commands.append('print_status') self.last_module = None self.log_handler = log_handler if self.log_handler is None: self._disabled_commands.append('logging') self.logger = logging.getLogger('termineter.interpreter') self.frmwk = termineter.core.Framework(stdout=stdout) self.print_exception = self.frmwk.print_exception self.print_error = self.frmwk.print_error self.print_good = self.frmwk.print_good self.print_line = self.frmwk.print_line self.print_status = self.frmwk.print_status if check_rc_file: check_rc_file = os.path.join(self.frmwk.directories.user_data, 'console.rc') if os.path.isfile(check_rc_file) and os.access(check_rc_file, os.R_OK): self.print_status('Running commands from resource file: ' + check_rc_file) self.run_rc_file(check_rc_file) elif isinstance(check_rc_file, str): if os.path.isfile(check_rc_file) and os.access(check_rc_file, os.R_OK): self.print_status('Running commands from resource file: ' + check_rc_file) self.run_rc_file(check_rc_file) else: self.logger.error('could not access resource file: ' + check_rc_file) self.print_error('Could not access resource file: ' + check_rc_file) try: import readline readline.read_history_file(self.frmwk.directories.user_data + 'history.txt') readline.set_completer_delims(readline.get_completer_delims().replace('/', '')) except (ImportError, IOError): pass @property def intro(self): intro = os.linesep intro += ' ______ _ __ ' + os.linesep intro += ' /_ __/__ ______ _ (_)__ ___ / /____ ____' + os.linesep intro += ' / / / -_) __/ \' \/ / _ \/ -_) __/ -_) __/' + os.linesep intro += ' /_/ \__/_/ /_/_/_/_/_//_/\__/\__/\__/_/ ' + os.linesep intro += os.linesep fmt_string = " <[ {0:<18} {1:>18}" intro += fmt_string.format(self.__name__, 'v' + termineter.__version__) + os.linesep intro += fmt_string.format('model:', codename) + os.linesep intro += fmt_string.format('loaded modules:', len(self.frmwk.modules)) + os.linesep return intro @property def prompt(self): if self.frmwk.current_module: module_name = self.frmwk.current_module.name if self.frmwk.use_colors: module_name = termcolor.colored(module_name, 'yellow', attrs=('bold',)) return self.__name__ + ' (' + module_name + ') > ' else: return self.__name__ + ' > ' def reload_module(self, module): is_current = self.frmwk.current_module and module.path == self.frmwk.current_module.path try: module = self.frmwk.modules.reload(module.path) except termineter.errors.FrameworkRuntimeError: self.print_error('Failed to reload the module') return except Exception as error: self.print_exception(error) return self.print_status('Successfully reloaded module: ' + module.path) if is_current: self.frmwk.current_module = module return module def run_rc_file(self, rc_file): self.logger.info('processing "' + rc_file + '" for commands') return super(InteractiveInterpreter, self).run_rc_file(rc_file) @classmethod def serve(cls, *args, **kwargs): init_kwargs = kwargs.pop('init_kwargs', {}) init_kwargs['check_rc_file'] = False kwargs['init_kwargs'] = init_kwargs super(InteractiveInterpreter, cls).serve(*args, **kwargs) @termineter.cmd.command('Stop using a module and return back to the framework context.') def do_back(self, args): self.frmwk.current_module = None @termineter.cmd.command('Display the banner') def do_banner(self, args): self.print_line(self.intro) @termineter.cmd.command('Change the current working directory.') @termineter.cmd.argument('path', help='the new path to change into') def do_cd(self, args): if not args.path: self.print_error('must specify a path') return if not os.path.isdir(args.path): self.print_error('invalid path') return os.chdir(args.path) def complete_cd(self, text, line, begidx, endidx): return complete_path(text, allow_files=False) @termineter.cmd.command('Connect the serial interface.') def do_connect(self, args): if self.frmwk.is_serial_connected(): self.print_status('Already connected') return missing_options = self.frmwk.options.get_missing_options() if missing_options: self.print_error('The following options must be set: ' + ', '.join(missing_options)) return try: self.frmwk.test_serial_connection() except Exception as error: self.print_exception(error) return self.print_good('Successfully connected and the device is responding') @termineter.cmd.command('Exit the interpreter.') def do_exit(self, args): quotes = ( 'I\'ll be back.', 'Hasta la vista, baby.', 'Come with me if you want to live.', 'Where\'s John Connor?' ) self.logger.info('received exit command, now exiting') self.print_status(random.choice(quotes)) try: import readline readline.write_history_file(self.frmwk.directories.user_data + 'history.txt') except (ImportError, IOError): pass return True def do_exploit(self, args): """Alias of the 'run' command""" self.do_run(args) def do_help(self, args): super(InteractiveInterpreter, self).do_help(args) self.print_line('') @termineter.cmd.command('Set and show logging options') @termineter.cmd.argument('level', nargs='?', help='the logging level to set') def do_logging(self, args): if self.log_handler is None: self.print_error('No log handler is defined') return if args.level is None: loglvl = self.log_handler.level self.print_status('Effective logging level is: ' + ({10: 'DEBUG', 20: 'INFO', 30: 'WARNING', 40: 'ERROR', 50: 'CRITICAL'}.get(loglvl) or 'UNKNOWN')) return new_level = args.level.upper() new_level = next((level for level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') if level.startswith(new_level)), None) if new_level is None: self.print_error('Invalid logging level: ' + args.level) self.log_handler.setLevel(getattr(logging, new_level)) self.print_status('Successfully changed the logging level to: ' + new_level) def complete_logging(self, text, line, begidx, endidx): return [i for i in ['debug', 'info', 'warning', 'error', 'critical'] if i.startswith(text.lower())] @termineter.cmd.command('Show module information') @termineter.cmd.argument('module', nargs='?', help='the module whose information is to be shown') def do_info(self, args): if args.module is None: if self.frmwk.current_module is None: self.print_error('Must select module to show information') return module = self.frmwk.current_module elif args.module in self.frmwk.modules: module = self.frmwk.modules[args.module] else: self.print_error('Invalid module name') return self.print_line('') self.print_line(' Name: ' + module.name) if len(module.author) == 1: self.print_line(' Author: ' + module.author[0]) elif len(module.author) > 1: self.print_line(' Authors: ' + module.author[0]) for additional_author in module.author[1:]: self.print_line(' ' + additional_author) if isinstance(module, termineter.module.TermineterModuleOptical): self.print_line(' Connection: ' + module.connection_state.name) self.print_line('') self.print_line('Basic Options: ') longest_name = 16 longest_value = 10 for option_name, option_def in module.options.items(): longest_name = max(longest_name, len(option_name)) longest_value = max(longest_value, len(str(module.options[option_name]))) fmt_string = " {0:<" + str(longest_name) + "} {1:<" + str(longest_value) + "} {2}" self.print_line(fmt_string.format('Name', 'Value', 'Description')) self.print_line(fmt_string.format('----', '-----', '-----------')) for option_name in module.options.keys(): option_value = module.options[option_name] if option_value is None: option_value = '' option_desc = module.options.get_option(option_name).help self.print_line(fmt_string.format(option_name, str(option_value), option_desc)) self.print_line('') self.print_line('Description:') for line in textwrap.wrap(textwrap.dedent(module.detailed_description), 78): self.print_line(' ' + line) self.print_line('') def complete_info(self, text, line, begidx, endidx): return [i for i in self.frmwk.modules.keys() if i.startswith(text)] @termineter.cmd.command('Start an interactive Python interpreter') def do_ipy(self, args): """Start an interactive Python interpreter""" import c1218.data import c1219.data from c1219.access.general import C1219GeneralAccess from c1219.access.security import C1219SecurityAccess from c1219.access.log import C1219LogAccess from c1219.access.telephone import C1219TelephoneAccess vars = { 'termineter.__version__': termineter.__version__, 'C1218Packet': c1218.data.C1218Packet, 'C1218ReadRequest': c1218.data.C1218ReadRequest, 'C1218WriteRequest': c1218.data.C1218WriteRequest, 'C1219ProcedureInit': c1219.data.C1219ProcedureInit, 'C1219GeneralAccess': C1219GeneralAccess, 'C1219SecurityAccess': C1219SecurityAccess, 'C1219LogAccess': C1219LogAccess, 'C1219TelephoneAccess': C1219TelephoneAccess, 'frmwk': self.frmwk, 'os': os, 'sys': sys } banner = 'Python ' + sys.version + ' on ' + sys.platform + os.linesep banner += os.linesep banner += 'The framework instance is in the \'frmwk\' variable.' if self.frmwk.is_serial_connected(): vars['conn'] = self.frmwk.serial_connection banner += os.linesep banner += 'The connection instance is in the \'conn\' variable.' try: import IPython.terminal.embed except ImportError: pyconsole = code.InteractiveConsole(vars) savestdin = os.dup(sys.stdin.fileno()) savestdout = os.dup(sys.stdout.fileno()) savestderr = os.dup(sys.stderr.fileno()) try: pyconsole.interact(banner) except SystemExit: sys.stdin = os.fdopen(savestdin, 'r', 0) sys.stdout = os.fdopen(savestdout, 'w', 0) sys.stderr = os.fdopen(savestderr, 'w', 0) else: self.print_line(banner) pyconsole = IPython.terminal.embed.InteractiveShellEmbed( ipython_dir=os.path.join(self.frmwk.directories.user_data, 'ipython') ) pyconsole.mainloop(vars) @termineter.cmd.command('Prepare the optical probe driver') @termineter.cmd.argument('vendor_id', help='the 4 hex digits of the vendor id') @termineter.cmd.argument('product_id', help='the 4 hex digits of the product id') def do_prep_driver(self, args): if os.getuid(): self.print_error('Must be running as root to prep the driver') return vendor_id = args.vendor_id if vendor_id.startswith('0x'): vendor_id = vendor_id[2:] product_id = args.product_id if product_id.startswith('0x'): product_id = product_id[2:] linux_kernel_version = platform.uname()[2].split('.')[:2] linux_kernel_version = tuple(int(part) for part in linux_kernel_version) if linux_kernel_version < (3, 12): proc_args = ['modprobe', 'ftdi-sio', "vendor=0x{0}".format(vendor_id), "product=0x{0}".format(product_id)] else: proc_args = ['modprobe', 'ftdi-sio'] proc_h = subprocess.Popen(proc_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=True, shell=False) if proc_h.wait(): self.print_error('modprobe exited with a non-zero status code') return if linux_kernel_version >= (3, 12) and os.path.isfile('/sys/bus/usb-serial/drivers/ftdi_sio/new_id'): with open('/sys/bus/usb-serial/drivers/ftdi_sio/new_id', 'w') as file_h: file_h.write("{0} {1}".format(vendor_id, product_id)) self.print_status('Finished driver preparation') @termineter.cmd.command('Use the last specified module.') def do_previous(self, args): if self.last_module is None: self.frmwk.print_error('no module has been previously selected') return self.frmwk.current_module, self.last_module = self.last_module, self.frmwk.current_module @termineter.cmd.command('Print a message in the interface.') @termineter.cmd.argument('message', help='the message to print') def do_print_status(self, args): self.print_status(args.message) @termineter.cmd.command('Load the protocon engine') @termineter.cmd.argument('-u', '--url', help='the connection URL (defaults to the serial device)') @termineter.cmd.argument('scripts', metavar='script', nargs='*', help='the script to execute') def do_protocon(self, args): try: import protocon except ImportError: self.print_error('The protocon package is unavailable, please install it first') return if args.url: url = args.url else: url = "serial://{0}?baudrate={1}&bytesize={2}&parity=N&stopbits={3}".format( self.frmwk.options['SERIAL_CONNECTION'], self.frmwk.advanced_options['SERIAL_BAUD_RATE'], self.frmwk.advanced_options['SERIAL_BYTE_SIZE'], self.frmwk.advanced_options['SERIAL_STOP_BITS'] ) try: engine = protocon.Engine.from_url(url) except protocon.ProtoconDriverError as error: self.print_error('Driver error: ' + error.message) except Exception as error: self.print_exception(error) else: engine.entry(args.scripts) engine.connection.close() return 0 @termineter.cmd.command('Reload a module into the framework') @termineter.cmd.argument('module', nargs='?', help='the module to reload') def do_reload(self, args): """Reload a module in to the framework""" if args.module is not None: if args.module not in self.frmwk.modules: self.print_error('Invalid Module Selected.') return module = self.frmwk.modules[args.module] elif self.frmwk.current_module: module = self.frmwk.current_module else: self.print_error('Must \'use\' module first') return self.reload_module(module) def complete_reload(self, text, line, begidx, endidx): return [i for i in self.frmwk.modules.keys() if i.startswith(text)] @termineter.cmd.command('Run one or more resource files') @termineter.cmd.argument('resource_files', metavar='resource_file', nargs='+', help='the resource files to run') def do_resource(self, args): for rc_file in args.resource_files: if not os.path.isfile(rc_file): self.print_error('Invalid resource file: ' + rc_file + ' (not found)') continue if not os.access(rc_file, os.R_OK): self.print_error('Invalid resource file: ' + rc_file + ' (no read permissions)') continue self.print_status('Running commands from resource file: ' + rc_file) self.run_rc_file(rc_file) def complete_resource(self, text, line, begidx, endidx): return complete_path(text, allow_files=True) @termineter.cmd.command('Run the specified module') @termineter.cmd.argument('-r', '--reload', action='store_true', default=False, help='reload the module before running it') @termineter.cmd.argument('module', nargs='?', help='the module to run') def do_run(self, args): old_module = None if args.module is None: if self.frmwk.current_module is None: self.print_error('Must \'use\' module first') return else: if args.module not in self.frmwk.modules: self.print_error('Invalid module specified: ' + args.module) return old_module = self.frmwk.current_module self.frmwk.current_module = self.frmwk.modules[args.module] module = self.frmwk.current_module if args.reload: module = self.reload_module(module) if module is None: return missing_options = module.get_missing_options() if missing_options: self.print_error('The following options must be set: ' + ', '.join(missing_options)) return del missing_options try: self.frmwk.run() except KeyboardInterrupt: self.print_line('') except Exception as error: self.print_exception(error) old_module = None if old_module: self.frmwk.current_module = old_module def complete_run(self, text, line, begidx, endidx): return [i for i in self.frmwk.modules.keys() if i.startswith(text)] @termineter.cmd.command('Set an option\'s value') @termineter.cmd.argument('option_name', metavar='option', help='the option\'s name') @termineter.cmd.argument('option_value', metavar='value', help='the option\'s new value') def do_set(self, args): if self.frmwk.current_module: options = self.frmwk.current_module.options advanced_options = self.frmwk.current_module.advanced_options else: options = self.frmwk.options advanced_options = self.frmwk.advanced_options if args.option_name in options: pass elif args.option_name in advanced_options: options = advanced_options else: self.print_error('Unknown option: ' + args.option_name) return try: success = options.set_option_value(args.option_name, args.option_value) except TypeError: self.print_error('Invalid data type') return if success: self.print_line(args.option_name + ' => ' + args.option_value) def complete_set(self, text, line, begidx, endidx): if self.frmwk.current_module: options = self.frmwk.current_module.options else: options = self.frmwk.options return [i + ' ' for i in options.keys() if i.startswith(text.upper())] @termineter.cmd.command('Show the specified information') @termineter.cmd.argument('thing', choices=('advanced', 'modules', 'options'), default='options', nargs='?', help='what to show') def do_show(self, args): """Valid parameters for the "show" command are: modules, options""" self.print_line('') if args.thing == 'modules': self.print_line('Modules' + os.linesep + '=======') headers = ('Name', 'Description') rows = [(module.path, module.description) for module in self.frmwk.modules.values()] else: if self.frmwk.current_module and args.thing == 'options': options = self.frmwk.current_module.options self.print_line('Module Options' + os.linesep + '==============') if self.frmwk.current_module and args.thing == 'advanced': options = self.frmwk.current_module.advanced_options self.print_line('Advanced Module Options' + os.linesep + '=======================') elif self.frmwk.current_module is None and args.thing == 'options': options = self.frmwk.options self.print_line('Framework Options' + os.linesep + '=================') elif self.frmwk.current_module is None and args.thing == 'advanced': options = self.frmwk.advanced_options self.print_line('Advanced Framework Options' + os.linesep + '==========================') headers = ('Name', 'Value', 'Description') raw_options = [options.get_option(name) for name in options] rows = [(option.name, str(option.value), option.help) for option in raw_options] rows = sorted(rows, key=lambda row: row[0]) self.print_line('') self.frmwk.print_table(rows, headers=headers, line_prefix=' ') self.print_line('') return def complete_show(self, text, line, begidx, endidx): return [i for i in ['advanced', 'modules', 'options'] if i.startswith(text.lower())] @termineter.cmd.command('Select a module to use') @termineter.cmd.argument('module', help='the module to use') def do_use(self, args): if args.module not in self.frmwk.modules: self.logger.error('failed to change context to module: ' + args.module) self.print_error('Failed to change context to module: ' + args.module) return self.last_module = self.frmwk.current_module self.frmwk.current_module = self.frmwk.modules[args.module] def complete_use(self, text, line, begidx, endidx): return [i for i in self.frmwk.modules.keys() if i.startswith(text)] @termineter.cmd.command('Show the framework version information') def do_version(self, args): fmt_string = "{0:<18} {1:>24}" self.print_line(fmt_string.format(self.__name__ + ':', 'v' + termineter.__version__)) revision = ('unknown' if termineter.revision is None else termineter.revision[:12]) self.print_line(fmt_string.format('revision:', revision)) self.print_line(fmt_string.format('model:', codename)) self.print_line(fmt_string.format('loaded modules:', len(self.frmwk.modules))) termineter-1.0.4/lib/termineter/its.py000066400000000000000000000034321325174767400200110ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/its.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import sys frozen = getattr(sys, 'frozen', False) on_linux = sys.platform.startswith('linux') on_windows = sys.platform.startswith('win') py_v2 = sys.version_info[0] == 2 py_v3 = sys.version_info[0] == 3 termineter-1.0.4/lib/termineter/module.py000066400000000000000000000130121325174767400204720ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/module.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import collections import collections.abc import enum import importlib import logging import termineter.errors import termineter.options import pluginbase _ModuleReference = collections.namedtuple('_ModuleReference', ('instance', 'pymodule')) class ConnectionState(enum.Enum): none = 'none' connected = 'connected' authenticated = 'authenticated' class ManagerManager(collections.abc.Mapping): def __init__(self, frmwk, searchpath): self.logger = logging.getLogger('termineter.module_manager') self.frmwk = frmwk self.source = pluginbase.PluginBase(package='termineter.modules').make_plugin_source( searchpath=searchpath ) self._modules = {} for module_id in self.source.list_plugins(): self._init_pymodule(self.source.load_plugin(module_id)) def __getitem__(self, item): return self._modules[item].instance def __iter__(self): return iter(self._modules) def __len__(self): return len(self._modules) def _init_pymodule(self, pymodule): module_id = pymodule.__name__.split('.', 3)[-1] if not hasattr(pymodule, 'Module'): self.logger.error('module: ' + module_id + ' is missing the Module class') return if not issubclass(pymodule.Module, TermineterModule): self.logger.error('module: ' + module_id + ' is not derived from the TermineterModule class') return module_instance = pymodule.Module(self.frmwk) if not isinstance(module_instance.options, termineter.options.Options): self.logger.critical('module: ' + module_id + ' options must be an Options instance') raise termineter.errors.FrameworkRuntimeError('options must be a termineter.options.Options instance') if not isinstance(module_instance.advanced_options, termineter.options.Options): self.logger.critical('module: ' + module_id + ' advanced_options must be an Options instance') raise termineter.errors.FrameworkRuntimeError('advanced_options must be a termineter.options.Options instance') self._modules[module_instance.name] = _ModuleReference(instance=module_instance, pymodule=pymodule) return module_instance def reload(self, module_path): modref = self._modules[module_path] importlib.reload(modref.pymodule) return self._init_pymodule(modref.pymodule) class TermineterModule(object): frmwk_required_options = () def __init__(self, frmwk): self.frmwk = frmwk self.author = ['Anonymous'] self.description = 'This module is undocumented.' self.detailed_description = 'This module is undocumented.' self.options = termineter.options.Options(frmwk.directories) self.advanced_options = termineter.options.AdvancedOptions(frmwk.directories) def __repr__(self): return '<' + self.__class__.__name__ + ' ' + self.name + ' >' def get_missing_options(self): frmwk_missing_options = self.frmwk.options.get_missing_options() frmwk_missing_options.extend(self.frmwk.advanced_options.get_missing_options()) missing_options = [] for required_option in self.frmwk_required_options: if required_option in frmwk_missing_options: missing_options.append(required_option) missing_options.extend(self.options.get_missing_options()) missing_options.extend(self.advanced_options.get_missing_options()) return missing_options @property def logger(self): return self.frmwk.get_module_logger(self.name) @property def name(self): return self.path.split('/')[-1] @property def path(self): return self.__module__.split('.', 3)[-1].replace('.', '/') def run(self): raise NotImplementedError() class TermineterModuleOptical(TermineterModule): frmwk_required_options = ( 'SERIAL_CONNECTION', 'USERNAME', 'USER_ID', 'PASSWORD', 'PASSWORD_HEX', 'SERIAL_BAUD_RATE', 'SERIAL_BYTE_SIZE', 'CACHE_TABLES', 'SERIAL_STOP_BITS', 'NUMBER_PACKETS', 'PACKET_SIZE' ) connection_state = ConnectionState.authenticated connection_states = ConnectionState def __init__(self, *args, **kwargs): super(TermineterModuleOptical, self).__init__(*args, **kwargs) @property def connection(self): return self.frmwk.serial_connection termineter-1.0.4/lib/termineter/modules/000077500000000000000000000000001325174767400203065ustar00rootroot00000000000000termineter-1.0.4/lib/termineter/modules/__init__.py000066400000000000000000000000001325174767400224050ustar00rootroot00000000000000termineter-1.0.4/lib/termineter/modules/brute_force_login.py000066400000000000000000000141171325174767400243530ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/brute_force_login.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import binascii import os import re import time from termineter.module import TermineterModuleOptical from termineter.utilities import StringGenerator class BruteForce: def __init__(self, dictionary_path=None): if dictionary_path is None: self.dictionary = None else: self.dictionary = open(dictionary_path, 'r') def __iter__(self): if self.dictionary is None: for password in StringGenerator(20): yield password else: password = self.dictionary.readline() while password: yield password password = self.dictionary.readline() self.dictionary.close() raise StopIteration def from_hex(data): return binascii.a2b_hex(data) def to_hex(data): return binascii.b2a_hex(data).decode('utf-8') class Module(TermineterModuleOptical): connection_state = TermineterModuleOptical.connection_states.none def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Brute Force Credentials' self.detailed_description = 'This module is used for brute forcing credentials on the smart meter. Passwords are not limited to ASCII values and in order to test the entire character space the user will have to provide a dictionary of hex strings and set USE_HEX to true.' self.options.add_boolean('USE_HEX', 'values in word list are in hex', default=True) self.options.add_rfile('DICTIONARY', 'dictionary of passwords to try', required=False, default='$DATA_PATH smeter_passwords.txt') self.options.add_string('USERNAME', 'user name to attempt to log in as', default='0000') self.options.add_integer('USER_ID', 'user id to attempt to log in as', default=1) self.advanced_options.add_boolean('PURE_BRUTEFORCE', 'perform a pure bruteforce', default=False) self.advanced_options.add_boolean('STOP_ON_SUCCESS', 'stop after the first successful login', default=True) self.advanced_options.add_float('DELAY', 'time in seconds to wait between attempts', default=0.20) def run(self): conn = self.frmwk.serial_connection logger = self.logger use_hex = self.options['USE_HEX'] dictionary_path = self.options['DICTIONARY'] username = self.options['USERNAME'] user_id = self.options['USER_ID'] time_delay = self.advanced_options['DELAY'] if len(username) > 10: self.frmwk.print_error('Username cannot be longer than 10 characters') return if not (0 <= user_id <= 0xffff): self.frmwk.print_error('User id must be between 0 and 0xffff') return if self.advanced_options['PURE_BRUTEFORCE']: self.frmwk.print_status('A pure brute force will take a very very long time') use_hex = True # if doing a prue brute force, it has to be True pw_generator = BruteForce() else: if not os.path.isfile(dictionary_path): self.frmwk.print_error('Can not find dictionary path') return pw_generator = BruteForce(dictionary_path) hex_regex = re.compile('^([0-9a-fA-F]{2})+$') self.frmwk.print_status('Starting brute force') for password in pw_generator: if not self.advanced_options['PURE_BRUTEFORCE']: if use_hex: password = password.strip() if hex_regex.match(password) is None: logger.error('invalid characters found while searching for hex') self.frmwk.print_error('Invalid characters found while searching for hex') return password = from_hex(password) else: password = password.rstrip() if len(password) > 20: if use_hex: logger.warning('skipping password: ' + to_hex(password) + ' due to length (can not be exceed 20 bytes)') else: logger.warning('skipping password: ' + password + ' due to length (can not be exceed 20 bytes)') continue while not conn.start(): time.sleep(time_delay) time.sleep(time_delay) if conn.login(username, user_id, password): if use_hex: self.frmwk.print_good('Successfully logged in. Username: ' + username + ' User ID: ' + str(user_id) + ' Password: ' + to_hex(password)) else: self.frmwk.print_good('Successfully logged in. Username: ' + username + ' User ID: ' + str(user_id) + ' Password: ' + password) if self.advanced_options['STOP_ON_SUCCESS']: conn.stop(force=True) break else: if use_hex: logger.warning('Failed logged in. Username: ' + username + ' User ID: ' + str(user_id) + ' Password: ' + to_hex(password)) else: logger.warning('Failed logged in. Username: ' + username + ' User ID: ' + str(user_id) + ' Password: ' + password) while not conn.stop(force=True): time.sleep(time_delay) time.sleep(time_delay) return termineter-1.0.4/lib/termineter/modules/diff_tables.py000066400000000000000000000151411325174767400231240ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/diff_tables.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # MA 02110-1301, USA. from __future__ import unicode_literals import binascii import difflib import c1219.constants from termineter.module import TermineterModule HTML_HEADER = """ Diff Tables """ HTML_TABLE_LEGEND = """
Legend
 Added ChangedDeleted
""" HTML_TABLE_HEADER = """ """ HTML_TABLE_FOOTER = """
Table NumberTable Data
""" HTML_FOOTER = """ """ class Module(TermineterModule): def __init__(self, *args, **kwargs): TermineterModule.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Check C12.19 Tables For Differences' self.detailed_description = 'This module will compare two CSV files created with dump_tables and display differences in a formatted HTML file.' self.options.add_string('FIRST_FILE', 'the first csv file to compare') self.options.add_string('SECOND_FILE', 'the second csv file to compare') self.options.add_string('REPORT_FILE', 'file to write the report data into', default='table_diff.html') self.advanced_options.add_boolean('ALL_TABLES', 'do not skip tables that typically change', default=False) def run(self): first_file = self.options['FIRST_FILE'] first_file = open(first_file, 'r') second_file = self.options['SECOND_FILE'] second_file = open(second_file, 'r') self.report = open(self.options['REPORT_FILE'], 'w', 1) self.differ = difflib.HtmlDiff() self.tables_to_skip = [ c1219.constants.PROC_INITIATE_TBL, c1219.constants.PROC_RESPONSE_TBL, c1219.constants.PRESENT_REGISTER_DATA_TBL ] self.report.write(HTML_HEADER) self.report.write(HTML_TABLE_LEGEND) self.report.write('
\n') self.report.write(HTML_TABLE_HEADER) self.highlight_table = True self.frmwk.print_status('Generating Diff...') fid, fline = self.get_line(first_file) sid, sline = self.get_line(second_file) while fid is not None or sid is not None: if (fid is None or sid is None) or fid == sid: self.report_line(fline, sline, (fid or sid)) fid, fline = self.get_line(first_file) sid, sline = self.get_line(second_file) elif fid < sid: self.report_line(fline, b'', fid) fid, fline = self.get_line(first_file) elif sid < fid: self.report_line(b'', sline, sid) sid, sline = self.get_line(second_file) self.report.write(HTML_TABLE_FOOTER) self.report.write(HTML_FOOTER) self.report.close() second_file.close() first_file.close() return def get_line(self, csv_file): line = csv_file.readline() if not line: return None, b'' line = line.strip().split(',') if not line: return None, b'' lid, ldata = int(line[0]), binascii.a2b_hex(line[-1]) return lid, ldata def report_line(self, fline, sline, lineno): if not self.advanced_options['ALL_TABLES'] and lineno in self.tables_to_skip: return seq = difflib.SequenceMatcher(None, fline, sline) opcodes = seq.get_opcodes() if len(opcodes) > 1 or len(fline) != len(sline): lineno = "{lineno}".format(lineno=lineno) span_tag = "" row_header = " {lineno:<8}" if self.highlight_table: highlight_table = 'class="diff_highlight" ' else: highlight_table = '' self.highlight_table = not self.highlight_table top_row = row_header.format(lineno=lineno, highlight_table=highlight_table, highlight_row='') bottom_row = row_header.format(lineno=lineno, highlight_table=highlight_table, highlight_row='class="diff_highlight" ') for tag, i1, i2, j1, j2 in opcodes: top_chunk = binascii.b2a_hex(fline[i1:i2]).decode('utf-8') bottom_chunk = binascii.b2a_hex(sline[j1:j2]).decode('utf-8') if tag != 'equal': top_row += span_tag.format(dtype=tag[:3]) bottom_row += span_tag.format(dtype=tag[:3]) while len(top_chunk) < len(bottom_chunk): top_chunk += ' ' while len(bottom_chunk) < len(top_chunk): bottom_chunk += ' ' top_row += top_chunk.replace(' ', ' ') bottom_row += bottom_chunk.replace(' ', ' ') if tag != 'equal': top_row += '' bottom_row += '' top_row += '' bottom_row += '' self.report.write(top_row + '\n') self.report.write(bottom_row + '\n') termineter-1.0.4/lib/termineter/modules/dump_tables.py000066400000000000000000000100121325174767400231510ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/dump_tables.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import binascii import os import time from c1218.errors import C1218ReadTableError from c1219.data import C1219_TABLES from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Write Readable C12.19 Tables To A CSV File' self.detailed_description = 'This module will enumerate the readable tables on the smart meter and write them out to a CSV formated file for analysis. The format is table id, table name, table data length, table data. The table data is represented in hex.' self.options.add_integer('LOWER', 'table id to start reading from', default=0) self.options.add_integer('UPPER', 'table id to stop reading from', default=256) self.options.add_string('FILE', 'file to write the csv data into', default='smart_meter_tables.csv') def run(self): conn = self.frmwk.serial_connection logger = self.logger lower_boundary = self.options['LOWER'] upper_boundary = self.options['UPPER'] out_file = open(self.options['FILE'], 'w', 1) number_of_tables = 0 self.frmwk.print_status('Starting dump, writing table data to: ' + self.options['FILE']) for tableid in range(lower_boundary, (upper_boundary + 1)): try: data = conn.get_table_data(tableid) except C1218ReadTableError as error: data = None if error.code == 10: # ISSS conn.stop() logger.warning('received ISSS error, connection stopped, will sleep before retrying') time.sleep(0.5) if not self.frmwk.serial_login(): logger.warning('meter login failed, some tables may not be accessible') try: data = conn.get_table_data(tableid) except C1218ReadTableError as error: data = None if error.code == 10: raise error # tried to re-sync communications but failed, you should reconnect and rerun the module if not data: continue tablename = C1219_TABLES.get(tableid, 'UNKNOWN') tableid = str(tableid) self.frmwk.print_status('Found readable table, ID: ' + tableid + ' Name: ' + tablename) # format is: table id, table name, table data length, table data out_file.write(','.join([tableid, tablename, str(len(data)), binascii.b2a_hex(data).decode('utf-8')]) + os.linesep) number_of_tables += 1 out_file.close() self.frmwk.print_status('Successfully copied ' + str(number_of_tables) + ' tables to disk.') termineter-1.0.4/lib/termineter/modules/enum_tables.py000066400000000000000000000071021325174767400231560ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/enum_tables.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals from time import sleep from c1218.errors import C1218ReadTableError from c1219.data import C1219_TABLES from termineter.module import TermineterModuleOptical # 0 - 2039 Standard Tables # 2048 - 4087 Manufacturer Tables 0 - 2039 # 4096 - 6135 Standard Pending Tables 0 - 2039 # 6144 - 8183 Manufacturer Pending Tables 0 - 2039 # 8192 - 10231 User Defined Tables 0 - 2039 # 12288 - 14327 User Defined Pending Tables 0 - 2039 class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Enumerate Readable C12.19 Tables From The Device' self.detailed_description = """\ This module will enumerate the readable tables on the smart meter by attempting to transfer each one. Tables are grouped into decades. """ self.options.add_integer('LOWER', 'table id to start reading from', default=0) self.options.add_integer('UPPER', 'table id to stop reading from', default=256) def run(self): conn = self.frmwk.serial_connection logger = self.logger lower_boundary = self.options['LOWER'] upper_boundary = self.options['UPPER'] number_of_tables = 0 self.frmwk.print_status('Enumerating tables, please wait...') for table_id in range(lower_boundary, (upper_boundary + 1)): try: conn.get_table_data(table_id) except C1218ReadTableError: self.frmwk.serial_disconnect() logger.warning('received ISSS error, connection stopped, will sleep before retrying') sleep(0.5) self.frmwk.serial_connect() if not self.frmwk.serial_login(): logger.warning('meter login failed, some tables may not be accessible') conn = self.frmwk.serial_connection else: self.frmwk.print_status('Found readable table, ID: ' + str(table_id) + ' Name: ' + (C1219_TABLES.get(table_id) or 'UNKNOWN')) number_of_tables += 1 self.frmwk.print_status("Found {0:,} tables in range {1}-{2}.".format(number_of_tables, lower_boundary, upper_boundary)) termineter-1.0.4/lib/termineter/modules/enum_user_ids.py000066400000000000000000000062711325174767400235270ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/enum_user_ids.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals from time import sleep from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): connection_state = TermineterModuleOptical.connection_states.none def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Enumerate Valid User IDs From The Device' self.detailed_description = 'This module will enumerate existing user IDs from the device.' self.options.add_string('USERNAME', 'user name to attempt to log in as', default='0000') self.options.add_integer('LOWER', 'user id to start enumerating from', default=0) self.options.add_integer('UPPER', 'user id to stop enumerating from', default=50) self.advanced_options.add_float('DELAY', 'time in seconds to wait between attempts', default=0.20) def run(self): conn = self.frmwk.serial_connection lower_boundary = self.options['LOWER'] upper_boundary = self.options['UPPER'] if lower_boundary > 0xffff: self.frmwk.print_error('LOWER option set to high (exceeds 0xffff)') return if upper_boundary > 0xffff: self.frmwk.print_error('UPPER option set to high (exceeds 0xffff)') return time_delay = self.advanced_options['DELAY'] self.frmwk.print_status('Enumerating user IDs, please wait...') for user_id in range(lower_boundary, (upper_boundary + 1)): while not conn.start(): sleep(time_delay) if conn.login(self.options['USERNAME'], user_id): self.frmwk.print_good('Found a valid User ID: ' + str(user_id)) while not conn.stop(force=True): sleep(time_delay) sleep(time_delay) return termineter-1.0.4/lib/termineter/modules/get_identification.py000066400000000000000000000062371325174767400245200ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/get_identification.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import c1218.data from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): connection_state = TermineterModuleOptical.connection_states.none def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Read And Parse The Identification Information' self.detailed_description = 'This module reads and parses the information from the C12.18 identification service.' def run(self): conn = self.frmwk.serial_connection conn.send(c1218.data.C1218IdentRequest()) resp = c1218.data.C1218Packet(conn.recv()) self.frmwk.print_status('Received Identity Response:') self.frmwk.print_hexdump(resp.data) if resp.data[0] != c1218.data.C1218_RESPONSE_CODES['ok']: self.frmwk.print_error("Non-ok response status 0x{0} ({1}) received".format( resp.data[0], c1218.data.C1218_RESPONSE_CODES.get(resp.data[0], 'unknown response code') )) if len(resp.data) < 5: self.frmwk.print_error('Received less that the expected amount of data') return standard, ver, rev = resp.data[1:4] standard = { 0: 'ANSI C12.18', 1: 'Reserved', 2: 'ANSI C12.21', 3: 'ANSI C12.22' }.get(standard, "Unknown (0x{0:02x})".format(standard)) rows = [ ('Reference Standard', standard), ('Standard Version', "{0}.{1}".format(ver, rev)) ] cursor = 4 if resp.data[cursor] == 0: rows.append(('Feature', 'N/A')) # the feature list is null terminated as defined in the c12.18 standard self.frmwk.print_table(rows, headers=('Name', 'Value')) termineter-1.0.4/lib/termineter/modules/get_info.py000066400000000000000000000100341325174767400224500ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/get_info.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals from c1218.errors import C1218ReadTableError from c1219.access.general import C1219GeneralAccess from termineter.module import TermineterModuleOptical STATUS_FLAGS = flags = ( 'Unprogrammed', 'Configuration Error', 'Self Check Error', 'RAM Failure', 'ROM Failure', 'Non Volatile Memory Failure', 'Clock Error', 'Measurement Error', 'Low Battery', 'Low Loss Potential', 'Demand Overload', 'Power Failure', 'Tamper Detect', 'Reverse Rotation' ) class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Get Basic Meter Information By Reading Tables' self.detailed_description = 'This module retreives some basic meter information and displays it in a human-readable way.' def run(self): conn = self.frmwk.serial_connection try: general_ctl = C1219GeneralAccess(conn) except C1218ReadTableError: self.frmwk.print_error('Could not read the necessary tables') return meter_info = {} meter_info['Character Encoding'] = general_ctl.char_format meter_info['Device Type'] = general_ctl.nameplate_type meter_info['C12.19 Version'] = {0: 'Pre-release', 1: 'C12.19-1997', 2: 'C12.19-2008'}.get(general_ctl.std_version_no) or 'Unknown' meter_info['Manufacturer'] = general_ctl.manufacturer meter_info['Model'] = general_ctl.ed_model meter_info['Hardware Version'] = str(general_ctl.hw_version_no) + '.' + str(general_ctl.hw_revision_no) meter_info['Firmware Version'] = str(general_ctl.fw_version_no) + '.' + str(general_ctl.fw_revision_no) meter_info['Serial Number'] = general_ctl.mfg_serial_no if general_ctl.ed_mode is not None: modes = [] flags = ['Metering', 'Test Mode', 'Meter Shop Mode', 'Factory'] for i in range(len(flags)): if general_ctl.ed_mode & (2 ** i): modes.append(flags[i]) if len(modes): meter_info['Mode Flags'] = ', '.join(modes) if general_ctl.std_status is not None: status = [] for i, flag in enumerate(STATUS_FLAGS): if general_ctl.std_status & (2 ** i): status.append(flag) if len(status): meter_info['Status Flags'] = ', '.join(status) if general_ctl.device_id is not None: meter_info['Device ID'] = general_ctl.device_id self.frmwk.print_status('General Information:') fmt_string = " {0:.<38}.{1}" keys = sorted(list(meter_info.keys())) for key in keys: self.frmwk.print_status(fmt_string.format(key, meter_info[key])) termineter-1.0.4/lib/termineter/modules/get_local_display_info.py000066400000000000000000000066351325174767400253630ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/get_local_display_info.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals from c1219.access.local_display import C1219LocalDisplayAccess from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Get Information From The Local Display Tables' self.detailed_description = '''\ Get and display information from the Local Display (3x decade) tables. This information will include what is being shown on the meters display and where the data is being read from. ''' def run(self): conn = self.frmwk.serial_connection loc_disp = C1219LocalDisplayAccess(conn) self.frmwk.print_status('Local Display List Records:') for entry, record in enumerate(loc_disp.pri_disp_list, 1): if entry > 1: self.frmwk.print_line('') self.frmwk.print_status(" record #{0}".format(entry)) self.frmwk.print_status(" on time: {0} seconds ({1}programmable)".format(record.on_time, ('' if loc_disp.on_time_flag else 'non'))) self.frmwk.print_status(" off time: {0} seconds ({1}programmable)".format(record.off_time, ('' if loc_disp.off_time_flag else 'non'))) self.frmwk.print_status(" hold time: {0} minutes ({1}programmable)".format(record.hold_time, ('' if loc_disp.hold_time_flag else 'non'))) self.frmwk.print_status(" default list: {0} ({1})".format( record.default_list, { 0: 'comm-link only', 1: 'normal', 2: 'alternate', 3: 'test' }.get(record.default_list, 'reserved') )) self.frmwk.print_status(" number of items: {0}".format(record.nbr_items)) self.frmwk.print_line('') self.frmwk.print_status('Local Display List Sources:') for source in loc_disp.pri_disp_sources: self.frmwk.print_status(" {0:<5} (0x{0:>04x})".format(source)) termineter-1.0.4/lib/termineter/modules/get_log_info.py000066400000000000000000000072031325174767400233150ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/get_log_info.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals from c1218.errors import C1218ReadTableError from c1219.access.log import C1219LogAccess from c1219.data import C1219_EVENT_CODES from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Get Information About The Meter\'s Logs' self.detailed_description = """\ This module reads various C1219 tables from decade 70 to gather log information from the smart meter. If successful the parsed contents of the logs will be displayed. """ def run(self): conn = self.frmwk.serial_connection try: log_ctl = C1219LogAccess(conn) except C1218ReadTableError: self.frmwk.print_error('Could not read necessary tables, logging may not be enabled') return if len(log_ctl.logs) == 0: self.frmwk.print_status('Log History Table Contains No Entries') return else: self.frmwk.print_status('Log History Table Contains ' + str(log_ctl.nbr_history_entries) + ' Entries') log_entry = log_ctl.logs[0] topline = '' line = '' if 'Time' in log_entry: topline += "{0:<19} ".format('Time Stamp') line += "{0:<19} ".format('----------') if 'Event Number' in log_entry: topline += "{0:<5} ".format('Event Number') line += "{0:<5} ".format('------------') topline += "{0:<6} {1:<58} {2}".format('UID', 'Procedure Number', 'Arguments') line += "{0:<6} {1:<58} {2}".format('---', '----------------', '---------') self.frmwk.print_line(topline) self.frmwk.print_line(line) for log_entry in log_ctl.logs: line = '' if 'Time' in log_entry: topline += "{0:<19} ".format('Time Stamp') line += "{0:<19} ".format(log_entry['Time']) if 'Event Number' in log_entry: topline += "{0:<5} ".format('Event Number') line += "{0:<5} ".format(log_entry['Event Number']) line += "{0:<6} {1:<58} {2}".format(log_entry['User ID'], C1219_EVENT_CODES[log_entry['Procedure Number']], log_entry['Arguments'].encode('hex')) self.frmwk.print_line(line) termineter-1.0.4/lib/termineter/modules/get_modem_info.py000066400000000000000000000073301325174767400236360ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/get_modem_info.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals from c1218.errors import C1218ReadTableError from c1219.access.telephone import C1219TelephoneAccess from c1219.data import C1219_CALL_STATUS_FLAGS from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Get Information About The Integrated Modem' self.detailed_description = 'This module reads various C12.19 tables from decade 90 to gather information about the integrated modem. If successfully parsed, useful information will be displayed.' def run(self): conn = self.frmwk.serial_connection try: telephone_ctl = C1219TelephoneAccess(conn) except C1218ReadTableError: self.frmwk.print_error('Could not read necessary tables, a modem is not likely present') return info = {} info['Can Answer'] = telephone_ctl.can_answer info['Extended Status Available'] = telephone_ctl.use_extended_status info['Number of Originating Phone Numbers'] = telephone_ctl.nbr_originate_numbers info['PSEM Identity'] = telephone_ctl.psem_identity if telephone_ctl.global_bit_rate: info['Global Bit Rate'] = telephone_ctl.global_bit_rate else: info['Originate Bit Rate'] = telephone_ctl.originate_bit_rate info['Answer Bit Rate'] = telephone_ctl.answer_bit_rate info['Dial Delay'] = telephone_ctl.dial_delay if len(telephone_ctl.prefix_number): info['Prefix Number'] = telephone_ctl.prefix_number keys = info.keys() keys.sort() self.frmwk.print_status('General Information:') fmt_string = " {0:.<38}.{1}" for key in keys: self.frmwk.print_status(fmt_string.format(key, info[key])) self.frmwk.print_status('Stored Telephone Numbers:') fmt_string = " {0:<6} {1:<16} {2:<32}" self.frmwk.print_status(fmt_string.format('Index', 'Number', 'Last Status')) self.frmwk.print_status(fmt_string.format('-----', '------', '-----------')) for idx, entry in telephone_ctl.originating_numbers.items(): self.frmwk.print_status(fmt_string.format(entry['idx'], entry['number'].strip(), C1219_CALL_STATUS_FLAGS[entry['status']])) termineter-1.0.4/lib/termineter/modules/get_security_info.py000066400000000000000000000115411325174767400244030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/get_security_info.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals from c1218.errors import C1218ReadTableError from c1219.access.security import C1219SecurityAccess from c1219.constants import C1219_TABLES, C1219_PROCEDURE_NAMES from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Get Information About The Meter\'s Access Control' self.detailed_description = 'This module reads various tables from 40 to gather information regarding access control. Password constraints, and access permissions to procedures and tables can be gathered with this module.' def run(self): conn = self.frmwk.serial_connection try: security_ctl = C1219SecurityAccess(conn) except C1218ReadTableError: self.frmwk.print_error('Could not read necessary tables') return security_info = {} security_info['Number of Passwords'] = security_ctl.nbr_passwords security_info['Max Password Length'] = security_ctl.password_len security_info['Number of Keys'] = security_ctl.nbr_keys security_info['Number of Permissions'] = security_ctl.nbr_perm_used self.frmwk.print_status('Security Information:') fmt_string = " {0:.<38}.{1}" keys = security_info.keys() keys.sort() for key in keys: self.frmwk.print_status(fmt_string.format(key, security_info[key])) self.frmwk.print_status('Passwords and Permissions:') fmt_string = " {0:<5} {1:<40} {2}" self.frmwk.print_status(fmt_string.format('Index', 'Password (In Hex)', 'Group Flags')) self.frmwk.print_status(fmt_string.format('-----', '-----------------', '-----------')) for idx, entry in security_ctl.passwords.items(): self.frmwk.print_status(fmt_string.format(idx, entry['password'].encode('hex'), entry['groups'])) self.frmwk.print_status('Table Permissions:') fmt_string = " {0:<64} {1:<14} {2:<14}" self.frmwk.print_status(fmt_string.format('Table Number', 'World Readable', 'World Writable')) self.frmwk.print_status(fmt_string.format('------------', '--------------', '--------------')) fmt_string = " {0:.<64} {1:<14} {2:<14}" for idx, entry in security_ctl.table_permissions.items(): self.frmwk.print_status(fmt_string.format('#' + str(idx) + ' ' + (C1219_TABLES.get(idx) or 'Unknown'), str(entry['anyread']), str(entry['anywrite']))) self.frmwk.print_status('Procedure Permissions:') fmt_string = " {0:<64} {1:<14} {2:<16}" self.frmwk.print_status(fmt_string.format('Procedure Number', 'World Readable', 'World Executable')) self.frmwk.print_status(fmt_string.format('----------------', '--------------', '----------------')) fmt_string = " {0:.<64} {1:<14} {2:<16}" for idx, entry in security_ctl.procedure_permissions.items(): self.frmwk.print_status(fmt_string.format('#' + str(idx) + ' ' + (C1219_PROCEDURE_NAMES.get(idx) or 'Unknown'), str(entry['anyread']), str(entry['anywrite']))) if len(security_ctl.keys): self.frmwk.print_status('Stored Keys:') fmt_string = " {0:<5} {1}" self.frmwk.print_status(fmt_string.format('Index', 'Hex Value')) self.frmwk.print_status(fmt_string.format('-----', '---------')) for idx, entry in security_ctl.keys.items(): self.frmwk.print_status(fmt_string.format(idx, entry.encode('hex'))) return termineter-1.0.4/lib/termineter/modules/read_table.py000066400000000000000000000046421325174767400227500ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/read_table.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals from c1218.errors import C1218ReadTableError from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Read Data From A C12.19 Table' self.detailed_description = 'This module allows individual tables to be read from the smart meter.' self.options.add_integer('TABLE_ID', 'table to read from', True) def run(self): conn = self.frmwk.serial_connection tableid = self.options['TABLE_ID'] try: data = conn.get_table_data(tableid) except C1218ReadTableError as error: self.frmwk.print_error('Caught C1218ReadTableError: ' + str(error)) self.frmwk.print_status('Read ' + str(len(data)) + ' bytes') self.frmwk.print_hexdump(data) termineter-1.0.4/lib/termineter/modules/remote_reset.py000066400000000000000000000053671325174767400233700ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/remote_reset.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import struct from c1218.errors import C1218ReadTableError, C1218WriteTableError from c1219.errors import C1219ProcedureError from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Initiate A Reset Procedure' self.detailed_description = 'Initiate a remote reset procedure. Despite the name, this module is used locally through the optical interface.' self.options.add_boolean('DEMAND', 'perform a demand reset', default=False) self.options.add_boolean('SELF_READ', 'perform a self read', default=False) def run(self): conn = self.frmwk.serial_connection params = 0 if self.options['DEMAND']: params |= 0b01 if self.options['SELF_READ']: params |= 0b10 self.frmwk.print_status('Initiating Reset Procedure') try: conn.run_procedure(9, False, struct.pack('B', params)) except (C1218ReadTableError, C1218WriteTableError, C1219ProcedureError) as error: self.frmwk.print_exception(error) else: self.frmwk.print_good('Successfully Reset The Meter') termineter-1.0.4/lib/termineter/modules/run_procedure.py000066400000000000000000000071061325174767400235400ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/run_procedure.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import binascii import re from c1219.constants import C1219_PROCEDURE_NAMES, C1219_PROC_RESULT_CODES from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Initiate A Custom Procedure' self.detailed_description = 'This module executes a user defined procedure and returns the response. This is achieved by writing to the Procedure Initiate Table (#7) and then reading the result from the Procedure Response Table (#8).' self.options.add_integer('PROC_NUMBER', 'procedure number to execute') self.options.add_string('PARAMS', 'parameters to pass to the executed procedure', default='') self.options.add_boolean('USE_HEX', 'specifies that the \'PARAMS\' option is represented in hex', default=True) self.advanced_options.add_boolean('STD_VS_MFG', 'if true, specifies that this procedure is defined by the manufacturer', default=False) def run(self): conn = self.frmwk.serial_connection data = self.options['PARAMS'] if self.options['USE_HEX']: data = data.replace(' ', '') hex_regex = re.compile('^([0-9a-fA-F]{2})+$') if hex_regex.match(data) is None: self.frmwk.print_error('Non-hex characters found in \'PARAMS\'') return data = binascii.a2b_hex(data) else: data = data.encode('utf-8') self.frmwk.print_status('Initiating procedure ' + (C1219_PROCEDURE_NAMES.get(self.options['PROC_NUMBER']) or '#' + str(self.options['PROC_NUMBER']))) error_code, data = conn.run_procedure(self.options['PROC_NUMBER'], self.advanced_options['STD_VS_MFG'], data) self.frmwk.print_status('Finished running procedure #' + str(self.options['PROC_NUMBER'])) self.frmwk.print_status('Received response from procedure: ' + (C1219_PROC_RESULT_CODES.get(error_code) or 'UNKNOWN')) if len(data): self.frmwk.print_status('Received data output from procedure: ') self.frmwk.print_hexdump(data) termineter-1.0.4/lib/termineter/modules/set_meter_id.py000066400000000000000000000054531325174767400233320ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/set_meter_id.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals from c1219.access.general import C1219GeneralAccess from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Set The Meter\'s I.D.' self.detailed_description = 'This module will over write the Smart Meter\'s device ID with the new value specified in METER_ID.' self.options.add_string('METER_ID', 'value to set the meter id to', True) def run(self): conn = self.frmwk.serial_connection logger = self.logger meter_id = self.options['METER_ID'] gen_ctl = C1219GeneralAccess(conn) if gen_ctl.id_form == 0: logger.info('device id stored in 20 byte string') if len(meter_id) > 20: self.frmwk.print_error('METER_ID length exceeds the allowed 20 bytes') return else: logger.info('device id stored in BCD(10)') if len(meter_id) > 10: self.frmwk.print_error('METER_ID length exceeds the allowed 10 bytes') return if gen_ctl.set_device_id(meter_id): self.frmwk.print_error('Could not set the Meter\'s ID') else: self.frmwk.print_status('Successfully updated the Meter\'s ID to: ' + meter_id) termineter-1.0.4/lib/termineter/modules/set_meter_mode.py000066400000000000000000000067721325174767400236670ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/set_meter_mode.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import struct from c1218.errors import C1218ReadTableError, C1218WriteTableError from c1219.constants import C1219_METER_MODE_NAMES, C1219_PROC_RESULT_CODES from c1219.errors import C1219ProcedureError from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Change the Meter\'s Operating Mode' self.detailed_description = 'Change the operating mode of the meter. Accepted values for MODE are METERING, TEST, METERSHOP, and FACTORY.' self.options.add_string('MODE', 'the mode to set the meter to', True) def run(self): conn = self.frmwk.serial_connection logger = self.logger mode = self.options['MODE'] mode = mode.upper() mode = mode.replace('_', '') mode = mode.replace(' ', '') if mode[-4:] == 'MODE': mode = mode[:-4] mode_dict = C1219_METER_MODE_NAMES if not mode in mode_dict: self.frmwk.print_error('unknown mode, please use METERING, TEST, METERSHOP, or FACTORY') return logger.info('setting mode to: ' + mode) self.frmwk.print_status('Setting Mode To: ' + mode) mode = mode_dict[mode] try: result_code, response_data = conn.run_procedure(6, False, struct.pack('B', mode)) except C1218ReadTableError as error: self.frmwk.print_exception(error) return except C1218WriteTableError as error: if error.code == 4: # onp/operation not possible self.frmwk.print_error('Meter responded that it can not set the mode to the desired type') else: self.frmwk.print_exception(error) return except C1219ProcedureError as error: self.frmwk.print_exception(error) return if result_code < 2: self.frmwk.print_good(C1219_PROC_RESULT_CODES[result_code]) else: self.frmwk.print_error(C1219_PROC_RESULT_CODES.get(result_code, "Unknown status code: {0}".format(result_code))) termineter-1.0.4/lib/termineter/modules/write_table.py000066400000000000000000000070331325174767400231640ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/modules/write_table.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import binascii import re from c1218.errors import C1218WriteTableError from termineter.module import TermineterModuleOptical class Module(TermineterModuleOptical): def __init__(self, *args, **kwargs): TermineterModuleOptical.__init__(self, *args, **kwargs) self.author = ['Spencer McIntyre'] self.description = 'Write Data To A C12.19 Table' self.detailed_description = '''\ This will over write the data in a write able table on the smart meter. If USE_HEX is set to true then the DATA option is expected to be represented as a string of hex characters. ''' self.options.add_integer('TABLE_ID', 'table to read from', True) self.options.add_string('DATA', 'data to write to the table', True) self.options.add_boolean('USE_HEX', 'specifies that the \'DATA\' option is represented in hex', default=True) self.options.add_integer('OFFSET', 'offset to start writing data at', required=False, default=0) self.advanced_options.add_boolean('VERIFY', 'verify that the data was written with a read request', default=True) def run(self): conn = self.frmwk.serial_connection tableid = self.options['TABLE_ID'] data = self.options['DATA'] offset = self.options['OFFSET'] if self.options['USE_HEX']: data = data.replace(' ', '') hex_regex = re.compile('^([0-9a-fA-F]{2})+$') if hex_regex.match(data) is None: self.frmwk.print_error('Non-hex characters found in \'DATA\'') return data = binascii.a2b_hex(data) else: data = data.encode('utf-8') try: conn.set_table_data(tableid, data, offset) except C1218WriteTableError as error: self.frmwk.print_exception(error) else: self.frmwk.print_status('Successfully Wrote Data') if self.advanced_options['VERIFY']: table = conn.get_table_data(tableid) if table[offset:offset + len(data)] == data: self.frmwk.print_status('Table Write Verification Passed') else: self.frmwk.print_error('Table Write Verification Failed') self.frmwk.print_hexdump(table) termineter-1.0.4/lib/termineter/options.py000066400000000000000000000202611325174767400207040ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/options.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import collections.abc import os def string_is_hex(string): if not len(string): return False return bool(not filter(lambda c: c not in '0123456789abcdefABCDEF', string)) class Option(object): __slots__ = ('callback', 'default', 'help', 'name', 'required', 'type', 'value') def __init__(self, name, type, help, required, default=None, callback=None): self.name = name self.type = type self.help = help self.required = required self.default = default self.value = default self.callback = callback def __repr__(self): return "<{0} name='{1}' value={2!r} >".format(self.__class__.__name__, self.name, self.value) class Options(collections.abc.Mapping): """ This is a generic options container, it is used to organize framework and module options. Once the options are defined and set, the values can be retreived by referencing this object like a dictionary such as myoptions['OPTIONNAME'] will return 'OPTIONVALUE' """ def __init__(self, directories): """ :param directories: An object with attributes of various directories. """ self.directories = directories self._options = {} def __getitem__(self, item): return self.get_option_value(item) def __iter__(self): return iter(self._options) def __len__(self): return len(self._options) def add_string(self, name, help, required=True, default=None): """ Add a new option with a type of String. :param str name: The name of the option, how it will be referenced. :param str help: The string returned as help to describe how the option is used. :param bool required: Whether to require that this option be set or not. :param str default: The default value for this option. If required is True and the user must specify it, set to anything but None. """ self._options[name] = Option(name, 'str', help, required, default=default) def add_integer(self, name, help, required=True, default=None): """ Add a new option with a type of Integer. :param str name: The name of the option, how it will be referenced. :param str help: The string returned as help to describe how the option is used. :param bool required: Whether to require that this option be set or not. :param int default: The default value for this option. If required is True and the user must specify it, set to anything but None. """ self._options[name] = Option(name, 'int', help, required, default=default) def add_float(self, name, help, required=True, default=None): """ Add a new option with a type of Float. :param str name: The name of the option, how it will be referenced. :param str help: The string returned as help to describe how the option is used. :param bool required: Whether to require that this option be set or not. :param float default: The default value for this option. If required is True and the user must specify it, set to anything but None. """ self._options[name] = Option(name, 'flt', help, required, default=default) def add_boolean(self, name, help, required=True, default=None): """ Add a new option with a type of Boolean. :param str name: The name of the option, how it will be referenced. :param str help: The string returned as help to describe how the option is used. :param bool required: Whether to require that this option be set or not. :param bool default: The default value for this option. If required is True and the user must specify it, set to anything but None. """ self._options[name] = Option(name, 'bool', help, required, default=default) def add_rfile(self, name, help, required=True, default=None): """ Add a new option with a type of a readable file. This is the same as the string option with the exception that the default value will have the following variables replaced within it: $USER_DATA The path to the users data directory $DATA_PATH The path to the framework's data directory This will NOT check that the file exists or is readable. :param str name: The name of the option, how it will be referenced. :param str help: The string returned as help to describe how the option is used. :param bool required: Whether to require that this option be set or not. :param str default: The default value for this option. If required is True and the user must specify it, set to anything but None. """ if isinstance(default, str): default = default.replace('$DATA_PATH ', self.directories.data_path + os.path.sep) default = default.replace('$USER_DATA ', self.directories.user_data + os.path.sep) self._options[name] = Option(name, 'rfile', help, required, default=default) def set_callback(self, name, callback): """ Set a callback function for the specified option. This function is called when the option's value changes. :param str name: The name of the option to set the callback for. :param callback: This function to be called when the option is changed. """ self.get_option(name).callback = callback def set_option_value(self, name, value): """ Set an option's value. :param str name: The name of the option to set the value for. :param str value: The value to set the option to, it will be converted from a string. :return: The previous value for the specified option. """ option = self.get_option(name) old_value = option.value if option.type in ('str', 'rfile'): option.value = value elif option.type == 'int': value = value.lower() if not value.isdigit(): if value.startswith('0x') and string_is_hex(value[2:]): value = int(value[2:], 16) else: raise TypeError('invalid value type') option.value = int(value) elif option.type == 'flt': if value.count('.') > 1: raise TypeError('invalid value type') if not value.replace('.', '').isdigit(): raise TypeError('invalid value type') option.value = float(value) elif option.type == 'bool': if value.lower() in ['true', '1', 'on']: option.value = True elif value.lower() in ['false', '0', 'off']: option.value = False else: raise TypeError('invalid value type') else: raise Exception('unknown value type') if option.callback and not option.callback(value, old_value): option.value = old_value return False return True def get_missing_options(self): """ Get a list of options that are required, but with default values of None. """ return [option.name for option in self._options.values() if option.required and option.value is None] def get_option(self, name): """ Get the option instance. :param str name: The name of the option to retrieve. :return: The option instance. """ return self._options[name] def get_option_value(self, name): return self.get_option(name).value class AdvancedOptions(Options): pass termineter-1.0.4/lib/termineter/utilities.py000066400000000000000000000070641325174767400212320ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # termineter/utilities.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import unicode_literals import copy import itertools import serial DEFAULT_SERIAL_SETTINGS = { 'parity': serial.PARITY_NONE, 'baudrate': 9600, 'bytesize': serial.EIGHTBITS, 'xonxoff': False, 'interCharTimeout': None, 'rtscts': False, 'timeout': 1, 'stopbits': serial.STOPBITS_ONE, 'dsrdtr': False, 'writeTimeout': None } def get_default_serial_settings(): return copy.copy(DEFAULT_SERIAL_SETTINGS) class Namespace: """ This class is used to hold attributes of the framework. It doesn't really do anything, it's used for organizational purposes only. """ pass def unique(seq, idfunc=None): """ Unique a list or tuple and preserve the order @type idfunc: Function or None @param idfunc: If idfunc is provided it will be called during the comparison process. """ if idfunc is None: idfunc = lambda x: x preserved_type = type(seq) seen = {} result = [] for item in seq: marker = idfunc(item) if marker in seen: continue seen[marker] = 1 result.append(item) return preserved_type(result) class StringGenerator: def __init__(self, startlen, endlen=None, charset=None): """ This class is used to generate raw strings for bruteforcing. @type startlen: Integer @param startlen: The minimum size of the string to bruteforce. @type endlen: Integer @param endlen: The maximum size of the string to bruteforce. @type charset: String, Tuple or None @param charset: the character set to use while generating the strings. If None, the full binary space will be used (0 - 255). """ self.startlen = startlen if endlen is None: self.endlen = startlen else: self.endlen = endlen if charset is None: charset = map(chr, range(0, 256)) elif type(charset) == str: charset = list(charset) charset = unique(charset) charset.sort() self.charset = tuple(charset) def __iter__(self): length = self.startlen while length <= self.endlen: for string in itertools.product(self.charset, repeat=length): yield ''.join(string) length += 1 raise StopIteration termineter-1.0.4/requirements.txt000066400000000000000000000002161325174767400171750ustar00rootroot00000000000000# these are duplicated in setup.py crcelk>=1.0 pluginbase>=0.5 pyasn1>=0.1.7 pyserial>=2.6 smoke-zephyr>=1.2 tabulate>=0.8.1 termcolor>=1.1.0 termineter-1.0.4/setup.py000077500000000000000000000063111325174767400154300ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # vim: tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab # # setup.py # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import sys base_directory = os.path.dirname(__file__) try: from setuptools import setup, find_packages except ImportError: print('This project needs setuptools in order to build. Install it using your package') print('manager (usually python-setuptools) or via pip (pip install setuptools).') sys.exit(1) try: import pypandoc long_description = pypandoc.convert(os.path.join(base_directory, 'README.md'), 'rst') except (ImportError, OSError): print('The pypandoc module is unavailable, can not generate the long description', file=sys.stderr) long_description = None DESCRIPTION = """\ Termineter is a Python framework which provides a platform for the security \ testing of smart meters.\ """ setup( name='termineter', version='1.0.4', author='Spencer McIntyre', author_email='smcintyre@securestate.com', maintainer='Spencer McIntyre', description=DESCRIPTION, long_description=long_description, url='https://github.com/securestate/termineter', license='BSD', # these are duplicated in requirements.txt install_requires=[ 'crcelk>=1.0', 'pluginbase>=0.5', 'pyasn1>=0.1.7', 'pyserial>=2.6', 'smoke-zephyr>=1.2', 'tabulate>=0.8.1', 'termcolor>=1.1.0' ], package_dir={'': 'lib'}, packages=find_packages('lib'), package_data={ '': ['data/*'], }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Security' ], scripts=['termineter'] ) termineter-1.0.4/termineter000077500000000000000000000057451325174767400160310ustar00rootroot00000000000000#!/usr/bin/python3 -B # -*- coding: utf-8 -*- # # termineter # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of the project nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from __future__ import absolute_import from __future__ import unicode_literals import argparse import logging import os import sys lib_directory = os.path.join(os.path.dirname(__file__), 'lib') if os.path.isdir(os.path.join(lib_directory, 'termineter')): sys.path.insert(0, lib_directory) from termineter import __version__ from termineter.interface import InteractiveInterpreter def main(): parser = argparse.ArgumentParser(description='Termineter: Python Smart Meter Testing Framework', conflict_handler='resolve') parser.add_argument('-v', '--version', action='version', version=parser.prog + ' Version: ' + __version__) parser.add_argument('-L', '--log', dest='loglvl', action='store', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], default='CRITICAL', help='set the logging level') parser.add_argument('-r', '--rc-file', dest='resource_file', action='store', default=True, help='execute a resource file') arguments = parser.parse_args() logging.getLogger('').setLevel(logging.DEBUG) console_log_handler = logging.StreamHandler() console_log_handler.setLevel(getattr(logging, arguments.loglvl)) console_log_handler.setFormatter(logging.Formatter("%(levelname)-8s %(message)s")) logging.getLogger('').addHandler(console_log_handler) rc_file = arguments.resource_file del arguments, parser interpreter = InteractiveInterpreter(rc_file, log_handler=console_log_handler) interpreter.cmdloop() logging.shutdown() if __name__ == '__main__': main()