pax_global_header00006660000000000000000000000064146116457470014531gustar00rootroot0000000000000052 comment=1774ea3849ce393680962f34128d3798f92df17d pyCEC-0.6.0/000077500000000000000000000000001461164574700124775ustar00rootroot00000000000000pyCEC-0.6.0/.coveragerc000066400000000000000000000006711461164574700146240ustar00rootroot00000000000000[run] source = pycec omit = pycec/__main__.py pycec/__init__.py pycec/tcp.py pycec/cec.py [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError pyCEC-0.6.0/.dockerignore000066400000000000000000000000121461164574700151440ustar00rootroot00000000000000.tox .git pyCEC-0.6.0/.github/000077500000000000000000000000001461164574700140375ustar00rootroot00000000000000pyCEC-0.6.0/.github/workflows/000077500000000000000000000000001461164574700160745ustar00rootroot00000000000000pyCEC-0.6.0/.github/workflows/ci.yml000066400000000000000000000024001461164574700172060ustar00rootroot00000000000000name: Tests on: [push, pull_request] jobs: pytest: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r test_requirements.txt pip install coveralls==2.1.2 - name: Run Tests run: pytest -vvv --cov=pycec tests/ - name: Upload Coverage run: coveralls env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: ${{ matrix.python-version }} COVERALLS_PARALLEL: true - name: Test Build run: python setup.py sdist style: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r lint_requirements.txt - name: Run Flake8 run: flake8 . pyCEC-0.6.0/.gitignore000066400000000000000000000001031461164574700144610ustar00rootroot00000000000000.idea build .eggs .cache .coverage pyCEC.egg-info dist __pycache__ pyCEC-0.6.0/CHANGELOG.rst000066400000000000000000000023601461164574700145210ustar00rootroot00000000000000Change Log ########## `0.6.0`_ 2024-01-27 ************* Added ===== - Publicly expose mute and volume status - New vendor ID mapping for Vizio Changed ======= - Base Docker image on balenalib instead of resin `0.5.2`_ 2022-07-08 ************* Added ===== - Added Python 3.9 support. - Added Python 3.10 support. `0.5.1`_ 2020-10-24 ******************* Fixed ===== - Fixed a ``TypeError`` exception when using the pyCEC server. `0.5.0`_ 2020-10-04 ******************* Added ===== - Added a changelog. - Added Python 3.8 testing. Changed ======= - Updated PyPi classifiers. - Updated asyncio syntax to use ``await`` and ``async def``. Removed ======= - Drop Python 3.4 support (EOL). Fixed ===== - Allow ``TcpAdapter`` to recover from a lost connection. - Added a missing ``await`` for an ``asyncio.sleep``. - Fixed long_description field for PyPi releases, the README will now render. `0.4.14`_ 2020-09-27 ******************** Changed ======= - Removed `typing` requirement. .. _Unreleased: https://github.com/konikvranik/pyCEC/compare/v0.5.1..HEAD .. _0.5.1: https://github.com/konikvranik/pyCEC/releases/tag/v0.5.1 .. _0.5.0: https://github.com/konikvranik/pyCEC/releases/tag/v0.5.0 .. _0.4.14: https://github.com/konikvranik/pyCEC/releases/tag/v0.4.14 pyCEC-0.6.0/Dockerfile000066400000000000000000000002471461164574700144740ustar00rootroot00000000000000FROM balenalib/armv7hf-debian-golang VOLUME /tmp/pycec RUN [ "apt-get", "-y", "install", "python3", "python3-pip", "libcec-dev" ] RUN [ "pip3", "install", "cec" ] pyCEC-0.6.0/LICENSE000066400000000000000000000020731461164574700135060ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Paulus Schoutsen 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. pyCEC-0.6.0/MANIFEST.in000066400000000000000000000001141461164574700142310ustar00rootroot00000000000000include README.rst include LICENSE graft pycec recursive-exclude * *.py[co] pyCEC-0.6.0/README.rst000066400000000000000000000053751461164574700142000ustar00rootroot00000000000000|Build Status| |PyPi Version| |Issue Count| |Coverage Status| ``pyCEC`` ========= Purpose of this project is to provide object API to libcec for home-assistant hdmi\_cec module as `primary goal `__ and to make TCP <=> HDMI bridge to control HDMI devices over TCP network as a `secondary goal `__. ``libcec`` dependency [1]_ -------------------------- `libcec `__ must be installed [2]_ for this module to work in direct mode. Follow the installation instructions for your environment, provided at the link. ``libcec`` installs Python 3 bindings by default as a system Python module. If you are running ``pyCEC`` in a *Python virtual environment*, make sure it can access the system module, by either symlinking it or using the ``--system-site-packages`` flag. .. [1] \:bulb: When using ``pyCEC`` as a network client, ``libcec`` is not needed. .. [2] \:warning: Do not use ``pip3 install cec``. This will fail. `Compile `__ ``libcec`` instead. running server -------------- You can run ``pyCEC`` server which will provide bridge between HDMI CEC port and TCP network by exexcuting ``python3 -m pycec``. Server will bind to default port ``9526`` on all interfaces. Then you can connect by client part of ``pyCEC`` without need of libcec or HDMI port on client's machine. Just use ``TcpAdapter`` instead of ``CecAdapter``. You can also connect to ``9526`` by `NetCat `_ and send CEC commands directly. home-assistant with multiple on/off switches -------------------------------------------- You can not only add a `hdmi_cec` instance to home-assistant with specified `host` for remote control of your TV, but also add switches for multiple TVs to turn on or off: .. code-block:: yaml switch: - platform: telnet switches: some_device_id: name: "Some Device Name" resource: xxx.xxx.xxx.xxx port: 9526 command_on: '10:04' command_off: '10:36' command_state: '10:8f' value_template: '{{ value == "01:90:00" }}' timeout: 1 .. |PyPi Version| image:: https://img.shields.io/pypi/v/pyCEC :target: https://pypi.org/project/pyCEC/ .. |Build Status| image:: https://github.com/konikvranik/pyCEC/workflows/Tests/badge.svg :target: https://github.com/konikvranik/pyCEC/actions .. |Issue Count| image:: https://img.shields.io/github/issues-raw/konikvranik/pyCEC :target: https://github.com/konikvranik/pyCEC/issues .. |Coverage Status| image:: https://img.shields.io/coveralls/github/konikvranik/pyCEC :target: https://coveralls.io/github/konikvranik/pyCEC pyCEC-0.6.0/lint_requirements.txt000066400000000000000000000000711461164574700170070ustar00rootroot00000000000000flake8==3.8.3 flake8-bugbear==20.1.4 pep8-naming==0.11.1 pyCEC-0.6.0/pycec.service000066400000000000000000000001511461164574700151610ustar00rootroot00000000000000[Unit] Description=pycec [Service] ExecStart=/usr/local/bin/pycec [Install] WantedBy=multi-user.target pyCEC-0.6.0/pycec/000077500000000000000000000000001461164574700136025ustar00rootroot00000000000000pyCEC-0.6.0/pycec/__init__.py000066400000000000000000000001601461164574700157100ustar00rootroot00000000000000"""pyCEC""" import logging _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 9526 DEFAULT_HOST = '0.0.0.0' pyCEC-0.6.0/pycec/__main__.py000066400000000000000000000131201461164574700156710ustar00rootroot00000000000000import asyncio import configparser import functools import logging import os from optparse import OptionParser from pycec import DEFAULT_PORT, DEFAULT_HOST from pycec.cec import CecAdapter from pycec.commands import CecCommand, PollCommand from . import _LOGGER from .network import HDMINetwork async def async_show_devices(network, loop): while True: for d in network.devices: _LOGGER.debug("Present device %s", d) await asyncio.sleep(10) def main(): config = configure() # Configure logging setup_logger(config) transports = set() loop = asyncio.get_event_loop() network = HDMINetwork(CecAdapter("pyCEC", activate_source=False), loop=loop) class CECServerProtocol(asyncio.Protocol): transport = None buffer = '' def connection_made(self, transport): _LOGGER.info("Connection opened by %s", transport.get_extra_info('peername')) self.transport = transport transports.add(transport) def data_received(self, data): self.buffer += bytes.decode(data) for line in self.buffer.splitlines(keepends=True): if line.endswith('\r') or line.endswith('\n'): line = line.rstrip() if len(line) == 2: _LOGGER.info("Received poll %s from %s", line, self.transport.get_extra_info('peername')) d = CecCommand(line).dst t = network._adapter.poll_device(d) t.add_done_callback( functools.partial(_after_poll, d)) else: _LOGGER.info("Received command %s from %s", line, self.transport.get_extra_info('peername')) network.send_command(CecCommand(line)) self.buffer = '' else: self.buffer = line def connection_lost(self, exc): _LOGGER.info("Connection with %s lost", self.transport.get_extra_info('peername')) transports.remove(self.transport) def _after_poll(d, f): if f.result(): cmd = PollCommand(network._adapter.get_logical_address(), src=d) _send_command_to_tcp(cmd) def _send_command_to_tcp(command): for t in transports: _LOGGER.info("Sending %s to %s", command, t.get_extra_info('peername')) t.write(str.encode("%s\r\n" % command.raw)) network.set_command_callback(_send_command_to_tcp) loop.run_until_complete(network.async_init()) _LOGGER.info("CEC initialized... Starting server.") # Each client connection will create a new protocol instance coro = loop.create_server(CECServerProtocol, config['DEFAULT']['host'], int(config['DEFAULT']['port'])) server = loop.run_until_complete(coro) # Serve requests until Ctrl+C is pressed _LOGGER.info('Serving on {}'.format(server.sockets[0].getsockname())) if _LOGGER.level >= logging.DEBUG: loop.create_task(async_show_devices(network, loop)) try: loop.run_forever() except KeyboardInterrupt: pass # Close the server server.close() loop.run_until_complete(server.wait_closed()) loop.close() def configure(): parser = OptionParser() parser.add_option("-i", "--interface", dest="host", action="store", type="string", default=DEFAULT_HOST, help=("Address of interface to bind to. Default is '%s'." % DEFAULT_HOST)) parser.add_option("-p", "--port", dest="port", action="store", type="int", default=DEFAULT_PORT, help=("Port to bind to. Default is '%s'." % DEFAULT_PORT)) parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Increase verbosity.") parser.add_option("-q", "--quiet", dest="quiet", action="count", default=0, help="Decrease verbosity.") (options, args) = parser.parse_args() script_dir = os.path.dirname(os.path.realpath(__file__)) config = configparser.ConfigParser() config['DEFAULT'] = {'host': options.host, 'port': options.port, 'logLevel': logging.INFO + ( (options.quiet - options.verbose) * 10)} paths = ['/etc/pycec.conf', script_dir + '/pycec.conf'] if 'HOME' in os.environ: paths.append(os.environ['HOME'] + '/.pycec') config.read(paths) return config def setup_logger(config): try: log_level = int(config['DEFAULT']['logLevel']) except ValueError: log_level = config['DEFAULT']['logLevel'] _LOGGER.setLevel(log_level) ch = logging.StreamHandler() ch.setLevel(log_level) try: from colorlog import ColoredFormatter formatter = ColoredFormatter( "%(log_color)s%(levelname)-8s %(message)s", datefmt=None, reset=True, log_colors={ 'DEBUG': 'cyan', 'INFO': 'green', 'WARNING': 'yellow', 'ERROR': 'red', 'CRITICAL': 'red', } ) except ImportError: formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) _LOGGER.addHandler(ch) if __name__ == "__main__": main() pyCEC-0.6.0/pycec/cec.py000066400000000000000000000064641461164574700147200ustar00rootroot00000000000000from concurrent.futures import ThreadPoolExecutor import logging from pycec.commands import CecCommand, KeyPressCommand from pycec.const import VENDORS, ADDR_RECORDINGDEVICE1 from pycec.network import AbstractCecAdapter _LOGGER = logging.getLogger(__name__) # pragma: no cover class CecAdapter(AbstractCecAdapter): def __init__(self, name: str = None, monitor_only: bool = None, activate_source: bool = None, device_type=ADDR_RECORDINGDEVICE1): super().__init__() self._adapter = None self._io_executor = ThreadPoolExecutor(1) import cec self._cecconfig = cec.libcec_configuration() if monitor_only is not None: self._cecconfig.bMonitorOnly = 1 if monitor_only else 0 self._cecconfig.strDeviceName = name[:13] if activate_source is not None: self._cecconfig.bActivateSource = 1 if activate_source else 0 self._cecconfig.deviceTypes.Add(device_type) def set_command_callback(self, callback): self._cecconfig.SetKeyPressCallback( lambda key, delay: callback(KeyPressCommand(key).raw)) self._cecconfig.SetCommandCallback(callback) def standby_devices(self): self._loop.run_in_executor(self._io_executor, self._adapter.StandbyDevices) def poll_device(self, device): return self._loop.run_in_executor( self._io_executor, self._adapter.PollDevice, device) def shutdown(self): self._io_executor.shutdown() if self._adapter: self._adapter.Close() def get_logical_address(self): return self._adapter.GetLogicalAddresses().primary def power_on_devices(self): self._loop.run_in_executor(self._io_executor, self._adapter.PowerOnDevices) def transmit(self, command: CecCommand): self._loop.run_in_executor( self._io_executor, self._adapter.Transmit, self._adapter.CommandFromString(command.raw)) def init(self, callback: callable = None): return self._loop.run_in_executor(self._io_executor, self._init, callback) def _init(self, callback: callable = None): import cec if not self._cecconfig.clientVersion: self._cecconfig.clientVersion = cec.LIBCEC_VERSION_CURRENT _LOGGER.debug("Initializing CEC...") adapter = cec.ICECAdapter.Create(self._cecconfig) _LOGGER.debug("Created adapter") a = None adapters = adapter.DetectAdapters() for a in adapters: _LOGGER.info("found a CEC adapter:") _LOGGER.info("port: " + a.strComName) _LOGGER.info("vendor: " + ( VENDORS[a.iVendorId] if a.iVendorId in VENDORS else hex( a.iVendorId))) _LOGGER.info("product: " + hex(a.iProductId)) a = a.strComName if a is None: _LOGGER.warning("No adapters found") else: if adapter.Open(a): _LOGGER.info("connection opened") self._adapter = adapter self._initialized = True else: _LOGGER.error("failed to open a connection to the CEC adapter") if callback: callback() pyCEC-0.6.0/pycec/commands.py000066400000000000000000000044601461164574700157610ustar00rootroot00000000000000from typing import List from pycec.const import CMD_KEY_PRESS, CMD_KEY_RELEASE, CMD_POLL class CecCommand: def __init__(self, cmd, dst: int = None, src: int = None, att: List[int] = None, raw: str = None): self._src = src self._dst = dst self._cmd = cmd self._att = att if raw is not None: self._raw(raw) elif isinstance(cmd, (str,)): self._raw(cmd) else: self.src = src self.dst = dst self._cmd = cmd self._att = att @property def src(self) -> int: return self._src @src.setter def src(self, value: int): self._src = value @property def dst(self) -> int: return self._dst @dst.setter def dst(self, value: int): self._dst = value @property def cmd(self) -> int: return self._cmd @property def att(self) -> List[int]: return self._att if self._att else [] def _att(self, value: List[int]): # pragma: no cover self._att = value @property def raw(self) -> str: atts = "".join(((":%02x" % i) for i in self.att)) cmd = ("" if self.cmd is None else (":%02x" % self.cmd)) return "%1x%1x%s%s" % (self.src if self.src is not None else 0xf, self.dst if self.dst is not None else 0xf, cmd, atts) def _raw(self, value: str): atts = value.split(':') self.src = int(atts[0][0], 16) self.dst = int(atts[0][1], 16) if len(atts) > 1: self._cmd = int(atts[1], 16) self._att = list(int(x, 16) for x in atts[2:]) else: self._cmd = None self._att = None def __str__(self): return self.raw class KeyPressCommand(CecCommand): def __init__(self, key, dst: int = None, src: int = None): super().__init__(CMD_KEY_PRESS, dst, src, [key]) self._key = key @property def key(self): return self._key class KeyReleaseCommand(CecCommand): def __init__(self, dst: int = None, src: int = None): super().__init__(CMD_KEY_RELEASE, dst, src) class PollCommand(CecCommand): def __init__(self, dst, src: int = None): super().__init__(CMD_POLL, dst, src) pyCEC-0.6.0/pycec/const.py000066400000000000000000000063141461164574700153060ustar00rootroot00000000000000VENDORS = {0x0020C7: 'Akai', 0x0010FA: 'Apple', 0x002467: 'AOC', 0x8065E9: 'Benq', 0x18C086: 'Broadcom', 0x009053: 'Daewoo', 0x0005CD: 'Denon', 0x001A11: 'Google', 0x00D0D5: 'Grundig', 0x001950: 'Harman Kardon', 0x9C645E: 'Harman Kardon', 0x00E091: 'LG', 0x000982: 'Loewe', 0x000678: 'Marantz', 0x000CB8: 'Medion', 0x0009B0: 'Onkyo', 0x008045: 'Panasonic', 0x00903E: 'Philips', 0x00E036: 'Pioneer', 0x001582: 'Pulse Eight', 0x8AC72E: 'Roku', 0x0000F0: 'Samsung', 0x08001F: 'Sharp', 0x534850: 'Sharp', 0x080046: 'Sony', 0x000039: 'Toshiba', 0x000CE7: 'Toshiba', 0x6B746D: 'Vizio', 0x00199D: 'Vizio', 0x00A0DE: 'Yamaha', 0: 'Unknown'} CEC_LOGICAL_TO_TYPE = [0, # TV0 1, # Recorder 1 1, # Recorder 2 3, # Tuner 1 4, # Playback 1 5, # Audio 3, # Tuner 2 3, # Tuner 3 4, # Playback 2 1, # Recorder 3 3, # Tuner 4 4, # Playback 3 2, # Reserved 1 2, # Reserved 2 2, # Free use 2] # Broadcast DEVICE_TYPE_NAMES = ["TV", "Recorder", "UNKNOWN", "Tuner", "Playback", "Audio"] TYPE_TV = 0 TYPE_RECORDER = 1 TYPE_UNKNOWN = 2 TYPE_TUNER = 3 TYPE_PLAYBACK = 4 TYPE_AUDIO = 5 CMD_PHYSICAL_ADDRESS = (0x83, 0x84) CMD_POWER_STATUS = (0x8f, 0x90) CMD_AUDIO_STATUS = (0x71, 0x7a) CMD_VENDOR = (0x8c, 0x87) CMD_MENU_LANGUAGE = (0x91, 0x32) CMD_OSD_NAME = (0x46, 0x47) CMD_AUDIO_MODE_STATUS = (0x7d, 0x7e) CMD_DECK_STATUS = (0x1a, 0x1b) CMD_TUNER_STATUS = (0x07, 0x08) CMD_MENU_STATUS = (0x8d, 0x8e) CMD_ACTIVE_SOURCE = 0x82 CMD_STREAM_PATH = 0x86 CMD_KEY_PRESS = 0x44 CMD_KEY_RELEASE = 0x45 CMD_PLAY = 0x41 CMD_STANDBY = 0x36 CMD_POLL = None STATUS_PLAY = 0x11 STATUS_RECORD = 0x12 STATUS_STILL = 0x14 STATUS_STOP = 0x1a STATUS_OTHER = 0x1f POWER_ON = 0x00 POWER_OFF = 0x01 PLAY_FORWARD = 0x24 PLAY_STILL = 0x25 PLAY_FAST_FORWARD_MEDIUM = 0x06 PLAY_FAST_REVERSE_MEDIUM = 0x0a KEY_VOLUME_DOWN = 0x42 KEY_VOLUME_UP = 0x41 KEY_MUTE_TOGGLE = 0x43 KEY_MUTE_ON = 0x65 KEY_MUTE_OFF = 0x66 KEY_PLAY = 0x44 KEY_MUTE_FUNCTION = 0x65 KEY_STOP = 0x45 KEY_PAUSE = 0x46 KEY_RECORD = 0x47 KEY_REWIND = 0x48 KEY_FAST_FORWARD = 0x49 KEY_EJECT = 0x4a KEY_FORWARD = 0x4b KEY_BACKWARD = 0x4c KEY_STOP_RECORD = 0x4d KEY_PAUSE_RECORD = 0x4e KEY_INPUT_SELECT = 0x34 KEY_POWER = 0x40 KEY_POWER_ON = 0x6d KEY_POWER_OFF = 0x6c KEY_POWER_TOGGLE = 0x6b ADDR_UNKNOWN = -1 ADDR_TV = 0 ADDR_RECORDINGDEVICE1 = 1 ADDR_RECORDINGDEVICE2 = 2 ADDR_TUNER1 = 3 ADDR_PLAYBACKDEVICE1 = 4 ADDR_AUDIOSYSTEM = 5 ADDR_TUNER2 = 6 ADDR_TUNER3 = 7 ADDR_PLAYBACKDEVICE2 = 8 ADDR_RECORDINGDEVICE3 = 9 ADDR_TUNER4 = 10 ADDR_PLAYBACKDEVICE3 = 11 ADDR_RESERVED1 = 12 ADDR_RESERVED2 = 13 ADDR_FREEUSE = 14 ADDR_UNREGISTERED = 15 ADDR_BROADCAST = 15 pyCEC-0.6.0/pycec/network.py000066400000000000000000000407351461164574700156560ustar00rootroot00000000000000import asyncio import functools from functools import reduce from multiprocessing import Queue from typing import List import time from pycec import _LOGGER from pycec.commands import CecCommand from pycec.const import CMD_OSD_NAME, VENDORS, DEVICE_TYPE_NAMES, \ CMD_ACTIVE_SOURCE, CMD_STREAM_PATH, ADDR_BROADCAST, CMD_DECK_STATUS, \ CMD_AUDIO_STATUS from pycec.const import CMD_PHYSICAL_ADDRESS, CMD_POWER_STATUS, CMD_VENDOR DEFAULT_SCAN_INTERVAL = 30 DEFAULT_UPDATE_PERIOD = 30 DEFAULT_SCAN_DELAY = 1 UPDATEABLE = {CMD_POWER_STATUS: "_update_power_status", CMD_OSD_NAME: "_update_osd_name", CMD_VENDOR: "_update_vendor", CMD_PHYSICAL_ADDRESS: "_update_physical_address", CMD_DECK_STATUS: "_update_playing_status", CMD_AUDIO_STATUS: "_update_audio_status"} class PhysicalAddress: def __init__(self, address): self._physical_address = int() if isinstance(address, (str,)): address = int(address.replace('.', '').replace(':', ''), 16) if isinstance(address, (tuple, list,)): if len(address) == 2: self._physical_address = int("%02x%02x" % tuple(address), 16) elif len(address) == 4: self._physical_address = int("%x%x%x%x" % tuple(address), 16) else: raise AttributeError("Incorrect count of members in list!") elif isinstance(address, (int,)): self._physical_address = address @property def asattr(self) -> List[int]: return [self._physical_address // 0x100, self._physical_address % 0x100] @property def asint(self) -> int: return self._physical_address @property def ascmd(self) -> str: return "%x%x:%x%x" % tuple( x for x in _to_digits(self._physical_address)) @property def asstr(self) -> str: return ".".join(("%x" % x) for x in _to_digits(self._physical_address)) def __str__(self): return self.asstr class AbstractCecAdapter: def __init__(self): self._initialized = False self._loop = None def init(self, callback: callable = None): raise NotImplementedError def poll_device(self, device): raise NotImplementedError def get_logical_address(self): raise NotImplementedError def transmit(self, command: CecCommand): raise NotImplementedError def standby_devices(self): raise NotImplementedError def power_on_devices(self): raise NotImplementedError def set_command_callback(self, callback): raise NotImplementedError def shutdown(self): raise NotImplementedError @property def initialized(self): return self._initialized def set_event_loop(self, loop): self._loop = loop class HDMIDevice: def __init__(self, logical_address: int, network=None, update_period=DEFAULT_UPDATE_PERIOD, loop=None): self._loop = loop self._logical_address = logical_address self.name = "hdmi_%x" % logical_address self._physical_address = None self._power_status = int() self._audio_status = int() self._is_active_source = False self._vendor_id = int() self._menu_language = str() self._osd_name = str() self._audio_mode_status = int() self._volume_status = int() self._mute_status = False self._deck_status = int() self._tuner_status = int() self._menu_status = int() self._record_status = int() self._timer_cleared_status = int() self._timer_status = int() self._network = network self._updates = {cmd: False for cmd in UPDATEABLE} self._stop = False self._update_period = update_period self._type = int() self._update_callback = None self._status = None self._task = None @property def logical_address(self) -> int: return self._logical_address @property def physical_address(self) -> PhysicalAddress: return self._physical_address @property def power_status(self) -> int: return self._power_status @property def status(self) -> int: return self._status @property def vendor_id(self) -> int: return self._vendor_id @property def vendor(self) -> str: return ( VENDORS[self._vendor_id] if self._vendor_id in VENDORS else hex( self._vendor_id)) @property def osd_name(self) -> str: return self._osd_name @property def is_on(self): return self.power_status == 0x00 @property def is_off(self): return self.power_status == 0x01 def turn_on(self): # pragma: no cover self._loop.create_task(self.async_turn_on()) async def async_turn_on(self): # pragma: no cover command = CecCommand(0x44, self.logical_address, att=[0x6d]) await self.async_send_command(command) def turn_off(self): # pragma: no cover self._loop.create_task(self.async_turn_off()) async def async_turn_off(self): # pragma: no cover command = CecCommand(0x44, self.logical_address, att=[0x6c]) await self.async_send_command(command) def toggle(self): # pragma: no cover self._loop.create_task(self.async_toggle()) async def async_toggle(self): # pragma: no cover command = CecCommand(0x44, self.logical_address, att=[0x40]) await self.async_send_command(command) @property def type(self): return self._type @property def type_name(self): return ( DEVICE_TYPE_NAMES[self.type] if self.type in range(6) else DEVICE_TYPE_NAMES[2]) @property def mute_status(self) -> bool: return self._mute_status @property def volume_status(self) -> int: return self._volume_status def update_callback(self, command: CecCommand): result = False for prop in filter(lambda x: x[1] == command.cmd, UPDATEABLE): getattr(self, UPDATEABLE[prop])(command) self._updates[prop[0]] = True result = True if result: # pragma: no cover if self._update_callback: self._loop.call_soon_threadsafe(self._update_callback, self) return result def _update_osd_name(self, command): self._osd_name = reduce(lambda x, y: x + chr(y), command.att, "") def _update_vendor(self, command): self._vendor_id = reduce(lambda x, y: x * 0x100 + y, command.att) def _update_playing_status(self, command): self._status = command.att[0] def _update_power_status(self, command): self._power_status = command.att[0] def _update_physical_address(self, command): self._physical_address = PhysicalAddress(command.att[0:2]) if len(command.att) > 2: self._type = command.att[2] def _update_audio_status(self, command): self._mute_status = bool(command.att[0] & 0x80) raw_volume_status = command.att[0] & 0x7f if raw_volume_status == 0x7f: # Volume is unknown self._updates[CMD_AUDIO_STATUS[0]] = False else: # Valid volumes cover a range of 0-100, just clamp invalid values self._volume_status = min(raw_volume_status, 100) @property def task(self): return self._task @task.setter def task(self, task): self._task = task async def async_run(self): _LOGGER.debug("Starting device %d", self.logical_address) while not self._stop: for prop in UPDATEABLE: if not self._stop: await self.async_request_update(prop[0]) start_time = self._loop.time() while not self._stop and self._loop.time() <= ( start_time + self._update_period ): await asyncio.sleep(0.3) _LOGGER.info("HDMI device %s stopped.", self) # pragma: no cover def stop(self): # pragma: no cover _LOGGER.debug("HDMI device %s stopping", self) self._stop = True async def async_request_update(self, cmd: int): self._updates[cmd] = False command = CecCommand(cmd) await self.async_send_command(command) def send_command(self, command): self._loop.create_task(self.async_send_command(command)) async def async_send_command(self, command: CecCommand): command.dst = self._logical_address await self._network.async_send_command(command) def active_source(self): self._loop.create_task( self._network.async_active_source(self.physical_address)) @property def is_updated(self, cmd): return self._updates[cmd] def __eq__(self, other): return (isinstance(other, ( HDMIDevice,)) and self.logical_address == other.logical_address) def __hash__(self): return self._logical_address def __str__(self): return "HDMI %d: %s, %s (%s), power %s" % ( self.logical_address, self.vendor, self.osd_name, str(self.physical_address), self.power_status) def set_update_callback(self, callback): # pragma: no cover self._update_callback = callback class HDMINetwork: def __init__(self, adapter: AbstractCecAdapter, scan_interval=DEFAULT_SCAN_INTERVAL, loop=None): self._running = False self._device_status = dict() self._managed_loop = loop is None if self._managed_loop: self._loop = asyncio.new_event_loop() else: _LOGGER.warning("Be aware! Network is using shared event loop!") self._loop = loop self._adapter = adapter self._adapter.set_event_loop(self._loop) self._scan_delay = DEFAULT_SCAN_DELAY self._scan_interval = scan_interval self._command_queue = Queue() self._devices = dict() self._command_callback = None self._device_added_callback = None self._initialized_callback = None self._device_removed_callback = None @property def initialized(self): return self._adapter.initialized def init(self): self._loop.create_task(self.async_init()) async def async_init(self): _LOGGER.debug("initializing") # pragma: no cover _LOGGER.debug("setting callback") # pragma: no cover self._adapter.set_command_callback(self.command_callback) _LOGGER.debug("Callback set") # pragma: no cover task = self._adapter.init(self._initialized_callback) self._running = True while not (task.done() or task.cancelled()) and self._running: _LOGGER.debug("Init pending - %s", task) # pragma: no cover await asyncio.sleep(1) _LOGGER.debug("Init done") # pragma: no cover def scan(self): self._loop.create_task(self.async_scan()) def _after_polled(self, device, task): self._device_status[device] = task.result() if self._device_status[device] and device not in self._devices: self._devices[device] = HDMIDevice(device, self, loop=self._loop) if self._device_added_callback: self._loop.call_soon_threadsafe(self._device_added_callback, self._devices[device]) task = self._loop.create_task(self._devices[device].async_run()) self._devices[device].task = task _LOGGER.debug("Found device %d", device) elif not self._device_status[device] and device in self._devices: self.get_device(device).stop() if self._device_removed_callback: self._loop.call_soon_threadsafe(self._device_removed_callback, self._devices[device]) del (self._devices[device]) async def async_scan(self): _LOGGER.info("Looking for new devices...") if not self.initialized: _LOGGER.error("Device not initialized!!!") # pragma: no cover return for d in range(15): task = self._adapter.poll_device(d) task.add_done_callback(functools.partial(self._after_polled, d)) def send_command(self, command): self._loop.create_task(self.async_send_command(command)) async def async_send_command(self, command): if isinstance(command, str): command = CecCommand(command) _LOGGER.debug("<< %s", command) if command.src is None or command.src == 0xf: command.src = self._adapter.get_logical_address() self._loop.call_soon_threadsafe(self._adapter.transmit, command) def standby(self): self._loop.create_task(self.async_standby()) async def async_standby(self): _LOGGER.debug("Queuing system standby") # pragma: no cover self._loop.call_soon_threadsafe(self._adapter.standby_devices) def power_on(self): self._loop.create_task(self.async_power_on()) async def async_power_on(self): _LOGGER.debug("Queuing power on") # pragma: no cover self._loop.call_soon_threadsafe(self._adapter.power_on_devices) def active_source(self, source: PhysicalAddress): self._loop.create_task(self.async_active_source(source)) async def async_active_source(self, addr: PhysicalAddress): await self.async_send_command( CecCommand(CMD_ACTIVE_SOURCE, ADDR_BROADCAST, att=addr.asattr)) await self.async_send_command( CecCommand(CMD_STREAM_PATH, ADDR_BROADCAST, att=addr.asattr)) @property def devices(self) -> tuple: return tuple(self._devices.values()) def get_device(self, i) -> HDMIDevice: return self._devices.get(i, None) async def async_watch(self, loop=None): _LOGGER.debug("Start watching...") # pragma: no cover if loop is None: loop = self._loop _LOGGER.debug("loop: %s", loop) while self._running: if self.initialized: _LOGGER.debug("Scanning...") # pragma: no cover await self.async_scan() _LOGGER.debug("Sleep...") # pragma: no cover start_time = self._loop.time() while ( self._loop.time() <= (start_time + self._scan_interval) and self._running ): await asyncio.sleep(0.3) else: _LOGGER.warning("Not initialized. Waiting for init.") await asyncio.sleep(1) _LOGGER.info("No watching anymore") def start(self): _LOGGER.info("HDMI network starting...") # pragma: no cover self._running = True self._loop.create_task(self.async_init()) self._loop.create_task(self.async_watch()) if self._managed_loop: self._loop.run_in_executor(None, self._loop.run_forever) def command_callback(self, raw_command): _LOGGER.debug("%s", raw_command) # pragma: no cover self._loop.call_soon_threadsafe(self._async_callback, raw_command) def _async_callback(self, raw_command): command = CecCommand(raw_command[3:]) updated = False if command.src == 15: for i in range(15): updated |= self.get_device(i).update_callback(command) pass elif command.src in self._devices: updated = self.get_device(command.src).update_callback(command) pass if not updated: if self._command_callback: self._loop.call_soon_threadsafe( self._command_callback, command) def stop(self): _LOGGER.debug("HDMI network shutdown.") # pragma: no cover self._running = False for d in self._devices.values(): d.stop() if self._managed_loop: self._loop.stop() while self._loop.is_running(): time.sleep(.1) self._loop.close() self._adapter.shutdown() _LOGGER.info("HDMI network stopped.") # pragma: no cover def set_command_callback(self, callback): self._command_callback = callback def set_new_device_callback(self, callback): self._device_added_callback = callback def set_device_removed_callback(self, callback): self._device_removed_callback = callback def set_initialized_callback(self, callback): self._initialized_callback = callback def _to_digits(x: int) -> List[int]: for x in ("%04x" % x): yield int(x, 16) pyCEC-0.6.0/pycec/tcp.py000066400000000000000000000136241461164574700147500ustar00rootroot00000000000000import asyncio import functools import logging import time from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand, \ PollCommand from pycec.const import CMD_STANDBY, KEY_POWER from pycec.network import AbstractCecAdapter, HDMINetwork DEFAULT_PORT = 9526 MAX_CONNECTION_ATTEMPTS = 5 CONNECTION_ATTEMPT_DELAY = 3 _LOGGER = logging.getLogger(__name__) # pragma: no cover class TcpAdapter(AbstractCecAdapter): def __init__(self, host, port=DEFAULT_PORT, name=None, activate_source=None): super().__init__() self._polling = dict() self._command_callback = None self._tcp_loop = asyncio.new_event_loop() self._host = host self._port = port self._transport = None self._osd_name = name self._activate_source = activate_source def _after_init(self, callback, f): if self._transport: _LOGGER.debug("New client: %s", self._transport) self._initialized = True self._tcp_loop.run_in_executor(None, self._tcp_loop.run_forever) if callback: callback() def _init(self): for i in range(0, MAX_CONNECTION_ATTEMPTS): try: self._transport, protocol = self._tcp_loop.run_until_complete( self._tcp_loop.create_connection(lambda: TcpProtocol(self), host=self._host, port=self._port)) _LOGGER.debug("Connection started.") break except (ConnectionRefusedError, RuntimeError) as e: _LOGGER.warning( "Unable to connect due to %s. Trying again in %d seconds, " "%d attempts remaining.", e, CONNECTION_ATTEMPT_DELAY, MAX_CONNECTION_ATTEMPTS - i) time.sleep(CONNECTION_ATTEMPT_DELAY) else: _LOGGER.error("Unable to connect! Giving up.") self.shutdown() def init(self, callback: callable = None): _LOGGER.debug("Starting connection...") task = self._loop.run_in_executor(None, self._init) task.add_done_callback(functools.partial(self._after_init, callback)) return task def shutdown(self): self._initialized = False if self._transport and not self._transport.is_closing(): self._transport.close() self._transport = None def _poll_device(self, device): req = self._loop.time() poll_bucket = self._polling.get(device, set()) poll_bucket.add(req) self._polling.update({device: poll_bucket}) self.transmit(PollCommand(device)) while True: if req not in self._polling.get(device, set()): _LOGGER.debug("Found device %d.", device) return True if self._loop.time() > (req + 5): return False time.sleep(.1) def poll_device(self, device): return self._loop.run_in_executor(None, self._poll_device, device) def get_logical_address(self): return 0xf def standby_devices(self): self.transmit(CecCommand(CMD_STANDBY)) def transmit(self, command: CecCommand): self._transport.write(("%s\r\n" % command.raw).encode()) def set_command_callback(self, callback): self._command_callback = callback def power_on_devices(self): self.transmit(KeyPressCommand(KEY_POWER)) self.transmit(KeyReleaseCommand()) def set_transport(self, transport): self._transport = transport class TcpProtocol(asyncio.Protocol): buffer = '' def __init__(self, adapter: TcpAdapter): self._adapter = adapter self.transport = None def connection_made(self, transport): self.transport = transport self._adapter.set_transport = transport def data_received(self, data: bytes): self.buffer += bytes.decode(data) for line in self.buffer.splitlines(keepends=True): if line.count('\n') or line.count('\r'): line = line.rstrip() _LOGGER.debug("Received %s from %s", line, self.transport.get_extra_info('peername')) if len(line) == 2: cmd = CecCommand(line) if cmd.src in self._adapter._polling: del self._adapter._polling[cmd.src] else: self._adapter._command_callback("<< " + line) self.buffer = '' else: self.buffer = line def eof_received(self): self._adapter.shutdown() def connection_lost(self, exc): _LOGGER.warning("Connection lost. Trying to reconnect...") self._adapter.shutdown() self._adapter._tcp_loop.stop() self._adapter.init() def main(): """For testing purpose""" tcp_adapter = TcpAdapter("192.168.1.5", name="HASS", activate_source=False) hdmi_network = HDMINetwork(tcp_adapter) hdmi_network.start() while True: for d in hdmi_network.devices: _LOGGER.info("Device: %s", d) time.sleep(7) if __name__ == '__main__': # Configure logging _LOGGER.setLevel(logging.DEBUG) ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) try: from colorlog import ColoredFormatter formatter = ColoredFormatter( "%(log_color)s%(levelname)-8s %(message)s", datefmt=None, reset=True, log_colors={ 'DEBUG': 'cyan', 'INFO': 'green', 'WARNING': 'yellow', 'ERROR': 'red', 'CRITICAL': 'red', } ) except ImportError: formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) _LOGGER.addHandler(ch) main() pyCEC-0.6.0/pyproject.toml000066400000000000000000000000361461164574700154120ustar00rootroot00000000000000[tool.black] line-length = 79 pyCEC-0.6.0/scripts/000077500000000000000000000000001461164574700141665ustar00rootroot00000000000000pyCEC-0.6.0/scripts/travis.sh000077500000000000000000000045461461164574700160460ustar00rootroot00000000000000#!/bin/bash # Based on a test script from avsm/ocaml repo https://github.com/avsm/ocaml CHROOT_DIR=/tmp/arm-chroot MIRROR=http://archive.raspbian.org/raspbian VERSION=wheezy CHROOT_ARCH=armhf # Debian package dependencies for the host HOST_DEPENDENCIES="debootstrap qemu-user-static binfmt-support sbuild" # Debian package dependencies for the chrooted environment GUEST_DEPENDENCIES="build-essential git m4 sudo python" # Command used to run the tests TEST_COMMAND="$@" function setup_arm_chroot { # Host dependencies sudo apt-get install -qq -y ${HOST_DEPENDENCIES} # Create chrooted environment sudo mkdir ${CHROOT_DIR} sudo debootstrap --foreign --no-check-gpg --include=fakeroot,build-essential \ --arch=${CHROOT_ARCH} ${VERSION} ${CHROOT_DIR} ${MIRROR} sudo cp /usr/bin/qemu-arm-static ${CHROOT_DIR}/usr/bin/ sudo chroot ${CHROOT_DIR} ./debootstrap/debootstrap --second-stage sudo sbuild-createchroot --arch=${CHROOT_ARCH} --foreign --setup-only \ ${VERSION} ${CHROOT_DIR} ${MIRROR} # Create file with environment variables which will be used inside chrooted # environment echo "export ARCH=${ARCH}" > envvars.sh echo "export TRAVIS_BUILD_DIR=${TRAVIS_BUILD_DIR}" >> envvars.sh chmod a+x envvars.sh # Install dependencies inside chroot sudo chroot ${CHROOT_DIR} apt-get update sudo chroot ${CHROOT_DIR} apt-get --allow-unauthenticated install \ -qq -y ${GUEST_DEPENDENCIES} # Create build dir and copy travis build files to our chroot environment sudo mkdir -p ${CHROOT_DIR}/${TRAVIS_BUILD_DIR} sudo rsync -av ${TRAVIS_BUILD_DIR}/ ${CHROOT_DIR}/${TRAVIS_BUILD_DIR}/ # Indicate chroot environment has been set up sudo touch ${CHROOT_DIR}/.chroot_is_done # Call ourselves again which will cause tests to run sudo chroot ${CHROOT_DIR} bash -c "cd ${TRAVIS_BUILD_DIR} && ./scripts/travis.sh" } if [ -e "/.chroot_is_done" ]; then # We are inside ARM chroot echo "Running inside chrooted environment" . ./envvars.sh else if [ "${ARCH}" = "arm" ]; then # ARM test run, need to set up chrooted environment first echo "Setting up chrooted ARM environment" setup_arm_chroot fi fi apt-get install libcec-dev pip install -r requirements.txt pip install coveralls python setup.py -q install echo "Running tests" echo "Environment: $(uname -a)" ${TEST_COMMAND}pyCEC-0.6.0/setup.cfg000066400000000000000000000001621461164574700143170ustar00rootroot00000000000000[tool:pytest] addopts = --verbose testpaths = tests norecursedirs = .git testing_config [bdist_wheel] universal=1pyCEC-0.6.0/setup.py000077500000000000000000000024731461164574700142220ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- from setuptools import setup, find_packages import os this_dir = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(this_dir, "README.rst"), "r") as f: long_description = f.read() PACKAGES = find_packages(exclude=["tests", "tests.*", "build"]) setup( name="pyCEC", version="0.6.0", author="Petr Vraník", author_email="hpa@suteren.net", description=( "Provide HDMI CEC devices as objects," " especially for use with Home Assistant" ), license="MIT", keywords="cec hdmi home-assistant", url="https://github.com/konikvranik/pycec/", packages=PACKAGES, install_requires=[], long_description=long_description, test_suite="tests", classifiers=[ "Development Status :: 4 - Beta", "Topic :: Utilities", "Topic :: Home Automation", "Topic :: Multimedia", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], entry_points={ "console_scripts": [ "pycec=pycec.__main__:main", ], }, ) pyCEC-0.6.0/test_requirements.txt000066400000000000000000000000401461164574700170140ustar00rootroot00000000000000pytest==7.1.2 pytest-cov==3.0.0 pyCEC-0.6.0/tests/000077500000000000000000000000001461164574700136415ustar00rootroot00000000000000pyCEC-0.6.0/tests/__init__.py000066400000000000000000000000211461164574700157430ustar00rootroot00000000000000"""Unit tests""" pyCEC-0.6.0/tests/datastruct/000077500000000000000000000000001461164574700160175ustar00rootroot00000000000000pyCEC-0.6.0/tests/datastruct/test_cec_command.py000066400000000000000000000032011461164574700216540ustar00rootroot00000000000000from pycec.commands import CecCommand from pycec.const import CMD_POLL def test_src(): cc = CecCommand("1f:90:02") assert cc.src == 0x1 cc = CecCommand("52:90:02") assert cc.src == 0x5 cc = CecCommand(0x8F, 0x5, 0x3) assert cc.src == 0x3 cc = CecCommand("52:90:02") cc.src = 0x4 assert ("%s" % cc) == "42:90:02" cc = CecCommand(0x8F, 0x5, 0x3, raw="78:a5:89:45") assert cc.src == 0x7 def test_dst(): cc = CecCommand("1f:90:02") assert cc.dst == 0xF cc = CecCommand("52:90:02") assert cc.dst == 0x2 cc = CecCommand(0x8F, 0x5, 0x3) assert cc.dst == 0x5 cc = CecCommand("52:90:02") cc.dst = 0x4 assert ("%s" % cc) == "54:90:02" cc = CecCommand(0x8F, 0x5, 0x3, raw="78:a5:89:45") assert cc.dst == 0x8 def test_cmd(): cc = CecCommand("1f:90:02") assert cc.cmd == 0x90 cc = CecCommand("52:8f:02") assert cc.cmd == 0x8F cc = CecCommand(0x8F, 0x5, 0x3) assert cc.cmd == 0x8F cc = CecCommand(0x8F, 0x5, 0x3, raw="78:a5:89:45") assert cc.cmd == 0xA5 def test_att(): cc = CecCommand("1f:90:02") assert cc.att == [0x02] cc = CecCommand("52:8f:02:56") assert cc.att == [0x02, 0x56] cc = CecCommand(0x8F, 0x5, 0x3, [0x45, 0x56, 0x98]) assert cc.att == [0x45, 0x56, 0x98] cc = CecCommand(0x8F, 0x5, 0x3, raw="78:a5:89:45") assert cc.att == [0x89, 0x45] def test_raw(): cc = CecCommand("1f:90:02:05:89") assert cc.raw == "1f:90:02:05:89" cc = CecCommand("1f:90") assert cc.raw == "1f:90" cc = CecCommand("2c") assert cc.raw == "2c" cc = CecCommand(CMD_POLL, dst=3) assert cc.raw == "f3" pyCEC-0.6.0/tests/datastruct/test_physical_address.py000066400000000000000000000030231461164574700227470ustar00rootroot00000000000000import pytest from pycec.network import PhysicalAddress def test_creation(): pa = PhysicalAddress("8F:65") assert 0x8F65 == pa.asint pa = PhysicalAddress("0F:60") assert 0x0F60 == pa.asint pa = PhysicalAddress("2.F.6.5") assert 0x2F65 == pa.asint assert "2.f.6.5" == pa.asstr pa = PhysicalAddress("0.F.6.0") assert 0x0F60 == pa.asint pa = PhysicalAddress([2, 15, 6, 4]) assert 0x2F64 == pa.asint pa = PhysicalAddress([0, 15, 6, 0]) assert 0x0F60 == pa.asint pa = PhysicalAddress(0x0F60) assert 0x0F60 == pa.asint def test_aslist(): pa = PhysicalAddress("8f:ab") assert pa.asattr == [0x8F, 0xAB] pa = PhysicalAddress("00:00") assert pa.asattr == [0x0, 0x0] pa = PhysicalAddress("00:10") assert pa.asattr == [0x0, 0x10] def test_asint(): pa = PhysicalAddress("8f:ab") assert pa.asint == 0x8FAB pa = PhysicalAddress("00:00") assert pa.asint == 0x0000 pa = PhysicalAddress("00:10") assert pa.asint == 0x0010 def test_ascmd(): pa = PhysicalAddress("8f:ab") assert pa.ascmd == "8f:ab" pa = PhysicalAddress("00:00") assert pa.ascmd == "00:00" pa = PhysicalAddress("00:10") assert pa.ascmd == "00:10" def test_str(): pa = PhysicalAddress("8f:ab") assert ("%s" % pa) == "8.f.a.b" pa = PhysicalAddress("00:00") assert ("%s" % pa) == "0.0.0.0" pa = PhysicalAddress("00:10") assert ("%s" % pa) == "0.0.1.0" def test_raises(): with pytest.raises(AttributeError): PhysicalAddress([0] * 8) pyCEC-0.6.0/tests/test_hdmi_device.py000066400000000000000000000062051461164574700175150ustar00rootroot00000000000000from pycec.commands import CecCommand from pycec.const import ( CMD_POWER_STATUS, CMD_VENDOR, CMD_OSD_NAME, CMD_PHYSICAL_ADDRESS, ) from pycec.network import HDMIDevice def test_logical_address(): device = HDMIDevice(2) assert device.logical_address == 2 def test_update(): device = HDMIDevice(2) cmd = CecCommand( "02:%02x:4f:6e:6b:79:6f:20:48:54:58:2d:32:32:48:44:58" % CMD_OSD_NAME[1] ) device.update_callback(cmd) assert device.osd_name == "Onkyo HTX-22HDX" cmd = CecCommand("02:%02x:01" % CMD_POWER_STATUS[1]) device.update_callback(cmd) assert device.power_status == 1 cmd = CecCommand("02:%02x:02" % CMD_POWER_STATUS[1]) device.update_callback(cmd) assert device.power_status == 2 cmd = CecCommand("02:%02x:18:C0:86" % CMD_VENDOR[1]) device.update_callback(cmd) assert device.vendor_id == 0x18C086 assert device.vendor == "Broadcom" cmd = CecCommand("02:%02x:C0:86:01" % CMD_PHYSICAL_ADDRESS[1]) device.update_callback(cmd) assert device.physical_address.ascmd == "c0:86" assert device.physical_address.asattr == [0xC0, 0x86] def test_is_on(): device = HDMIDevice(2) device._power_status = 1 assert device.is_on is False device._power_status = 0 assert device.is_on is True def test_is_off(): device = HDMIDevice(2) device._power_status = 1 assert device.is_off is True device._power_status = 0 assert device.is_off is False def test_type(): device = HDMIDevice(2) device._type = 2 assert 2 == device.type def test_type_name(): device = HDMIDevice(2) device._type = 0 assert "TV" == device.type_name device._type = 1 assert "Recorder" == device.type_name device._type = 2 assert "UNKNOWN" == device.type_name device._type = 3 assert "Tuner" == device.type_name device._type = 4 assert "Playback" == device.type_name device._type = 5 assert "Audio" == device.type_name device._type = 6 assert "UNKNOWN" == device.type_name device._type = 7 assert "UNKNOWN" == device.type_name def test_update_callback(): device = HDMIDevice(3) device.update_callback( CecCommand(CMD_PHYSICAL_ADDRESS[1], att=[0x11, 0x00, 0x02]) ) assert "1.1.0.0" == str(device.physical_address) assert 2 == device.type device.update_callback(CecCommand(CMD_POWER_STATUS[1], att=[0x01])) assert 1 == device.power_status assert device.is_off is True assert device.is_on is False device.update_callback(CecCommand(CMD_POWER_STATUS[1], att=[0x00])) assert 0 == device.power_status assert device.is_on is True assert device.is_off is False device.update_callback(CecCommand(CMD_POWER_STATUS[1], att=[0x02])) assert 2 == device.power_status assert device.is_on is False assert device.is_off is False device.update_callback( CecCommand(CMD_OSD_NAME[1], att=list(map(lambda x: ord(x), "Test4"))) ) assert "Test4" == device.osd_name device.update_callback(CecCommand(CMD_VENDOR[1], att=[0x00, 0x80, 0x45])) assert 0x008045 == device.vendor_id assert "Panasonic" == device.vendor pyCEC-0.6.0/tests/test_hdmi_network.py000066400000000000000000000104021461164574700177410ustar00rootroot00000000000000import asyncio from pycec.commands import CecCommand from pycec.const import ( CMD_POWER_STATUS, CMD_OSD_NAME, CMD_VENDOR, CMD_PHYSICAL_ADDRESS, CMD_DECK_STATUS, CMD_AUDIO_STATUS, ) from pycec.network import HDMINetwork, HDMIDevice, AbstractCecAdapter def test_devices(): loop = asyncio.get_event_loop() network = HDMINetwork( MockAdapter( [ True, True, False, True, False, True, False, False, False, False, False, False, False, False, False, False, ] ), scan_interval=0, loop=loop, ) network._scan_delay = 0 # network._adapter.set_command_callback(network.command_callback) network.init() network.scan() loop.run_until_complete(asyncio.sleep(0.1)) loop.stop() loop.run_forever() for i in [0, 1, 3, 5]: assert HDMIDevice(i) in network.devices for i in [2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14]: assert HDMIDevice(i) not in network.devices for d in network.devices: d.stop() network.stop() loop.stop() loop.run_forever() def test_scan(): loop = asyncio.get_event_loop() network = HDMINetwork( MockAdapter( [ True, True, False, True, False, True, False, False, False, False, False, False, False, False, False, False, ] ), scan_interval=0, loop=loop, ) network._scan_delay = 0 # network._adapter.set_command_callback(network.command_callback) network.init() network.scan() loop.run_until_complete(asyncio.sleep(0.1)) loop.stop() loop.run_forever() assert HDMIDevice(0) in network.devices device = network.get_device(0) assert "Test0" == device.osd_name assert 2 == device.power_status assert HDMIDevice(1) in network.devices device = network.get_device(1) assert "Test1" == device.osd_name assert 2 == device.power_status assert HDMIDevice(2) not in network.devices assert HDMIDevice(3) in network.devices device = network.get_device(3) assert "Test3" == device.osd_name assert 2 == device.power_status for d in network.devices: d.stop() network.stop() loop.stop() loop.run_forever() class MockAdapter(AbstractCecAdapter): def __init__(self, data): self._data = data self._command_callback = None super().__init__() def shutdown(self): pass def init(self, callback: callable = None): f = asyncio.Future() f.set_result(True) self._initialized = True return f def power_on_devices(self): pass def standby_devices(self): pass def set_command_callback(self, callback): self._command_callback = callback def poll_device(self, i): f = asyncio.Future() f.set_result(self._data[i]) return f def transmit(self, command): cmd = None att = None if command.cmd == CMD_POWER_STATUS[0]: cmd = CMD_POWER_STATUS[1] att = [2] elif command.cmd == CMD_OSD_NAME[0]: cmd = CMD_OSD_NAME[1] att = (ord(i) for i in ("Test%d" % command.dst)) elif command.cmd == CMD_VENDOR[0]: cmd = CMD_VENDOR[1] att = [0x00, 0x09, 0xB0] elif command.cmd == CMD_PHYSICAL_ADDRESS[0]: cmd = CMD_PHYSICAL_ADDRESS[1] att = [0x09, 0xB0, 0x02] elif command.cmd == CMD_DECK_STATUS[0]: cmd = CMD_DECK_STATUS[1] att = [0x09] elif command.cmd == CMD_AUDIO_STATUS[0]: cmd = CMD_AUDIO_STATUS[1] att = [0x65] response = CecCommand(cmd, src=command.dst, dst=command.src, att=att) self._command_callback(">> " + response.raw) def get_logical_address(self): return 2