├── .gitignore ├── .pylintrc ├── .pytest_cache └── README.md ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── insteonplm ├── __init__.py ├── address.py ├── constants.py ├── devices │ ├── __init__.py │ ├── climateControl.py │ ├── dimmableLightingControl.py │ ├── generalController.py │ ├── ipdb.py │ ├── securityHealthSafety.py │ ├── sensorsActuators.py │ ├── switchedLightingControl.py │ ├── unknowndevice.py │ ├── windowCoverings.py │ └── x10.py ├── linkedDevices.py ├── messagecallback.py ├── messages │ ├── __init__.py │ ├── allLinkCleanupFailureReport.py │ ├── allLinkCleanupStatusReport.py │ ├── allLinkComplete.py │ ├── allLinkRecordResponse.py │ ├── buttonEventReport.py │ ├── cancelAllLinking.py │ ├── extendedReceive.py │ ├── extendedSend.py │ ├── getFirstAllLinkRecord.py │ ├── getIMInfo.py │ ├── getImConfiguration.py │ ├── getNextAllLinkRecord.py │ ├── manageAllLinkRecord.py │ ├── message.py │ ├── messageFlags.py │ ├── resetIM.py │ ├── sendAlllinkCommand.py │ ├── setImConfiguration.py │ ├── standardReceive.py │ ├── standardSend.py │ ├── startAllLinking.py │ ├── userReset.py │ ├── userdata.py │ ├── x10received.py │ └── x10send.py ├── plm.py ├── states │ ├── __init__.py │ ├── cover.py │ ├── dimmable.py │ ├── onOff.py │ ├── sensor.py │ ├── statusReport.py │ ├── thermostat.py │ └── x10.py ├── tools.py └── utils.py ├── requirements.txt ├── requirements_test.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── mockCallbacks.py ├── mockConnection.py ├── mockPLM.py ├── test_address.py ├── test_aldb.py ├── test_device.py ├── test_dimmableLightingControl.py ├── test_linkedDevices.py ├── test_message.py ├── test_messageFlags.py ├── test_message_callback.py ├── test_messages.py ├── test_plm.py ├── test_securityHealthSafety.py ├── test_sensorsActuators.py ├── test_switchedLightingControl.py ├── test_userdata.py ├── test_x10.py └── todo_test_default_links.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | /.vs/python-insteonplm/v15/.suo 91 | /.vs/ProjectSettings.json 92 | /.vs/slnx.sqlite 93 | /.vs 94 | /python-insteonplm.pyproj 95 | /python-insteonplm.sln 96 | /insteonplm/plm.old 97 | /.mypy_cache/3.6 98 | /.pytest_cache/v/cache 99 | /.pypirc 100 | /.vscode 101 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | ignore=tests, samples 3 | # Use a conservative default here; 2 should speed up most setups and not hurt 4 | # any too bad. Override on command line as appropriate. 5 | jobs=2 6 | persistent=no 7 | class-naming-style=any 8 | 9 | [BASIC] 10 | good-names=id,ex,devices,on,off,scenes,commands 11 | 12 | [MESSAGES CONTROL] 13 | # Reasons disabled: 14 | # format - handled by black 15 | # locally-disabled - it spams too much 16 | # duplicate-code - unavoidable 17 | # cyclic-import - doesn't test if both import on load 18 | # abstract-class-little-used - prevents from setting right foundation 19 | # unused-argument - generic callbacks and setup methods create a lot of warnings 20 | # global-statement - used for the on-demand requirement installation 21 | # redefined-variable-type - this is Python, we're duck typing! 22 | # too-many-* - are not enforced for the sake of readability 23 | # too-few-* - same as too-many-* 24 | # abstract-method - with intro of async there are always methods missing 25 | # inconsistent-return-statements - doesn't handle raise 26 | # unnecessary-pass - readability for functions which only contain pass 27 | # import-outside-toplevel - TODO 28 | # too-many-ancestors - it's too strict. 29 | disable= 30 | format, 31 | abstract-class-little-used, 32 | abstract-method, 33 | cyclic-import, 34 | duplicate-code, 35 | fixme, 36 | global-statement, 37 | import-outside-toplevel, 38 | inconsistent-return-statements, 39 | locally-disabled, 40 | not-context-manager, 41 | redefined-variable-type, 42 | too-few-public-methods, 43 | too-many-ancestors, 44 | too-many-arguments, 45 | too-many-branches, 46 | too-many-instance-attributes, 47 | too-many-lines, 48 | too-many-locals, 49 | too-many-public-methods, 50 | too-many-return-statements, 51 | too-many-statements, 52 | too-many-boolean-expressions, 53 | unnecessary-pass, 54 | unused-argument, 55 | invalid-name 56 | 57 | enable= 58 | use-symbolic-message-instead 59 | 60 | [REPORTS] 61 | score=no 62 | 63 | [TYPECHECK] 64 | # For attrs 65 | ignored-classes=_CountingAttr 66 | 67 | [FORMAT] 68 | expected-line-ending-format=LF 69 | 70 | [EXCEPTIONS] 71 | overgeneral-exceptions=Exception 72 | -------------------------------------------------------------------------------- /.pytest_cache/README.md: -------------------------------------------------------------------------------- 1 | # pytest cache directory # 2 | 3 | This directory contains data from the pytest's cache plugin, 4 | which provides the `--lf` and `--ff` options, as well as the `cache` fixture. 5 | 6 | **Do not** commit this to version control. 7 | 8 | See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information. 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | # python: 3 | # - 3.5.3 4 | # - 3.6 5 | # env: 6 | 7 | matrix: 8 | fast_finish: true 9 | include: 10 | - python: "3.5.3" 11 | env: TOXENV=py35 12 | - python: "3.5.3" 13 | env: TOXENV=flake8 14 | - python: "3.5.3" 15 | env: TOXENV=pylint 16 | - python: "3.5.3" 17 | env: TOXENV=pydocstyle 18 | - python: "3.6" 19 | env: TOXENV=py36 20 | - python: "3.7" 21 | env: TOXENV=py37 22 | dist: xenial 23 | sudo: required 24 | # - python: "3.8-dev" 25 | # env: TOXENV=py38 26 | # dist: xenial 27 | # sudo: required 28 | # allow_failures: 29 | # - python: "3.8-dev" 30 | # env: TOXENV=py38 31 | 32 | cache: 33 | directories: 34 | - $HOME/.cache/pip 35 | install: pip install -U tox 36 | #coveralls 37 | language: python 38 | script: travis_wait 30 tox --develop 39 | #after_success: coveralls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 David McNett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | .PHONY: dist update 4 | dist: 5 | rm -f dist/*.whl dist/*.tar.gz 6 | python3 setup.py sdist 7 | python3 setup.py bdist_wheel 8 | 9 | release: 10 | twine upload dist/*.tar.gz 11 | twine upload dist/*.whl 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | insteonplm 2 | ========== 3 | 4 | |Build Status| |GitHub release| |PyPI| 5 | 6 | This is a Python package to interface with an Insteon Modem. It has been 7 | tested to work with most USB or RS-232 serial based devices such as the 8 | `2413U `__, 9 | `2412S `__, 10 | `2448A7 `__ and Hub models 11 | `2242 `__ 12 | and `2245 `__. Other models have 13 | not been tested but the underlying protocol has not changed much over 14 | time so it would not be surprising if it worked with a number of other 15 | models. If you find success with something, please let us know. 16 | 17 | This **insteonplm** package was created primarily to support an INSTEON 18 | platform for the `Home Assistant `__ 19 | automation platform but it is structured to be general-purpose and 20 | should be usable for other applications as well. 21 | 22 | Requirements 23 | ------------ 24 | 25 | - Python 3.5.3 or higher, 3.6 or 3.7 26 | - Posix or Windows based system 27 | - Some form of Insteon PLM or Hub 28 | - At least one Insteon device 29 | 30 | Installation 31 | ------------ 32 | 33 | You can, of course, just install the most recent release of this package 34 | using ``pip``. This will download the more recent version from 35 | `PyPI `__ and install it to 36 | your host. 37 | 38 | :: 39 | 40 | pip install insteonplm 41 | 42 | If you want to grab the the development code, you can also clone this 43 | git repository and install from local sources: 44 | 45 | :: 46 | 47 | cd python-insteonplm 48 | pip install . 49 | 50 | And, as you probably expect, you can live the developer's life by 51 | working with the live repo and edit to your heart's content: 52 | 53 | :: 54 | 55 | cd python-insteonplm 56 | pip install -e . 57 | 58 | Device Permissions 59 | ^^^^^^^^^^^^^^^^^^ 60 | 61 | Any user account that you want to be able to access the PLM will need 62 | permissions to access the USB or Serial device in ``/dev``. In Linux, 63 | you'll probably want to do something like this: 64 | 65 | :: 66 | 67 | sudo usermod -a -G dialout 68 | 69 | In FreeBSD, it'll be something like: 70 | 71 | :: 72 | 73 | sudo pw usermod -G dialer 74 | 75 | You may find that you have to log out and log back in as that user for 76 | the change to take effect on existing sessions. 77 | 78 | First Start 79 | ^^^^^^^^^^^ 80 | 81 | When the module starts it reads the IM's All-Link Database to find 82 | linked devices. In order for this module to communicate with a device, 83 | it must be linked to the IM. For help with linking please see the 84 | section on the `Command Line Interface` below. 85 | 86 | After the module loads the All-Link database it queries each device to 87 | identify what type of device it is. This can take quite a while (5-15 88 | sec per device). Once it identifies the devices it saves them in the 89 | ``WORKDIR`` so that future startups are faster. 90 | 91 | Currently there is an issue with the command line `Tools` not 92 | finding battery operated devices since they don't respond to device 93 | information requests. This is being addressed in future releases. This 94 | is not an issue if used with `Home 95 | Assistant `__ through the use of device 96 | overrides. 97 | 98 | Tools 99 | ----- 100 | 101 | The package installs a message monitor and a command line interface. 102 | 103 | Message Monitoring 104 | ^^^^^^^^^^^^^^^^^^ 105 | 106 | You can monitor messages flowing across your INSTEON network with the 107 | monitor command line tool. To invoke the monitor use the command: 108 | 109 | :: 110 | 111 | insteonplm_monitor --device /dev/ttyUSB0 --workdir /home/username 112 | 113 | Command line options for the monitor are: 114 | 115 | :: 116 | 117 | -h, --help show this help message and exit 118 | --device DEVICE Path to PLM device 119 | --verbose, -v Set logging level to verbose 120 | --workdir WORKDIR Working directory for reading and saving device 121 | information. 122 | 123 | Command Line Interface 124 | ^^^^^^^^^^^^^^^^^^^^^^ 125 | 126 | The command line tool creates an interactive session to allow certain 127 | functions to be performed on the INSTEON devices. To invoke the command 128 | line tool use the command: 129 | 130 | :: 131 | 132 | `insteonplm_interactive --device /dev/ttyUSB0 --workdir /home/username` 133 | 134 | Command line options for the interactive tool are: 135 | 136 | :: 137 | 138 | -h, --help show this help message and exit 139 | --device DEVICE Path to PLM device 140 | -v, --verbose Set logging level to verbose 141 | --workdir WORKDIR Working directory for reading and saving device 142 | information. 143 | 144 | Inside the command line tool use ``help`` to obtain a list of available 145 | commands. The current list of available commands is: 146 | 147 | :: 148 | 149 | - add_all_link Add an All-Link record to the IM and a device. 150 | - add_device_override Add a device override to the IM. 151 | - add_x10_device Add an X10 device to the IM 152 | - connect Connect to the IM 153 | - del_all_link Delete an all link record from the IM and a device 154 | - exit Exit the tool 155 | - help List available commands 156 | - list_devices Print a list of the available devices 157 | - load_aldb Read and load a device All-Link database 158 | - on_off_test Test a device with simple on/off commands 159 | - print_aldb Print the All-Link database for a device 160 | - running_tasks List tasks running in the background 161 | - set_device Set the IM device path 162 | - set_log_level Set the log message display level 163 | - set_workdir Set the WORKDIR to load and save device info 164 | - write_aldb Write a record to the device All-Link database 165 | !!!! BE CAREFUL WITH THIS COMMAND !!!!! 166 | 167 | For help with a specific command type ``help command_name``. 168 | 169 | Known Issues 170 | ------------ 171 | 172 | - The 173 | `documentation `__ 174 | is limited. 175 | - Other issues are located in our 176 | `issues `__ list 177 | on GitHub. 178 | 179 | How You Can Help 180 | ---------------- 181 | 182 | Development 183 | ^^^^^^^^^^^ 184 | 185 | - First and foremost, you can help by forking this project and coding. 186 | Features, bug fixes, documentation, and sample code will all add 187 | tremendously to the quality of this project. 188 | 189 | - If you have a feature you'd love to see added to the project but you 190 | don't think that you're able to do the work, I'm someone is probably 191 | happy to perform the directed development in the form of a bug or 192 | feature bounty. 193 | 194 | Testing, Feature Requests and Issue Identification 195 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 196 | 197 | - If you're anxious for a feature but it's not actually worth money to 198 | you, please open an issue here on Github describing the problem or 199 | limitation. If you never ask, it'll never happen 200 | 201 | Documentation 202 | ^^^^^^^^^^^^^ 203 | 204 | Please see our 205 | `Wiki `__ section for 206 | documentation. This documentation is limited. If you would like to drive 207 | this effort please send a resume and a list of references to... 208 | Honestly, we will take anyone. 209 | 210 | PLEASE help. An 211 | `issue `__ has 212 | been opened so just post your interest there. 213 | 214 | Credits 215 | ------- 216 | 217 | - This package was written by David McNett. 218 | - https://github.com/nugget 219 | - https://keybase.io/nugget 220 | 221 | - Significant updates were provided by Tom Harris 222 | - https://github.com/teharris1 223 | 224 | - Many thanks to `Ryan Stanley `__ for 225 | his invaluable help with debugging and development. 226 | 227 | Interesting Links 228 | ----------------- 229 | 230 | - `Project Home `__ 231 | - `Why Nikola Tesla was the greatest geek who ever 232 | lived `__ 233 | 234 | .. |Build Status| image:: https://travis-ci.org/nugget/python-insteonplm.svg?branch=master 235 | :target: https://travis-ci.org/nugget/python-insteonplm 236 | .. |GitHub release| image:: https://img.shields.io/github/release/nugget/python-insteonplm.svg 237 | :target: https://github.com/nugget/python-insteonplm/releases 238 | .. |PyPI| image:: https://img.shields.io/pypi/v/insteonplm.svg 239 | :target: https://pypi.python.org/pypi/insteonplm 240 | -------------------------------------------------------------------------------- /insteonplm/address.py: -------------------------------------------------------------------------------- 1 | """Helper objects for maintaining PLM state and interfaces.""" 2 | import logging 3 | import binascii 4 | import insteonplm.utils 5 | 6 | __all__ = "Address" 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | 10 | class Address: 11 | """Datatype definition for INSTEON device address handling.""" 12 | 13 | def __init__(self, addr): 14 | """Create an Address object.""" 15 | self._is_x10 = False 16 | self.addr = self._normalize(addr) 17 | 18 | def __repr__(self): 19 | """Representation of the Address object.""" 20 | return self.id 21 | 22 | def __str__(self): 23 | """Return the Address object as a string.""" 24 | return self.id 25 | 26 | def __eq__(self, other): 27 | """Test for equality.""" 28 | equals = False 29 | if hasattr(other, "addr"): 30 | equals = self.addr == other.addr 31 | return equals 32 | 33 | def __ne__(self, other): 34 | """Test for not equals.""" 35 | not_equals = True 36 | if hasattr(other, "addr"): 37 | not_equals = self.addr != other.addr 38 | return not_equals 39 | 40 | def __lt__(self, other): 41 | """Test for less than.""" 42 | if isinstance(other, Address): 43 | return str(self) < str(other) 44 | raise TypeError 45 | 46 | def __gt__(self, other): 47 | """Test for greater than.""" 48 | if isinstance(other, Address): 49 | return str(self) > str(other) 50 | raise TypeError 51 | 52 | def __hash__(self): 53 | """Create a hash code for the Address object.""" 54 | return hash(self.id) 55 | 56 | def matches_pattern(self, other): 57 | """Test Address object matches the pattern of another object.""" 58 | matches = False 59 | if hasattr(other, "addr"): 60 | if self.addr is None or other.addr is None: 61 | matches = True 62 | else: 63 | matches = self.addr == other.addr 64 | return matches 65 | 66 | def _normalize(self, addr): 67 | """Take any format of address and turn it into a hex string.""" 68 | normalize = None 69 | if isinstance(addr, Address): 70 | normalize = addr.addr 71 | self._is_x10 = addr.is_x10 72 | 73 | elif isinstance(addr, bytearray): 74 | normalize = binascii.unhexlify(binascii.hexlify(addr).decode()) 75 | 76 | elif isinstance(addr, bytes): 77 | normalize = addr 78 | 79 | elif isinstance(addr, str): 80 | addr = addr.replace(".", "") 81 | addr = addr[0:6] 82 | if addr[0:3].lower() == "x10": 83 | x10_addr = Address.x10(addr[3:4], int(addr[4:6])) 84 | normalize = x10_addr.addr 85 | self._is_x10 = True 86 | else: 87 | normalize = binascii.unhexlify(addr.lower()) 88 | 89 | elif addr is None: 90 | normalize = None 91 | 92 | else: 93 | _LOGGER.warning( 94 | "Address class init with unknown type %s: %r", type(addr), addr 95 | ) 96 | return normalize 97 | 98 | @property 99 | def human(self): 100 | """Emit the address in human-readible format (AA.BB.CC).""" 101 | addrstr = "00.00.00" 102 | if self.addr: 103 | if self._is_x10: 104 | housecode_byte = self.addr[1] 105 | housecode = insteonplm.utils.byte_to_housecode(housecode_byte) 106 | unitcode_byte = self.addr[2] 107 | unitcode = insteonplm.utils.byte_to_unitcode(unitcode_byte) 108 | addrstr = "X10.{}.{:02d}".format(housecode.upper(), unitcode) 109 | else: 110 | addrstr = "{}.{}.{}".format( 111 | self.hex[0:2], self.hex[2:4], self.hex[4:6] 112 | ).upper() 113 | return addrstr 114 | 115 | @property 116 | def hex(self): 117 | """Emit the address in bare hex format (aabbcc).""" 118 | addrstr = "000000" 119 | if self.addr is not None: 120 | addrstr = binascii.hexlify(self.addr).decode() 121 | return addrstr 122 | 123 | @property 124 | def bytes(self): 125 | """Emit the address in bytes format.""" 126 | addrbyte = b"\x00\x00\x00" 127 | if self.addr is not None: 128 | addrbyte = self.addr 129 | return addrbyte 130 | 131 | @property 132 | def id(self): 133 | """Return the ID of the device address.""" 134 | dev_id = "" 135 | if self._is_x10: 136 | dev_id = "x10{}{:02d}".format(self.x10_housecode, self.x10_unitcode) 137 | else: 138 | dev_id = self.hex 139 | return dev_id 140 | 141 | @property 142 | def is_x10(self): 143 | """Test if this is an X10 address.""" 144 | return self._is_x10 145 | 146 | @is_x10.setter 147 | def is_x10(self, val: bool): 148 | """Set if this is an X10 address.""" 149 | self._is_x10 = val 150 | 151 | @property 152 | def x10_housecode_byte(self): 153 | """Emit the X10 house code byte value.""" 154 | housecode = None 155 | if self.is_x10: 156 | housecode = self.addr[1] 157 | return housecode 158 | 159 | @property 160 | def x10_unitcode_byte(self): 161 | """Emit the X10 unit code byte value.""" 162 | unitcode = None 163 | if self.is_x10: 164 | unitcode = self.addr[2] 165 | return unitcode 166 | 167 | @property 168 | def x10_housecode(self): 169 | """Emit the X10 house code.""" 170 | housecode = None 171 | if self.is_x10: 172 | housecode = insteonplm.utils.byte_to_housecode(self.addr[1]) 173 | return housecode 174 | 175 | @property 176 | def x10_unitcode(self): 177 | """Emit the X10 unit code.""" 178 | unitcode = None 179 | if self.is_x10: 180 | unitcode = insteonplm.utils.byte_to_unitcode(self.addr[2]) 181 | return unitcode 182 | 183 | @classmethod 184 | def x10(cls, housecode, unitcode): 185 | """Create an X10 device address.""" 186 | if housecode.lower() in [ 187 | "a", 188 | "b", 189 | "c", 190 | "d", 191 | "e", 192 | "f", 193 | "g", 194 | "h", 195 | "i", 196 | "j", 197 | "k", 198 | "l", 199 | "m", 200 | "n", 201 | "o", 202 | "p", 203 | ]: 204 | byte_housecode = insteonplm.utils.housecode_to_byte(housecode) 205 | else: 206 | if isinstance(housecode, str): 207 | _LOGGER.error("X10 house code error: %s", housecode) 208 | else: 209 | _LOGGER.error("X10 house code is not a string") 210 | raise ValueError 211 | 212 | # 20, 21 and 22 for All Units Off, All Lights On and All Lights Off 213 | # 'fake' units 214 | if unitcode in range(1, 17) or unitcode in range(20, 23): 215 | byte_unitcode = insteonplm.utils.unitcode_to_byte(unitcode) 216 | else: 217 | if isinstance(unitcode, int): 218 | _LOGGER.error("X10 unit code error: %d", unitcode) 219 | else: 220 | _LOGGER.error("X10 unit code is not an integer 1 - 16") 221 | raise ValueError 222 | 223 | addr = Address(bytearray([0x00, byte_housecode, byte_unitcode])) 224 | addr.is_x10 = True 225 | return addr 226 | -------------------------------------------------------------------------------- /insteonplm/devices/climateControl.py: -------------------------------------------------------------------------------- 1 | """INSTEON Climate Control Device Class.""" 2 | import logging 3 | 4 | from insteonplm.devices import Device 5 | from insteonplm.constants import COMMAND_EXTENDED_GET_SET_0X2E_0X00 6 | from insteonplm.messages.extendedSend import ExtendedSend 7 | from insteonplm.messages.userdata import Userdata 8 | from insteonplm.states.thermostat import ( 9 | Temperature, 10 | Humidity, 11 | SystemMode, 12 | FanMode, 13 | CoolSetPoint, 14 | HeatSetPoint, 15 | ) 16 | from insteonplm.states.statusReport import StatusReport 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | 21 | class ClimateControl_2441th(Device): 22 | """Thermostat model 2441TH.""" 23 | 24 | def __init__( 25 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 26 | ): 27 | """Init the DimmableLightingControl Class.""" 28 | Device.__init__( 29 | self, plm, address, cat, subcat, product_key, description, model 30 | ) 31 | 32 | self._stateList[0x01] = CoolSetPoint( 33 | self._address, 34 | "coolSetPoint", 35 | 0x01, 36 | self._send_msg, 37 | self._message_callbacks, 38 | 0x00, 39 | ) 40 | 41 | self._stateList[0x02] = HeatSetPoint( 42 | self._address, 43 | "heatSetPoint", 44 | 0x02, 45 | self._send_msg, 46 | self._message_callbacks, 47 | 0x00, 48 | ) 49 | 50 | self._stateList[0xEF] = StatusReport( 51 | self._address, 52 | "statusReport", 53 | 0xEF, 54 | self._send_msg, 55 | self._message_callbacks, 56 | 0x00, 57 | ) 58 | 59 | self._system_mode = SystemMode( 60 | self._address, 61 | "systemMode", 62 | 0x10, 63 | self._send_msg, 64 | self._message_callbacks, 65 | 0x00, 66 | ) 67 | 68 | self._fan_mode = FanMode( 69 | self._address, 70 | "fanMode", 71 | 0x11, 72 | self._send_msg, 73 | self._message_callbacks, 74 | 0x00, 75 | ) 76 | 77 | self._temp = Temperature( 78 | self._address, 79 | "temperature", 80 | 0x12, 81 | self._send_msg, 82 | self._message_callbacks, 83 | 0x00, 84 | ) 85 | 86 | self._humidity = Humidity( 87 | self._address, 88 | "humidity", 89 | 0x13, 90 | self._send_msg, 91 | self._message_callbacks, 92 | 0x00, 93 | ) 94 | 95 | @property 96 | def cool_set_point(self): 97 | """Return the cool set point state.""" 98 | return self._stateList[0x01] 99 | 100 | @property 101 | def heat_set_point(self): 102 | """Return the heat set point state.""" 103 | return self._stateList[0x02] 104 | 105 | @property 106 | def system_mode(self): 107 | """Return the mode state.""" 108 | return self._system_mode 109 | 110 | @property 111 | def fan_mode(self): 112 | """Return the mode state.""" 113 | return self._fan_mode 114 | 115 | @property 116 | def temperature(self): 117 | """Return the temperature state.""" 118 | return self._temp 119 | 120 | @property 121 | def humidity(self): 122 | """Return the humidity state.""" 123 | return self._humidity 124 | 125 | def async_refresh_state(self): 126 | """Request each state to provide status update.""" 127 | _LOGGER.debug("Setting up extended status") 128 | ext_status = ExtendedSend( 129 | address=self._address, 130 | commandtuple=COMMAND_EXTENDED_GET_SET_0X2E_0X00, 131 | cmd2=0x02, 132 | userdata=Userdata(), 133 | ) 134 | ext_status.set_crc() 135 | _LOGGER.debug("Sending ext status: %s", ext_status) 136 | self._send_msg(ext_status) 137 | _LOGGER.debug("Sending temp status request") 138 | self.temperature.async_refresh_state() 139 | 140 | # pylint: disable=unused-argument 141 | def _mode_changed(self, addr, group, val): 142 | self.async_refresh_state() 143 | -------------------------------------------------------------------------------- /insteonplm/devices/dimmableLightingControl.py: -------------------------------------------------------------------------------- 1 | """INSTEON Device Type Dimmable Lighting Control Module.""" 2 | from insteonplm.devices import Device 3 | from insteonplm.states.dimmable import ( 4 | DimmableSwitch, 5 | DimmableSwitch_Fan, 6 | DimmableKeypadA, 7 | ) 8 | from insteonplm.states.onOff import OnOffKeypad, OnOffKeypadLed 9 | 10 | 11 | class DimmableLightingControl(Device): 12 | """Dimmable Lighting Controller. 13 | 14 | INSTEON On/Off switch device class. Available device control options are: 15 | - light_on(onlevel=0xff) 16 | - light_on_fast(onlevel=0xff) 17 | - light_off() 18 | - light_off_fast() 19 | 20 | To monitor changes to the state of the device subscribe to the state 21 | monitor: 22 | - lightOnLevel.connect(callback) (state='LightOnLevel') 23 | 24 | where callback defined as: 25 | - callback(self, device_id, state, state_value) 26 | """ 27 | 28 | def __init__( 29 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 30 | ): 31 | """Init the DimmableLightingControl Class.""" 32 | Device.__init__( 33 | self, plm, address, cat, subcat, product_key, description, model 34 | ) 35 | 36 | self._stateList[0x01] = DimmableSwitch( 37 | self._address, 38 | "lightOnLevel", 39 | 0x01, 40 | self._send_msg, 41 | self._message_callbacks, 42 | 0x00, 43 | ) 44 | 45 | 46 | class DimmableLightingControl_2475F(DimmableLightingControl): 47 | """FanLinc model 2475F Dimmable Lighting Control. 48 | 49 | Device Class 0x01 subcat 0x2e 50 | 51 | Two separate INSTEON On/Off switch devices are created with ID 52 | 1) Ligth 53 | - ID: xxxxxx (where xxxxxx is the Insteon address of the device) 54 | - Controls: 55 | - light_on(onlevel=0xff) 56 | - light_on_fast(onlevel=0xff) 57 | - light_off() 58 | - light_off_fast() 59 | - Monitor: lightOnLevel.connect(callback) 60 | 2) Fan 61 | - ID: xxxxxx_2 (where xxxxxx is the Insteon address of the device) 62 | - Controls: 63 | - fan_on(onlevel=0xff) 64 | - fan_off() 65 | - light_on(onlevel=0xff) - Same as fan_on(onlevel=0xff) 66 | - light_off() - Same as fan_off() 67 | - Monitor: fanSpeed.connect(callback) 68 | 69 | where callback defined as: 70 | - callback(self, device_id, state, state_value) 71 | """ 72 | 73 | def __init__( 74 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 75 | ): 76 | """Init the DimmableLightingControl_2475F Class.""" 77 | super().__init__(plm, address, cat, subcat, product_key, description, model) 78 | 79 | self._stateList[0x01] = DimmableSwitch( 80 | self._address, 81 | "lightOnLevel", 82 | 0x01, 83 | self._send_msg, 84 | self._message_callbacks, 85 | 0x00, 86 | ) 87 | self._stateList[0x02] = DimmableSwitch_Fan( 88 | self._address, 89 | "fanOnLevel", 90 | 0x02, 91 | self._send_msg, 92 | self._message_callbacks, 93 | 0x00, 94 | ) 95 | 96 | 97 | class DimmableLightingControl_2334_222(Device): 98 | """On/Off KeypadLinc Switched Lighting Control.""" 99 | 100 | def __init__( 101 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 102 | ): 103 | """Init the SwichedLightingControlKeypad device class.""" 104 | super().__init__(plm, address, cat, subcat, product_key, description, model) 105 | 106 | self._leds = OnOffKeypadLed( 107 | self._address, 108 | "keypadLEDs", 109 | 0x00, 110 | self._send_msg, 111 | self._message_callbacks, 112 | 0x00, 113 | self._plm.loop, 114 | ) 115 | 116 | self._stateList[0x01] = DimmableKeypadA( 117 | self._address, 118 | "keypadButtonMain", 119 | 0x01, 120 | self._send_msg, 121 | self._message_callbacks, 122 | 0x00, 123 | self._leds, 124 | ) 125 | 126 | def _add_buttons(self, button_list): 127 | for group in button_list: 128 | self._stateList[group] = OnOffKeypad( 129 | self._address, 130 | "keypadButton{}".format(button_list[group]), 131 | group, 132 | self._send_msg, 133 | self._message_callbacks, 134 | 0x00, 135 | self._plm.loop, 136 | self._leds, 137 | ) 138 | 139 | self._leds.register_led_updates(self._stateList[group].led_changed, group) 140 | 141 | 142 | class DimmableLightingControl_2334_222_8(DimmableLightingControl_2334_222): 143 | """Dimmable 8 Button KeypadLinc Switched Lighting Control.""" 144 | 145 | def __init__( 146 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 147 | ): 148 | """Init the SwitchedLightingControl_2487S device class.""" 149 | super().__init__(plm, address, cat, subcat, product_key, description, model) 150 | 151 | button_list = {2: "B", 3: "C", 4: "D", 5: "E", 6: "F", 7: "G", 8: "H"} 152 | self._add_buttons(button_list) 153 | 154 | 155 | class DimmableLightingControl_2334_222_6(DimmableLightingControl_2334_222): 156 | """Dimmable 6 Button KeypadLinc Switched Lighting Control.""" 157 | 158 | def __init__( 159 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 160 | ): 161 | """Init the SwitchedLightingControl_2487S device class.""" 162 | super().__init__(plm, address, cat, subcat, product_key, description, model) 163 | 164 | button_list = {3: "A", 4: "B", 5: "C", 6: "D"} 165 | self._add_buttons(button_list) 166 | -------------------------------------------------------------------------------- /insteonplm/devices/generalController.py: -------------------------------------------------------------------------------- 1 | """INSTEON General Controller Device Class.""" 2 | from insteonplm.devices import Device, ALDB, ALDBVersion 3 | from insteonplm.states.dimmable import DimmableRemote 4 | 5 | 6 | class GeneralController(Device): 7 | """General Controller Device Class. 8 | 9 | Device cat: 0x00 10 | 11 | Example: ControLinc, RemoteLinc, SignaLinc, etc. 12 | """ 13 | 14 | def __init__( 15 | self, plm, address, cat, subcat, product_key=0, description="", model="" 16 | ): 17 | """Init the GeneralController class.""" 18 | super().__init__(plm, address, cat, subcat, product_key, description, model) 19 | self._aldb = ALDB(None, None, self._address, version=ALDBVersion.Null) 20 | 21 | 22 | class GeneralController_2342(Device): 23 | """INSTEON Device Mini-Remote - 1 Scene.""" 24 | 25 | def __init__( 26 | self, plm, address, cat, subcat, product_key=0, description="", model="" 27 | ): 28 | """Init the GeneralController_2342 class.""" 29 | super().__init__(plm, address, cat, subcat, product_key, description, model) 30 | 31 | self._stateList[0x01] = DimmableRemote( 32 | self._address, 33 | "remoteButton", 34 | 0x01, 35 | self._send_msg, 36 | self._message_callbacks, 37 | 0x00, 38 | ) 39 | 40 | 41 | class GeneralController_2342_4(Device): 42 | """INSTEON Device Mini-Remote - 4 Scene.""" 43 | 44 | def __init__( 45 | self, plm, address, cat, subcat, product_key=0, description="", model="" 46 | ): 47 | """Init the GeneralController_2342_4 class.""" 48 | super().__init__(plm, address, cat, subcat, product_key, description, model) 49 | 50 | button_list = {1: "A", 2: "B", 3: "C", 4: "D"} 51 | for group in button_list: 52 | self._stateList[group] = DimmableRemote( 53 | self._address, 54 | "remoteButton{}".format(button_list[group]), 55 | group, 56 | self._send_msg, 57 | self._message_callbacks, 58 | 0x00, 59 | ) 60 | 61 | 62 | class GeneralController_2342_8(Device): 63 | """INSTEON Device Mini-Remote - 8 Scene.""" 64 | 65 | def __init__( 66 | self, plm, address, cat, subcat, product_key=0, description="", model="" 67 | ): 68 | """Init the GeneralController_2342_8 class.""" 69 | super().__init__(plm, address, cat, subcat, product_key, description, model) 70 | 71 | button_list = {1: "B", 2: "A", 3: "D", 4: "C", 5: "F", 6: "E", 7: "H", 8: "G"} 72 | for group in button_list: 73 | self._stateList[group] = DimmableRemote( 74 | self._address, 75 | "remoteButton{}".format(button_list[group]), 76 | group, 77 | self._send_msg, 78 | self._message_callbacks, 79 | 0x00, 80 | ) 81 | -------------------------------------------------------------------------------- /insteonplm/devices/securityHealthSafety.py: -------------------------------------------------------------------------------- 1 | """INSTEON Security Health and Safety Device Class Module.""" 2 | 3 | from insteonplm.states.sensor import ( 4 | VariableSensor, 5 | OnOffSensor, 6 | SmokeCO2Sensor, 7 | LeakSensorDryWet, 8 | LeakSensorHeartbeat, 9 | LeakSensorState, 10 | ) 11 | from insteonplm.devices import Device 12 | 13 | 14 | class SecurityHealthSafety(Device): 15 | """Security Health Safety Control Device Class. 16 | 17 | Device cat: 0x10 subcat: Any 18 | 19 | INSTEON Security Health Safety Control Device Class. 20 | These are typically binary sensors with On/Off status. 21 | There are no state change commands that can be sent to the device. 22 | 23 | To monitor the state of the device subscribe to the state monitor: 24 | - sensor.connect(callback) (state='Sensor') 25 | where callback defined as: 26 | - callback(self, device_id, state, state_value) 27 | """ 28 | 29 | def __init__( 30 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 31 | ): 32 | """Init the SecurityHealthSafety device class.""" 33 | super().__init__(plm, address, cat, subcat, product_key, description, model) 34 | 35 | self._product_data_in_aldb = True 36 | 37 | self._stateList[0x01] = VariableSensor( 38 | self._address, 39 | "generalSensor", 40 | 0x01, 41 | self._send_msg, 42 | self._message_callbacks, 43 | 0x00, 44 | ) 45 | 46 | 47 | class SecurityHealthSafety_2421(Device): 48 | """Security Health Safety Control Device Class. 49 | 50 | TriggerLinc model 2421. 51 | Device cat: 0x10 subcat: 0x02. 52 | 53 | INSTEON Security Health Safety Control Device Class. 54 | This device is a binary sensors with On/Off status. 55 | There are no state change commands that can be sent to the device. 56 | 57 | To monitor the state of the device subscribe to the state monitor: 58 | - sensor.connect(callback) (state='Sensor') 59 | where callback defined as: 60 | - callback(self, device_id, state, state_value) 61 | """ 62 | 63 | def __init__( 64 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 65 | ): 66 | """Init the SecurityHealthSafety_2421 device class.""" 67 | super().__init__(plm, address, cat, subcat, product_key, description, model) 68 | 69 | self._product_data_in_aldb = True 70 | 71 | self._stateList[0x01] = OnOffSensor( 72 | self._address, 73 | "openClosedSensor", 74 | 0x01, 75 | self._send_msg, 76 | self._message_callbacks, 77 | 0x00, 78 | ) 79 | 80 | 81 | class SecurityHealthSafety_2842_222(Device): 82 | """Security Health Safety Control Device Class. 83 | 84 | Moton Sensor model 2842-222. 85 | Device cat: 0x10 subcat: 0x01. 86 | 87 | INSTEON Security Health Safety Control Device Class. 88 | This device is a binary sensors with On/Off status. 89 | There are no state change commands that can be sent to the device. 90 | 91 | To monitor the state of the device subscribe to the state monitor: 92 | - sensor.connect(callback) (state='Sensor') 93 | where callback defined as: 94 | - callback(self, device_id, state, state_value) 95 | """ 96 | 97 | def __init__( 98 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 99 | ): 100 | """Init the SecurityHealthSafety_2842_222 device class.""" 101 | super().__init__(plm, address, cat, subcat, product_key, description, model) 102 | 103 | self._product_data_in_aldb = True 104 | 105 | self._stateList[0x01] = OnOffSensor( 106 | self._address, 107 | "motionSensor", 108 | 0x01, 109 | self._send_msg, 110 | self._message_callbacks, 111 | 0x00, 112 | ) 113 | 114 | self._stateList[0x02] = OnOffSensor( 115 | self._address, 116 | "lightSensor", 117 | 0x02, 118 | self._send_msg, 119 | self._message_callbacks, 120 | 0x00, 121 | ) 122 | 123 | self._stateList[0x03] = OnOffSensor( 124 | self._address, 125 | "batterySensor", 126 | 0x03, 127 | self._send_msg, 128 | self._message_callbacks, 129 | 0x00, 130 | ) 131 | 132 | 133 | class SecurityHealthSafety_2845_222(Device): 134 | """Security Health Safety Control Device Class. 135 | 136 | Hidden Door Sensor model 2845-222. 137 | Device cat: 0x10 subcat: 0x11. 138 | 139 | INSTEON Security Health Safety Control Device Class. 140 | This device is a binary sensors with On/Off status. 141 | There are no state change commands that can be sent to the device. 142 | 143 | To monitor the state of the device subscribe to the state monitor: 144 | - sensor.connect(callback) (state='Sensor') 145 | where callback defined as: 146 | - callback(self, device_id, state, state_value) 147 | """ 148 | 149 | def __init__( 150 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 151 | ): 152 | """Init the SecurityHealthSafety_2845_222 device class.""" 153 | super().__init__(plm, address, cat, subcat, product_key, description, model) 154 | 155 | self._product_data_in_aldb = True 156 | 157 | self._stateList[0x01] = OnOffSensor( 158 | self._address, 159 | "doorSensor", 160 | 0x01, 161 | self._send_msg, 162 | self._message_callbacks, 163 | 0x00, 164 | ) 165 | 166 | 167 | class SecurityHealthSafety_2852_222(Device): 168 | """Security Health Safety Control Device Class. 169 | 170 | Water Leak Sensor model 2852-222. 171 | Device cat: 0x10 subcat: 0x08. 172 | 173 | INSTEON Security Health Safety Control Device Class. 174 | This device is a binary sensors with On/Off status. 175 | There are no state change commands that can be sent to the device. 176 | 177 | To monitor the state of the device subscribe to the state monitor: 178 | - sensor.connect(callback) (state='Sensor') 179 | where callback defined as: 180 | - callback(self, device_id, state, state_value) 181 | """ 182 | 183 | def __init__( 184 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 185 | ): 186 | """Init the SecurityHealthSafety_2852_222 device class.""" 187 | super().__init__(plm, address, cat, subcat, product_key, description, model) 188 | 189 | self._product_data_in_aldb = True 190 | 191 | self._stateList[0x01] = LeakSensorDryWet( 192 | self._address, 193 | "dryLeakSensor", 194 | 0x01, 195 | self._send_msg, 196 | self._message_callbacks, 197 | defaultvalue=0x01, 198 | dry_wet=LeakSensorState.DRY, 199 | ) 200 | self._stateList[0x02] = LeakSensorDryWet( 201 | self._address, 202 | "wetLeakSensor", 203 | 0x02, 204 | self._send_msg, 205 | self._message_callbacks, 206 | defaultvalue=0x00, 207 | dry_wet=LeakSensorState.WET, 208 | ) 209 | self._stateList[0x04] = LeakSensorHeartbeat( 210 | self._address, 211 | "heartbeatLeakSensor", 212 | 0x04, 213 | self._send_msg, 214 | self._message_callbacks, 215 | defaultvalue=0x11, 216 | ) 217 | 218 | self._stateList[0x01].register_dry_wet_callback(self._stateList[0x02].set_value) 219 | self._stateList[0x01].register_dry_wet_callback(self._stateList[0x04].set_value) 220 | 221 | self._stateList[0x02].register_dry_wet_callback(self._stateList[0x01].set_value) 222 | self._stateList[0x02].register_dry_wet_callback(self._stateList[0x04].set_value) 223 | 224 | self._stateList[0x04].register_dry_wet_callback(self._stateList[0x01].set_value) 225 | self._stateList[0x04].register_dry_wet_callback(self._stateList[0x02].set_value) 226 | 227 | 228 | class SecurityHealthSafety_2982_222(Device): 229 | """Security Health Safety Control Device Class. 230 | 231 | Smoke Bridge model 2982-222 232 | Device cat: 0x10 subcat: 0x0a 233 | 234 | INSTEON Security Health Safety Control Device Class. 235 | This device is a variable sensors with the following values: 236 | 0: All clear 237 | 1: Smoke detected 238 | 2: CO2 detected 239 | 3: Test detected 240 | 4: Unknown message detected 241 | 5: All Clear detected 242 | 6: Low Battery 243 | 7: Sensor malfunction 244 | There are no state change commands that can be sent to the device. 245 | 246 | To monitor the state of the device subscribe to the state monitor: 247 | - sensor.connect(callback) (state='Sensor') 248 | where callback defined as: 249 | - callback(self, device_id, state, state_value) 250 | """ 251 | 252 | def __init__( 253 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 254 | ): 255 | """Init the SecurityHealthSafety_2982_222 Class.""" 256 | super().__init__(plm, address, cat, subcat, product_key, description, model) 257 | 258 | self._product_data_in_aldb = True 259 | 260 | self._stateList[0x01] = SmokeCO2Sensor( 261 | self._address, 262 | "smokeCO2Sensor", 263 | 0x01, 264 | self._send_msg, 265 | self._message_callbacks, 266 | 0x00, 267 | ) 268 | -------------------------------------------------------------------------------- /insteonplm/devices/sensorsActuators.py: -------------------------------------------------------------------------------- 1 | """INSTEON Sensors Actuators Device Class Module.""" 2 | from insteonplm.devices import Device 3 | from insteonplm.states.onOff import OpenClosedRelay 4 | from insteonplm.states.sensor import IoLincSensor 5 | 6 | 7 | class SensorsActuators(Device): 8 | """Sensors And Actuator Device Class. 9 | 10 | Device cat: 0x07 subcat: Any 11 | 12 | There are 3 known device types in this category: 13 | 1) I/O Linc [2450] & [2450-50-60] 14 | 2) Smartenit IO devices (various input and output channels, 15 | see http://sandbox.smartenit.com/downloads/IoTxx_Command_Set.pdf) 16 | Including 17 | a) EZSns1W Sensor Interface Module 18 | b) EZIO8T I/O Module 19 | c) EZIO2X4 20 | d) EZIO8SA / IOT8 21 | e) EZSnsRF 22 | f) EZISnsRf 23 | g) EZIO6I 24 | h) EZIO4O 25 | 3) SynchroLinc [2423A5] 26 | (http://cache.insteon.com/developer/2423A5dev-112010-en.pdf) 27 | 28 | Each device type is sufficiently different as to require their own device 29 | class. However, they all seem to have a common element of a relay and a 30 | sensor. 31 | """ 32 | 33 | def __init__( 34 | self, plm, address, cat, subcat, product_key=0, description="", model="" 35 | ): 36 | """Init the SensorsActuators Class.""" 37 | super().__init__(plm, address, cat, subcat, product_key, description, model) 38 | 39 | self._stateList[0x01] = OpenClosedRelay( 40 | self._address, 41 | "openClosedRelay", 42 | 0x01, 43 | self._send_msg, 44 | self._message_callbacks, 45 | 0x00, 46 | ) 47 | 48 | 49 | class SensorsActuators_2450(SensorsActuators): 50 | """I/O Linc [2450] & [2450-50-60] Device Class. 51 | 52 | I/O Linc model 2450 and 2450-50-60. 53 | Device cat: 0x07 subcat: 0x00 54 | 55 | Two separate INSTEON devices are created 56 | 1) Relay 57 | - ID: xxxxxx (where xxxxxx is the Insteon address of the device) 58 | - Controls: 59 | - relay_close() 60 | - relay_open() 61 | - Monitor: relay.connect(callback) 62 | - Closed: 0x00 63 | - Open: 0xff 64 | 2) Sensor 65 | - ID: xxxxxx_2 (where xxxxxx is the Insteon address of the device) 66 | - Controls: None 67 | - Monitor: sensor.connect(callback) 68 | - Closed: 0x00 69 | - Open: 0x01 70 | 71 | where callback defined as: 72 | - callback(self, device_id, state, state_value) 73 | """ 74 | 75 | def __init__( 76 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 77 | ): 78 | """Init the SensorsActuators_2450 Class.""" 79 | super().__init__(plm, address, cat, subcat, product_key, description, model) 80 | 81 | self._stateList[0x01] = OpenClosedRelay( 82 | self._address, 83 | "openClosedRelay", 84 | 0x01, 85 | self._send_msg, 86 | self._message_callbacks, 87 | 0x00, 88 | ) 89 | 90 | # Both the Relay and the Sensor are linked via group 1. 91 | # The sensor status updates are sent as group 1 updates. 92 | # The relay status is not relevent. 93 | self._stateList[0x02] = IoLincSensor( 94 | self._address, 95 | "ioLincSensor", 96 | 0x02, 97 | self._send_msg, 98 | self._message_callbacks, 99 | 0x00, 100 | ) 101 | -------------------------------------------------------------------------------- /insteonplm/devices/switchedLightingControl.py: -------------------------------------------------------------------------------- 1 | """Insteon Switched Lighting Control Device Class Module.""" 2 | from insteonplm.devices import Device 3 | from insteonplm.states.onOff import ( 4 | OnOffSwitch, 5 | OnOffSwitch_OutletTop, 6 | OnOffSwitch_OutletBottom, 7 | OnOffKeypadA, 8 | OnOffKeypad, 9 | OnOffKeypadLed, 10 | ) 11 | 12 | 13 | class SwitchedLightingControl(Device): 14 | """Switched Lighting Control. 15 | 16 | Device Class 0x02 subcat Any 17 | 18 | Available device control options are: 19 | - light_on() 20 | - light_on_fast() 21 | - light_off() 22 | - light_off_fast() 23 | To monitor the state of the device subscribe to the state monitor: 24 | - lightOnLevel.connect(callback) 25 | where callback defined as: 26 | - callback(self, device_id, state, state_value) 27 | """ 28 | 29 | def __init__( 30 | self, plm, address, cat, subcat, product_key=0x00, description="", model="" 31 | ): 32 | """Init the SwitchedLightingControl device class.""" 33 | super().__init__(plm, address, cat, subcat, product_key, description, model) 34 | 35 | self._stateList[0x01] = OnOffSwitch( 36 | self._address, 37 | "lightOnOff", 38 | 0x01, 39 | self._send_msg, 40 | self._message_callbacks, 41 | 0x00, 42 | ) 43 | 44 | 45 | class SwitchedLightingControl_2663_222(Device): 46 | """On/Off outlet model 2663-222 Switched Lighting Control. 47 | 48 | Device Class 0x02 subcat 0x39 49 | 50 | Two separate INSTEON On/Off switch devices are created with ID 51 | - 'address': Top Outlet 52 | - 'address_2': Bottom Outlet 53 | 54 | Available device control options are: 55 | - light_on() 56 | - light_on_fast() 57 | - light_off() 58 | - light_off_fast() 59 | To monitor the state of the device subscribe to the state monitor: 60 | - lightOnLevel.connect(callback) 61 | where callback defined as: 62 | - callback(self, device_id, state, state_value) 63 | """ 64 | 65 | def __init__( 66 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 67 | ): 68 | """Init the SwitchedLightingControl_2663_222 device class.""" 69 | super().__init__(plm, address, cat, subcat, product_key, description, model) 70 | 71 | self._stateList[0x01] = OnOffSwitch_OutletTop( 72 | self._address, 73 | "outletTopOnOff", 74 | 0x01, 75 | self._send_msg, 76 | self._message_callbacks, 77 | 0x00, 78 | ) 79 | self._stateList[0x02] = OnOffSwitch_OutletBottom( 80 | self._address, 81 | "outletBottomOnOff", 82 | 0x02, 83 | self._send_msg, 84 | self._message_callbacks, 85 | 0x00, 86 | ) 87 | 88 | 89 | class SwichedLightingControlKeypad(Device): 90 | """On/Off KeypadLinc Switched Lighting Control.""" 91 | 92 | def __init__( 93 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 94 | ): 95 | """Init the SwichedLightingControlKeypad device class.""" 96 | super().__init__(plm, address, cat, subcat, product_key, description, model) 97 | 98 | self._leds = OnOffKeypadLed( 99 | self._address, 100 | "keypadLEDs", 101 | 0x00, 102 | self._send_msg, 103 | self._message_callbacks, 104 | 0x00, 105 | self._plm.loop, 106 | ) 107 | 108 | self._stateList[0x01] = OnOffKeypadA( 109 | self._address, 110 | "keypadButtonMain", 111 | 0x01, 112 | self._send_msg, 113 | self._message_callbacks, 114 | 0x00, 115 | self._leds, 116 | ) 117 | 118 | def _add_buttons(self, button_list): 119 | for group in button_list: 120 | self._stateList[group] = OnOffKeypad( 121 | self._address, 122 | "onOffButton{}".format(button_list[group]), 123 | group, 124 | self._send_msg, 125 | self._message_callbacks, 126 | 0x00, 127 | self._plm.loop, 128 | self._leds, 129 | ) 130 | 131 | self._leds.register_led_updates(self._stateList[group].led_changed, group) 132 | 133 | 134 | class SwitchedLightingControl_2334_222_8(SwichedLightingControlKeypad): 135 | """On/Off KeypadLinc Switched Lighting Control.""" 136 | 137 | def __init__( 138 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 139 | ): 140 | """Init the SwitchedLightingControl_2487S device class.""" 141 | super().__init__(plm, address, cat, subcat, product_key, description, model) 142 | 143 | button_list = {2: "B", 3: "C", 4: "D", 5: "E", 6: "F", 7: "G", 8: "H"} 144 | self._add_buttons(button_list) 145 | 146 | 147 | class SwitchedLightingControl_2334_222_6(SwichedLightingControlKeypad): 148 | """On/Off KeypadLinc Switched Lighting Control.""" 149 | 150 | def __init__( 151 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 152 | ): 153 | """Init the SwitchedLightingControl_2487S device class.""" 154 | super().__init__(plm, address, cat, subcat, product_key, description, model) 155 | 156 | button_list = {3: "A", 4: "B", 5: "C", 6: "D"} 157 | self._add_buttons(button_list) 158 | -------------------------------------------------------------------------------- /insteonplm/devices/unknowndevice.py: -------------------------------------------------------------------------------- 1 | """INSTEON Device Unknown Device Type.""" 2 | from insteonplm.devices import Device, ALDB, ALDBVersion 3 | 4 | 5 | class UnknownDevice(Device): 6 | """Unknown Device Class. 7 | 8 | Unknown Device used when only the device address is known but no other 9 | information. Available methods: 10 | id_request() 11 | product_data_request() 12 | assign_to_all_link_group() 13 | delete_from_all_link_group() 14 | device_text_string_request() 15 | enter_linking_mode() 16 | enter_unlinking_mode() 17 | get_engine_version() 18 | ping() 19 | read_aldb() 20 | write_aldb() 21 | """ 22 | 23 | def __init__( 24 | self, 25 | plm, 26 | address, 27 | cat=None, 28 | subcat=None, 29 | product_key=0x00, 30 | description="", 31 | model="", 32 | ): 33 | """Init the UnknownDevice Class.""" 34 | super().__init__(plm, address, cat, subcat, product_key, description, model) 35 | self._aldb = ALDB(None, None, self._address, version=ALDBVersion.Null) 36 | -------------------------------------------------------------------------------- /insteonplm/devices/windowCoverings.py: -------------------------------------------------------------------------------- 1 | """INSTEON Device Type Window Coverings Control Module.""" 2 | from insteonplm.devices import Device 3 | from insteonplm.states.cover import Cover 4 | 5 | 6 | class WindowCovering(Device): 7 | """Window Covering Controller. 8 | 9 | INSTEON Window Covering device class. Available device control options are: 10 | - open() 11 | - open_fast() 12 | - set_position(openlevel=0xff) 13 | - close() 14 | - close_fast() 15 | 16 | To monitor changes to the state of the device subscribe to the state 17 | monitor: 18 | - _states[0x01].connect(callback) (state='LightOnLevel') 19 | 20 | where callback defined as: 21 | - callback(self, device_id, state, state_value) 22 | """ 23 | 24 | def __init__( 25 | self, plm, address, cat, subcat, product_key=None, description=None, model=None 26 | ): 27 | """Init the WindowCovering Class.""" 28 | Device.__init__( 29 | self, plm, address, cat, subcat, product_key, description, model 30 | ) 31 | 32 | self._stateList[0x01] = Cover( 33 | self._address, 34 | "coverOpenLevel", 35 | 0x01, 36 | self._send_msg, 37 | self._message_callbacks, 38 | 0x00, 39 | ) 40 | -------------------------------------------------------------------------------- /insteonplm/devices/x10.py: -------------------------------------------------------------------------------- 1 | """INSTEON General Controller Device Class.""" 2 | import asyncio 3 | import logging 4 | 5 | from insteonplm.constants import ( 6 | X10_COMMAND_ALL_UNITS_OFF, 7 | X10_COMMAND_ALL_LIGHTS_ON, 8 | X10_COMMAND_ALL_LIGHTS_OFF, 9 | ) 10 | from insteonplm.devices import X10Device 11 | from insteonplm.messages.x10received import X10Received 12 | from insteonplm.states.x10 import ( 13 | X10OnOffSwitch, 14 | X10DimmableSwitch, 15 | X10OnOffSensor, 16 | X10AllUnitsOffSensor, 17 | X10AllLightsOnSensor, 18 | X10AllLightsOffSensor, 19 | ) 20 | 21 | 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | 25 | class X10OnOff(X10Device): 26 | """General X10 On / Off Switch Device Class.""" 27 | 28 | def __init__(self, plm, housecode, unitcode): 29 | """Init the X10OnOff class.""" 30 | super().__init__(plm, housecode, unitcode) 31 | self._description = "X10 On / Off Device" 32 | 33 | self._stateList[0x01] = X10OnOffSwitch( 34 | self._address, 35 | "x10OnOffSwitch", 36 | 0x01, 37 | self._send_msg, 38 | self._message_callbacks, 39 | 0x00, 40 | ) 41 | 42 | 43 | class X10Dimmable(X10Device): 44 | """General X10 Dimmable Switch Device Class.""" 45 | 46 | def __init__(self, plm, housecode, unitcode, dim_steps=22): 47 | """Init the X10Dimmable class.""" 48 | super().__init__(plm, housecode, unitcode) 49 | self._description = "X10 Dimmable Device" 50 | 51 | self._stateList[0x01] = X10DimmableSwitch( 52 | self._address, 53 | "x10DimmableSwitch", 54 | 0x01, 55 | self._send_msg, 56 | self._message_callbacks, 57 | 0x00, 58 | dim_steps, 59 | ) 60 | 61 | 62 | class X10Sensor(X10Device): 63 | """General X10 On / Off Sensor Device Class.""" 64 | 65 | def __init__(self, plm, housecode, unitcode): 66 | """Init the X10Sensor class.""" 67 | super().__init__(plm, housecode, unitcode) 68 | self._description = "X10 On / Off Sensor Device" 69 | 70 | self._stateList[0x01] = X10OnOffSensor( 71 | self._address, 72 | "x10OnOffSensor", 73 | 0x01, 74 | self._send_msg, 75 | self._message_callbacks, 76 | 0x00, 77 | ) 78 | 79 | 80 | class X10AllUnitsOff(X10Device): 81 | """X10 All Units Off Device.""" 82 | 83 | def __init__(self, plm, housecode, unitcode): 84 | """Init the X10AllUnitsOff class.""" 85 | super().__init__(plm, housecode, 20) 86 | self._description = "X10 All Units Off Device" 87 | 88 | self._stateList[0x01] = X10AllUnitsOffSensor( 89 | self._address, 90 | "x10AllUnitsOffSensor", 91 | 0x01, 92 | self._send_msg, 93 | self._message_callbacks, 94 | 0xFF, 95 | ) 96 | 97 | self._stateList[0x01].register_updates(self._reset_state) 98 | self._register_messages() 99 | 100 | # pylint: disable=unused-argument 101 | def _reset_state(self, addr, name, val): 102 | loop = self._plm.loop 103 | if val == 0x00: 104 | asyncio.ensure_future(self._reset_state_value(loop), loop=loop) 105 | 106 | async def _reset_state_value(self, loop): 107 | await asyncio.sleep(1, loop=loop) 108 | self._stateList[0x01].reset() 109 | 110 | def _register_messages(self): 111 | all_units_off_msg = X10Received.command_msg( 112 | self.address.x10_housecode, X10_COMMAND_ALL_UNITS_OFF 113 | ) 114 | self._plm.message_callbacks.add(all_units_off_msg, self.receive_message) 115 | 116 | 117 | class X10AllLightsOn(X10Device): 118 | """X10 All Lights On Device.""" 119 | 120 | def __init__(self, plm, housecode, unitcode): 121 | """Init the X10AllLightsOn class.""" 122 | super().__init__(plm, housecode, 21) 123 | self._description = "X10 All Lights On Device" 124 | 125 | self._stateList[0x01] = X10AllLightsOnSensor( 126 | self._address, 127 | "X10AllLightsOnSensor", 128 | 0x01, 129 | self._send_msg, 130 | self._message_callbacks, 131 | 0x00, 132 | ) 133 | 134 | self._stateList[0x01].register_updates(self._reset_state) 135 | self._register_messages() 136 | 137 | # pylint: disable=unused-argument 138 | def _reset_state(self, addr, name, val): 139 | loop = self._plm.loop 140 | if val == 0xFF: 141 | asyncio.ensure_future(self._reset_state_value(loop), loop=loop) 142 | 143 | async def _reset_state_value(self, loop): 144 | await asyncio.sleep(1, loop=loop) 145 | self._stateList[0x01].reset() 146 | 147 | def _register_messages(self): 148 | all_on_msg = X10Received.command_msg( 149 | self.address.x10_housecode, X10_COMMAND_ALL_LIGHTS_ON 150 | ) 151 | self._plm.message_callbacks.add(all_on_msg, self.receive_message) 152 | 153 | 154 | class X10AllLightsOff(X10Device): 155 | """X10 All Lights Off Device.""" 156 | 157 | def __init__(self, plm, housecode, unitcode): 158 | """Init the X10AllLightsOff class.""" 159 | super().__init__(plm, housecode, 22) 160 | self._description = "X10 All Lights Off Device" 161 | 162 | self._stateList[0x01] = X10AllLightsOffSensor( 163 | self._address, 164 | "X10AllLightsOffSensor", 165 | 0x01, 166 | self._send_msg, 167 | self._message_callbacks, 168 | 0xFF, 169 | ) 170 | 171 | self._stateList[0x01].register_updates(self._reset_state) 172 | self._register_messages() 173 | 174 | # pylint: disable=unused-argument 175 | def _reset_state(self, addr, name, val): 176 | loop = self._plm.loop 177 | if val == 0x00: 178 | asyncio.ensure_future(self._reset_state_value(loop), loop=loop) 179 | 180 | async def _reset_state_value(self, loop): 181 | await asyncio.sleep(1, loop=loop) 182 | self._stateList[0x01].reset() 183 | 184 | def _register_messages(self): 185 | all_off_msg = X10Received.command_msg( 186 | self.address.x10_housecode, X10_COMMAND_ALL_LIGHTS_OFF 187 | ) 188 | self._plm.message_callbacks.add(all_off_msg, self.receive_message) 189 | -------------------------------------------------------------------------------- /insteonplm/messagecallback.py: -------------------------------------------------------------------------------- 1 | """Message callback handler matching message pattern to inbound messages.""" 2 | 3 | import logging 4 | 5 | _LOGGER = logging.getLogger(__name__) 6 | 7 | 8 | class MessageCallback: 9 | """Message callback handler. 10 | 11 | Message patterns or templates are used as the key to a message/callback 12 | tuple. For example, a message with the following attributes: 13 | 14 | {'code': 0x50, 'address': 1A.2B.3C, 'target': None, 15 | 'flags': None, 'cmd1': 0x11, 'cmd2': None} 16 | 17 | Would match any message with: 18 | code == 0x50 19 | address == 1A.2B.3C 20 | target == any value 21 | flags == any value 22 | cmd1 == 0x11 23 | cmd2 == any value 24 | 25 | The above example is an inbound Standard Receive message (0x50) 26 | with the "Light On" message and any light level value in cmd2. 27 | """ 28 | 29 | def __init__(self): 30 | """Init the MessageCallback class.""" 31 | self._dict = {} 32 | 33 | def __len__(self): 34 | """Return the number of callbacks in the list.""" 35 | return len(self._dict) 36 | 37 | def __iter__(self): 38 | """Iterate through the callback list.""" 39 | for itm in self._dict: 40 | yield itm 41 | 42 | def __getitem__(self, key): 43 | """Return an item from the callback list. 44 | 45 | Accepts any message type as a key and returns the callbacks 46 | associated with that message template. 47 | """ 48 | return self._dict.get(key, []) 49 | 50 | def __setitem__(self, key, value): 51 | """Set a callback method to a message key. 52 | 53 | Key: any message template. 54 | Value: callback method. 55 | """ 56 | callbacks = self._dict.get(key, []) 57 | if isinstance(value, list): 58 | for callback in value: 59 | callbacks.append(callback) 60 | else: 61 | callbacks.append(value) 62 | self._dict[key] = callbacks 63 | 64 | def add(self, msg, callback, override=False): 65 | """Add a callback to the callback list. 66 | 67 | msg: Message template. 68 | callback: Callback method 69 | override: True - replace all existing callbacks for that template 70 | False - append the list of callbacks for that message 71 | Default is False 72 | """ 73 | if override: 74 | if isinstance(callback, list): 75 | self._dict[msg] = callback 76 | else: 77 | self._dict[msg] = [callback] 78 | else: 79 | cb = self[msg] 80 | cb.append(callback) 81 | self._dict[msg] = cb 82 | 83 | def remove(self, msg, callback): 84 | """Remove a callback from the callback list. 85 | 86 | msg: Message template 87 | callback: Callback method to remove. 88 | 89 | If callback is None, all callbacks for the message template are 90 | removed. 91 | """ 92 | if callback is None: 93 | self._dict.pop(msg, None) 94 | else: 95 | cb = self._dict.get(msg, []) 96 | try: 97 | cb.remove(callback) 98 | except ValueError: 99 | pass 100 | if cb: 101 | _LOGGER.debug("%d callbacks for message: %s", len(cb), msg) 102 | self.add(msg, cb, True) 103 | else: 104 | self._dict.pop(msg, None) 105 | _LOGGER.debug("Removed all callbacks for message: %s", msg) 106 | 107 | def get_callbacks_from_message(self, msg): 108 | """Return the callbacks associated with a message template.""" 109 | callbacks = [] 110 | for key in self._find_matching_keys(msg): 111 | for callback in self[key]: 112 | callbacks.append(callback) 113 | return callbacks 114 | 115 | def _find_matching_keys(self, msg): 116 | for key in self._dict: 117 | if key.matches_pattern(msg) and msg.matches_pattern(key): 118 | yield key 119 | -------------------------------------------------------------------------------- /insteonplm/messages/__init__.py: -------------------------------------------------------------------------------- 1 | """INSTEON Messages Module.""" 2 | 3 | import logging 4 | import binascii 5 | 6 | from insteonplm.constants import ( 7 | MESSAGE_ALL_LINK_CEANUP_FAILURE_REPORT_0X56, 8 | MESSAGE_ALL_LINK_CLEANUP_STATUS_REPORT_0X58, 9 | MESSAGE_ALL_LINK_RECORD_RESPONSE_0X57, 10 | MESSAGE_ALL_LINKING_COMPLETED_0X53, 11 | MESSAGE_BUTTON_EVENT_REPORT_0X54, 12 | MESSAGE_CANCEL_ALL_LINKING_0X65, 13 | MESSAGE_EXTENDED_MESSAGE_RECEIVED_0X51, 14 | MESSAGE_GET_FIRST_ALL_LINK_RECORD_0X69, 15 | MESSAGE_GET_IM_CONFIGURATION_0X73, 16 | MESSAGE_GET_IM_INFO_0X60, 17 | MESSAGE_GET_NEXT_ALL_LINK_RECORD_0X6A, 18 | MESSAGE_RESET_IM_0X67, 19 | MESSAGE_SEND_ALL_LINK_COMMAND_0X61, 20 | MESSAGE_SEND_STANDARD_MESSAGE_0X62, 21 | MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50, 22 | MESSAGE_START_ALL_LINKING_0X64, 23 | MESSAGE_START_CODE_0X02, 24 | MESSAGE_USER_RESET_DETECTED_0X55, 25 | MESSAGE_X10_MESSAGE_RECEIVED_0X52, 26 | MESSAGE_X10_MESSAGE_SEND_0X63, 27 | MESSAGE_SET_IM_CONFIGURATION_0X6B, 28 | MESSAGE_MANAGE_ALL_LINK_RECORD_0X6F, 29 | ) 30 | from insteonplm.messages.standardReceive import StandardReceive 31 | from insteonplm.messages.extendedReceive import ExtendedReceive 32 | from insteonplm.messages.x10received import X10Received 33 | from insteonplm.messages.allLinkComplete import AllLinkComplete 34 | from insteonplm.messages.buttonEventReport import ButtonEventReport 35 | from insteonplm.messages.userReset import UserReset 36 | from insteonplm.messages.allLinkCleanupFailureReport import AllLinkCleanupFailureReport 37 | from insteonplm.messages.allLinkRecordResponse import AllLinkRecordResponse 38 | from insteonplm.messages.allLinkCleanupStatusReport import AllLinkCleanupStatusReport 39 | from insteonplm.messages.getIMInfo import GetImInfo 40 | from insteonplm.messages.sendAlllinkCommand import SendAllLinkCommand 41 | from insteonplm.messages.standardSend import StandardSend 42 | from insteonplm.messages.x10send import X10Send 43 | from insteonplm.messages.startAllLinking import StartAllLinking 44 | from insteonplm.messages.cancelAllLinking import CancelAllLinking 45 | from insteonplm.messages.resetIM import ResetIM 46 | from insteonplm.messages.setImConfiguration import SetIMConfiguration 47 | from insteonplm.messages.getFirstAllLinkRecord import GetFirstAllLinkRecord 48 | from insteonplm.messages.getNextAllLinkRecord import GetNextAllLinkRecord 49 | from insteonplm.messages.getImConfiguration import GetImConfiguration 50 | from insteonplm.messages.manageAllLinkRecord import ManageAllLinkRecord 51 | 52 | 53 | _LOGGER = logging.getLogger(__name__) 54 | 55 | 56 | def create(rawmessage): 57 | """Return an INSTEON message class based on a raw byte stream.""" 58 | rawmessage = _trim_buffer_garbage(rawmessage) 59 | 60 | if len(rawmessage) < 2: 61 | return (None, rawmessage) 62 | 63 | code = rawmessage[1] 64 | msgclass = _get_msg_class(code) 65 | 66 | msg = None 67 | 68 | remaining_data = rawmessage 69 | if msgclass is None: 70 | _LOGGER.debug("Did not find message class 0x%02x", rawmessage[1]) 71 | rawmessage = rawmessage[1:] 72 | rawmessage = _trim_buffer_garbage(rawmessage, False) 73 | if rawmessage: 74 | _LOGGER.debug("Create: %s", create) 75 | _LOGGER.debug("rawmessage: %s", binascii.hexlify(rawmessage)) 76 | msg, remaining_data = create(rawmessage) 77 | else: 78 | remaining_data = rawmessage 79 | else: 80 | if iscomplete(rawmessage): 81 | msg = msgclass.from_raw_message(rawmessage) 82 | if msg: 83 | remaining_data = rawmessage[len(msg.bytes) :] 84 | # _LOGGER.debug("Returning msg: %s", msg) 85 | # _LOGGER.debug('Returning buffer: %s', binascii.hexlify(remaining_data)) 86 | return (msg, remaining_data) 87 | 88 | 89 | def iscomplete(rawmessage): 90 | """Test if the raw message is a complete message.""" 91 | if len(rawmessage) < 2: 92 | return False 93 | 94 | if rawmessage[0] != 0x02: 95 | raise ValueError("message does not start with 0x02") 96 | 97 | messageBuffer = bytearray() 98 | filler = bytearray(30) 99 | messageBuffer.extend(rawmessage) 100 | messageBuffer.extend(filler) 101 | 102 | msg = _get_msg_class(rawmessage[1]) 103 | 104 | if hasattr(msg, "receivedSize") and msg.receivedSize: 105 | expectedSize = msg.receivedSize 106 | else: 107 | _LOGGER.error("Unable to find a receivedSize for code 0x%x", rawmessage[1]) 108 | return ValueError 109 | 110 | is_expected_size = False 111 | if len(rawmessage) >= expectedSize: 112 | is_expected_size = True 113 | 114 | return is_expected_size 115 | 116 | 117 | def _get_msg_class(code): 118 | """Get the message class based on the message code.""" 119 | msg_classes = {} 120 | msg_classes = _add_msg_class( 121 | msg_classes, MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50, StandardReceive 122 | ) 123 | msg_classes = _add_msg_class( 124 | msg_classes, MESSAGE_EXTENDED_MESSAGE_RECEIVED_0X51, ExtendedReceive 125 | ) 126 | msg_classes = _add_msg_class( 127 | msg_classes, MESSAGE_X10_MESSAGE_RECEIVED_0X52, X10Received 128 | ) 129 | msg_classes = _add_msg_class( 130 | msg_classes, MESSAGE_ALL_LINKING_COMPLETED_0X53, AllLinkComplete 131 | ) 132 | msg_classes = _add_msg_class( 133 | msg_classes, MESSAGE_BUTTON_EVENT_REPORT_0X54, ButtonEventReport 134 | ) 135 | msg_classes = _add_msg_class( 136 | msg_classes, MESSAGE_USER_RESET_DETECTED_0X55, UserReset 137 | ) 138 | msg_classes = _add_msg_class( 139 | msg_classes, 140 | MESSAGE_ALL_LINK_CEANUP_FAILURE_REPORT_0X56, 141 | AllLinkCleanupFailureReport, 142 | ) 143 | msg_classes = _add_msg_class( 144 | msg_classes, MESSAGE_ALL_LINK_RECORD_RESPONSE_0X57, AllLinkRecordResponse 145 | ) 146 | msg_classes = _add_msg_class( 147 | msg_classes, 148 | MESSAGE_ALL_LINK_CLEANUP_STATUS_REPORT_0X58, 149 | AllLinkCleanupStatusReport, 150 | ) 151 | msg_classes = _add_msg_class(msg_classes, MESSAGE_GET_IM_INFO_0X60, GetImInfo) 152 | msg_classes = _add_msg_class( 153 | msg_classes, MESSAGE_SEND_ALL_LINK_COMMAND_0X61, SendAllLinkCommand 154 | ) 155 | msg_classes = _add_msg_class( 156 | msg_classes, MESSAGE_SEND_STANDARD_MESSAGE_0X62, StandardSend 157 | ) 158 | msg_classes = _add_msg_class(msg_classes, MESSAGE_X10_MESSAGE_SEND_0X63, X10Send) 159 | msg_classes = _add_msg_class( 160 | msg_classes, MESSAGE_START_ALL_LINKING_0X64, StartAllLinking 161 | ) 162 | msg_classes = _add_msg_class( 163 | msg_classes, MESSAGE_CANCEL_ALL_LINKING_0X65, CancelAllLinking 164 | ) 165 | msg_classes = _add_msg_class(msg_classes, MESSAGE_RESET_IM_0X67, ResetIM) 166 | msg_classes = _add_msg_class( 167 | msg_classes, MESSAGE_GET_FIRST_ALL_LINK_RECORD_0X69, GetFirstAllLinkRecord 168 | ) 169 | msg_classes = _add_msg_class( 170 | msg_classes, MESSAGE_GET_NEXT_ALL_LINK_RECORD_0X6A, GetNextAllLinkRecord 171 | ) 172 | msg_classes = _add_msg_class( 173 | msg_classes, MESSAGE_MANAGE_ALL_LINK_RECORD_0X6F, ManageAllLinkRecord 174 | ) 175 | msg_classes = _add_msg_class( 176 | msg_classes, MESSAGE_SET_IM_CONFIGURATION_0X6B, SetIMConfiguration 177 | ) 178 | msg_classes = _add_msg_class( 179 | msg_classes, MESSAGE_GET_IM_CONFIGURATION_0X73, GetImConfiguration 180 | ) 181 | 182 | return msg_classes.get(code, None) 183 | 184 | 185 | def _add_msg_class(msg_list, code, msg_class): 186 | msg_list[code] = msg_class 187 | return msg_list 188 | 189 | 190 | def _trim_buffer_garbage(rawmessage, debug=True): 191 | """Remove leading bytes from a byte stream. 192 | 193 | A proper message byte stream begins with 0x02. 194 | """ 195 | while rawmessage and rawmessage[0] != MESSAGE_START_CODE_0X02: 196 | if debug: 197 | _LOGGER.debug("Buffer content: %s", binascii.hexlify(rawmessage)) 198 | _LOGGER.debug("Trimming leading buffer garbage") 199 | rawmessage = rawmessage[1:] 200 | return rawmessage 201 | -------------------------------------------------------------------------------- /insteonplm/messages/allLinkCleanupFailureReport.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message All-LinkCleanup Failup Report.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_ALL_LINK_CEANUP_FAILURE_REPORT_0X56, 5 | MESSAGE_ALL_LINK_CEANUP_FAILURE_REPORT_SIZE, 6 | ) 7 | from insteonplm.address import Address 8 | 9 | 10 | class AllLinkCleanupFailureReport(Message): 11 | """INSTEON All-Link Failure Report Message. 12 | 13 | Message type 0x56 14 | """ 15 | 16 | _code = MESSAGE_ALL_LINK_CEANUP_FAILURE_REPORT_0X56 17 | _sendSize = MESSAGE_ALL_LINK_CEANUP_FAILURE_REPORT_SIZE 18 | _receivedSize = MESSAGE_ALL_LINK_CEANUP_FAILURE_REPORT_SIZE 19 | _description = "INSTEON All-Link Failure Report Message" 20 | 21 | def __init__(self, group, address): 22 | """Init the AllLinkCleanupFailureReport Class.""" 23 | self._group = group 24 | self._address = Address(address) 25 | self._failedFlag = 0x01 26 | 27 | @classmethod 28 | def from_raw_message(cls, rawmessage): 29 | """Create message from a raw byte stream.""" 30 | return AllLinkCleanupFailureReport(rawmessage[3], rawmessage[4:7]) 31 | 32 | @property 33 | def group(self): 34 | """Return the All-Link Group.""" 35 | return self._group 36 | 37 | @property 38 | def address(self): 39 | """Return the device address.""" 40 | return self._address 41 | 42 | def _message_properties(self): 43 | return [ 44 | {"failedFlag": self._failedFlag}, 45 | {"group": self._group}, 46 | {"address": self._address}, 47 | ] 48 | -------------------------------------------------------------------------------- /insteonplm/messages/allLinkCleanupStatusReport.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message All-Link Cleanup Status Report.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_ALL_LINK_CLEANUP_STATUS_REPORT_0X58, 5 | MESSAGE_ALL_LINK_CLEANUP_STATUS_REPORT_SIZE, 6 | MESSAGE_ACK, 7 | MESSAGE_NAK, 8 | ) 9 | 10 | 11 | class AllLinkCleanupStatusReport(Message): 12 | """INSTEON All-Link Cleanup Status Report Message. 13 | 14 | Message type 0x58 15 | """ 16 | 17 | _code = MESSAGE_ALL_LINK_CLEANUP_STATUS_REPORT_0X58 18 | _sendSize = MESSAGE_ALL_LINK_CLEANUP_STATUS_REPORT_SIZE 19 | _receivedSize = MESSAGE_ALL_LINK_CLEANUP_STATUS_REPORT_SIZE 20 | _description = "INSTEON All-Link Cleanup Status Report Message Received" 21 | 22 | def __init__(self, acknak): 23 | """Init the AllLinkCleanupStatusReport Class.""" 24 | self._acknak = self._setacknak(acknak) 25 | 26 | @classmethod 27 | def from_raw_message(cls, rawmessage): 28 | """Create message from raw byte stream.""" 29 | return AllLinkCleanupStatusReport(rawmessage[2]) 30 | 31 | @property 32 | def acknak(self): 33 | """Return ACK/NAK byte.""" 34 | return self._acknak 35 | 36 | @property 37 | def isack(self): 38 | """Test if this is an ACK message.""" 39 | return self._acknak & MESSAGE_ACK == MESSAGE_ACK 40 | 41 | @property 42 | def isnak(self): 43 | """Test if this is a NAK message.""" 44 | return self._acknak & MESSAGE_NAK == MESSAGE_NAK 45 | 46 | def _message_properties(self): 47 | return [{"acknak": self._acknak}] 48 | -------------------------------------------------------------------------------- /insteonplm/messages/allLinkComplete.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message All-Link Complete.""" 2 | 3 | from insteonplm.address import Address 4 | from insteonplm.constants import ( 5 | MESSAGE_ALL_LINKING_COMPLETED_0X53, 6 | MESSAGE_ALL_LINKING_COMPLETED_SIZE, 7 | ) 8 | from insteonplm.messages.message import Message 9 | 10 | 11 | class AllLinkComplete(Message): 12 | """INSTEON ALL-Linking Completed Message. 13 | 14 | Message type 0x53 15 | """ 16 | 17 | _code = MESSAGE_ALL_LINKING_COMPLETED_0X53 18 | _sendSize = MESSAGE_ALL_LINKING_COMPLETED_SIZE 19 | _receivedSize = MESSAGE_ALL_LINKING_COMPLETED_SIZE 20 | _description = "INSTEON ALL-Linking Completed Message Received" 21 | 22 | def __init__(self, linkcode, group, address, cat, subcat, firmware): 23 | """Init the AllLinkComplete Class.""" 24 | self._linkcode = linkcode 25 | self._group = group 26 | self._address = Address(address) 27 | self._category = cat 28 | self._subcategory = subcat 29 | self._firmware = firmware 30 | 31 | @classmethod 32 | def from_raw_message(cls, rawmessage): 33 | """Create a message from a raw byte stream.""" 34 | return AllLinkComplete( 35 | rawmessage[2], 36 | rawmessage[3], 37 | rawmessage[4:7], 38 | rawmessage[7], 39 | rawmessage[8], 40 | rawmessage[9], 41 | ) 42 | 43 | @property 44 | def linkcode(self): 45 | """Return link record link code.""" 46 | return self._linkcode 47 | 48 | @property 49 | def group(self): 50 | """Return link record group.""" 51 | return self._group 52 | 53 | @property 54 | def address(self): 55 | """Return the device address.""" 56 | return self._address 57 | 58 | @property 59 | def category(self): 60 | """Return the device category.""" 61 | return self._category 62 | 63 | @property 64 | def subcategory(self): 65 | """Return the device subcategory.""" 66 | return self._subcategory 67 | 68 | @property 69 | def firmware(self): 70 | """Return the device firmware version.""" 71 | return self._firmware 72 | 73 | @property 74 | def isresponder(self): 75 | """Return if the link record is a responder.""" 76 | return bool(self.linkcode == 0) 77 | 78 | @property 79 | def iscontroller(self): 80 | """Return if the link record is a controller.""" 81 | return bool(self.linkcode == 1) 82 | 83 | @property 84 | def isdeleted(self): 85 | """Return if the link record is deleted.""" 86 | return bool(self.linkcode == 0xFF) 87 | 88 | def _message_properties(self): 89 | return [ 90 | {"linkcode": self._linkcode}, 91 | {"group": self._group}, 92 | {"address": self._address}, 93 | {"category": self._category}, 94 | {"subcategory": self._subcategory}, 95 | {"firmware": self._firmware}, 96 | ] 97 | -------------------------------------------------------------------------------- /insteonplm/messages/allLinkRecordResponse.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message All-Link Record Response.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_ALL_LINK_RECORD_RESPONSE_0X57, 5 | MESSAGE_ALL_LINK_RECORD_RESPONSE_SIZE, 6 | ) 7 | from insteonplm.address import Address 8 | 9 | CONTROL_FLAG_RECORD_IN_USE = 0x80 10 | CONTROL_FLAG_CONTROLLER = 0x40 11 | 12 | 13 | class AllLinkRecordResponse(Message): 14 | """INSTEON ALL-Link Record Response. 15 | 16 | Message type 0x57 17 | """ 18 | 19 | _code = MESSAGE_ALL_LINK_RECORD_RESPONSE_0X57 20 | _sendSize = MESSAGE_ALL_LINK_RECORD_RESPONSE_SIZE 21 | _receivedSize = MESSAGE_ALL_LINK_RECORD_RESPONSE_SIZE 22 | _description = "INSTEON ALL-Link Record Response" 23 | 24 | def __init__(self, flags, group, address, linkdata1, linkdata2, linkdata3): 25 | """Init the AllLinkRecordResponse Class.""" 26 | self._controlFlags = flags 27 | self._group = group 28 | self._address = Address(address) 29 | self._linkdata1 = linkdata1 30 | self._linkdata2 = linkdata2 31 | self._linkdata3 = linkdata3 32 | 33 | @classmethod 34 | def from_raw_message(cls, rawmessage): 35 | """Create message from raw byte stream.""" 36 | return AllLinkRecordResponse( 37 | rawmessage[2], 38 | rawmessage[3], 39 | rawmessage[4:7], 40 | rawmessage[7], 41 | rawmessage[8], 42 | rawmessage[9], 43 | ) 44 | 45 | @property 46 | def controlFlags(self): 47 | """Return the link record control flags.""" 48 | return self._controlFlags 49 | 50 | @property 51 | def group(self): 52 | """Return the link record group.""" 53 | return self._group 54 | 55 | @property 56 | def address(self): 57 | """Return the device address.""" 58 | return self._address 59 | 60 | @property 61 | def linkdata1(self): 62 | """Return the first link data field.""" 63 | return self._linkdata1 64 | 65 | @property 66 | def linkdata2(self): 67 | """Return the second link data field.""" 68 | return self._linkdata2 69 | 70 | @property 71 | def linkdata3(self): 72 | """Return the third link data field.""" 73 | return self._linkdata3 74 | 75 | @property 76 | def isRecordinuse(self): 77 | """Test if the link record is in use.""" 78 | return ( 79 | self._controlFlags & CONTROL_FLAG_RECORD_IN_USE 80 | ) == CONTROL_FLAG_RECORD_IN_USE 81 | 82 | @property 83 | def isController(self): 84 | """Test if the link group is a controller.""" 85 | return (self._controlFlags & CONTROL_FLAG_CONTROLLER) == CONTROL_FLAG_CONTROLLER 86 | 87 | @property 88 | def isSlave(self): 89 | """Test if the link group is a slave or responder.""" 90 | return not self.isController 91 | 92 | def _message_properties(self): 93 | return [ 94 | {"controlFlags": self._controlFlags}, 95 | {"group": self._group}, 96 | {"address": self._address}, 97 | {"linkdata1": self._linkdata1}, 98 | {"linkdata2": self._linkdata2}, 99 | {"linkdata3": self._linkdata3}, 100 | ] 101 | -------------------------------------------------------------------------------- /insteonplm/messages/buttonEventReport.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Button Event Report.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_BUTTON_EVENT_REPORT_0X54, 5 | MESSAGE_BUTTON_EVENT_REPORT_SIZE, 6 | ) 7 | 8 | 9 | class ButtonEventReport(Message): 10 | """Insteon Button Event Report Message Received. 11 | 12 | Message type 0x54 13 | """ 14 | 15 | _code = MESSAGE_BUTTON_EVENT_REPORT_0X54 16 | _sendSize = MESSAGE_BUTTON_EVENT_REPORT_SIZE 17 | _receivedSize = MESSAGE_BUTTON_EVENT_REPORT_SIZE 18 | _description = "INSTEON Standard Message Received" 19 | 20 | def __init__(self, event): 21 | """Init the ButtonEventReport Class.""" 22 | self._event = event 23 | 24 | self._events = { 25 | 0x02: "SET button tapped", 26 | 0x03: "SET button press and hold", 27 | 0x04: "SET button released", 28 | 0x12: "Button 2 tapped", 29 | 0x13: "Button 2 press and hold", 30 | 0x14: "Button 2 released", 31 | 0x22: "Button 3 tapped", 32 | 0x23: "Button 3 press and hold", 33 | 0x24: "Button 3 released", 34 | } 35 | 36 | @classmethod 37 | def from_raw_message(cls, rawmessage): 38 | """Create message from raw byte stream.""" 39 | return ButtonEventReport(rawmessage[2]) 40 | 41 | @property 42 | def event(self): 43 | """Return the button event property.""" 44 | return self._event 45 | 46 | @property 47 | def eventText(self): 48 | """Return the button event text.""" 49 | return self._events.get(self.event, None) 50 | 51 | def _message_properties(self): 52 | return [{"event": self._event}] 53 | -------------------------------------------------------------------------------- /insteonplm/messages/cancelAllLinking.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Cancel All-Linking.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_CANCEL_ALL_LINKING_0X65, 5 | MESSAGE_CANCEL_ALL_LINKING_SIZE, 6 | MESSAGE_CANCEL_ALL_LINKING_RECEIVED_SIZE, 7 | MESSAGE_ACK, 8 | MESSAGE_NAK, 9 | ) 10 | 11 | 12 | class CancelAllLinking(Message): 13 | """INSTEON Cancel All-Linking. 14 | 15 | Message type 0x65 16 | """ 17 | 18 | _code = MESSAGE_CANCEL_ALL_LINKING_0X65 19 | _sendSize = MESSAGE_CANCEL_ALL_LINKING_SIZE 20 | _receivedSize = MESSAGE_CANCEL_ALL_LINKING_RECEIVED_SIZE 21 | _description = "INSTEON Cancel All-Linking" 22 | 23 | def __init__(self, acknak=None): 24 | """Init the CancelAllLinking Class.""" 25 | self._acknak = self._setacknak(acknak) 26 | 27 | @classmethod 28 | def from_raw_message(cls, rawmessage): 29 | """Create message from raw byte stream.""" 30 | return CancelAllLinking(rawmessage[2:3]) 31 | 32 | @property 33 | def acknak(self): 34 | """Return the ACK/NAK byte.""" 35 | return self._acknak 36 | 37 | @property 38 | def isack(self): 39 | """Test if this is an ACK message.""" 40 | return self._acknak is not None and self._acknak == MESSAGE_ACK 41 | 42 | @property 43 | def isnak(self): 44 | """Test if this is a NAK message.""" 45 | return self._acknak is not None and self._acknak == MESSAGE_NAK 46 | 47 | def _message_properties(self): 48 | return [{"acknak": self._acknak}] 49 | -------------------------------------------------------------------------------- /insteonplm/messages/extendedReceive.py: -------------------------------------------------------------------------------- 1 | """INSTEON Extended Receive Message Type 0x51.""" 2 | 3 | from insteonplm.constants import ( 4 | MESSAGE_EXTENDED_MESSAGE_RECEIVED_0X51, 5 | MESSAGE_EXTENDED_MESSAGE_RECEIVED_SIZE, 6 | ) 7 | from insteonplm.address import Address 8 | from insteonplm.messages.message import Message 9 | from insteonplm.messages.messageFlags import MessageFlags 10 | from insteonplm.messages.userdata import Userdata 11 | 12 | 13 | class ExtendedReceive(Message): 14 | """Insteon Extended Length Message Received. 15 | 16 | Message type 0x51 17 | """ 18 | 19 | _code = MESSAGE_EXTENDED_MESSAGE_RECEIVED_0X51 20 | _sendSize = MESSAGE_EXTENDED_MESSAGE_RECEIVED_SIZE 21 | _receivedSize = MESSAGE_EXTENDED_MESSAGE_RECEIVED_SIZE 22 | _description = "INSTEON Extended Message Received" 23 | 24 | def __init__(self, address, target, commandtuple, userdata, cmd2=None, flags=0x10): 25 | """Init the ExtendedRecieve message class.""" 26 | if commandtuple.get("cmd1", None) is not None: 27 | cmd1 = commandtuple["cmd1"] 28 | cmd2out = commandtuple["cmd2"] 29 | else: 30 | raise ValueError 31 | 32 | if cmd2 is not None: 33 | cmd2out = cmd2 34 | 35 | if cmd2out is None: 36 | raise ValueError 37 | 38 | self._address = Address(address) 39 | self._target = Address(target) 40 | self._messageFlags = MessageFlags(flags) 41 | # self._messageFlags.extended = 1 42 | self._cmd1 = cmd1 43 | self._cmd2 = cmd2out 44 | self._userdata = Userdata(userdata) 45 | 46 | @classmethod 47 | def from_raw_message(cls, rawmessage): 48 | """Create message from raw byte stream.""" 49 | userdata = Userdata.from_raw_message(rawmessage[11:25]) 50 | return ExtendedReceive( 51 | rawmessage[2:5], 52 | rawmessage[5:8], 53 | {"cmd1": rawmessage[9], "cmd2": rawmessage[10]}, 54 | userdata, 55 | flags=rawmessage[8], 56 | ) 57 | 58 | # pylint: disable=protected-access 59 | @classmethod 60 | def template( 61 | cls, 62 | address=None, 63 | target=None, 64 | commandtuple=None, 65 | userdata=None, 66 | cmd2=-1, 67 | flags=None, 68 | ): 69 | """Create message template for callbacks.""" 70 | msgraw = bytearray([0x02, cls._code]) 71 | msgraw.extend(bytes(cls._receivedSize)) 72 | msg = ExtendedReceive.from_raw_message(msgraw) 73 | 74 | if commandtuple: 75 | cmd1 = commandtuple.get("cmd1") 76 | cmd2out = commandtuple.get("cmd2") 77 | else: 78 | cmd1 = None 79 | cmd2out = None 80 | 81 | if cmd2 is not -1: 82 | cmd2out = cmd2 83 | 84 | msg._address = Address(address) 85 | msg._target = Address(target) 86 | msg._messageFlags = MessageFlags(flags) 87 | msg._cmd1 = cmd1 88 | msg._cmd2 = cmd2out 89 | msg._userdata = Userdata.create_pattern(userdata) 90 | return msg 91 | 92 | @property 93 | def address(self): 94 | """Return address of sending device.""" 95 | return self._address 96 | 97 | @property 98 | def target(self): 99 | """Return address of target device.""" 100 | return self._target 101 | 102 | @property 103 | def cmd1(self): 104 | """Return cmd1 property of message.""" 105 | return self._cmd1 106 | 107 | @property 108 | def cmd2(self): 109 | """Return cmd2 property of message.""" 110 | return self._cmd2 111 | 112 | @property 113 | def userdata(self): 114 | """Return user data of extended message.""" 115 | return self._userdata 116 | 117 | @property 118 | def flags(self): 119 | """Return message flags.""" 120 | return self._messageFlags 121 | 122 | @property 123 | def targetLow(self): 124 | """Return the low byte of the target address field. 125 | 126 | Used in All-Link Cleanup messages. 127 | """ 128 | low_byte = None 129 | if self.target is not None and self._messageFlags.isBroadcast: 130 | low_byte = self.target.bytes[0] 131 | return low_byte 132 | 133 | @property 134 | def targetMed(self): 135 | """Return the middle byte of the target address field. 136 | 137 | Used in All-Link Cleanup messages. 138 | """ 139 | med_byte = None 140 | if self.target is not None and self._messageFlags.isBroadcast: 141 | med_byte = self.target.bytes[1] 142 | return med_byte 143 | 144 | @property 145 | def targetHi(self): 146 | """Return the high byte of the target address field. 147 | 148 | Used in All-Link Cleanup messages. 149 | """ 150 | hi_byte = None 151 | if self.target is not None and self._messageFlags.isBroadcast: 152 | hi_byte = self.target.bytes[2] 153 | return hi_byte 154 | 155 | def _message_properties(self): 156 | return [ 157 | {"address": self._address}, 158 | {"target": self._target}, 159 | {"flags": self._messageFlags}, 160 | {"cmd1": self._cmd1}, 161 | {"cmd2": self._cmd2}, 162 | {"userdata": self._userdata}, 163 | ] 164 | -------------------------------------------------------------------------------- /insteonplm/messages/extendedSend.py: -------------------------------------------------------------------------------- 1 | """INSTEON Extended Send Message Type 0x62.""" 2 | 3 | from insteonplm.constants import ( 4 | MESSAGE_ACK, 5 | MESSAGE_NAK, 6 | MESSAGE_SEND_EXTENDED_MESSAGE_0X62, 7 | MESSAGE_SEND_EXTENDED_MESSAGE_RECEIVED_SIZE, 8 | MESSAGE_SEND_EXTENDED_MESSAGE_SIZE, 9 | ) 10 | from insteonplm.address import Address 11 | from insteonplm.messages.message import Message 12 | from insteonplm.messages.messageFlags import MessageFlags 13 | from insteonplm.messages.userdata import Userdata 14 | 15 | 16 | class ExtendedSend(Message): 17 | """Send an INSTEON Extended message. 18 | 19 | address: A device hex address in any form. 20 | cmd1: Required command in the cmd1 field 21 | cmd2: Required command in the cmd2 field 22 | **kwarg: otional userdata dictionary with the following keys: 23 | 'd1' - User data byte 0 as byte or int 24 | 'd2' - user data byte 1 as byte or int 25 | ... 26 | 'd14' - user data byte 14 as byte or int 27 | 'd1' to 'd14' are assumed to equal 0x00 unless explicitly set 28 | """ 29 | 30 | _code = MESSAGE_SEND_EXTENDED_MESSAGE_0X62 31 | _sendSize = MESSAGE_SEND_EXTENDED_MESSAGE_SIZE 32 | _receivedSize = MESSAGE_SEND_EXTENDED_MESSAGE_RECEIVED_SIZE 33 | _description = "INSTEON Standard Message Send" 34 | 35 | def __init__( 36 | self, address, commandtuple, userdata, cmd2=None, flags=0x10, acknak=None 37 | ): 38 | """Init the ExtendedSend message class.""" 39 | if commandtuple.get("cmd1", None) is not None: 40 | cmd1 = commandtuple["cmd1"] 41 | cmd2out = commandtuple["cmd2"] 42 | else: 43 | raise ValueError 44 | 45 | if cmd2 is not None: 46 | cmd2out = cmd2 47 | 48 | if cmd2out is None: 49 | raise ValueError 50 | 51 | self._address = Address(address) 52 | self._messageFlags = MessageFlags(flags) 53 | self._messageFlags.extended = 1 54 | self._cmd1 = cmd1 55 | self._cmd2 = cmd2out 56 | self._userdata = Userdata(userdata) 57 | self._acknak = self._setacknak(acknak) 58 | 59 | @classmethod 60 | def from_raw_message(cls, rawmessage): 61 | """Create a message from a raw byte stream.""" 62 | userdata_dict = Userdata(rawmessage[8:22]) 63 | return ExtendedSend( 64 | rawmessage[2:5], 65 | {"cmd1": rawmessage[6], "cmd2": rawmessage[7]}, 66 | userdata_dict, 67 | flags=rawmessage[5], 68 | acknak=rawmessage[22:23], 69 | ) 70 | 71 | # pylint: disable=protected-access 72 | @classmethod 73 | def template( 74 | cls, 75 | address=None, 76 | commandtuple=None, 77 | userdata=None, 78 | cmd2=-1, 79 | flags=None, 80 | acknak=None, 81 | ): 82 | """Create a message template used for callbacks.""" 83 | msgraw = bytearray([0x02, cls._code]) 84 | msgraw.extend(bytes(cls._receivedSize)) 85 | msg = ExtendedSend.from_raw_message(msgraw) 86 | 87 | if commandtuple: 88 | cmd1 = commandtuple.get("cmd1") 89 | cmd2out = commandtuple.get("cmd2") 90 | else: 91 | cmd1 = None 92 | cmd2out = None 93 | 94 | if cmd2 is not -1: 95 | cmd2out = cmd2 96 | 97 | msg._address = Address(address) 98 | msg._messageFlags = MessageFlags(flags) 99 | msg._messageFlags.extended = 1 100 | msg._cmd1 = cmd1 101 | msg._cmd2 = cmd2out 102 | msg._userdata = Userdata.template(userdata) 103 | msg._acknak = acknak 104 | return msg 105 | 106 | @property 107 | def address(self): 108 | """Return the address of the device.""" 109 | return self._address 110 | 111 | @property 112 | def cmd1(self): 113 | """Return the cmd1 property of the message.""" 114 | return self._cmd1 115 | 116 | @property 117 | def cmd2(self): 118 | """Return the cmd2 property of the message.""" 119 | return self._cmd2 120 | 121 | @property 122 | def userdata(self): 123 | """Return the extended message user data.""" 124 | return self._userdata 125 | 126 | @property 127 | def flags(self): 128 | """Return the message flags.""" 129 | return self._messageFlags 130 | 131 | @property 132 | def acknak(self): 133 | """Return the ACK/NAK byte.""" 134 | return self._acknak 135 | 136 | @acknak.setter 137 | def acknak(self, val): 138 | """Set the ACK/NAK byte.""" 139 | if val in [None, 0x06, 0x15]: 140 | self._acknak = val 141 | else: 142 | raise ValueError 143 | 144 | @property 145 | def isack(self): 146 | """Test if message is an ACK.""" 147 | return self._acknak is not None and self._acknak == MESSAGE_ACK 148 | 149 | @property 150 | def isnak(self): 151 | """Test if message is a NAK.""" 152 | return self._acknak is not None and self._acknak == MESSAGE_NAK 153 | 154 | def set_checksum(self): 155 | """Set byte 14 of the userdata to a checksum value.""" 156 | data_sum = self.cmd1 + self.cmd2 157 | for i in range(1, 14): 158 | data_sum += self._userdata["d{:d}".format(i)] 159 | chksum = 0xFF - (data_sum & 0xFF) + 1 160 | self._userdata["d14"] = chksum & 0xff 161 | 162 | def set_crc(self): 163 | """Set Userdata[13] and Userdata[14] to the CRC value.""" 164 | data = self.bytes[6:20] 165 | crc = int(0) 166 | for b in data: 167 | # pylint: disable=unused-variable 168 | for bit in range(0, 8): 169 | fb = b & 0x01 170 | fb = fb ^ 0x01 if (crc & 0x8000) else fb 171 | fb = fb ^ 0x01 if (crc & 0x4000) else fb 172 | fb = fb ^ 0x01 if (crc & 0x1000) else fb 173 | fb = fb ^ 0x01 if (crc & 0x0008) else fb 174 | crc = ((crc << 1) | fb) & 0xFFFF 175 | b = b >> 1 176 | self._userdata["d13"] = (crc >> 8) & 0xFF 177 | self._userdata["d14"] = crc & 0xFF 178 | 179 | def _message_properties(self): 180 | return [ 181 | {"address": self._address}, 182 | {"flags": self._messageFlags}, 183 | {"cmd1": self._cmd1}, 184 | {"cmd2": self._cmd2}, 185 | {"userdata": self._userdata}, 186 | {"acknak": self._acknak}, 187 | ] 188 | -------------------------------------------------------------------------------- /insteonplm/messages/getFirstAllLinkRecord.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Get First All-Link Record.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_GET_FIRST_ALL_LINK_RECORD_0X69, 5 | MESSAGE_GET_FIRST_ALL_LINK_RECORD_SIZE, 6 | MESSAGE_GET_FIRST_ALL_LINK_RECORD_RECEIVED_SIZE, 7 | MESSAGE_ACK, 8 | MESSAGE_NAK, 9 | ) 10 | 11 | 12 | class GetFirstAllLinkRecord(Message): 13 | """Insteon Get First All Link Record Message. 14 | 15 | Message type 0x69 16 | """ 17 | 18 | _code = MESSAGE_GET_FIRST_ALL_LINK_RECORD_0X69 19 | _sendSize = MESSAGE_GET_FIRST_ALL_LINK_RECORD_SIZE 20 | _receivedSize = MESSAGE_GET_FIRST_ALL_LINK_RECORD_RECEIVED_SIZE 21 | _description = "Insteon Get First All Link Record Message" 22 | 23 | def __init__(self, acknak=None): 24 | """Init the GetFirstAllLinkRecord Class.""" 25 | self._acknak = self._setacknak(acknak) 26 | 27 | @classmethod 28 | def from_raw_message(cls, rawmessage): 29 | """Create message from raw byte stream.""" 30 | return GetFirstAllLinkRecord(rawmessage[2:3]) 31 | 32 | @property 33 | def acknak(self): 34 | """Return the ACK/NAK byte.""" 35 | return self._acknak 36 | 37 | @property 38 | def isack(self): 39 | """Test if this is an ACK message.""" 40 | return self._acknak is not None and self._acknak == MESSAGE_ACK 41 | 42 | @property 43 | def isnak(self): 44 | """Test if this is a NAK message.""" 45 | return self._acknak is not None and self._acknak == MESSAGE_NAK 46 | 47 | def _message_properties(self): 48 | return [{"acknak": self._acknak}] 49 | -------------------------------------------------------------------------------- /insteonplm/messages/getIMInfo.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Get IM Info.""" 2 | 3 | from insteonplm.messages.message import Message 4 | from insteonplm.constants import ( 5 | MESSAGE_GET_IM_INFO_0X60, 6 | MESSAGE_GET_IM_INFO_SIZE, 7 | MESSAGE_GET_IM_INFO_RECEIVED_SIZE, 8 | MESSAGE_ACK, 9 | MESSAGE_NAK, 10 | ) 11 | from insteonplm.address import Address 12 | 13 | 14 | class GetImInfo(Message): 15 | """INSTEON Get Insteon Modem Info Message. 16 | 17 | Message type 0x60 18 | """ 19 | 20 | _code = MESSAGE_GET_IM_INFO_0X60 21 | _sendSize = MESSAGE_GET_IM_INFO_SIZE 22 | _receivedSize = MESSAGE_GET_IM_INFO_RECEIVED_SIZE 23 | _description = "INSTEON Get Insteon Modem Info Message Received" 24 | 25 | def __init__(self, address=None, cat=None, subcat=None, firmware=None, acknak=None): 26 | """Init the GetImInfo Class.""" 27 | self._address = Address(address) 28 | self._category = cat 29 | self._subcategory = subcat 30 | self._firmware = firmware 31 | self._acknak = self._setacknak(acknak) 32 | 33 | @classmethod 34 | def from_raw_message(cls, rawmessage): 35 | """Create message from raw byte stream.""" 36 | return GetImInfo( 37 | rawmessage[2:5], rawmessage[5], rawmessage[6], rawmessage[7], rawmessage[8] 38 | ) 39 | 40 | @property 41 | def address(self): 42 | """Return the device address.""" 43 | return self._address 44 | 45 | @property 46 | def category(self): 47 | """Return the IM device category.""" 48 | return self._category 49 | 50 | @property 51 | def subcategory(self): 52 | """Return the IM device subcategory.""" 53 | return self._subcategory 54 | 55 | @property 56 | def firmware(self): 57 | """Return the IM device firmware version.""" 58 | return self._firmware 59 | 60 | @property 61 | def acknak(self): 62 | """Return the ACK/NAK byte.""" 63 | return self._acknak 64 | 65 | @property 66 | def isack(self): 67 | """Test if this is an ACK message.""" 68 | return self._acknak is not None and self._acknak == MESSAGE_ACK 69 | 70 | @property 71 | def isnak(self): 72 | """Test if this is a NAK message.""" 73 | return self._acknak is not None and self._acknak == MESSAGE_NAK 74 | 75 | def _message_properties(self): 76 | return [ 77 | {"address": self._address}, 78 | {"category": self._category}, 79 | {"subcategory": self._subcategory}, 80 | {"firmware": self._firmware}, 81 | {"acknak": self._acknak}, 82 | ] 83 | -------------------------------------------------------------------------------- /insteonplm/messages/getImConfiguration.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Get IM Configuration.""" 2 | from insteonplm.constants import ( 3 | MESSAGE_GET_IM_CONFIGURATION_0X73, 4 | MESSAGE_GET_IM_CONFIGURATION_SIZE, 5 | MESSAGE_GET_IM_CONFIGURATION_RECEIVED_SIZE, 6 | MESSAGE_ACK, 7 | MESSAGE_NAK, 8 | ) 9 | from insteonplm.messages.message import Message 10 | 11 | 12 | class GetImConfiguration(Message): 13 | """Insteon Get IM Configuration Message. 14 | 15 | Message type 0x62 16 | """ 17 | 18 | _code = MESSAGE_GET_IM_CONFIGURATION_0X73 19 | _sendSize = MESSAGE_GET_IM_CONFIGURATION_SIZE 20 | _receivedSize = MESSAGE_GET_IM_CONFIGURATION_RECEIVED_SIZE 21 | _description = "Insteon Get IM Configuration Message" 22 | 23 | def __init__(self, flags=None, acknak=None): 24 | """Init the GetImConfiguration Class.""" 25 | self._imConfigurationFlags = flags 26 | self._spare1 = None 27 | self._spare2 = None 28 | self._acknak = self._setacknak(acknak) 29 | 30 | @classmethod 31 | def from_raw_message(cls, rawmesssage): 32 | """Create message from a raw byte stream.""" 33 | return GetImConfiguration(rawmesssage[2], rawmesssage[5]) 34 | 35 | @property 36 | def imConfigurationFlags(self): 37 | """Return the IM configuration flags.""" 38 | return self._imConfigurationFlags 39 | 40 | @property 41 | def acknak(self): 42 | """Return the ACK/NAK byte.""" 43 | return self._acknak 44 | 45 | @property 46 | def isack(self): 47 | """Test if this is an ACK message.""" 48 | return self._acknak is not None and self._acknak == MESSAGE_ACK 49 | 50 | @property 51 | def isnak(self): 52 | """Test if this is a NAK message.""" 53 | return self._acknak is not None and self._acknak == MESSAGE_NAK 54 | 55 | def _message_properties(self): 56 | if self._imConfigurationFlags is not None: 57 | self._spare1 = 0x00 58 | self._spare2 = 0x00 59 | else: 60 | self._spare1 = None 61 | self._spare2 = None 62 | return [ 63 | {"imConfigurationFlags": self._imConfigurationFlags}, 64 | {"spare1": self._spare1}, 65 | {"spare2": self._spare2}, 66 | {"acknak": self.acknak}, 67 | ] 68 | -------------------------------------------------------------------------------- /insteonplm/messages/getNextAllLinkRecord.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Get Next All-Link Record.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_GET_NEXT_ALL_LINK_RECORD_0X6A, 5 | MESSAGE_GET_NEXT_ALL_LINK_RECORD_SIZE, 6 | MESSAGE_GET_NEXT_ALL_LINK_RECORD_RECEIVED_SIZE, 7 | MESSAGE_ACK, 8 | MESSAGE_NAK, 9 | ) 10 | 11 | 12 | class GetNextAllLinkRecord(Message): 13 | """Insteon Get Next All Link Record Message. 14 | 15 | Message type 0x6A 16 | """ 17 | 18 | _code = MESSAGE_GET_NEXT_ALL_LINK_RECORD_0X6A 19 | _sendSize = MESSAGE_GET_NEXT_ALL_LINK_RECORD_SIZE 20 | _receivedSize = MESSAGE_GET_NEXT_ALL_LINK_RECORD_RECEIVED_SIZE 21 | _description = "Insteon Get Next All Link Record Message" 22 | 23 | def __init__(self, acknak=None): 24 | """Init the GetNextAllLinkRecord Class.""" 25 | self._acknak = self._setacknak(acknak) 26 | 27 | @classmethod 28 | def from_raw_message(cls, rawmessage): 29 | """Create message from raw byte stream.""" 30 | return GetNextAllLinkRecord(rawmessage[2:3]) 31 | 32 | @property 33 | def acknak(self): 34 | """Return the ACK/NAK byte.""" 35 | return self._acknak 36 | 37 | @property 38 | def isack(self): 39 | """Test if this is an ACK message.""" 40 | return self._acknak is not None and self._acknak == MESSAGE_ACK 41 | 42 | @property 43 | def isnak(self): 44 | """Test if this is a NAK message.""" 45 | return self._acknak is not None and self._acknak == MESSAGE_NAK 46 | 47 | def _message_properties(self): 48 | return [{"acknak": self._acknak}] 49 | -------------------------------------------------------------------------------- /insteonplm/messages/manageAllLinkRecord.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Manage All-Link Record.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_MANAGE_ALL_LINK_RECORD_0X6F, 5 | MESSAGE_MANAGE_ALL_LINK_RECORD_SIZE, 6 | MESSAGE_MANAGE_ALL_LINK_RECORD_RECEIVED_SIZE, 7 | MESSAGE_ACK, 8 | MESSAGE_NAK, 9 | ) 10 | from insteonplm.address import Address 11 | 12 | 13 | # pylint: disable=too-many-instance-attributes 14 | class ManageAllLinkRecord(Message): 15 | """Insteon Get First All Link Record Message. 16 | 17 | Message type 0x69 18 | """ 19 | 20 | _code = MESSAGE_MANAGE_ALL_LINK_RECORD_0X6F 21 | _sendSize = MESSAGE_MANAGE_ALL_LINK_RECORD_SIZE 22 | _receivedSize = MESSAGE_MANAGE_ALL_LINK_RECORD_RECEIVED_SIZE 23 | _description = "Insteon Manage All Link Record Message" 24 | 25 | def __init__( 26 | self, 27 | control_code, 28 | flags, 29 | group, 30 | address, 31 | linkdata1, 32 | linkdata2, 33 | linkdata3, 34 | acknak=None, 35 | ): 36 | """Init the ManageAllLinkRecord Class.""" 37 | self._controlCode = control_code 38 | self._controlFlags = flags 39 | self._group = group 40 | self._address = Address(address) 41 | self._linkdata1 = linkdata1 42 | self._linkdata2 = linkdata2 43 | self._linkdata3 = linkdata3 44 | self._acknak = self._setacknak(acknak) 45 | 46 | @classmethod 47 | def from_raw_message(cls, rawmessage): 48 | """Create message from raw byte stream.""" 49 | return ManageAllLinkRecord( 50 | rawmessage[2:3], 51 | rawmessage[3:4], 52 | rawmessage[4:7], 53 | rawmessage[7:8], 54 | rawmessage[8:9], 55 | rawmessage[9:10], 56 | rawmessage[10:11], 57 | ) 58 | 59 | @property 60 | def controlFlags(self): 61 | """Return the link record control flags.""" 62 | return self._controlFlags 63 | 64 | @property 65 | def group(self): 66 | """Return the link record group.""" 67 | return self._group 68 | 69 | @property 70 | def address(self): 71 | """Return the device address.""" 72 | return self._address 73 | 74 | @property 75 | def linkdata1(self): 76 | """Return the first link data field.""" 77 | return self._linkdata1 78 | 79 | @property 80 | def linkdata2(self): 81 | """Return the second link data field.""" 82 | return self._linkdata2 83 | 84 | @property 85 | def linkdata3(self): 86 | """Return the third link data field.""" 87 | return self._linkdata3 88 | 89 | @property 90 | def acknak(self): 91 | """Return the ACK/NAK byte.""" 92 | return self._acknak 93 | 94 | @property 95 | def isack(self): 96 | """Test if this is an ACK message.""" 97 | return self._acknak is not None and self._acknak == MESSAGE_ACK 98 | 99 | @property 100 | def isnak(self): 101 | """Test if this is a NAK message.""" 102 | return self._acknak is not None and self._acknak == MESSAGE_NAK 103 | 104 | def _message_properties(self): 105 | return [ 106 | {"controlCode": self._controlCode}, 107 | {"controlFlags": self._controlFlags}, 108 | {"group": self._group}, 109 | {"address": self._address}, 110 | {"linkdata1": self._linkdata1}, 111 | {"linkdata2": self._linkdata2}, 112 | {"linkdata3": self._linkdata3}, 113 | {"acknak": self._acknak}, 114 | ] 115 | -------------------------------------------------------------------------------- /insteonplm/messages/message.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Class Module.""" 2 | import logging 3 | import binascii 4 | from insteonplm.address import Address 5 | from insteonplm.constants import MESSAGE_START_CODE_0X02 6 | from insteonplm.messages.messageFlags import MessageFlags 7 | from insteonplm.messages.userdata import Userdata 8 | 9 | _LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | class ClassPropertyMetaClass(type): 13 | """Meta class for Message class. 14 | 15 | This is meta class magic to allow class attributes to also appear as an 16 | instance property. 17 | """ 18 | 19 | @property 20 | def code(cls): 21 | """Message code used to determine message type.""" 22 | return cls._code 23 | 24 | @property 25 | def sendSize(cls): 26 | """Size of the message sent in bytes.""" 27 | return cls._sendSize 28 | 29 | @property 30 | def receivedSize(cls): 31 | """Size of the message received in bytes.""" 32 | return cls._receivedSize 33 | 34 | @property 35 | def description(cls): 36 | """Return a description of the message type.""" 37 | return cls._description 38 | 39 | 40 | class Message(metaclass=ClassPropertyMetaClass): 41 | """Base message class for an INSTEON message.""" 42 | 43 | _code = 0 44 | _sendSize = 0 45 | _receivedSize = 0 46 | _description = "Empty message" 47 | 48 | def __str__(self): 49 | """Return a string representation of an INSTEON message.""" 50 | props = self._message_properties() 51 | msgstr = "{}'code': 0x{}".format( 52 | "{", binascii.hexlify(bytes([self._code])).decode() 53 | ) 54 | for prop in props: 55 | for key, val in prop.items(): 56 | msgstr = msgstr + ", " 57 | if isinstance(val, Address): 58 | msgstr = "{}'{}': {}".format(msgstr, key, val.human) 59 | elif isinstance(val, MessageFlags): 60 | msgstr = "{}'{}': 0x{}".format(msgstr, key, val.hex) 61 | elif isinstance(val, int): 62 | msgstr = "{}'{}': 0x{}".format( 63 | msgstr, key, binascii.hexlify(bytes([val])).decode() 64 | ) 65 | elif isinstance(val, bytearray): 66 | msgstr = "{}'{}': 0x{}".format( 67 | msgstr, key, binascii.hexlify(val).decode() 68 | ) 69 | elif isinstance(val, bytes): 70 | msgstr = "{}'{}': 0x{}".format( 71 | msgstr, key, binascii.hexlify(val).decode() 72 | ) 73 | else: 74 | msgstr = "{}'{}': 0x{}".format(msgstr, key, str(val)) 75 | msgstr = "{}{}".format(msgstr, "}") 76 | return msgstr 77 | 78 | def __eq__(self, other): 79 | """Test for equality.""" 80 | match = False 81 | if isinstance(other, Message) and other.code == self._code: 82 | match = str(self) == str(other) 83 | return match 84 | 85 | def __ne__(self, other): 86 | """Test for inequality.""" 87 | match = True 88 | if isinstance(other, Message) and other.code == self._code: 89 | match = str(self) != str(other) 90 | return match 91 | 92 | def __lt__(self, other): 93 | """Test for less than.""" 94 | less_than = False 95 | if isinstance(other, Message): 96 | less_than = str(self) < str(other) 97 | else: 98 | raise TypeError 99 | return less_than 100 | 101 | def __gt__(self, other): 102 | """Test for greater than.""" 103 | greater_than = False 104 | if isinstance(other, Message): 105 | greater_than = str(self) > str(other) 106 | else: 107 | raise TypeError 108 | return greater_than 109 | 110 | def __hash__(self): 111 | """Create a has of the message.""" 112 | return hash(str(self)) 113 | 114 | @property 115 | def code(self): 116 | """Messasge code used to determine message type.""" 117 | return self._code 118 | 119 | @property 120 | def sendSize(self): 121 | """Size of the sent messsage in bytes.""" 122 | return self._sendSize 123 | 124 | @property 125 | def receivedSize(self): 126 | """Size of the received message in bytes.""" 127 | return self._receivedSize 128 | 129 | @property 130 | def description(self): 131 | """Return the description of the message type.""" 132 | return self._description 133 | 134 | @property 135 | def hex(self): 136 | """Hexideciaml representation of the message in bytes.""" 137 | props = self._message_properties() 138 | msg = bytearray([MESSAGE_START_CODE_0X02, self._code]) 139 | 140 | for prop in props: 141 | # pylint: disable=unused-variable 142 | for key, val in prop.items(): 143 | if val is None: 144 | pass 145 | elif isinstance(val, int): 146 | msg.append(val) 147 | elif isinstance(val, Address): 148 | if val.addr is None: 149 | pass 150 | else: 151 | msg.extend(val.bytes) 152 | elif isinstance(val, MessageFlags): 153 | msg.extend(val.bytes) 154 | elif isinstance(val, bytearray): 155 | msg.extend(val) 156 | elif isinstance(val, bytes): 157 | msg.extend(val) 158 | elif isinstance(val, Userdata): 159 | msg.extend(val.bytes) 160 | 161 | return binascii.hexlify(msg).decode() 162 | 163 | @property 164 | def bytes(self): 165 | """Return the bytes representation of the message.""" 166 | return binascii.unhexlify(self.hex) 167 | 168 | def matches_pattern(self, other): 169 | """Return if the current message matches a message template. 170 | 171 | Compare the current message to a template message to test matches 172 | to a pattern. 173 | """ 174 | properties = self._message_properties() 175 | ismatch = False 176 | if isinstance(other, Message) and self.code == other.code: 177 | for prop in properties: 178 | for key, prop_val in prop.items(): 179 | if hasattr(other, key): 180 | key_val = getattr(other, key) 181 | ismatch = self._test_match(prop_val, key_val) 182 | else: 183 | ismatch = False 184 | if not ismatch: 185 | break 186 | if not ismatch: 187 | break 188 | return ismatch 189 | 190 | @staticmethod 191 | def _setacknak(acknak): 192 | _acknak = acknak 193 | if isinstance(acknak, bytearray) and acknak: 194 | _acknak = acknak[0] 195 | return _acknak 196 | 197 | def _message_properties(self): 198 | raise NotImplementedError 199 | 200 | @staticmethod 201 | def _test_match(prop_val, key_val): 202 | ismatch = False 203 | if isinstance(prop_val, MessageFlags): 204 | ismatch = prop_val.matches_pattern(key_val) 205 | elif isinstance(prop_val, Address): 206 | ismatch = prop_val.matches_pattern(key_val) 207 | elif isinstance(prop_val, Userdata): 208 | ismatch = prop_val.matches_pattern(key_val) 209 | else: 210 | if prop_val is None or key_val is None: 211 | ismatch = True 212 | else: 213 | ismatch = prop_val == key_val 214 | return ismatch 215 | -------------------------------------------------------------------------------- /insteonplm/messages/resetIM.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Reset IM.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_RESET_IM_0X67, 5 | MESSAGE_RESET_IM_SIZE, 6 | MESSAGE_RESET_IM_RECEIVED_SIZE, 7 | MESSAGE_ACK, 8 | MESSAGE_NAK, 9 | ) 10 | 11 | 12 | class ResetIM(Message): 13 | """Insteon Reset IM Message. 14 | 15 | Message type 0x67 16 | """ 17 | 18 | _code = MESSAGE_RESET_IM_0X67 19 | _sendSize = MESSAGE_RESET_IM_SIZE 20 | _receivedSize = MESSAGE_RESET_IM_RECEIVED_SIZE 21 | _description = "Insteon Reset IM Message" 22 | 23 | def __init__(self, acknak=None): 24 | """Init the ResetIM Class.""" 25 | self._acknak = self._setacknak(acknak) 26 | 27 | @classmethod 28 | def from_raw_message(cls, rawmessage): 29 | """Create message from raw byte stream.""" 30 | return ResetIM(rawmessage[2:3]) 31 | 32 | @property 33 | def acknak(self): 34 | """Return the ACK/NAK byte.""" 35 | return self._acknak 36 | 37 | @property 38 | def isack(self): 39 | """Test if this is an ACK message.""" 40 | return self._acknak is not None and self._acknak == MESSAGE_ACK 41 | 42 | @property 43 | def isnak(self): 44 | """Test if this is a NAK message.""" 45 | return self._acknak is not None and self._acknak == MESSAGE_NAK 46 | 47 | def _message_properties(self): 48 | return [{"acknak": self._acknak}] 49 | -------------------------------------------------------------------------------- /insteonplm/messages/sendAlllinkCommand.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Send All-Link Command.""" 2 | from insteonplm.constants import ( 3 | MESSAGE_SEND_ALL_LINK_COMMAND_0X61, 4 | MESSAGE_SEND_ALL_LINK_COMMAND_SIZE, 5 | MESSAGE_SEND_ALL_LINK_COMMAND_RECEIVED_SIZE, 6 | MESSAGE_ACK, 7 | MESSAGE_NAK, 8 | ) 9 | from insteonplm.messages.message import Message 10 | 11 | 12 | class SendAllLinkCommand(Message): 13 | """Insteon Send All Link Command Message. 14 | 15 | Message type 0x6A 16 | """ 17 | 18 | _code = MESSAGE_SEND_ALL_LINK_COMMAND_0X61 19 | _sendSize = MESSAGE_SEND_ALL_LINK_COMMAND_SIZE 20 | _receivedSize = MESSAGE_SEND_ALL_LINK_COMMAND_RECEIVED_SIZE 21 | _description = "Insteon Get Next All Link Record Message" 22 | 23 | def __init__(self, group, allLinkCommand, broadcastCommand, acknak=None): 24 | """Init the SendAllLinkCommand Class.""" 25 | self._group = group 26 | self._allLinkCommmand = allLinkCommand 27 | self._broadcastCommand = broadcastCommand 28 | 29 | self._acknak = self._setacknak(acknak) 30 | 31 | @classmethod 32 | def from_raw_message(cls, rawmessage): 33 | """Create message from raw byte stream.""" 34 | return SendAllLinkCommand( 35 | rawmessage[2], rawmessage[3], rawmessage[4], rawmessage[5:6] 36 | ) 37 | 38 | @property 39 | def group(self): 40 | """Return the All-Link group.""" 41 | return self._group 42 | 43 | @property 44 | def allLinkCommmand(self): 45 | """Return the All-Link command.""" 46 | return self._allLinkCommmand 47 | 48 | @property 49 | def broadcastCommand(self): 50 | """Return the broadcase command.""" 51 | return self._broadcastCommand 52 | 53 | @property 54 | def isack(self): 55 | """Test if this is an ACK message.""" 56 | return self._acknak is not None and self._acknak == MESSAGE_ACK 57 | 58 | @property 59 | def isnak(self): 60 | """Test if this is a NAK message.""" 61 | return self._acknak is not None and self._acknak == MESSAGE_NAK 62 | 63 | def _message_properties(self): 64 | return [ 65 | {"group": self._group}, 66 | {"allLinkCommand": self._allLinkCommmand}, 67 | {"broadcastCommand": self._broadcastCommand}, 68 | {"acknak": self._acknak}, 69 | ] 70 | -------------------------------------------------------------------------------- /insteonplm/messages/setImConfiguration.py: -------------------------------------------------------------------------------- 1 | """INSTEON Set IM Configuration Message.""" 2 | 3 | from insteonplm.messages.message import Message 4 | from insteonplm.constants import ( 5 | MESSAGE_SET_IM_CONFIGURATION_0X6B, 6 | MESSAGE_SET_IM_CONFIGURATION_SIZE, 7 | MESSAGE_SET_IM_CONFIGURATION_RECEIVED_SIZE, 8 | MESSAGE_ACK, 9 | MESSAGE_NAK, 10 | ) 11 | 12 | 13 | class SetIMConfiguration(Message): 14 | """INSTEON Get Insteon Modem Info Message. 15 | 16 | Message type 0x60 17 | """ 18 | 19 | _code = MESSAGE_SET_IM_CONFIGURATION_0X6B 20 | _sendSize = MESSAGE_SET_IM_CONFIGURATION_SIZE 21 | _receivedSize = MESSAGE_SET_IM_CONFIGURATION_RECEIVED_SIZE 22 | _description = "INSTEON Set IM Configuration Message" 23 | 24 | def __init__(self, flags=None, acknak=None): 25 | """Init the GetImInfo Class.""" 26 | self._imConfigurationFlags = flags 27 | self._acknak = self._setacknak(acknak) 28 | 29 | @classmethod 30 | def from_raw_message(cls, rawmessage): 31 | """Create message from raw byte stream.""" 32 | return SetIMConfiguration(rawmessage[2], rawmessage[3]) 33 | 34 | @property 35 | def imConfigurationFlags(self): 36 | """Return the IM configuration flags.""" 37 | return self._imConfigurationFlags 38 | 39 | @property 40 | def acknak(self): 41 | """Return the ACK/NAK byte.""" 42 | return self._acknak 43 | 44 | @property 45 | def isack(self): 46 | """Test if this is an ACK message.""" 47 | return self._acknak is not None and self._acknak == MESSAGE_ACK 48 | 49 | @property 50 | def isnak(self): 51 | """Test if this is a NAK message.""" 52 | return self._acknak is not None and self._acknak == MESSAGE_NAK 53 | 54 | def _message_properties(self): 55 | return [ 56 | {"imConfigurationFlags": self._imConfigurationFlags}, 57 | {"acknak": self.acknak}, 58 | ] 59 | -------------------------------------------------------------------------------- /insteonplm/messages/standardReceive.py: -------------------------------------------------------------------------------- 1 | """INSTEON Standard Receive Message Type 0x50.""" 2 | 3 | from insteonplm.constants import ( 4 | MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50, 5 | MESSAGE_STANDARD_MESSAGE_RECIEVED_SIZE, 6 | ) 7 | from insteonplm.address import Address 8 | from insteonplm.messages.message import Message 9 | from insteonplm.messages.messageFlags import MessageFlags 10 | 11 | 12 | class StandardReceive(Message): 13 | """Insteon Standard Length Message Received. 14 | 15 | Message type 0x50 16 | """ 17 | 18 | _code = MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50 19 | _sendSize = MESSAGE_STANDARD_MESSAGE_RECIEVED_SIZE 20 | _receivedSize = MESSAGE_STANDARD_MESSAGE_RECIEVED_SIZE 21 | _description = "INSTEON Standard Message Received" 22 | 23 | def __init__(self, address, target, commandtuple, cmd2=None, flags=0x00): 24 | """Init the StandardReceive message class.""" 25 | if commandtuple.get("cmd1") is not None: 26 | cmd1 = commandtuple["cmd1"] 27 | cmd2out = commandtuple["cmd2"] 28 | else: 29 | raise ValueError 30 | 31 | if cmd2 is not None: 32 | cmd2out = cmd2 33 | 34 | if cmd2out is None: 35 | raise ValueError 36 | 37 | self._address = Address(address) 38 | self._target = Address(target) 39 | self._messageFlags = MessageFlags(flags) 40 | # self._messageFlags.extended = 0 41 | self._cmd1 = cmd1 42 | self._cmd2 = cmd2out 43 | 44 | @classmethod 45 | def from_raw_message(cls, rawmessage): 46 | """Create message from a raw byte stream.""" 47 | return StandardReceive( 48 | rawmessage[2:5], 49 | rawmessage[5:8], 50 | {"cmd1": rawmessage[9], "cmd2": rawmessage[10]}, 51 | flags=rawmessage[8], 52 | ) 53 | 54 | # pylint: disable=protected-access 55 | @classmethod 56 | def template( 57 | cls, address=None, target=None, commandtuple=None, cmd2=-1, flags=None 58 | ): 59 | """Create a message template used for callbacks.""" 60 | msgraw = bytearray([0x02, cls._code]) 61 | msgraw.extend(bytes(cls._receivedSize)) 62 | msg = StandardReceive.from_raw_message(msgraw) 63 | 64 | if commandtuple: 65 | cmd1 = commandtuple.get("cmd1") 66 | cmd2out = commandtuple.get("cmd2") 67 | else: 68 | cmd1 = None 69 | cmd2out = None 70 | 71 | if cmd2 is not -1: 72 | cmd2out = cmd2 73 | 74 | msg._address = Address(address) 75 | msg._target = Address(target) 76 | msg._messageFlags = MessageFlags(flags) 77 | msg._cmd1 = cmd1 78 | msg._cmd2 = cmd2out 79 | return msg 80 | 81 | @property 82 | def address(self): 83 | """Return the address of the device.""" 84 | return self._address 85 | 86 | @property 87 | def target(self): 88 | """Return the address of the target device.""" 89 | return self._target 90 | 91 | @property 92 | def cmd1(self): 93 | """Return the cmd1 property of the message.""" 94 | return self._cmd1 95 | 96 | @property 97 | def cmd2(self): 98 | """Return the cmd2 property of the message.""" 99 | return self._cmd2 100 | 101 | @property 102 | def flags(self): 103 | """Return the message flags.""" 104 | return self._messageFlags 105 | 106 | @property 107 | def targetLow(self): 108 | """Return the low byte of the target message property. 109 | 110 | Used in All-Link Cleanup message types. 111 | """ 112 | low_byte = None 113 | if self.target.addr is not None and self._messageFlags.isBroadcast: 114 | low_byte = self.target.bytes[0] 115 | return low_byte 116 | 117 | @property 118 | def targetMed(self): 119 | """Return the middle byte of the target message property. 120 | 121 | Used in All-Link Cleanup message types. 122 | """ 123 | med_byte = None 124 | if self.target.addr is not None and self._messageFlags.isBroadcast: 125 | med_byte = self.target.bytes[1] 126 | return med_byte 127 | 128 | @property 129 | def targetHi(self): 130 | """Return the high byte of the target message property. 131 | 132 | Used in All-Link Cleanup message types. 133 | """ 134 | hi_byte = None 135 | if self.target.addr is not None and self._messageFlags.isBroadcast: 136 | hi_byte = self.target.bytes[2] 137 | return hi_byte 138 | 139 | def _message_properties(self): 140 | return [ 141 | {"address": self._address}, 142 | {"target": self._target}, 143 | {"flags": self._messageFlags}, 144 | {"cmd1": self._cmd1}, 145 | {"cmd2": self._cmd2}, 146 | ] 147 | -------------------------------------------------------------------------------- /insteonplm/messages/standardSend.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message type 0x62 Standard Send.""" 2 | 3 | from insteonplm.constants import ( 4 | MESSAGE_ACK, 5 | MESSAGE_FLAG_EXTENDED_0X10, 6 | MESSAGE_NAK, 7 | MESSAGE_SEND_STANDARD_MESSAGE_0X62, 8 | MESSAGE_SEND_STANDARD_MESSAGE_RECEIVED_SIZE, 9 | MESSAGE_SEND_STANDARD_MESSAGE_SIZE, 10 | ) 11 | from insteonplm.address import Address 12 | from insteonplm.messages.message import Message 13 | from insteonplm.messages.extendedSend import ExtendedSend 14 | from insteonplm.messages.messageFlags import MessageFlags 15 | 16 | 17 | class StandardSend(Message): 18 | """Insteon Standard Length Message Send 0x62.""" 19 | 20 | _code = MESSAGE_SEND_STANDARD_MESSAGE_0X62 21 | _sendSize = MESSAGE_SEND_STANDARD_MESSAGE_SIZE 22 | _receivedSize = MESSAGE_SEND_STANDARD_MESSAGE_RECEIVED_SIZE 23 | _description = "INSTEON Standard Message Send" 24 | 25 | def __init__(self, address, commandtuple, cmd2=None, flags=0x00, acknak=None): 26 | """Init the StandardSend message class.""" 27 | if commandtuple.get("cmd1", None) is not None: 28 | cmd1 = commandtuple["cmd1"] 29 | cmd2out = commandtuple["cmd2"] 30 | else: 31 | raise ValueError 32 | 33 | if cmd2 is not None: 34 | cmd2out = cmd2 35 | 36 | if cmd2out is None: 37 | raise ValueError 38 | 39 | self._address = Address(address) 40 | self._messageFlags = MessageFlags(flags) 41 | self._messageFlags.extended = 0 42 | self._cmd1 = cmd1 43 | self._cmd2 = cmd2out 44 | 45 | self._acknak = self._setacknak(acknak) 46 | 47 | @classmethod 48 | def from_raw_message(cls, rawmessage): 49 | """Create a message from a raw byte stream.""" 50 | if (rawmessage[5] & MESSAGE_FLAG_EXTENDED_0X10) == MESSAGE_FLAG_EXTENDED_0X10: 51 | if len(rawmessage) >= ExtendedSend.receivedSize: 52 | msg = ExtendedSend.from_raw_message(rawmessage) 53 | else: 54 | msg = None 55 | else: 56 | msg = StandardSend( 57 | rawmessage[2:5], 58 | {"cmd1": rawmessage[6], "cmd2": rawmessage[7]}, 59 | flags=rawmessage[5], 60 | acknak=rawmessage[8:9], 61 | ) 62 | return msg 63 | 64 | # pylint: disable=protected-access 65 | @classmethod 66 | def template( 67 | cls, address=None, commandtuple=None, cmd2=-1, flags=None, acknak=None 68 | ): 69 | """Create a message template for use in callbacks.""" 70 | msgraw = bytearray([0x02, cls._code]) 71 | msgraw.extend(bytes(cls._receivedSize)) 72 | msg = StandardSend.from_raw_message(msgraw) 73 | 74 | if commandtuple: 75 | cmd1 = commandtuple.get("cmd1") 76 | cmd2out = commandtuple.get("cmd2") 77 | else: 78 | cmd1 = None 79 | cmd2out = None 80 | 81 | if cmd2 is not -1: 82 | cmd2out = cmd2 83 | 84 | msg._address = Address(address) 85 | msg._messageFlags = MessageFlags(flags) 86 | msg._cmd1 = cmd1 87 | msg._cmd2 = cmd2out 88 | msg._acknak = acknak 89 | return msg 90 | 91 | @property 92 | def address(self): 93 | """Return the address of the device.""" 94 | return self._address 95 | 96 | @property 97 | def cmd1(self): 98 | """Return cmd1 property of the message.""" 99 | return self._cmd1 100 | 101 | @property 102 | def cmd2(self): 103 | """Return cmd2 property of the message.""" 104 | return self._cmd2 105 | 106 | @property 107 | def flags(self): 108 | """Return the message flags.""" 109 | return self._messageFlags 110 | 111 | @property 112 | def acknak(self): 113 | """Return the ACK/NAK message flag.""" 114 | return self._acknak 115 | 116 | @acknak.setter 117 | def acknak(self, val): 118 | """Set the ACK/NAK byte.""" 119 | if val in [None, 0x06, 0x15]: 120 | self._acknak = val 121 | else: 122 | raise ValueError 123 | 124 | @property 125 | def isack(self): 126 | """Test if the message is a message ACK.""" 127 | return self._acknak is not None and self._acknak == MESSAGE_ACK 128 | 129 | @property 130 | def isnak(self): 131 | """Test if the message is a message NAK.""" 132 | return self._acknak is not None and self._acknak == MESSAGE_NAK 133 | 134 | def _message_properties(self): 135 | return [ 136 | {"address": self._address}, 137 | {"flags": self._messageFlags}, 138 | {"cmd1": self._cmd1}, 139 | {"cmd2": self._cmd2}, 140 | {"acknak": self._acknak}, 141 | ] 142 | -------------------------------------------------------------------------------- /insteonplm/messages/startAllLinking.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message Start All-Linking.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_START_ALL_LINKING_0X64, 5 | MESSAGE_START_ALL_LINKING_SIZE, 6 | MESSAGE_START_ALL_LINKING_RECEIVED_SIZE, 7 | MESSAGE_ACK, 8 | MESSAGE_NAK, 9 | ) 10 | 11 | 12 | class StartAllLinking(Message): 13 | """Insteon Start All Linking Message. 14 | 15 | Message type 0x64 16 | """ 17 | 18 | _code = MESSAGE_START_ALL_LINKING_0X64 19 | _sendSize = MESSAGE_START_ALL_LINKING_SIZE 20 | _receivedSize = MESSAGE_START_ALL_LINKING_RECEIVED_SIZE 21 | _description = "Insteon Start All Linking Message" 22 | 23 | def __init__(self, linkCode, group, acknak=None): 24 | """Init the StartAllLinking Class.""" 25 | self._linkCode = linkCode 26 | self._group = group 27 | 28 | self._acknak = self._setacknak(acknak) 29 | 30 | @classmethod 31 | def from_raw_message(cls, rawmessage): 32 | """Create message from raw byte stream.""" 33 | return StartAllLinking(rawmessage[2], rawmessage[3], rawmessage[4:5]) 34 | 35 | @property 36 | def linkCode(self): 37 | """Return the link code.""" 38 | return self._linkCode 39 | 40 | @property 41 | def group(self): 42 | """Return the All-Link group.""" 43 | return self._group 44 | 45 | @property 46 | def acknak(self): 47 | """Return the ACK/NAK byte.""" 48 | return self._acknak 49 | 50 | @property 51 | def isack(self): 52 | """Test if this is an ACK message.""" 53 | return self._acknak is not None and self._acknak == MESSAGE_ACK 54 | 55 | @property 56 | def isnak(self): 57 | """Test if this is a NAK message.""" 58 | return self._acknak is not None and self._acknak == MESSAGE_NAK 59 | 60 | def _message_properties(self): 61 | return [ 62 | {"linkCode": self._linkCode}, 63 | {"group": self._group}, 64 | {"acknak": self._acknak}, 65 | ] 66 | -------------------------------------------------------------------------------- /insteonplm/messages/userReset.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message User Reset.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_USER_RESET_DETECTED_0X55, 5 | MESSAGE_USER_RESET_DETECTED_SIZE, 6 | ) 7 | 8 | 9 | class UserReset(Message): 10 | """Insteon User Reset Message Received. 11 | 12 | Message type 0x55 13 | """ 14 | 15 | _code = MESSAGE_USER_RESET_DETECTED_0X55 16 | _sendSize = MESSAGE_USER_RESET_DETECTED_SIZE 17 | _receivedSize = MESSAGE_USER_RESET_DETECTED_SIZE 18 | _description = "INSTEON User Reset Message Received" 19 | 20 | # pylint: disable=unused-argument 21 | @classmethod 22 | def from_raw_messsage(cls, rawmessage): 23 | """Create message from raw byte stream.""" 24 | return UserReset() 25 | 26 | def _message_properties(self): 27 | return [] 28 | -------------------------------------------------------------------------------- /insteonplm/messages/userdata.py: -------------------------------------------------------------------------------- 1 | """Extended Message User Data Type.""" 2 | import logging 3 | import binascii 4 | 5 | _LOGGER = logging.getLogger(__name__) 6 | 7 | 8 | class Userdata: 9 | """Extended Message User Data Type.""" 10 | 11 | def __init__(self, userdata=None): 12 | """Init the Userdata Class.""" 13 | self._userdata = self.normalize(self.create_empty(0x00), userdata) 14 | 15 | def __len__(self): 16 | """Init Userdata Class.""" 17 | return len(self._userdata) 18 | 19 | def __str__(self): 20 | """Return string representation of user data.""" 21 | return self.human 22 | 23 | def __iter__(self): 24 | """Iterate through the user data bytes.""" 25 | for itm in self._userdata: 26 | yield itm 27 | 28 | def __getitem__(self, key): 29 | """Return a single byte of the user data.""" 30 | return self._userdata.get(key) 31 | 32 | def __setitem__(self, key, val): 33 | """Set a user data element.""" 34 | self._userdata[key] = val 35 | 36 | def __eq__(self, other): 37 | """Test if the current user data equals another user data instance.""" 38 | isequal = False 39 | if isinstance(other, Userdata): 40 | for key in self._userdata: 41 | if self._userdata[key] == other[key]: 42 | isequal = True 43 | else: 44 | isequal = False 45 | break 46 | return isequal 47 | 48 | def __ne__(self, other): 49 | """Test if the current user data is not equal to another instance.""" 50 | return bool(self != other) 51 | 52 | @property 53 | def human(self): 54 | """Emit the address in human-readible format (AA.BB.CC).""" 55 | strout = "" 56 | first = True 57 | for i in range(0, 28, 2): 58 | if first: 59 | first = False 60 | else: 61 | strout = strout + "." 62 | strout = strout + self.hex[i : i + 2] 63 | return strout 64 | 65 | @property 66 | def hex(self): 67 | """Emit the address in bare hex format.""" 68 | return binascii.hexlify(self.bytes).decode() 69 | 70 | @property 71 | def bytes(self): 72 | """Emit the address in bytes format.""" 73 | byteout = bytearray() 74 | for i in range(1, 15): 75 | key = "d" + str(i) 76 | if self._userdata[key] is not None: 77 | byteout.append(self._userdata[key]) 78 | else: 79 | byteout.append(0x00) 80 | return byteout 81 | 82 | @classmethod 83 | def from_raw_message(cls, rawmessage): 84 | """Create a user data instance from a raw byte stream.""" 85 | empty = cls.create_empty(0x00) 86 | userdata_dict = cls.normalize(empty, rawmessage) 87 | return Userdata(userdata_dict) 88 | 89 | @classmethod 90 | def create_pattern(cls, userdata): 91 | """Create a user data instance with all values the same.""" 92 | empty = cls.create_empty(None) 93 | userdata_dict = cls.normalize(empty, userdata) 94 | return Userdata(userdata_dict) 95 | 96 | @classmethod 97 | def create(cls): 98 | """Create an empty user data instance.""" 99 | empty = cls.create_empty(0x00) 100 | return Userdata(empty) 101 | 102 | @classmethod 103 | def template(cls, userdata): 104 | """Create a template instance used for message callbacks.""" 105 | ud = Userdata(cls.normalize(cls.create_empty(None), userdata)) 106 | return ud 107 | 108 | def matches_pattern(self, other): 109 | """Test if the current instance matches a template instance.""" 110 | ismatch = False 111 | if isinstance(other, Userdata): 112 | for key in self._userdata: 113 | if self._userdata[key] is None or other[key] is None: 114 | ismatch = True 115 | elif self._userdata[key] == other[key]: 116 | ismatch = True 117 | else: 118 | ismatch = False 119 | break 120 | return ismatch 121 | 122 | def get(self, key): 123 | """Return a single byte of the user data.""" 124 | return self[key] 125 | 126 | def to_dict(self): 127 | """Return userdata as a dict object.""" 128 | return self._userdata 129 | 130 | @classmethod 131 | def _dict_to_dict(cls, empty, userdata): 132 | if isinstance(userdata, dict): 133 | for key in userdata: 134 | if key in [ 135 | "d1", 136 | "d2", 137 | "d3", 138 | "d4", 139 | "d5", 140 | "d6", 141 | "d7", 142 | "d8", 143 | "d9", 144 | "d10", 145 | "d11", 146 | "d12", 147 | "d13", 148 | "d14", 149 | ]: 150 | empty[key] = userdata[key] 151 | return empty 152 | 153 | @classmethod 154 | def _bytes_to_dict(cls, empty, userdata): 155 | if len(userdata) == 14: 156 | for i in range(1, 15): 157 | key = "d{}".format(i) 158 | empty[key] = userdata[i - 1] 159 | else: 160 | raise ValueError 161 | return empty 162 | 163 | @classmethod 164 | def create_empty(cls, val=0x00): 165 | """Create an empty Userdata object. 166 | 167 | val: value to fill the empty user data fields with (default is 0x00) 168 | """ 169 | userdata_dict = {} 170 | for i in range(1, 15): 171 | key = "d{}".format(i) 172 | userdata_dict.update({key: val}) 173 | return userdata_dict 174 | 175 | @classmethod 176 | def normalize(cls, empty, userdata): 177 | """Return normalized user data as a dictionary. 178 | 179 | empty: an empty dictionary 180 | userdata: data in the form of Userdata, dict or None 181 | """ 182 | if isinstance(userdata, Userdata): 183 | return userdata.to_dict() 184 | if isinstance(userdata, dict): 185 | return cls._dict_to_dict(empty, userdata) 186 | if isinstance(userdata, (bytes, bytearray)): 187 | return cls._bytes_to_dict(empty, userdata) 188 | if userdata is None: 189 | return empty 190 | raise ValueError 191 | -------------------------------------------------------------------------------- /insteonplm/messages/x10received.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message X10 Received.""" 2 | from insteonplm.messages.message import Message 3 | from insteonplm.constants import ( 4 | MESSAGE_X10_MESSAGE_RECEIVED_0X52, 5 | MESSAGE_X10_MESSAGE_RECEIVED_SIZE, 6 | ) 7 | import insteonplm.utils 8 | 9 | 10 | class X10Received(Message): 11 | """Insteon X10 Received Message. 12 | 13 | Message type 0x52 14 | """ 15 | 16 | _code = MESSAGE_X10_MESSAGE_RECEIVED_0X52 17 | _sendSize = MESSAGE_X10_MESSAGE_RECEIVED_SIZE 18 | _receivedSize = MESSAGE_X10_MESSAGE_RECEIVED_SIZE 19 | _description = "Insteon Get Next All Link Record Message" 20 | 21 | def __init__(self, rawX10, flag): 22 | """Init X10Received Class.""" 23 | self._rawX10 = rawX10 24 | self._flag = flag 25 | 26 | @classmethod 27 | def from_raw_message(cls, rawmessage): 28 | """Create from raw byte stream.""" 29 | return X10Received(rawmessage[2], rawmessage[3]) 30 | 31 | @property 32 | def rawX10(self): 33 | """Return raw X10 message.""" 34 | return self._rawX10 35 | 36 | @property 37 | def flag(self): 38 | """Return X10 flag.""" 39 | return self._flag 40 | 41 | @staticmethod 42 | def unit_code_msg(housecode, unitcode): 43 | """Create an X10 message to send the house code and unit code.""" 44 | house_byte = 0 45 | unit_byte = 0 46 | if isinstance(housecode, str): 47 | house_byte = insteonplm.utils.housecode_to_byte(housecode) << 4 48 | unit_byte = insteonplm.utils.unitcode_to_byte(unitcode) 49 | elif isinstance(housecode, int) and housecode < 16: 50 | house_byte = housecode << 4 51 | unit_byte = unitcode 52 | else: 53 | house_byte = housecode 54 | unit_byte = unitcode 55 | return X10Received(house_byte + unit_byte, 0x00) 56 | 57 | @staticmethod 58 | def command_msg(housecode, command): 59 | """Create an X10 message to send the house code and a command code.""" 60 | house_byte = 0 61 | if isinstance(housecode, str): 62 | house_byte = insteonplm.utils.housecode_to_byte(housecode) << 4 63 | elif isinstance(housecode, int) and housecode < 16: 64 | house_byte = housecode << 4 65 | else: 66 | house_byte = housecode 67 | return X10Received(house_byte + command, 0x80) 68 | 69 | def _message_properties(self): 70 | return [{"rawX10": self._rawX10}, {"flag": self._flag}] 71 | -------------------------------------------------------------------------------- /insteonplm/messages/x10send.py: -------------------------------------------------------------------------------- 1 | """INSTEON Message X10 Send.""" 2 | from insteonplm.constants import ( 3 | MESSAGE_X10_MESSAGE_SEND_0X63, 4 | MESSAGE_X10_MESSAGE_SEND_SIZE, 5 | MESSAGE_X10_MESSAGE_SEND_RECEIVED_SIZE, 6 | MESSAGE_ACK, 7 | MESSAGE_NAK, 8 | ) 9 | from insteonplm.messages.message import Message 10 | import insteonplm.utils 11 | 12 | 13 | class X10Send(Message): 14 | """Insteon Get Next All Link Record Message. 15 | 16 | Message type 0x6A 17 | """ 18 | 19 | _code = MESSAGE_X10_MESSAGE_SEND_0X63 20 | _sendSize = MESSAGE_X10_MESSAGE_SEND_SIZE 21 | _receivedSize = MESSAGE_X10_MESSAGE_SEND_RECEIVED_SIZE 22 | _description = "Insteon Get Next All Link Record Message" 23 | 24 | def __init__(self, rawX10, flag, acknak=None): 25 | """Init the X10Send Class.""" 26 | self._rawX10 = rawX10 27 | self._flag = flag 28 | self._acknak = self._setacknak(acknak) 29 | 30 | @staticmethod 31 | def from_raw_message(rawmessage): 32 | """Create message from raw byte stream.""" 33 | return X10Send(rawmessage[2], rawmessage[3], rawmessage[4:5]) 34 | 35 | @staticmethod 36 | def unit_code_msg(housecode, unitcode): 37 | """Create an X10 message to send the house code and unit code.""" 38 | house_byte = 0 39 | unit_byte = 0 40 | if isinstance(housecode, str): 41 | house_byte = insteonplm.utils.housecode_to_byte(housecode) << 4 42 | unit_byte = insteonplm.utils.unitcode_to_byte(unitcode) 43 | elif isinstance(housecode, int) and housecode < 16: 44 | house_byte = housecode << 4 45 | unit_byte = unitcode 46 | else: 47 | house_byte = housecode 48 | unit_byte = unitcode 49 | return X10Send(house_byte + unit_byte, 0x00) 50 | 51 | @staticmethod 52 | def command_msg(housecode, command): 53 | """Create an X10 message to send the house code and a command code.""" 54 | house_byte = 0 55 | if isinstance(housecode, str): 56 | house_byte = insteonplm.utils.housecode_to_byte(housecode) << 4 57 | elif isinstance(housecode, int) and housecode < 16: 58 | house_byte = housecode << 4 59 | else: 60 | house_byte = housecode 61 | return X10Send(house_byte + command, 0x80) 62 | 63 | @property 64 | def rawX10(self): 65 | """Return the raw x10 bytes.""" 66 | return self._rawX10 67 | 68 | @property 69 | def flag(self): 70 | """Return the X10 flag.""" 71 | return self._flag 72 | 73 | @property 74 | def acknak(self): 75 | """Return the ACK/NAK byte.""" 76 | return self._acknak 77 | 78 | @property 79 | def isack(self): 80 | """Test if this is an ACK message.""" 81 | return self._acknak is not None and self._acknak == MESSAGE_ACK 82 | 83 | @property 84 | def isnak(self): 85 | """Test if this is a NAK message.""" 86 | return self._acknak is not None and self._acknak == MESSAGE_NAK 87 | 88 | def _message_properties(self): 89 | return [ 90 | {"rawX10": self._rawX10}, 91 | {"flag": self._flag}, 92 | {"acknak": self._acknak}, 93 | ] 94 | -------------------------------------------------------------------------------- /insteonplm/states/__init__.py: -------------------------------------------------------------------------------- 1 | """INSTEON State Entities.""" 2 | # pylint: disable=cyclic-import 3 | import logging 4 | from insteonplm.address import Address 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | 9 | # pylint: disable=too-many-instance-attributes 10 | class State: 11 | """INSTEON device state base class. 12 | 13 | Base class used by Insteon devices to hold a device state such as "Light On 14 | Level", "Temperature" or "Fan Mode". 15 | 16 | The class is defined with the following options: 17 | statename: Required text name of the state, such as "LightOnLevel". 18 | This value is returned when an async request is made to 19 | update the state value. 20 | updatemethod: Required callback method where callback is defined as: 21 | callback(self) 22 | defaultvalue: Optional parameter to set the default value of the state. 23 | 24 | The following public properties are available: 25 | 26 | name - Text name for the device state. 27 | value - Cached value of the state value. If this value is None, referencing 28 | this property forces an udpate by calling async_refresh_state 29 | 30 | The following public methods are available: 31 | 32 | register_updates(self, callback) - Used to monitor changes to the state of 33 | the device. This method registers a callback for notifiction of state 34 | changes eg.: 35 | - device.state[0].register_updates(callback) 36 | where callback defined as: 37 | - callback(self, device_id, stateName, state_value) 38 | 39 | async_refresh_state(self) - called by a device or a subscriber to force an 40 | update to the state value 41 | """ 42 | 43 | def __init__( 44 | self, 45 | address, 46 | statename, 47 | group, 48 | send_message_method, 49 | message_callbacks, 50 | defaultvalue=None, 51 | ): 52 | """Initialzie tthe State Class.""" 53 | self._address = Address(address) 54 | self._observer_callbacks = [] 55 | self._stateName = statename 56 | self._group = group 57 | self._value = defaultvalue 58 | self._is_responder = True 59 | self._is_controller = True 60 | self._linkdata1 = 0 61 | self._linkdata2 = 0 62 | self._linkdata3 = 0 63 | 64 | self._updatemethod = None 65 | self._send_method = send_message_method 66 | self._message_callbacks = message_callbacks 67 | 68 | @property 69 | def value(self): 70 | """Return the value of the state.""" 71 | if self._value is None: 72 | self.async_refresh_state() 73 | return self._value 74 | 75 | @property 76 | def name(self): 77 | """Return the name of the state.""" 78 | return self._stateName 79 | 80 | @property 81 | def address(self): 82 | """Return the device address.""" 83 | return self._address 84 | 85 | @property 86 | def group(self): 87 | """Return the link group of the state.""" 88 | return self._group 89 | 90 | @property 91 | def is_responder(self): 92 | """Return if this state responds to a controller.""" 93 | return self._is_responder 94 | 95 | @property 96 | def is_controller(self): 97 | """Return if this state controls a responder.""" 98 | return self._is_controller 99 | 100 | @property 101 | def linkdata1(self): 102 | """Return the default link data value as a responder.""" 103 | return self._linkdata1 104 | 105 | @property 106 | def linkdata2(self): 107 | """Return the default link data value as a responder.""" 108 | return self._linkdata2 109 | 110 | @property 111 | def linkdata3(self): 112 | """Return the default link data value as a responder.""" 113 | return self._linkdata3 114 | 115 | def async_refresh_state(self): 116 | """Call the update method to request current state value.""" 117 | if self._updatemethod is not None: 118 | # pylint: disable=not-callable 119 | self._updatemethod() 120 | 121 | def register_updates(self, callback): 122 | """Register a callback to notify a listener of state changes.""" 123 | _LOGGER.debug("Registered callback for state: %s", self._stateName) 124 | self._observer_callbacks.append(callback) 125 | 126 | def _update_subscribers(self, val): 127 | """Save state value and notify listeners of the change.""" 128 | self._value = val 129 | for callback in self._observer_callbacks: 130 | callback(self._address, self._group, val) 131 | -------------------------------------------------------------------------------- /insteonplm/states/cover.py: -------------------------------------------------------------------------------- 1 | """Window Coverings states.""" 2 | import logging 3 | 4 | from insteonplm.constants import ( 5 | COMMAND_LIGHT_INSTANT_CHANGE_0X21_NONE, 6 | COMMAND_LIGHT_MANUALLY_TURNED_OFF_0X22_0X00, 7 | COMMAND_LIGHT_MANUALLY_TURNED_ON_0X23_0X00, 8 | COMMAND_LIGHT_OFF_0X13_0X00, 9 | COMMAND_LIGHT_OFF_FAST_0X14_0X00, 10 | COMMAND_LIGHT_ON_0X11_NONE, 11 | COMMAND_LIGHT_ON_FAST_0X12_NONE, 12 | COMMAND_LIGHT_STATUS_REQUEST_0X19_0X00, 13 | COMMAND_LIGHT_STOP_MANUAL_CHANGE_0X18_0X00, 14 | MESSAGE_TYPE_ALL_LINK_BROADCAST, 15 | ) 16 | from insteonplm.messages.standardSend import StandardSend 17 | from insteonplm.messages.standardReceive import StandardReceive 18 | from insteonplm.messages.messageFlags import MessageFlags 19 | from insteonplm.states import State 20 | 21 | _LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | class Cover(State): 25 | """Device state representing cover that is controllable. 26 | 27 | Available methods are: 28 | open() 29 | close() 30 | set_position() 31 | async_refresh_state() 32 | """ 33 | 34 | def __init__( 35 | self, 36 | address, 37 | statename, 38 | group, 39 | send_message_method, 40 | message_callbacks, 41 | defaultvalue=None, 42 | ): 43 | """Init the Cover Class.""" 44 | super().__init__( 45 | address, 46 | statename, 47 | group, 48 | send_message_method, 49 | message_callbacks, 50 | defaultvalue, 51 | ) 52 | 53 | self._updatemethod = self._send_status_request 54 | self._register_messages() 55 | 56 | def _register_messages(self): 57 | _LOGGER.debug("Registering callbacks for Cover device %s", self._address.human) 58 | template_on_broadcast = StandardReceive.template( 59 | commandtuple=COMMAND_LIGHT_ON_0X11_NONE, 60 | address=self._address, 61 | target=bytearray([0x00, 0x00, self._group]), 62 | flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_BROADCAST, None), 63 | ) 64 | template_on_fast_broadcast = StandardReceive.template( 65 | commandtuple=COMMAND_LIGHT_ON_FAST_0X12_NONE, 66 | address=self._address, 67 | target=bytearray([0x00, 0x00, self._group]), 68 | flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_BROADCAST, None), 69 | ) 70 | template_off_broadcast = StandardReceive.template( 71 | commandtuple=COMMAND_LIGHT_OFF_0X13_0X00, 72 | address=self._address, 73 | target=bytearray([0x00, 0x00, self._group]), 74 | flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_BROADCAST, None), 75 | cmd2=None, 76 | ) 77 | template_off_fast_broadcast = StandardReceive.template( 78 | commandtuple=COMMAND_LIGHT_OFF_FAST_0X14_0X00, 79 | address=self._address, 80 | target=bytearray([0x00, 0x00, self._group]), 81 | flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_BROADCAST, None), 82 | cmd2=None, 83 | ) 84 | template_manual_broadcast = StandardReceive.template( 85 | commandtuple=COMMAND_LIGHT_STOP_MANUAL_CHANGE_0X18_0X00, 86 | address=self._address, 87 | target=bytearray([0x00, 0x00, self._group]), 88 | flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_BROADCAST, None), 89 | cmd2=None, 90 | ) 91 | template_instant_broadcast = StandardReceive.template( 92 | commandtuple=COMMAND_LIGHT_INSTANT_CHANGE_0X21_NONE, 93 | address=self._address, 94 | target=bytearray([0x00, 0x00, self._group]), 95 | flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_BROADCAST, None), 96 | cmd2=None, 97 | ) 98 | template_manual_off_broadcast = StandardReceive.template( 99 | commandtuple=COMMAND_LIGHT_MANUALLY_TURNED_OFF_0X22_0X00, 100 | address=self._address, 101 | target=bytearray([0x00, 0x00, self._group]), 102 | flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_BROADCAST, None), 103 | cmd2=None, 104 | ) 105 | template_manual_on_broadcast = StandardReceive.template( 106 | commandtuple=COMMAND_LIGHT_MANUALLY_TURNED_ON_0X23_0X00, 107 | address=self._address, 108 | target=bytearray([0x00, 0x00, self._group]), 109 | flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_BROADCAST, None), 110 | cmd2=None, 111 | ) 112 | 113 | self._message_callbacks.add(template_on_broadcast, self._open_message_received) 114 | self._message_callbacks.add( 115 | template_on_fast_broadcast, self._open_message_received 116 | ) 117 | self._message_callbacks.add( 118 | template_off_broadcast, self._closed_message_received 119 | ) 120 | self._message_callbacks.add( 121 | template_off_fast_broadcast, self._closed_message_received 122 | ) 123 | self._message_callbacks.add( 124 | template_manual_broadcast, self._manual_change_received 125 | ) 126 | self._message_callbacks.add( 127 | template_instant_broadcast, self._manual_change_received 128 | ) 129 | self._message_callbacks.add( 130 | template_manual_off_broadcast, self._manual_change_received 131 | ) 132 | self._message_callbacks.add( 133 | template_manual_on_broadcast, self._manual_change_received 134 | ) 135 | 136 | def open(self): 137 | """Turn the device ON.""" 138 | open_command = StandardSend( 139 | self._address, COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xFF 140 | ) 141 | self._send_method(open_command, self._open_message_received) 142 | 143 | def open_fast(self): 144 | """Turn the device ON Fast.""" 145 | open_command = StandardSend( 146 | self._address, COMMAND_LIGHT_ON_FAST_0X12_NONE, cmd2=0xFF 147 | ) 148 | self._send_method(open_command, self._open_message_received) 149 | 150 | def close(self): 151 | """Turn the device off.""" 152 | close_command = StandardSend(self._address, COMMAND_LIGHT_OFF_0X13_0X00) 153 | self._send_method(close_command, self._closed_message_received) 154 | 155 | def close_fast(self): 156 | """Turn the device off.""" 157 | close_command = StandardSend(self._address, COMMAND_LIGHT_OFF_FAST_0X14_0X00) 158 | self._send_method(close_command, self._closed_message_received) 159 | 160 | def set_position(self, val): 161 | """Set the devive OPEN LEVEL.""" 162 | if val == 0: 163 | self.close() 164 | else: 165 | setlevel = 255 166 | if val < 1: 167 | setlevel = val * 100 168 | elif val <= 0xFF: 169 | setlevel = val 170 | set_command = StandardSend( 171 | self._address, COMMAND_LIGHT_ON_0X11_NONE, cmd2=setlevel 172 | ) 173 | self._send_method(set_command, self._open_message_received) 174 | 175 | def set_position_fast(self, val): 176 | """Set the devive OPEN LEVEL.""" 177 | if val == 0: 178 | self.close_fast() 179 | else: 180 | setlevel = 255 181 | if val < 1: 182 | setlevel = val * 100 183 | elif val <= 0xFF: 184 | setlevel = val 185 | set_command = StandardSend( 186 | self._address, COMMAND_LIGHT_ON_FAST_0X12_NONE, cmd2=setlevel 187 | ) 188 | self._send_method(set_command, self._open_message_received) 189 | 190 | def _open_message_received(self, msg): 191 | cmd2 = msg.cmd2 if msg.cmd2 else 255 192 | self._update_subscribers(cmd2) 193 | 194 | # pylint: disable=unused-argument 195 | def _closed_message_received(self, msg): 196 | self._update_subscribers(0x00) 197 | 198 | # pylint: disable=unused-argument 199 | def _manual_change_received(self, msg): 200 | self._send_status_request() 201 | 202 | def _send_status_request(self): 203 | status_command = StandardSend( 204 | self._address, COMMAND_LIGHT_STATUS_REQUEST_0X19_0X00 205 | ) 206 | self._send_method(status_command, self._status_message_received) 207 | 208 | def _status_message_received(self, msg): 209 | _LOGGER.debug("Cover status message received called") 210 | self._update_subscribers(msg.cmd2) 211 | -------------------------------------------------------------------------------- /insteonplm/states/statusReport.py: -------------------------------------------------------------------------------- 1 | """Status Reporting State.""" 2 | 3 | from insteonplm.states import State 4 | 5 | 6 | class StatusReport(State): 7 | """Dummy state to allow linking to a required group.""" 8 | -------------------------------------------------------------------------------- /insteonplm/utils.py: -------------------------------------------------------------------------------- 1 | """Utility methods.""" 2 | 3 | 4 | from insteonplm.constants import ( 5 | HC_LOOKUP, 6 | UC_LOOKUP, 7 | X10_COMMAND_ALL_UNITS_OFF, 8 | X10_COMMAND_ALL_LIGHTS_ON, 9 | X10_COMMAND_ALL_LIGHTS_OFF, 10 | X10CommandType, 11 | ) 12 | 13 | 14 | def housecode_to_byte(housecode): 15 | """Return the byte value of an X10 housecode.""" 16 | return HC_LOOKUP.get(housecode.lower()) 17 | 18 | 19 | def unitcode_to_byte(unitcode): 20 | """Return the byte value of an X10 unitcode.""" 21 | return UC_LOOKUP.get(unitcode) 22 | 23 | 24 | def byte_to_housecode(bytecode): 25 | """Return an X10 housecode value from a byte value.""" 26 | hc = list(HC_LOOKUP.keys())[list(HC_LOOKUP.values()).index(bytecode)] 27 | return hc.upper() 28 | 29 | 30 | def byte_to_unitcode(bytecode): 31 | """Return an X10 unitcode value from a byte value.""" 32 | return list(UC_LOOKUP.keys())[list(UC_LOOKUP.values()).index(bytecode)] 33 | 34 | 35 | def x10_command_type(command): 36 | """Return the X10 command type from an X10 command.""" 37 | command_type = X10CommandType.DIRECT 38 | if command in [ 39 | X10_COMMAND_ALL_UNITS_OFF, 40 | X10_COMMAND_ALL_LIGHTS_ON, 41 | X10_COMMAND_ALL_LIGHTS_OFF, 42 | ]: 43 | command_type = X10CommandType.BROADCAST 44 | return command_type 45 | 46 | 47 | def rawX10_to_bytes(rawX10): 48 | """Return the byte value of a raw X10 command.""" 49 | yield rawX10 >> 4 50 | yield rawX10 & 0x0F 51 | 52 | 53 | def bit_is_set(bitmask, bit): 54 | """Return True if a specific bit is set in a bitmask. 55 | 56 | Uses the low bit is 1 and the high bit is 8. 57 | """ 58 | bitshift = bit - 1 59 | return bool(bitmask & (1 << bitshift)) 60 | 61 | 62 | def set_bit(bitmask, bit, is_on): 63 | """Set the value of a bit in a bitmask on or off. 64 | 65 | Uses the low bit is 1 and the high bit is 8. 66 | """ 67 | bitshift = bit - 1 68 | if is_on: 69 | return bitmask | (1 << bitshift) 70 | return bitmask & (0xFF & ~(1 << bitshift)) 71 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyserial==3.2.0 2 | pyserial-asyncio 3 | async_timeout 4 | aiohttp 5 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | # linters such as flake8 and pylint should be pinned, as new releases 2 | # make new things fail. Manually update these pins when pulling in a 3 | # new version 4 | flake8-docstrings==1.5.0 5 | flake8==3.7.9 6 | pydocstyle==5.0.2 7 | pylint==2.4.4 8 | mock-open==1.3.1 9 | mypy==0.761 10 | asynctest==0.13.0 11 | pytest-aiohttp==0.3.0 12 | pytest-cov==2.8.1 13 | pytest-sugar==0.9.2 14 | pytest-timeout==1.3.1 15 | pytest==5.3.3 16 | requests_mock==1.7.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license = MIT License 3 | license_file = LICENSE 4 | platforms = any 5 | description = Open-source Insteon library running on Python 3. 6 | long_description = file: README.rst 7 | keywords = home, automation, insteon 8 | classifier = 9 | Development Status :: 4 - Beta 10 | Intended Audience :: Developers 11 | License :: OSI Approved :: MIT License 12 | Operating System :: OS Independent 13 | Programming Language :: Python :: 3.5 14 | Programming Language :: Python :: 3.6 15 | Programming Language :: Python :: 3.7 16 | Topic :: Home Automation 17 | Topic :: Software Development :: Libraries :: Python Modules 18 | 19 | [tool:pytest] 20 | testpaths = tests 21 | norecursedirs = .git testing_config 22 | 23 | [flake8] 24 | ignore: D401 25 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Setup for insteonplm module.""" 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def readme(): 7 | """Return README file as a string.""" 8 | with open('README.rst', 'r') as f: 9 | return f.read() 10 | 11 | 12 | setup( 13 | name='insteonplm', 14 | version='0.16.8', 15 | author='David McNett', 16 | author_email='nugget@macnugget.org', 17 | url='https://github.com/nugget/python-insteonplm', 18 | license="MIT License", 19 | packages=find_packages(), 20 | scripts=[], 21 | description='Python API for controlling Insteon PowerLinc Modems', 22 | long_description=readme(), 23 | include_package_data=True, 24 | zip_safe=True, 25 | install_requires=[ 26 | 'pyserial==3.2.0', 27 | 'pyserial-asyncio', 28 | 'async_timeout', 29 | 'aiohttp' 30 | ], 31 | entry_points={ 32 | 'console_scripts': ['insteonplm_monitor = insteonplm.tools:monitor', 33 | 'insteonplm_interactive = ' 34 | 'insteonplm.tools:interactive'] 35 | } 36 | ) 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for insteonplm module.""" 2 | import logging 3 | _LOGGER = logging.getLogger(__name__) 4 | _INSTEONPLM_LOGGER = logging.getLogger('insteonplm') 5 | _INSTEONPLM_LOGGER.setLevel(logging.DEBUG) 6 | -------------------------------------------------------------------------------- /tests/mockCallbacks.py: -------------------------------------------------------------------------------- 1 | """Mock callback module to support device and state testing.""" 2 | import logging 3 | 4 | _LOGGER = logging.getLogger(__name__) 5 | 6 | 7 | # pylint: disable=unused-argument 8 | # pylint: disable=too-many-instance-attributes 9 | class MockCallbacks(): 10 | """Mock callback class to support device and state testing.""" 11 | 12 | def __init__(self): 13 | """Init the MockCallbacks Class.""" 14 | self.callbackvalue1 = None 15 | self.callbackvalue2 = None 16 | self.callbackvalue3 = None 17 | self.callbackvalue4 = None 18 | self.callbackvalue5 = None 19 | self.callbackvalue6 = None 20 | self.callbackvalue7 = None 21 | self.callbackvalue8 = None 22 | self.callbackvalue9 = None 23 | 24 | def callbackmethod1(self, addr, state, value): 25 | """Receive notice of callback method 1.""" 26 | self._report_callback(1, addr, state, value) 27 | self.callbackvalue1 = value 28 | 29 | def callbackmethod2(self, addr, state, value): 30 | """Receive notice of callback method 2.""" 31 | self._report_callback(2, addr, state, value) 32 | self.callbackvalue2 = value 33 | 34 | def callbackmethod3(self, addr, state, value): 35 | """Receive notice of callback method 3.""" 36 | self._report_callback(3, addr, state, value) 37 | self.callbackvalue3 = value 38 | 39 | def callbackmethod4(self, addr, state, value): 40 | """Receive notice of callback method 5.""" 41 | self._report_callback(4, addr, state, value) 42 | self.callbackvalue4 = value 43 | 44 | def callbackmethod5(self, addr, state, value): 45 | """Receive notice of callback method 5.""" 46 | self._report_callback(5, addr, state, value) 47 | self.callbackvalue5 = value 48 | 49 | def callbackmethod6(self, addr, state, value): 50 | """Receive notice of callback method 6.""" 51 | self._report_callback(6, addr, state, value) 52 | self.callbackvalue6 = value 53 | 54 | def callbackmethod7(self, addr, state, value): 55 | """Receive notice of callback method 7.""" 56 | self._report_callback(7, addr, state, value) 57 | self.callbackvalue7 = value 58 | 59 | def callbackmethod8(self, addr, state, value): 60 | """Receive notice of callback method 8.""" 61 | self._report_callback(8, addr, state, value) 62 | self.callbackvalue8 = value 63 | 64 | def callbackmethod9(self, addr, state, value): 65 | """Receive notice of callback method 9.""" 66 | _LOGGER.debug('Called method 9 callback') 67 | self.callbackvalue9 = value 68 | 69 | @staticmethod 70 | def _report_callback(callback, addr, state, value): 71 | _LOGGER.debug('Called method %d for address %s group %s value %s', 72 | callback, addr, state, value) 73 | -------------------------------------------------------------------------------- /tests/mockConnection.py: -------------------------------------------------------------------------------- 1 | """Mock Connection for the PLM.""" 2 | 3 | import asyncio 4 | import binascii 5 | import logging 6 | 7 | import async_timeout 8 | 9 | _LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | async def wait_for_plm_command(plm, cmd, loop): 13 | """Wait for a command to hit the PLM.""" 14 | try: 15 | with async_timeout.timeout(10, loop=loop): 16 | while not plm.transport.lastmessage == cmd.hex: 17 | await asyncio.sleep(.1, loop=loop) 18 | _LOGGER.info('Expected message sent %s', cmd) 19 | return True 20 | except asyncio.TimeoutError: 21 | _LOGGER.error('Expected message not sent %s', cmd) 22 | return False 23 | 24 | 25 | # pylint: disable=too-few-public-methods 26 | class MockConnection(): 27 | """A mock up of the Connection class.""" 28 | 29 | def __init__(self): 30 | """Instantiate the Connection object.""" 31 | self.loop = None 32 | self.protocol = None 33 | self.transport = None 34 | 35 | @classmethod 36 | async def create(cls, loop=None): 37 | """Create the MockConnection.""" 38 | from insteonplm.plm import PLM 39 | conn = cls() 40 | conn.loop = loop or asyncio.get_event_loop() 41 | conn.protocol = PLM( 42 | connection_lost_callback=None, 43 | loop=conn.loop) 44 | 45 | # pylint: disable=too-few-public-methods 46 | class Serial: 47 | """Mock serial class within Connection class.""" 48 | 49 | def __init__(self): 50 | """Init the mock Serial class.""" 51 | self.write_timeout = 0 52 | self.timeout = 0 53 | 54 | class Transport: 55 | """Mock transport class within Connection class.""" 56 | 57 | def __init__(self): 58 | """Init the mock Transport class.""" 59 | self.serial = Serial() 60 | self.lastmessage = None 61 | self._mock_buffer_size = 128 62 | 63 | def set_write_buffer_limits(self, num): 64 | """Mock set write buffer limits.""" 65 | pass 66 | 67 | def get_write_buffer_size(self): 68 | """Mock get write buffer size.""" 69 | return self._mock_buffer_size 70 | 71 | def write(self, data): 72 | """Mock write data to the Connection.""" 73 | self.lastmessage = binascii.hexlify(data).decode() 74 | _LOGGER.info('Message sent: %s', self.lastmessage) 75 | 76 | @staticmethod 77 | def is_closing(): 78 | """Return if the Mock Connection is closing.""" 79 | return False 80 | 81 | conn.transport = Transport() 82 | return conn 83 | -------------------------------------------------------------------------------- /tests/mockPLM.py: -------------------------------------------------------------------------------- 1 | """Mock PLM class for testing devices.""" 2 | import logging 3 | from insteonplm.messagecallback import MessageCallback 4 | from insteonplm.linkedDevices import LinkedDevices 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | _LOGGER.setLevel(logging.DEBUG) 8 | 9 | 10 | class MockPLM(): 11 | """Mock PLM class for testing devices.""" 12 | 13 | def __init__(self, loop=None): 14 | """Init the MockPLM class.""" 15 | self.sentmessage = '' 16 | self._message_callbacks = MessageCallback() 17 | self.loop = loop 18 | self.devices = LinkedDevices() 19 | 20 | @property 21 | def message_callbacks(self): 22 | """Return the message callback list.""" 23 | return self._message_callbacks 24 | 25 | # pylint: disable=unused-argument 26 | def send_msg(self, msg, wait_nak=True, wait_timeout=2): 27 | """Send a message mock routine.""" 28 | _LOGGER.debug('TX: %s:%s', id(msg), msg) 29 | self.sentmessage = msg.hex 30 | 31 | def message_received(self, msg): 32 | """Fake a message being received by the PLM.""" 33 | _LOGGER.debug('RX: %s:%s', id(msg), msg) 34 | if hasattr(msg, 'address'): 35 | device = self.devices[msg.address.id] 36 | if device: 37 | device.receive_message(msg) 38 | else: 39 | _LOGGER.info('Received message for unknown device %s', 40 | msg.address) 41 | for callback in ( 42 | self._message_callbacks.get_callbacks_from_message(msg)): 43 | callback(msg) 44 | 45 | # pylint: disable=unused-argument 46 | def start_all_linking(self, linkcode, group): 47 | """Fake start all linking.""" 48 | self.sentmessage = b'02112233445566' 49 | -------------------------------------------------------------------------------- /tests/test_address.py: -------------------------------------------------------------------------------- 1 | """Test insteonplm Address type.""" 2 | from insteonplm.address import Address 3 | 4 | 5 | def test_textstring(): 6 | """Test Address created from text string.""" 7 | addr = Address('1a2b3c') 8 | assert addr.human == '1A.2B.3C' 9 | assert addr.hex == '1a2b3c' 10 | assert addr.bytes == b'\x1a\x2b\x3c' 11 | 12 | 13 | def test_bytearray(): 14 | """Test Address created from bytearray.""" 15 | addr_ba = bytearray([0x1a, 0x2b, 0x3c]) 16 | addr = Address(addr_ba) 17 | assert addr.human == '1A.2B.3C' 18 | assert addr.hex == '1a2b3c' 19 | assert addr.bytes == b'\x1a\x2b\x3c' 20 | 21 | 22 | def test_bytes(): 23 | """Test Address created from bytes string.""" 24 | addr_b = b'\x1a\x2b\x3c' 25 | addr = Address(addr_b) 26 | assert addr.human == '1A.2B.3C' 27 | assert addr.hex == '1a2b3c' 28 | assert addr.bytes == b'\x1a\x2b\x3c' 29 | 30 | 31 | def test_none(): 32 | """Test Address equal to None.""" 33 | addr = Address(None) 34 | assert addr.human == '00.00.00' 35 | assert addr.hex == '000000' 36 | assert addr.bytes == b'\x00\x00\x00' 37 | 38 | 39 | def test_eq(): 40 | """Test Addresses pattern matching.""" 41 | addr1 = Address(None) 42 | addr2 = Address('1a2b3c') 43 | addr3 = Address('4d5e6f') 44 | 45 | assert addr1.matches_pattern(addr2) 46 | assert addr3.matches_pattern(addr1) 47 | assert not addr2.matches_pattern(addr3) 48 | assert addr2.matches_pattern(addr2) 49 | 50 | 51 | def test_x10(): 52 | """Test X10 device address.""" 53 | addr = Address.x10('A', 5) 54 | 55 | assert addr.hex == '000601' 56 | assert addr.human == 'X10.A.05' 57 | assert addr.is_x10 58 | assert addr.x10_housecode == 'A' 59 | assert addr.x10_unitcode == 5 60 | assert addr.x10_housecode_byte == 6 61 | assert addr.x10_unitcode_byte == 1 62 | 63 | addr2 = Address.x10('A', 20) 64 | assert addr2.human == 'X10.A.20' 65 | -------------------------------------------------------------------------------- /tests/test_aldb.py: -------------------------------------------------------------------------------- 1 | """Test insteonplm.devices.ALDB class.""" 2 | from insteonplm.devices import ControlFlags 3 | 4 | 5 | def test_control_flags(): 6 | """Test ControlFlags class for input and output.""" 7 | in_use = True 8 | not_in_use = False 9 | responder = 0 10 | controller = 1 11 | used_before = True 12 | not_used_before = False 13 | 14 | cf = ControlFlags(in_use, controller, used_before, bit5=0, bit4=0) 15 | assert cf.is_in_use 16 | assert not cf.is_available 17 | assert cf.is_controller 18 | assert not cf.is_responder 19 | assert cf.is_used_before 20 | assert not cf.is_high_water_mark 21 | assert cf.byte == 0xc2 22 | 23 | cf = ControlFlags(not_in_use, responder, not_used_before, bit5=0, bit4=0) 24 | assert not cf.is_in_use 25 | assert cf.is_available 26 | assert not cf.is_controller 27 | assert cf.is_responder 28 | assert not cf.is_used_before 29 | assert cf.is_high_water_mark 30 | assert cf.byte == 0x00 31 | -------------------------------------------------------------------------------- /tests/test_device.py: -------------------------------------------------------------------------------- 1 | """Test insteonplm.evices module.""" 2 | import asyncio 3 | import logging 4 | 5 | from insteonplm.constants import (COMMAND_LIGHT_OFF_0X13_0X00, 6 | COMMAND_LIGHT_ON_0X11_NONE, 7 | MESSAGE_ACK) 8 | from insteonplm.messages.standardSend import StandardSend 9 | from insteonplm.devices import create, DIRECT_ACK_WAIT_TIMEOUT 10 | from insteonplm.devices.dimmableLightingControl import DimmableLightingControl 11 | from tests.mockPLM import MockPLM 12 | 13 | _LOGGING = logging.getLogger(__name__) 14 | _LOGGING.setLevel(logging.DEBUG) 15 | _INSTEON_LOGGER = logging.getLogger('insteonplm') 16 | _INSTEON_LOGGER.setLevel(logging.DEBUG) 17 | 18 | 19 | def test_create_device(): 20 | """Test create device.""" 21 | async def run_test(loop): 22 | plm = MockPLM(loop) 23 | device = create(plm, '112233', 0x01, 0x0d, None) 24 | assert device.id == '112233' 25 | assert isinstance(device, DimmableLightingControl) 26 | 27 | loop = asyncio.get_event_loop() 28 | loop.run_until_complete(run_test(loop)) 29 | open_tasks = asyncio.Task.all_tasks(loop=loop) 30 | 31 | for task in open_tasks: 32 | if hasattr(task, 'name'): 33 | _LOGGING.error('Device: %s Task: %s', task.name, task) 34 | else: 35 | _LOGGING.error('Task: %s', task) 36 | if not task.done(): 37 | loop.run_until_complete(task) 38 | 39 | 40 | def test_create_device_from_bytearray(): 41 | """Test create device from byte array.""" 42 | async def run_test(loop): 43 | plm = MockPLM(loop) 44 | target = bytearray() 45 | target.append(0x01) 46 | target.append(0x0d) 47 | device = create(plm, '112233', target[0], target[1], None) 48 | assert device.id == '112233' 49 | assert isinstance(device, DimmableLightingControl) 50 | 51 | loop = asyncio.get_event_loop() 52 | loop.run_until_complete(run_test(loop)) 53 | 54 | open_tasks = asyncio.Task.all_tasks(loop=loop) 55 | 56 | for task in open_tasks: 57 | if hasattr(task, 'name'): 58 | _LOGGING.error('Device: %s Task: %s', task.name, task) 59 | else: 60 | _LOGGING.error('Task: %s', task) 61 | if not task.done(): 62 | loop.run_until_complete(task) 63 | 64 | 65 | def test_send_msg(): 66 | """Test sending a message.""" 67 | async def run_test(loop): 68 | mockPLM = MockPLM(loop) 69 | address = '1a2b3c' 70 | device = create(mockPLM, address, 0x01, 0x0d, 0x44) 71 | mockPLM.devices[address] = device 72 | 73 | # Send the ON command. This should be sent directly to the PLM 74 | device.states[0x01].on() 75 | await asyncio.sleep(.1, loop=loop) 76 | 77 | # Send the OFF command. This should wait in queue until the 78 | # Direct ACK timeout 79 | device.states[0x01].off() 80 | await asyncio.sleep(.1, loop=loop) 81 | 82 | # ACK the ON command 83 | msgreceived = StandardSend(address, COMMAND_LIGHT_ON_0X11_NONE, 84 | cmd2=0xff, flags=0x00, acknak=MESSAGE_ACK) 85 | mockPLM.message_received(msgreceived) 86 | asyncio.sleep(.1, loop=loop) 87 | 88 | _LOGGING.debug('Assert that the ON command is the command in the PLM') 89 | assert mockPLM.sentmessage == StandardSend( 90 | address, COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xff, flags=0x00).hex 91 | 92 | # Sleep until the Direct ACK time out should expire 93 | await asyncio.sleep(DIRECT_ACK_WAIT_TIMEOUT + .2, loop=loop) 94 | 95 | # Confirm that the OFF command made it to the PLM 96 | assert mockPLM.sentmessage == StandardSend( 97 | address, COMMAND_LIGHT_OFF_0X13_0X00).hex 98 | 99 | loop = asyncio.get_event_loop() 100 | loop.run_until_complete(run_test(loop)) 101 | 102 | open_tasks = asyncio.Task.all_tasks(loop=loop) 103 | 104 | for task in open_tasks: 105 | if hasattr(task, 'name'): 106 | _LOGGING.error('Device: %s Task: %s', task.name, task) 107 | else: 108 | _LOGGING.error('Task: %s', task) 109 | if not task.done(): 110 | loop.run_until_complete(task) 111 | -------------------------------------------------------------------------------- /tests/test_linkedDevices.py: -------------------------------------------------------------------------------- 1 | """Test insteonplm LinkedDevices Class.""" 2 | from insteonplm.address import Address 3 | from insteonplm.linkedDevices import LinkedDevices 4 | from insteonplm.devices.switchedLightingControl import SwitchedLightingControl 5 | from .mockPLM import MockPLM 6 | 7 | 8 | def test_create_device_from_category(): 9 | """Test device created from cateogory.""" 10 | plm = MockPLM() 11 | addr = Address('1a2b3c') 12 | cat = 0x02 13 | subcat = 0x13 14 | 15 | description = 'Icon SwitchLinc Relay (Lixar)' 16 | model = '2676R-B' 17 | 18 | linkedDevices = LinkedDevices() 19 | dev = linkedDevices.create_device_from_category(plm, addr, cat, subcat) 20 | 21 | assert isinstance(dev, SwitchedLightingControl) 22 | assert dev.cat == cat 23 | assert dev.subcat == subcat 24 | assert dev.description == description 25 | assert dev.model == model 26 | 27 | 28 | def test_create_device_from_category_generic_device(): 29 | """Test create device from category generic device.""" 30 | plm = MockPLM() 31 | addr = Address('1a2b3c') 32 | cat = 0x02 33 | subcat = 0xff # needs to be a subcat that is not in the IPDB 34 | 35 | description = 'Generic Switched Lighting Control' 36 | model = '' 37 | 38 | linkedDevices = LinkedDevices() 39 | dev = linkedDevices.create_device_from_category(plm, addr, cat, subcat) 40 | 41 | assert isinstance(dev, SwitchedLightingControl) 42 | assert dev.cat == cat 43 | assert dev.subcat == subcat 44 | assert dev.description == description 45 | assert dev.model == model 46 | -------------------------------------------------------------------------------- /tests/test_message.py: -------------------------------------------------------------------------------- 1 | """Test message creation.""" 2 | # pylint: disable=unused-variable 3 | import insteonplm.messages 4 | from insteonplm.messages.allLinkComplete import AllLinkComplete 5 | from insteonplm.messages.allLinkRecordResponse import ( 6 | AllLinkRecordResponse) 7 | from insteonplm.messages.buttonEventReport import ( 8 | ButtonEventReport) 9 | from insteonplm.messages.extendedReceive import ExtendedReceive 10 | from insteonplm.messages.extendedSend import ExtendedSend 11 | from insteonplm.messages.getIMInfo import GetImInfo 12 | from insteonplm.messages.standardReceive import StandardReceive 13 | from insteonplm.messages.standardSend import StandardSend 14 | from insteonplm.address import Address 15 | 16 | 17 | def test_create_standardReceive_message(): 18 | """Test create standardReceive message.""" 19 | address1 = 0x11 20 | address2 = 0x22 21 | address3 = 0x33 22 | target1 = 0x44 23 | target2 = 0x55 24 | target3 = 0x66 25 | flags = 0x77 26 | cmd1 = 0x88 27 | cmd2 = 0x99 28 | rawmessage = bytearray([0x02, 0x50, address1, address2, address3, target1, 29 | target2, target3, flags, cmd1, cmd2]) 30 | msg, buffer = insteonplm.messages.create(rawmessage) 31 | 32 | assert isinstance(msg, StandardReceive) 33 | assert msg.address == Address(bytearray([address1, address2, address3])) 34 | assert msg.target == Address(bytearray([target1, target2, target3])) 35 | assert msg.cmd1 == cmd1 36 | assert msg.cmd2 == cmd2 37 | 38 | 39 | def test_create_extendedReceive_message(): 40 | """Test create extendedReceive message.""" 41 | address1 = 0x01 42 | address2 = 0x02 43 | address3 = 0x03 44 | target1 = 0x04 45 | target2 = 0x05 46 | target3 = 0x06 47 | flags = 0x07 48 | cmd1 = 0x08 49 | cmd2 = 0x09 50 | rawmessage = bytearray([0x02, 0x51, address1, address2, address3, target1, 51 | target2, target3, flags, cmd1, cmd2, 52 | 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 53 | 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee]) 54 | msg, buffer = insteonplm.messages.create(rawmessage) 55 | 56 | assert isinstance(msg, ExtendedReceive) 57 | assert msg.address == Address(bytearray([address1, address2, address3])) 58 | assert msg.target == Address(bytearray([target1, target2, target3])) 59 | assert msg.cmd1 == cmd1 60 | assert msg.cmd2 == cmd2 61 | 62 | 63 | def test_create_allLinkComplete_message(): 64 | """Test create allLinkComplete message.""" 65 | linkcode = 0x11 66 | group = 0x22 67 | address1 = 0x33 68 | address2 = 0x44 69 | address3 = 0x55 70 | cat = 0x66 71 | subcat = 0x77 72 | firmware = 0x88 73 | 74 | rawmessage = bytearray([0x02, 0x53, linkcode, group, address1, address2, 75 | address3, cat, subcat, firmware]) 76 | msg, buffer = insteonplm.messages.create(rawmessage) 77 | 78 | assert isinstance(msg, AllLinkComplete) 79 | 80 | assert msg.linkcode == linkcode 81 | assert msg.group == group 82 | assert msg.address == Address(bytearray([address1, address2, address3])) 83 | assert msg.category == cat 84 | assert msg.subcategory == subcat 85 | assert msg.firmware == firmware 86 | 87 | 88 | def test_button_event_report(): 89 | """Test button event report.""" 90 | event = 0x02 91 | rawmessage = bytearray([0x02, 0x54, event]) 92 | msg, buffer = insteonplm.messages.create(rawmessage) 93 | 94 | assert isinstance(msg, ButtonEventReport) 95 | assert msg.event == event 96 | assert msg.eventText == 'SET button tapped' 97 | 98 | 99 | def test_AllLinkRecordResponse_message(): 100 | """Test AllLinkRecordResponse message.""" 101 | flags = 0x11 102 | group = 0x22 103 | address1 = 0x33 104 | address2 = 0x44 105 | address3 = 0x55 106 | linkdata1 = 0x66 107 | linkdata2 = 0x77 108 | linkdata3 = 0x88 109 | rawmessage = bytearray([0x02, 0x57, flags, group, address1, address2, 110 | address3, linkdata1, linkdata2, linkdata3]) 111 | msg, buffer = insteonplm.messages.create(rawmessage) 112 | 113 | assert isinstance(msg, AllLinkRecordResponse) 114 | assert msg.group == group 115 | assert msg.address == Address(bytearray([address1, address2, address3])) 116 | assert msg.linkdata1 == linkdata1 117 | assert msg.linkdata2 == linkdata2 118 | assert msg.linkdata3 == linkdata3 119 | 120 | 121 | def test_GetImInfo_message(): 122 | """Test GetImInfo message.""" 123 | address1 = 0x11 124 | address2 = 0x22 125 | address3 = 0x33 126 | cat = 0x44 127 | subcat = 0x55 128 | firmware = 0x66 129 | acknak = 0x77 130 | rawmessage = bytearray([0x02, 0x60, address1, address2, address3, cat, 131 | subcat, firmware, acknak]) 132 | msg, buffer = insteonplm.messages.create(rawmessage) 133 | 134 | assert isinstance(msg, GetImInfo) 135 | assert msg.address == Address(bytearray([address1, address2, address3])) 136 | assert msg.category == cat 137 | assert msg.subcategory == subcat 138 | assert msg.firmware == firmware 139 | 140 | 141 | def test_StandardSend_withAcknak_message(): 142 | """Test StandardSend withAcknak message.""" 143 | target1 = 0x11 144 | target2 = 0x22 145 | target3 = 0x33 146 | flags = 0xEF # 11101111 147 | cmd1 = 0x55 148 | cmd2 = 0x66 149 | acknak = 0x06 150 | rawmessage = bytearray([0x02, 0x62, target1, target2, target3, flags, 151 | cmd1, cmd2, acknak]) 152 | msg, buffer = insteonplm.messages.create(rawmessage) 153 | 154 | assert isinstance(msg, StandardSend) 155 | assert msg.cmd1 == cmd1 156 | assert msg.cmd2 == cmd2 157 | assert msg.isack 158 | assert not msg.isnak 159 | 160 | 161 | def test_ExtendedSend_withAcknak_message(): 162 | """Test ExtendedSend withAcknak message.""" 163 | target1 = 0x11 164 | target2 = 0x22 165 | target3 = 0x33 166 | flags = 0x10 # 00010000 167 | cmd1 = 0x55 168 | cmd2 = 0x66 169 | acknak = 0x06 170 | rawmessage = bytearray( 171 | [0x02, 0x62, target1, target2, target3, flags, cmd1, cmd2, 172 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 173 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, acknak]) 174 | msg, buffer = insteonplm.messages.create(rawmessage) 175 | 176 | assert isinstance(msg, ExtendedSend) 177 | assert msg.cmd1 == cmd1 178 | assert msg.cmd2 == cmd2 179 | assert msg.isack 180 | assert not msg.isnak 181 | 182 | 183 | def test_iscomplete_with_complete_message(): 184 | """Test iscomplete with complete message.""" 185 | rawmessage = bytearray([0x02, 0x50, 0x00, 0x00, 0x00, 0x00, 186 | 0x00, 0x00, 0x00, 0x00, 0x00]) 187 | assert insteonplm.messages.iscomplete(rawmessage) 188 | 189 | 190 | def test_iscomplete_with_incomplete_message(): 191 | """Test iscomplete with incomplete message.""" 192 | shortmessage = bytearray([0x02, 0x50, 0x00]) 193 | assert not insteonplm.messages.iscomplete(shortmessage) 194 | 195 | 196 | def test_incomplete_standard_message(): 197 | """Test incomplete standard message.""" 198 | rawmessage = bytearray([0x02, 0x50, 0x00, 0x00, 0x00, 0x00, 199 | 0x00, 0x00, 0x00, 0x00]) 200 | msg, buffer = insteonplm.messages.create(rawmessage) 201 | assert msg is None 202 | 203 | 204 | def test_incomplete_extended_message(): 205 | """Test incomplete extended message.""" 206 | rawmessage = bytearray([0x02, 0x62, 0x1a, 0x2b, 0x3c, 0x10, 0x7d, 0x8e, 207 | 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 208 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) 209 | msg, buffer = insteonplm.messages.create(rawmessage) 210 | assert msg is None 211 | 212 | rawmessage.append(0x06) 213 | msg, buffer = insteonplm.messages.create(rawmessage) 214 | assert isinstance(msg, ExtendedSend) 215 | 216 | 217 | def test_leading_unknown_messge(): 218 | """Test leading unknown messge.""" 219 | rawmessage = bytearray([0x02, 0x00, 0x15, 0x02, 0x50, 0x46, 0xd0, 0xe6, 220 | 0x43, 0x6c, 0x15, 0x40, 0x11, 0x01]) 221 | msg, buffer = insteonplm.messages.create(rawmessage) 222 | assert isinstance(msg, StandardReceive) 223 | assert msg.cmd1 == 0x11 224 | assert msg.cmd2 == 0x01 225 | -------------------------------------------------------------------------------- /tests/test_messageFlags.py: -------------------------------------------------------------------------------- 1 | """Test the MessageFlags class.""" 2 | 3 | from insteonplm.constants import (MESSAGE_TYPE_ALL_LINK_BROADCAST, 4 | MESSAGE_TYPE_ALL_LINK_CLEANUP, 5 | MESSAGE_TYPE_ALL_LINK_CLEANUP_ACK, 6 | MESSAGE_TYPE_ALL_LINK_CLEANUP_NAK, 7 | MESSAGE_TYPE_BROADCAST_MESSAGE, 8 | MESSAGE_TYPE_DIRECT_MESSAGE, 9 | MESSAGE_TYPE_DIRECT_MESSAGE_ACK, 10 | MESSAGE_TYPE_DIRECT_MESSAGE_NAK) 11 | from insteonplm.messages.messageFlags import MessageFlags 12 | 13 | 14 | def test_messageType(): 15 | """Test message flags match the expected message type.""" 16 | direct = MessageFlags(MESSAGE_TYPE_DIRECT_MESSAGE << 5) 17 | direct_ack = MessageFlags(MESSAGE_TYPE_DIRECT_MESSAGE_ACK << 5) 18 | all_link_cleanup = MessageFlags(MESSAGE_TYPE_ALL_LINK_CLEANUP << 5) 19 | all_link_cleanup_ack = MessageFlags(MESSAGE_TYPE_ALL_LINK_CLEANUP_ACK << 5) 20 | broadcast = MessageFlags(MESSAGE_TYPE_BROADCAST_MESSAGE << 5) 21 | direct_nak = MessageFlags(MESSAGE_TYPE_DIRECT_MESSAGE_NAK << 5) 22 | all_link_broadcast = MessageFlags(MESSAGE_TYPE_ALL_LINK_BROADCAST << 5) 23 | all_link_cleanup_nak = MessageFlags(MESSAGE_TYPE_ALL_LINK_CLEANUP_NAK << 5) 24 | 25 | assert direct.messageType == MESSAGE_TYPE_DIRECT_MESSAGE 26 | assert direct.isDirect 27 | 28 | assert direct_ack.messageType == MESSAGE_TYPE_DIRECT_MESSAGE_ACK 29 | assert direct_ack.isDirectACK 30 | 31 | assert all_link_cleanup.messageType == MESSAGE_TYPE_ALL_LINK_CLEANUP 32 | assert all_link_cleanup.isAllLinkCleanup 33 | 34 | assert (all_link_cleanup_ack.messageType == 35 | MESSAGE_TYPE_ALL_LINK_CLEANUP_ACK) 36 | assert all_link_cleanup_ack.isAllLinkCleanupACK 37 | 38 | assert broadcast.messageType == MESSAGE_TYPE_BROADCAST_MESSAGE 39 | assert broadcast.isBroadcast 40 | 41 | assert direct_nak.messageType == MESSAGE_TYPE_DIRECT_MESSAGE_NAK 42 | assert direct_nak.isDirectNAK 43 | 44 | assert all_link_broadcast.messageType == MESSAGE_TYPE_ALL_LINK_BROADCAST 45 | assert all_link_broadcast.isAllLinkBroadcast 46 | 47 | assert (all_link_cleanup_nak.messageType == 48 | MESSAGE_TYPE_ALL_LINK_CLEANUP_NAK) 49 | assert all_link_cleanup_nak.isAllLinkCleanupNAK 50 | 51 | 52 | def test_extended(): 53 | """Test the extended flag.""" 54 | assert MessageFlags(0x10).extended == 1 55 | assert MessageFlags(0x10).isExtended 56 | 57 | 58 | # pylint: disable=too-many-statements 59 | def test_eq(): 60 | """Test comarision for equality.""" 61 | flag1 = MessageFlags(0x80) 62 | flag2 = MessageFlags(0x25) 63 | flag3 = MessageFlags(0x27) 64 | flag4 = MessageFlags(0x16) 65 | flag5 = MessageFlags(0x37) 66 | flag6 = MessageFlags(0x6f) 67 | 68 | pattern1 = MessageFlags.template(MESSAGE_TYPE_BROADCAST_MESSAGE, 0) 69 | pattern2 = MessageFlags.template(MESSAGE_TYPE_BROADCAST_MESSAGE, None) 70 | pattern3 = MessageFlags.template(None, 0) 71 | pattern4 = MessageFlags.template(None, 1) 72 | pattern5 = MessageFlags.template(MESSAGE_TYPE_DIRECT_MESSAGE_ACK, 0) 73 | pattern6 = MessageFlags.template(MESSAGE_TYPE_DIRECT_MESSAGE_ACK, 1) 74 | pattern7 = MessageFlags.template(MESSAGE_TYPE_DIRECT_MESSAGE_ACK, None) 75 | 76 | assert flag1.matches_pattern(pattern1) 77 | assert not flag2.matches_pattern(pattern1) 78 | assert not flag3.matches_pattern(pattern1) 79 | assert not flag4.matches_pattern(pattern1) 80 | assert not flag5.matches_pattern(pattern1) 81 | assert not flag6.matches_pattern(pattern1) 82 | 83 | assert flag1.matches_pattern(pattern2) 84 | assert not flag2.matches_pattern(pattern2) 85 | assert not flag3.matches_pattern(pattern2) 86 | assert not flag4.matches_pattern(pattern2) 87 | assert not flag5.matches_pattern(pattern2) 88 | assert not flag6.matches_pattern(pattern2) 89 | 90 | assert flag1.matches_pattern(pattern3) 91 | assert flag2.matches_pattern(pattern3) 92 | assert flag3.matches_pattern(pattern3) 93 | assert not flag4.matches_pattern(pattern3) 94 | assert not flag5.matches_pattern(pattern3) 95 | assert flag6.matches_pattern(pattern3) 96 | 97 | assert not flag1.matches_pattern(pattern4) 98 | assert not flag2.matches_pattern(pattern4) 99 | assert not flag3.matches_pattern(pattern4) 100 | assert flag4.matches_pattern(pattern4) 101 | assert flag5.matches_pattern(pattern4) 102 | assert not flag6.matches_pattern(pattern4) 103 | 104 | assert not flag1.matches_pattern(pattern5) 105 | assert flag2.matches_pattern(pattern5) 106 | assert flag3.matches_pattern(pattern5) 107 | assert not flag4.matches_pattern(pattern5) 108 | assert not flag5.matches_pattern(pattern5) 109 | assert not flag6.matches_pattern(pattern5) 110 | 111 | assert not flag1.matches_pattern(pattern6) 112 | assert not flag2.matches_pattern(pattern6) 113 | assert not flag3.matches_pattern(pattern6) 114 | assert not flag4.matches_pattern(pattern6) 115 | assert flag5.matches_pattern(pattern6) 116 | assert not flag6.matches_pattern(pattern6) 117 | 118 | assert not flag1.matches_pattern(pattern7) 119 | assert flag2.matches_pattern(pattern7) 120 | assert flag3.matches_pattern(pattern7) 121 | assert not flag4.matches_pattern(pattern7) 122 | assert flag5.matches_pattern(pattern7) 123 | assert not flag6.matches_pattern(pattern7) 124 | -------------------------------------------------------------------------------- /tests/test_message_callback.py: -------------------------------------------------------------------------------- 1 | """Test message callbacks.""" 2 | 3 | import logging 4 | from insteonplm.messagecallback import MessageCallback 5 | from insteonplm.constants import (COMMAND_LIGHT_ON_0X11_NONE, 6 | MESSAGE_ACK, 7 | MESSAGE_NAK) 8 | from insteonplm.messages.standardReceive import StandardReceive 9 | from insteonplm.messages.standardSend import StandardSend 10 | from insteonplm.messages.extendedReceive import ExtendedReceive 11 | from insteonplm.messages.extendedSend import ExtendedSend 12 | from insteonplm.messages.allLinkRecordResponse import AllLinkRecordResponse 13 | from insteonplm.messages.getIMInfo import GetImInfo 14 | from insteonplm.messages.getNextAllLinkRecord import GetNextAllLinkRecord 15 | from insteonplm.messages.userdata import Userdata 16 | from .mockCallbacks import MockCallbacks 17 | 18 | 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | 22 | def test_messagecallback_basic(): 23 | """Test messagecallback basic.""" 24 | callbacks = MessageCallback() 25 | callbacktest1 = "test callback 1" 26 | 27 | msg_template = StandardReceive.template( 28 | commandtuple=COMMAND_LIGHT_ON_0X11_NONE, flags=0x80) 29 | callbacks[msg_template] = callbacktest1 30 | 31 | msg = StandardReceive('1a2b3c', '4d5e6f', COMMAND_LIGHT_ON_0X11_NONE, 32 | cmd2=0xff, flags=0x80) 33 | 34 | callback1 = callbacks.get_callbacks_from_message(msg) 35 | 36 | assert len(callback1) == 1 37 | assert callback1[0] == callbacktest1 38 | 39 | 40 | def test_messagecallback_msg(): 41 | """Test messagecallback msg.""" 42 | callbacks = MessageCallback() 43 | callbacktest = "test callback" 44 | address = '1a2b3c' 45 | target = '4d5e6f' 46 | 47 | template_on = StandardReceive.template( 48 | commandtuple=COMMAND_LIGHT_ON_0X11_NONE) 49 | callbacks.add(template_on, callbacktest) 50 | msg1 = StandardReceive(address, target, COMMAND_LIGHT_ON_0X11_NONE, 51 | cmd2=0x00) 52 | msg2 = StandardReceive(address, target, COMMAND_LIGHT_ON_0X11_NONE, 53 | cmd2=0xff) 54 | 55 | callback1 = callbacks.get_callbacks_from_message(msg1) 56 | callback2 = callbacks.get_callbacks_from_message(msg2) 57 | 58 | assert callback1[0] == callbacktest 59 | assert callback2[0] == callbacktest 60 | 61 | 62 | # pylint: disable=too-many-locals 63 | def test_messagecallback_acknak(): 64 | """Test messagecallback acknak.""" 65 | callbacks = MessageCallback() 66 | callbacktest1 = "test callback 1" 67 | callbacktest2 = "test callback 2" 68 | callbacktest3 = "test callback 3" 69 | callbacktest4 = "test callback 4" 70 | address = '1a2b3c' 71 | 72 | template_address = StandardSend.template(address=address) 73 | template_address_ack = StandardSend.template(address=address, 74 | acknak=MESSAGE_ACK) 75 | template_empty = StandardSend.template() 76 | template_nak = StandardSend.template(acknak=MESSAGE_NAK) 77 | callbacks.add(template_address, callbacktest1) 78 | callbacks.add(template_address_ack, callbacktest2) 79 | callbacks.add(template_empty, callbacktest3) 80 | callbacks.add(template_nak, callbacktest4) 81 | 82 | msg1 = StandardSend(address, COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xcd) 83 | msg2 = StandardSend('222222', COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xff) 84 | msg3 = StandardSend('333333', COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xff, 85 | acknak=MESSAGE_NAK) 86 | msg4 = StandardSend('444444', COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xff, 87 | acknak=MESSAGE_ACK) 88 | 89 | _LOGGER.debug('Getting callbacks for message 1') 90 | callback1 = callbacks.get_callbacks_from_message(msg1) 91 | _LOGGER.debug('Getting callbacks for message 2') 92 | callback2 = callbacks.get_callbacks_from_message(msg2) 93 | _LOGGER.debug('Getting callbacks for message 3') 94 | callback3 = callbacks.get_callbacks_from_message(msg3) 95 | _LOGGER.debug('Getting callbacks for message 4') 96 | callback4 = callbacks.get_callbacks_from_message(msg4) 97 | 98 | assert len(callback1) == 4 99 | assert len(callback2) == 2 100 | assert len(callback3) == 2 101 | assert len(callback4) == 1 102 | 103 | 104 | def test_message_callback_extended(): 105 | """Test message callback extended.""" 106 | callbacks = MessageCallback() 107 | callbacktest = "test callback" 108 | address = '1a2b3c' 109 | target = '4d5e6f' 110 | 111 | template_ext_on = ExtendedReceive.template( 112 | commandtuple=COMMAND_LIGHT_ON_0X11_NONE, 113 | userdata=Userdata({'d1': 0x02})) 114 | callbacks.add(template_ext_on, callbacktest) 115 | msg1 = ExtendedReceive(address, target, COMMAND_LIGHT_ON_0X11_NONE, 116 | Userdata({'d1': 0x02}), cmd2=0xff) 117 | msg2 = ExtendedReceive(address, target, COMMAND_LIGHT_ON_0X11_NONE, 118 | Userdata({'d1': 0x03, 'd2': 0x02}), cmd2=0xff) 119 | 120 | callback1 = callbacks.get_callbacks_from_message(msg1) 121 | callback2 = callbacks.get_callbacks_from_message(msg2) 122 | 123 | assert callback1[0] == callbacktest 124 | assert not callback2 125 | 126 | 127 | def test_delete_callback(): 128 | """Test delete callback.""" 129 | callbacks = MessageCallback() 130 | callbacktest1 = "test callback 1" 131 | callbacktest2 = "test callback 2" 132 | callbacktest3 = "test callback 3" 133 | 134 | callbacks.add(StandardSend.template(), callbacktest1) 135 | callbacks.add(StandardSend.template(), callbacktest2) 136 | callbacks.add(StandardSend.template(acknak=MESSAGE_NAK), callbacktest3) 137 | 138 | msg = StandardSend('333333', COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xff, 139 | acknak=MESSAGE_NAK) 140 | 141 | callback_list = callbacks.get_callbacks_from_message(msg) 142 | assert len(callback_list) == 3 143 | 144 | callbacks.remove(StandardSend.template(), callbacktest2) 145 | callback_list = callbacks.get_callbacks_from_message(msg) 146 | assert len(callback_list) == 2 147 | 148 | 149 | def test_misc_messages(): 150 | """Test misc messages.""" 151 | callbacks = MessageCallback() 152 | callbacktest1 = "test callback 1" 153 | callbacktest2 = "test callback 2" 154 | callbacktest3 = "test callback 3" 155 | 156 | msgtemplate1 = AllLinkRecordResponse(None, None, None, None, None, None) 157 | msgtemplate2 = GetImInfo() 158 | msgtemplate3 = GetNextAllLinkRecord(acknak=MESSAGE_NAK) 159 | callbacks.add(msgtemplate1, callbacktest1) 160 | callbacks.add(msgtemplate2, callbacktest2) 161 | callbacks.add(msgtemplate3, callbacktest3) 162 | 163 | msg1 = AllLinkRecordResponse(0x00, 0x01, '1a2b3c', 0x01, 0x02, 0x03) 164 | msg2 = GetImInfo() 165 | msg3 = GetNextAllLinkRecord(acknak=MESSAGE_ACK) 166 | msg4 = GetNextAllLinkRecord(acknak=MESSAGE_NAK) 167 | 168 | callback_list1 = callbacks.get_callbacks_from_message(msg1) 169 | callback_list2 = callbacks.get_callbacks_from_message(msg2) 170 | callback_list3 = callbacks.get_callbacks_from_message(msg3) 171 | callback_list4 = callbacks.get_callbacks_from_message(msg4) 172 | 173 | assert callback_list1[0] == callbacktest1 174 | assert callback_list2[0] == callbacktest2 175 | assert not callback_list3 176 | assert callback_list4[0] == callbacktest3 177 | 178 | 179 | def test_extended_ack(): 180 | """Test extended ack.""" 181 | callbacks = MockCallbacks() 182 | callbacks.callbackvalue1 = "Callback 1" 183 | callbacks.callbackvalue2 = "Callback 2" 184 | message_callbacks = MessageCallback() 185 | address = '1a2b3c' 186 | 187 | template_ext_ack = ExtendedSend.template(address, acknak=MESSAGE_ACK) 188 | template_std_ack = StandardSend.template(address, acknak=MESSAGE_ACK) 189 | 190 | message_callbacks.add(template_ext_ack, callbacks.callbackvalue1) 191 | message_callbacks.add(template_std_ack, callbacks.callbackvalue2) 192 | extmsg = ExtendedSend(address, COMMAND_LIGHT_ON_0X11_NONE, {'d1': 0x02}, 193 | cmd2=0xff, acknak=MESSAGE_ACK) 194 | stdmsg = StandardSend(address, COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xff, 195 | acknak=MESSAGE_ACK) 196 | result1 = message_callbacks.get_callbacks_from_message(extmsg) 197 | result2 = message_callbacks.get_callbacks_from_message(stdmsg) 198 | 199 | assert result2 == [callbacks.callbackvalue2] 200 | assert result1 == [callbacks.callbackvalue1] 201 | -------------------------------------------------------------------------------- /tests/test_sensorsActuators.py: -------------------------------------------------------------------------------- 1 | """Test INSTEON Sensor Actuator devices.""" 2 | 3 | import asyncio 4 | import logging 5 | 6 | from insteonplm.constants import (COMMAND_LIGHT_ON_0X11_NONE, 7 | COMMAND_LIGHT_STATUS_REQUEST_0X19_0X00, 8 | COMMAND_LIGHT_STATUS_REQUEST_0X19_0X01, 9 | MESSAGE_ACK, 10 | MESSAGE_TYPE_DIRECT_MESSAGE_ACK) 11 | # SensorsActuators class not tested ? 12 | from insteonplm.devices.sensorsActuators import SensorsActuators_2450 13 | from insteonplm.messages.standardSend import StandardSend 14 | from insteonplm.messages.standardReceive import StandardReceive 15 | from insteonplm.messages.messageFlags import MessageFlags 16 | from .mockPLM import MockPLM 17 | from .mockCallbacks import MockCallbacks 18 | 19 | _LOGGING = logging.getLogger(__name__) 20 | _LOGGING.setLevel(logging.DEBUG) 21 | 22 | 23 | def test_SensorsActuators_2450_status(): 24 | """Test SensorActuator device model 2450.""" 25 | async def run_test(loop): 26 | """Asyncio test run.""" 27 | plm = MockPLM(loop) 28 | address = '1a2b3c' 29 | target = '4d5e6f' 30 | 31 | cat = 0x07 32 | subcat = 0x00 33 | product_key = 0x00 34 | description = 'I/O Linc' 35 | model = '2450' 36 | 37 | callbacks = MockCallbacks() 38 | 39 | device = SensorsActuators_2450(plm, address, cat, subcat, 40 | product_key, description, model) 41 | plm.devices[address] = device 42 | assert device.states[0x01].name == 'openClosedRelay' 43 | assert device.states[0x02].name == 'ioLincSensor' 44 | 45 | device.states[0x01].register_updates(callbacks.callbackmethod1) 46 | device.states[0x02].register_updates(callbacks.callbackmethod2) 47 | 48 | device.async_refresh_state() 49 | await asyncio.sleep(.1, loop=loop) 50 | 51 | # First state 52 | sentmsg = StandardSend(address, COMMAND_LIGHT_STATUS_REQUEST_0X19_0X00) 53 | assert plm.sentmessage == sentmsg.hex 54 | 55 | receivedmsg = StandardSend(address, 56 | COMMAND_LIGHT_STATUS_REQUEST_0X19_0X00, 57 | acknak=MESSAGE_ACK) 58 | plm.message_received(receivedmsg) 59 | await asyncio.sleep(.1, loop=loop) 60 | receivedmsg = StandardReceive( 61 | address, target, COMMAND_LIGHT_ON_0X11_NONE, cmd2=0x55, 62 | flags=MessageFlags.create(MESSAGE_TYPE_DIRECT_MESSAGE_ACK, 0)) 63 | plm.message_received(receivedmsg) 64 | await asyncio.sleep(.3, loop=loop) 65 | assert callbacks.callbackvalue1 == 0xff 66 | 67 | # Second state 68 | receivedmsg = StandardSend(address, 69 | COMMAND_LIGHT_STATUS_REQUEST_0X19_0X01, 70 | acknak=MESSAGE_ACK) 71 | plm.message_received(receivedmsg) 72 | await asyncio.sleep(.1, loop=loop) 73 | receivedmsg = StandardReceive( 74 | address, target, {'cmd1': 0x01, 'cmd2': 0x00}, 75 | flags=MessageFlags.create(MESSAGE_TYPE_DIRECT_MESSAGE_ACK, 0)) 76 | plm.message_received(receivedmsg) 77 | await asyncio.sleep(.1, loop=loop) 78 | 79 | sentmsg = StandardSend(address, COMMAND_LIGHT_STATUS_REQUEST_0X19_0X01) 80 | assert plm.sentmessage == sentmsg.hex 81 | assert callbacks.callbackvalue1 == 0xff 82 | assert callbacks.callbackvalue2 == 0 83 | 84 | loop = asyncio.get_event_loop() 85 | loop.run_until_complete(run_test(loop)) 86 | open_tasks = asyncio.Task.all_tasks(loop=loop) 87 | 88 | for task in open_tasks: 89 | if hasattr(task, 'name'): 90 | _LOGGING.error('Device: %s Task: %s', task.name, task) 91 | else: 92 | _LOGGING.error('Task: %s', task) 93 | if not task.done(): 94 | loop.run_until_complete(task) 95 | -------------------------------------------------------------------------------- /tests/test_userdata.py: -------------------------------------------------------------------------------- 1 | """Test insteonplm User Data type class.""" 2 | from insteonplm.messages.userdata import Userdata 3 | 4 | 5 | def test_userdata_basic(): 6 | """Test insteonplm User Data type class.""" 7 | userdata = {'d1': 0x11, 'd2': 0x22, 'd3': 0x33, 'd4': 0x44, 'd5': 0x55, 8 | 'd6': 0x66, 'd7': 0x77, 'd8': 0x88, 'd9': 0x99, 'd10': 0xaa, 9 | 'd11': 0xbb, 'd12': 0xcc, 'd13': 0xdd, 'd14': 0xee} 10 | 11 | ud = Userdata(userdata) 12 | chk = Userdata.create_pattern(userdata) 13 | chk2 = Userdata.create_pattern({'d1': 0x11}) 14 | assert chk == ud 15 | assert ud.matches_pattern(chk2) 16 | assert chk2.matches_pattern(ud) 17 | -------------------------------------------------------------------------------- /tests/test_x10.py: -------------------------------------------------------------------------------- 1 | """Test insteonplm X10 devices.""" 2 | import asyncio 3 | import logging 4 | 5 | from insteonplm.constants import (X10_COMMAND_ON, 6 | X10_COMMAND_OFF, 7 | X10_COMMAND_ALL_UNITS_OFF) 8 | from insteonplm.devices.x10 import (X10OnOff, 9 | X10Dimmable, 10 | X10AllUnitsOff, 11 | X10AllLightsOff, 12 | X10AllLightsOn) 13 | from insteonplm.messages.x10send import X10Send 14 | from insteonplm.messages.x10received import X10Received 15 | from .mockCallbacks import MockCallbacks 16 | from .mockPLM import MockPLM 17 | 18 | 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | 22 | def test_x10OnOff(): 23 | """Test X10 On/Off device.""" 24 | async def run_test(loop): 25 | housecode = 'C' # byte 0x02 26 | unitcode = 9 # byte 0x07 27 | plm = MockPLM(loop) 28 | device = X10OnOff(plm, housecode, unitcode) 29 | plm.devices[device.address.id] = device 30 | _LOGGER.debug('X10 device id: %s', device.address.id) 31 | assert device.address.human == 'X10.{}.{:02d}'.format(housecode, 32 | unitcode) 33 | 34 | # Send On command and test both commands sent 35 | device.states[0x01].on() 36 | await asyncio.sleep(.1, loop=loop) 37 | assert plm.sentmessage == '02632700' 38 | msg = X10Send(0x27, 0x00, 0x06) 39 | device.receive_message(msg) 40 | await asyncio.sleep(.1, loop=loop) 41 | assert plm.sentmessage == '02632280' 42 | await asyncio.sleep(.1, loop=loop) 43 | msg = X10Send(0x22, 0x00, 0x06) 44 | device.receive_message(msg) 45 | await asyncio.sleep(.1, loop=loop) 46 | 47 | # Send Off command and test both commands sent 48 | device.states[0x01].off() 49 | await asyncio.sleep(.1, loop=loop) 50 | assert plm.sentmessage == '02632700' 51 | msg = X10Send(0x27, 0x00, 0x06) 52 | device.receive_message(msg) 53 | await asyncio.sleep(.1, loop=loop) 54 | assert plm.sentmessage == '02632380' 55 | await asyncio.sleep(.1, loop=loop) 56 | msg = X10Send(0x23, 0x00, 0x06) 57 | device.receive_message(msg) 58 | await asyncio.sleep(.1, loop=loop) 59 | 60 | loop = asyncio.get_event_loop() 61 | loop.run_until_complete(run_test(loop)) 62 | open_tasks = asyncio.Task.all_tasks(loop=loop) 63 | 64 | for task in open_tasks: 65 | if hasattr(task, 'name'): 66 | _LOGGER.error('Device: %s Task: %s', task.name, task) 67 | else: 68 | _LOGGER.error('Task: %s', task) 69 | if not task.done(): 70 | loop.run_until_complete(task) 71 | 72 | 73 | def test_dimmable(): 74 | """Test X10 dimmable device.""" 75 | async def run_test(loop): 76 | housecode = 'C' # byte 0x02 77 | unitcode = 9 # byte 0x07 78 | newval = 200 79 | steps = round(newval / (255 / 22)) + 1 80 | _LOGGER.info('Steps %d', steps) 81 | plm = MockPLM(loop) 82 | cb = MockCallbacks() 83 | device = X10Dimmable(plm, housecode, unitcode) 84 | device.states[0x01].register_updates(cb.callbackmethod1) 85 | plm.devices[device.address.id] = device 86 | 87 | device.states[0x01].set_level(200) 88 | # pylint: disable=unused-variable 89 | for i in range(0, steps): 90 | _LOGGER.info('Sending ACK messages') 91 | msg = X10Send(0x27, 0x00, 0x06) 92 | device.receive_message(msg) 93 | await asyncio.sleep(.1, loop=loop) 94 | 95 | await asyncio.sleep(.1, loop=loop) 96 | msg = X10Send(0x25, 0x80, 0x06) 97 | _LOGGER.info('New value: 0x%02x', cb.callbackvalue1) 98 | assert cb.callbackvalue1 == round((steps - 1) * (255 / 22)) 99 | 100 | loop = asyncio.get_event_loop() 101 | loop.run_until_complete(run_test(loop)) 102 | open_tasks = asyncio.Task.all_tasks(loop=loop) 103 | 104 | for task in open_tasks: 105 | if hasattr(task, 'name'): 106 | _LOGGER.error('Device: %s Task: %s', task.name, task) 107 | else: 108 | _LOGGER.error('Task: %s', task) 109 | if not task.done(): 110 | loop.run_until_complete(task) 111 | 112 | 113 | def test_on_received(): 114 | """Test X10 on message received.""" 115 | async def run_test(loop): 116 | housecode = 'C' # byte 0x02 117 | unitcode = 9 # byte 0x07 118 | plm = MockPLM(loop) 119 | cb = MockCallbacks() 120 | device = X10Dimmable(plm, housecode, unitcode, 22) 121 | plm.devices[device.address.id] = device 122 | device.states[0x01].register_updates(cb.callbackmethod1) 123 | 124 | msg = X10Received.command_msg(housecode, X10_COMMAND_ON) 125 | device.receive_message(msg) 126 | await asyncio.sleep(.1, loop=loop) 127 | assert cb.callbackvalue1 == 0xff 128 | 129 | msg = X10Received.command_msg(housecode, X10_COMMAND_OFF) 130 | device.receive_message(msg) 131 | await asyncio.sleep(.1, loop=loop) 132 | assert cb.callbackvalue1 == 0x00 133 | 134 | loop = asyncio.get_event_loop() 135 | loop.run_until_complete(run_test(loop)) 136 | open_tasks = asyncio.Task.all_tasks(loop=loop) 137 | 138 | for task in open_tasks: 139 | if hasattr(task, 'name'): 140 | _LOGGER.error('Device: %s Task: %s', task.name, task) 141 | else: 142 | _LOGGER.error('Task: %s', task) 143 | if not task.done(): 144 | loop.run_until_complete(task) 145 | 146 | 147 | def test_all_unit_types(): 148 | """Test X10 All Unit device types.""" 149 | plm = MockPLM() 150 | all_units_off = X10AllUnitsOff(plm, 'A', 20) 151 | all_lights_off = X10AllLightsOff(plm, 'A', 22) 152 | all_lights_on = X10AllLightsOn(plm, 'A', 21) 153 | 154 | assert all_units_off.description == 'X10 All Units Off Device' 155 | assert all_lights_off.description == 'X10 All Lights Off Device' 156 | assert all_lights_on.description == 'X10 All Lights On Device' 157 | assert all_units_off.id == 'x10A20' 158 | 159 | 160 | def test_all_units_on_off(): 161 | """Test X10 All Units Off command.""" 162 | async def run_test(loop): 163 | plm = MockPLM(loop) 164 | callbacks = MockCallbacks() 165 | all_units_off = X10AllUnitsOff(plm, 'A', 20) 166 | all_lights_off = X10AllLightsOff(plm, 'A', 22) 167 | all_lights_on = X10AllLightsOn(plm, 'A', 21) 168 | 169 | all_units_off.states[0x01].register_updates(callbacks.callbackmethod1) 170 | all_lights_off.states[0x01].register_updates(callbacks.callbackmethod2) 171 | all_lights_on.states[0x01].register_updates(callbacks.callbackmethod3) 172 | 173 | msg = X10Received.command_msg('A', X10_COMMAND_ALL_UNITS_OFF) 174 | plm.message_received(msg) 175 | _LOGGER.debug('Should have 1st callback') 176 | await asyncio.sleep(.1, loop=loop) 177 | assert callbacks.callbackvalue1 == 0x00 178 | _LOGGER.debug('Should have 2nd callback') 179 | await asyncio.sleep(2, loop=loop) 180 | assert callbacks.callbackvalue1 == 0xff 181 | 182 | loop = asyncio.get_event_loop() 183 | loop.run_until_complete(run_test(loop)) 184 | open_tasks = asyncio.Task.all_tasks(loop=loop) 185 | 186 | for task in open_tasks: 187 | if hasattr(task, 'name'): 188 | _LOGGER.error('Device: %s Task: %s', task.name, task) 189 | else: 190 | _LOGGER.error('Task: %s', task) 191 | if not task.done(): 192 | loop.run_until_complete(task) 193 | -------------------------------------------------------------------------------- /tests/todo_test_default_links.py: -------------------------------------------------------------------------------- 1 | """Test insteonplm.evices module.""" 2 | import asyncio 3 | import logging 4 | 5 | 6 | import insteonplm.devices 7 | from insteonplm.constants import (MESSAGE_ACK, 8 | COMMAND_ENTER_LINKING_MODE_0X09_NONE, 9 | COMMAND_EXTENDED_READ_WRITE_ALDB_0X2F_0X00) 10 | from insteonplm.messages.allLinkComplete import AllLinkComplete 11 | from insteonplm.messages.extendedReceive import ExtendedReceive 12 | from insteonplm.messages.extendedSend import ExtendedSend 13 | from insteonplm.messages.standardSend import StandardSend 14 | # from insteonplm.messages.startAllLinking import StartAllLinking 15 | from insteonplm.messages.userdata import Userdata 16 | from tests.mockPLM import MockPLM 17 | 18 | _LOGGING = logging.getLogger(__name__) 19 | _LOGGING.setLevel(logging.DEBUG) 20 | 21 | 22 | def test_create_device(): 23 | """Test create device.""" 24 | async def run_test(loop): 25 | mockPLM = MockPLM(loop) 26 | linkcode = 0x01 27 | group = 0x00 28 | address = '112233' 29 | cat = 0x02 30 | subcat = 0x39 31 | firmware = 0x44 32 | # Create OutletLinc 33 | device = insteonplm.devices.create(mockPLM, address, 34 | cat, subcat, firmware) 35 | 36 | # Start the process with an All-Link complete message with 37 | # the IM as a controller of Group 0x00 38 | msg = AllLinkComplete(linkcode, group, address, cat, subcat, firmware) 39 | device.receive_message(msg) 40 | await asyncio.sleep(.1, loop=loop) 41 | 42 | # The device should start linking based on the groups in 43 | # self.states 44 | assert mockPLM.sentmessage == StandardSend( 45 | device.address, COMMAND_ENTER_LINKING_MODE_0X09_NONE, 46 | cmd2=0x01).hex 47 | msg = StandardSend(device.address, 48 | COMMAND_ENTER_LINKING_MODE_0X09_NONE, 49 | cmd2=0x01, acknak=MESSAGE_ACK) 50 | device.receive_message(msg) 51 | await asyncio.sleep(.1, loop=loop) 52 | # Confirm that the link attempt to group 0x01 completed 53 | msg = AllLinkComplete(0x00, 0x01, address, cat, subcat, firmware) 54 | device.receive_message(msg) 55 | await asyncio.sleep(.1, loop=loop) 56 | 57 | # The device should then start linking to group 0x02 58 | assert mockPLM.sentmessage == StandardSend( 59 | device.address, COMMAND_ENTER_LINKING_MODE_0X09_NONE, 60 | cmd2=0x02).hex 61 | await asyncio.sleep(1, loop=loop) 62 | # Confirm that the link attempt to group 0x02 completed 63 | msg = AllLinkComplete(0x00, 0x01, address, cat, subcat, firmware) 64 | device.receive_message(msg) 65 | await asyncio.sleep(.1, loop=loop) 66 | 67 | # The device will now attempt to read the ALDB 68 | msg = ExtendedSend(address, 69 | COMMAND_EXTENDED_READ_WRITE_ALDB_0X2F_0X00, 70 | userdata=Userdata()) 71 | msg.set_checksum() 72 | assert mockPLM.sentmessage == msg.hex 73 | # Send a dummy ALDB record as a high water mark to end the process 74 | msg = ExtendedReceive( 75 | address, '111111', 76 | commandtuple=COMMAND_EXTENDED_READ_WRITE_ALDB_0X2F_0X00, 77 | userdata=Userdata({'d1': 0, 78 | 'd2': 0x01, 79 | 'd3': 0xff, 80 | 'd4': 0x77, 81 | 'd5': 0, 82 | 'd6': 0, 83 | 'd7': 0, 84 | 'd8': 0, 85 | 'd9': 0, 86 | 'd10': 0, 87 | 'd11': 0, 88 | 'd12': 0, 89 | 'd13': 0, 90 | 'd14': 0x3b})) 91 | device.receive_message(msg) 92 | await asyncio.sleep(1, loop=loop) 93 | 94 | loop = asyncio.get_event_loop() 95 | loop.run_until_complete(run_test(loop)) 96 | _LOGGING.error('Got here') 97 | open_tasks = asyncio.Task.all_tasks(loop=loop) 98 | 99 | for task in open_tasks: 100 | if hasattr(task, 'name'): 101 | _LOGGING.error('Device: %s Task: %s', task.name, task) 102 | else: 103 | _LOGGING.error('Task: %s', task) 104 | if not task.done(): 105 | loop.run_until_complete(task) 106 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35, py36, py37, py38, pylint, flake8, pydocstyle 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | setenv = 7 | PYTHONPATH = {toxinidir}:{toxinidir}/insteonplm 8 | 9 | whitelist_externals = /usr/bin/env 10 | install_command = /usr/bin/env pip install {opts} {packages} 11 | commands = 12 | py.test --timeout=300 {posargs} 13 | deps = 14 | -r{toxinidir}/requirements_test.txt 15 | 16 | [testenv:pylint] 17 | basepython = {env:PYTHON3_PATH:python3} 18 | ignore_errors = True 19 | deps = 20 | -r{toxinidir}/requirements.txt 21 | -r{toxinidir}/requirements_test.txt 22 | commands = 23 | pylint insteonplm 24 | 25 | [testenv:flake8] 26 | basepython = {env:PYTHON3_PATH:python3} 27 | deps = 28 | -r{toxinidir}/requirements_test.txt 29 | commands = 30 | flake8 --ignore=W504,W503,E203 --max-line-length=89 insteonplm 31 | 32 | [testenv:pydocstyle] 33 | basepython = {env:PYTHON3_PATH:python3} 34 | deps = 35 | -r{toxinidir}/requirements_test.txt 36 | commands = 37 | pydocstyle insteonplm --------------------------------------------------------------------------------