pax_global_header00006660000000000000000000000064143232437710014520gustar00rootroot0000000000000052 comment=ee6f53ae102f1e1e5207e2c46442714245b8673e stu-gott-pykira-ee6f53a/000077500000000000000000000000001432324377100153035ustar00rootroot00000000000000stu-gott-pykira-ee6f53a/.gitignore000066400000000000000000000004731432324377100172770ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject stu-gott-pykira-ee6f53a/LICENSE.md000066400000000000000000000020341432324377100167060ustar00rootroot00000000000000Copyright (c) 2017 Stu Gott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. stu-gott-pykira-ee6f53a/MANIFEST.in000066400000000000000000000000451432324377100170400ustar00rootroot00000000000000include LICENSE.md include README.md stu-gott-pykira-ee6f53a/README.md000066400000000000000000000043341432324377100165660ustar00rootroot00000000000000pyKira ====== Lightweight Python 2 and Python 3 module to send and respond to UDP packets from Keene Electronic's IR-IP bridge. Dependencies ------------ No dependencies outside the builtin python modules How to Use ---------- >> import pykira >> kira = pykira.KiraReceiver() >> kira.registerCode("exampleCodeName", "") >> kira.registerCallback(callbackFunction) >> kira.start() In this snippet, callbackFunction will be called with "exampleCodeName" every time the matching IR code is received. How to Capture Codes: ---------- Use netcat! $ nc -k -u -l 65432 The port used here is the default port that Kira modules use. Please note, Kira packets are UDP. Codes sent by Kira start with "K " and be followed by a series of 4-digit hex values. Other Code Types: ---------- In addition to native Kira code sequences, both KiraReceiver and KiraModule can use Pronto codes. Pronto codes must be obtained from third parties. Additionally, KiraReceiver is able to use NEC codes. These will be of the form "XXXX XXXX" and must be obtained from your manufacturer. Using in Home Assistant: ---------- PyKira primarily exists to be used as a platform in Home Assistant. When the kira platform is first loaded, a file named ```kira_codes.yaml``` will be created in your configuration directory. You will need to add each code the kira platform should respond to here. An example might look like this: ``` - name: LivingRoomTVOn code: "K 2322 228A 1126 023E 0227 023E 0207 023F 0658 025D 0207 023F 0227 0220 0227 023F 0222 023E 0222 0220 067D 023F 0658 0222 0227 025C 0640 023F 0658 025D 0640 023E 0658 025D 0640 023F 0222 025C 0207 0222 0678 023E 0207 023F 0227 023F 0222 025C 063B 025C 0640 023E 0660 023E 0658 025D 0207 0222 0678 023E 0660 0220 0678 023E 0202 025D 0207 023F 2000" - name: LivingRoomTVOff code: "K 2322 22A7 1113 0220 0222 027A 01FA 025C 0640 023F 0222 025C 0202 023F 020F 023E 0222 025C 0202 025D 0640 023F 0658 023F 020F 023E 0658 025D 0640 023F 0658 025D 0640 023F 0658 025D 0640 023F 0222 025C 0640 023F 0222 023E 0207 025D 0207 025D 063B 025C 0640 025D 0202 023F 0658 023F 0227 023F 0658 023F 0660 023E 0640 023F 0227 025D 0202 025D 2000" ``` License ------- This code is released under the MIT license. stu-gott-pykira-ee6f53a/pykira/000077500000000000000000000000001432324377100166025ustar00rootroot00000000000000stu-gott-pykira-ee6f53a/pykira/__init__.py000066400000000000000000000001421432324377100207100ustar00rootroot00000000000000# pylint: disable=import-error from .receiver import KiraReceiver from .module import KiraModule stu-gott-pykira-ee6f53a/pykira/constants.py000066400000000000000000000000561432324377100211710ustar00rootroot00000000000000DEFAULT_HOST = '0.0.0.0' DEFAULT_PORT = 65432 stu-gott-pykira-ee6f53a/pykira/module.py000066400000000000000000000022131432324377100204370ustar00rootroot00000000000000""" KIRA interface to send UDP packets to a Keene Electronics IR-IP bridge. """ # pylint: disable=import-error import socket import sys import time from . import utils from .constants import ( DEFAULT_PORT ) class KiraModule(object): """ Construct and send IR-IP packets. """ def __init__(self, host, port=DEFAULT_PORT): """Construct a KIRA interface object.""" self.host = host self.port = port self.codeMap = {} def registerCode(self, codeName, code, codeType=None): rawCode = utils.code2kira(code, codeType=codeType) if rawCode: self.codeMap[codeName] = rawCode def sendCode(self, codeName, repeat=1, delay=0.05): code = self.codeMap.get(codeName) if code: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect((self.host, self.port)) if sys.version_info[0] == 2: code = "%s\n" % code else: code = bytes("%s\n" % code, "ascii") for i in range(repeat): sock.send(code) time.sleep(delay) sock.close() stu-gott-pykira-ee6f53a/pykira/receiver.py000066400000000000000000000060201432324377100207560ustar00rootroot00000000000000""" KIRA interface to receive UDP packets from a Keene Electronics IR-IP bridge. """ # pylint: disable=import-error import os import select import socket import sys import threading import time from . import utils from .constants import ( DEFAULT_HOST, DEFAULT_PORT ) class KiraReceiver(threading.Thread): """ Open a UDP socket to monitor for incoming IR-IP packets. """ def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT): """Construct a KIRA interface object.""" threading.Thread.__init__(self) self.stopped = threading.Event() self._callbacks = [] self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.host = host self.port = port self._socket.bind((host, port)) self.codeMap = {} self._state = 'idle' def registerCode(self, codeName, code, codeType=None): code = utils.mangleCode(code, codeType=codeType) if code: self.codeMap[code] = codeName def registerCallback(self, callback): self._callbacks.append(callback) def run(self): """Main loop of KIRA thread.""" while not self.stopped.isSet(): try: # if the current state is idle, just block and wait forever # if the current state is any other state, then a timeout of 200ms should # be reasonable in all cases. timeout = (self._state != 'idle') and 0.2 or None rdlist, _, _ = select.select([self._socket.fileno()], [], [], timeout) if not rdlist: if self._state != 'idle': self._state = 'idle' continue data = self._socket.recv(1024) if not data: # check if the socket is still valid try: os.fstat(recv._socket.fileno()) except socket.error: break continue code = utils.mangleIR(data, ignore_errors=True) codeName = self.codeMap.get(code) # some manufacturers repeat their IR codes several times in rapid # succession. by tracking the last code, we can eliminate redundant # state changes if codeName and (self._state != codeName): self._state = codeName for callback in self._callbacks: callback(codeName) except: time.sleep(0.1) def stop(self): self.stopped.set() # force receiver thread to wake from select sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect((self.host, self.port)) msg = "stop" datagram = sys.version_info[0] == 2 and bytes(msg) or bytes(msg, "utf-8") sock.send(datagram) sock.close() self.join() self._socket.close() stu-gott-pykira-ee6f53a/pykira/utils.py000066400000000000000000000100441432324377100203130ustar00rootroot00000000000000import binascii import re # this mangled "shorthand" output basically throws out exact timings while # keeping the relative length of each time period. Good for comparing # received codes against known values def mangleIR(data, ignore_errors=False): """Mangle a raw Kira data packet into shorthand""" try: # Packet mangling algorithm inspired by Rex Becket's kirarx vera plugin # Determine a median value for the timing packets and categorize each # timing as longer or shorter than that. This will always work for signals # that use pulse width modulation (since varying by long-short is basically # the definition of what PWM is). By lucky coincidence this also works with # the RC-5/RC-6 encodings used by Phillips (manchester encoding) # because time variations of opposite-phase/same-phase are either N or 2*N if isinstance(data, bytes): data = data.decode('ascii') data = data.strip() times = [int(x, 16) for x in data.split()[2:]] minTime = min(times[2:-1]) maxTime = max(times[2:-1]) margin = (maxTime - minTime) / 2 + minTime return ''.join([(x < margin and 'S' or 'L') for x in times]) except: # Probably a mangled packet. if not ignore_errors: raise # pronto codes provide basically the same information as a Kira code # just in a slightly different form. (Kira represents timings in uS # while pronto uses multiples of the base clock cycle.) # Thus they can be used for transmission def pronto2kira(data): """Convert a pronto code to a discrete (single button press) Kira code""" octets = [int(x, 16) for x in data.split()] preamble = octets[:4] convert = lambda x: 1000.0 / (x * 0.241246) freq = convert(preamble[1]) period = 1000000.0 / (freq * 1000.0) dataLen = preamble[2] res = "K %02X%02X " %(freq, dataLen) res += " ".join(["%0.4X" % min(0x2000, (period * x)) for x in octets[4: 4+(2*dataLen)]]) return res def mangleNec(code, freq=40): """Convert NEC code to shorthand notation""" # base time is 550 microseconds # unit of burst time # lead in pattern: 214d 10b3 # "1" burst pattern: 0226 0960 # "0" burst pattern: 0226 0258 # lead out pattern: 0226 2000 # there's large disagreement between devices as to a common preamble # or the "long" off period for the representation of a binary 1 # thus we can't construct a code suitable for transmission # without more information--but it's good enough for creating # a shorthand representaiton for use with recv timings = [] for octet in binascii.unhexlify(code.replace(" ", "")): burst = lambda x: x and "0226 06AD" or "0226 0258" for bit in reversed("%08d" % int(bin(ord(octet))[2:])): bit = int(bit) timings.append(burst(bit)) return mangleIR("K %0X22 214d 10b3 " % freq + " ".join(timings) + " 0226 2000") def inferCodeType(data): # a series of L/S chars if re.match('^[LS]+$', data): return 'shorthand' # "K " followed by groups of 4 hex chars if re.match("^K ([0-9a-fA-F]{4} )*[0-9a-fA-F]{4}$", data): return 'kira' # 2 groups of 4 hex chars if re.match("^[0-9a-fA-F]{4} [0-9a-fA-F]{4}$", data): return 'nec' # multiple groups of 4 hex chars if re.match("^([0-9a-fA-F]{4} )*[0-9a-fA-F]{4}$", data): return 'pronto' # convert code into a raw kira code that can be transmitted def code2kira(code, codeType=None): if codeType is None: codeType = inferCodeType(code) if codeType == "kira": return code.strip() if codeType == "pronto": return pronto2kira(code) # convert code to a form ready for comparison def mangleCode(code, codeType=None): if codeType is None: codeType = inferCodeType(code) if codeType == "shorthand": return code.strip() if codeType == "kira": return mangleIR(code) if codeType == "pronto": return mangleIR(pronto2kira(code)) if codeType == "nec": return mangleNec(code) stu-gott-pykira-ee6f53a/setup.cfg000066400000000000000000000003751432324377100171310ustar00rootroot00000000000000[bdist_wheel] # This flag says that the code is written to work on both Python 2 and Python # 3. If at all possible, it is good practice to do this. If you cannot, you # will need to generate wheels for each Python version that you support. universal=1 stu-gott-pykira-ee6f53a/setup.py000066400000000000000000000066371432324377100170310ustar00rootroot00000000000000"""A setuptools based setup module. See: https://github.com/stu-gott/pykira """ # Always prefer setuptools over distutils from setuptools import setup, find_packages # To use a consistent encoding from codecs import open import os setup( name='pykira', # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html version='0.1.3', description='Communicate with Kira IR-IP modules', long_description=" ".join( ["Lightweight Python 2 and Python 3 module to send and respond to UDP packets", "from Keene Electronic's IR-IP bridge."]), # The project's main homepage. url='https://github.com/stu-gott/pykira', # Author details author='Stu Gott', author_email='stu-gott@users.noreply.github.com', # Choose your license license='MIT', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # How mature is this project? Common values are # 3 - Alpha # 4 - Beta # 5 - Production/Stable 'Development Status :: 4 - Beta', # Indicate who your project is intended for 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', # Pick your license as you wish (should match "license" above) 'License :: OSI Approved :: MIT License', # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', ], # What does your project relate to? keywords='kira infrared remote', # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). packages=find_packages(exclude=['contrib', 'docs', 'tests']), # Alternatively, if you want to distribute just a my_module.py, uncomment # this: #py_modules=["pykira"], # List run-time dependencies here. These will be installed by pip when # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html install_requires=[], # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, # for example: # $ pip install -e .[dev,test] extras_require={ 'dev': ['check-manifest'], 'test': ['coverage'], }, # If there are data files included in your packages that need to be # installed, specify them here. If using Python 2.6 or less, then these # have to be included in MANIFEST.in as well. package_data={ }, # Although 'package_data' is the preferred approach, in some case you may # need to place data files outside of your packages. See: # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa # In this case, 'data_file' will be installed into '/my_data' data_files=[], )