1# -*- coding: utf-8 -*- 2# This file is part of the libCEC(R) library. 3# 4# libCEC(R) is Copyright (C) 2011-2015 Pulse-Eight Limited. 5# All rights reserved. 6# libCEC(R) is an original work, containing original code. 7# 8# libCEC(R) is a trademark of Pulse-Eight Limited. 9# 10# This program is dual-licensed; you can redistribute it and/or modify 11# it under the terms of the GNU General Public License as published by 12# the Free Software Foundation; either version 2 of the License, or 13# (at your option) any later version. 14# 15# This program is distributed in the hope that it will be useful, 16# but WITHOUT ANY WARRANTY; without even the implied warranty of 17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18# GNU General Public License for more details. 19# 20# You should have received a copy of the GNU General Public License 21# along with this program; if not, write to the Free Software 22# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 23# 02110-1301 USA 24# 25# 26# Alternatively, you can license this library under a commercial license, 27# please contact Pulse-Eight Licensing for more information. 28# 29# For more information contact: 30# Pulse-Eight Licensing <license@pulse-eight.com> 31# http://www.pulse-eight.com/ 32# http://www.pulse-eight.net/ 33# 34# 35# The code contained within this file also falls under the GNU license of 36# EventGhost 37# 38# Copyright © 2005-2016 EventGhost Project <http://www.eventghost.org/> 39# 40# EventGhost is free software: you can redistribute it and/or modify it under 41# the terms of the GNU General Public License as published by the Free 42# Software Foundation, either version 2 of the License, or (at your option) 43# any later version. 44# 45# EventGhost is distributed in the hope that it will be useful, but WITHOUT 46# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 47# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 48# more details. 49# 50# You should have received a copy of the GNU General Public License along 51# with EventGhost. If not, see <http://www.gnu.org/licenses/>. 52 53from . import cec 54import threading 55import eg 56 57CEC_LOG_CONSTANTS = { 58 cec.CEC_LOG_ERROR: "ERROR: ", 59 cec.CEC_LOG_WARNING: "WARNING: ", 60 cec.CEC_LOG_NOTICE: "NOTICE: ", 61 cec.CEC_LOG_TRAFFIC: "TRAFFIC: ", 62 cec.CEC_LOG_DEBUG: "DEBUG: ", 63 cec.CEC_LOG_ALL: "ALL: " 64} 65 66CEC_POWER_CONSTANTS = { 67 cec.CEC_POWER_STATUS_ON: True, 68 cec.CEC_POWER_STATUS_IN_TRANSITION_ON_TO_STANDBY: False, 69 cec.CEC_POWER_STATUS_STANDBY: False, 70 cec.CEC_POWER_STATUS_IN_TRANSITION_STANDBY_TO_ON: True, 71 cec.CEC_POWER_STATUS_UNKNOWN: None 72} 73 74_CONTROL_CODES = [ 75 cec.CEC_USER_CONTROL_CODE_SELECT, 76 cec.CEC_USER_CONTROL_CODE_UP, 77 cec.CEC_USER_CONTROL_CODE_DOWN, 78 cec.CEC_USER_CONTROL_CODE_LEFT, 79 cec.CEC_USER_CONTROL_CODE_RIGHT, 80 cec.CEC_USER_CONTROL_CODE_RIGHT_UP, 81 cec.CEC_USER_CONTROL_CODE_RIGHT_DOWN, 82 cec.CEC_USER_CONTROL_CODE_LEFT_UP, 83 cec.CEC_USER_CONTROL_CODE_LEFT_DOWN, 84 cec.CEC_USER_CONTROL_CODE_ROOT_MENU, 85 cec.CEC_USER_CONTROL_CODE_SETUP_MENU, 86 cec.CEC_USER_CONTROL_CODE_CONTENTS_MENU, 87 cec.CEC_USER_CONTROL_CODE_FAVORITE_MENU, 88 cec.CEC_USER_CONTROL_CODE_EXIT, 89 cec.CEC_USER_CONTROL_CODE_TOP_MENU, 90 cec.CEC_USER_CONTROL_CODE_DVD_MENU, 91 cec.CEC_USER_CONTROL_CODE_NUMBER_ENTRY_MODE, 92 cec.CEC_USER_CONTROL_CODE_NUMBER11, 93 cec.CEC_USER_CONTROL_CODE_NUMBER12, 94 cec.CEC_USER_CONTROL_CODE_NUMBER0, 95 cec.CEC_USER_CONTROL_CODE_NUMBER1, 96 cec.CEC_USER_CONTROL_CODE_NUMBER2, 97 cec.CEC_USER_CONTROL_CODE_NUMBER3, 98 cec.CEC_USER_CONTROL_CODE_NUMBER4, 99 cec.CEC_USER_CONTROL_CODE_NUMBER5, 100 cec.CEC_USER_CONTROL_CODE_NUMBER6, 101 cec.CEC_USER_CONTROL_CODE_NUMBER7, 102 cec.CEC_USER_CONTROL_CODE_NUMBER8, 103 cec.CEC_USER_CONTROL_CODE_NUMBER9, 104 cec.CEC_USER_CONTROL_CODE_DOT, 105 cec.CEC_USER_CONTROL_CODE_ENTER, 106 cec.CEC_USER_CONTROL_CODE_CLEAR, 107 cec.CEC_USER_CONTROL_CODE_NEXT_FAVORITE, 108 cec.CEC_USER_CONTROL_CODE_CHANNEL_UP, 109 cec.CEC_USER_CONTROL_CODE_CHANNEL_DOWN, 110 cec.CEC_USER_CONTROL_CODE_PREVIOUS_CHANNEL, 111 cec.CEC_USER_CONTROL_CODE_SOUND_SELECT, 112 cec.CEC_USER_CONTROL_CODE_INPUT_SELECT, 113 cec.CEC_USER_CONTROL_CODE_DISPLAY_INFORMATION, 114 cec.CEC_USER_CONTROL_CODE_HELP, 115 cec.CEC_USER_CONTROL_CODE_PAGE_UP, 116 cec.CEC_USER_CONTROL_CODE_PAGE_DOWN, 117 cec.CEC_USER_CONTROL_CODE_POWER, 118 cec.CEC_USER_CONTROL_CODE_VOLUME_UP, 119 cec.CEC_USER_CONTROL_CODE_VOLUME_DOWN, 120 cec.CEC_USER_CONTROL_CODE_MUTE, 121 cec.CEC_USER_CONTROL_CODE_PLAY, 122 cec.CEC_USER_CONTROL_CODE_STOP, 123 cec.CEC_USER_CONTROL_CODE_PAUSE, 124 cec.CEC_USER_CONTROL_CODE_RECORD, 125 cec.CEC_USER_CONTROL_CODE_REWIND, 126 cec.CEC_USER_CONTROL_CODE_FAST_FORWARD, 127 cec.CEC_USER_CONTROL_CODE_EJECT, 128 cec.CEC_USER_CONTROL_CODE_FORWARD, 129 cec.CEC_USER_CONTROL_CODE_BACKWARD, 130 cec.CEC_USER_CONTROL_CODE_STOP_RECORD, 131 cec.CEC_USER_CONTROL_CODE_PAUSE_RECORD, 132 cec.CEC_USER_CONTROL_CODE_ANGLE, 133 cec.CEC_USER_CONTROL_CODE_SUB_PICTURE, 134 cec.CEC_USER_CONTROL_CODE_VIDEO_ON_DEMAND, 135 cec.CEC_USER_CONTROL_CODE_ELECTRONIC_PROGRAM_GUIDE, 136 cec.CEC_USER_CONTROL_CODE_TIMER_PROGRAMMING, 137 cec.CEC_USER_CONTROL_CODE_INITIAL_CONFIGURATION, 138 cec.CEC_USER_CONTROL_CODE_SELECT_BROADCAST_TYPE, 139 cec.CEC_USER_CONTROL_CODE_SELECT_SOUND_PRESENTATION, 140 cec.CEC_USER_CONTROL_CODE_PLAY_FUNCTION, 141 cec.CEC_USER_CONTROL_CODE_PAUSE_PLAY_FUNCTION, 142 cec.CEC_USER_CONTROL_CODE_RECORD_FUNCTION, 143 cec.CEC_USER_CONTROL_CODE_PAUSE_RECORD_FUNCTION, 144 cec.CEC_USER_CONTROL_CODE_STOP_FUNCTION, 145 cec.CEC_USER_CONTROL_CODE_MUTE_FUNCTION, 146 cec.CEC_USER_CONTROL_CODE_RESTORE_VOLUME_FUNCTION, 147 cec.CEC_USER_CONTROL_CODE_TUNE_FUNCTION, 148 cec.CEC_USER_CONTROL_CODE_SELECT_MEDIA_FUNCTION, 149 cec.CEC_USER_CONTROL_CODE_SELECT_AV_INPUT_FUNCTION, 150 cec.CEC_USER_CONTROL_CODE_SELECT_AUDIO_INPUT_FUNCTION, 151 cec.CEC_USER_CONTROL_CODE_POWER_TOGGLE_FUNCTION, 152 cec.CEC_USER_CONTROL_CODE_POWER_OFF_FUNCTION, 153 cec.CEC_USER_CONTROL_CODE_POWER_ON_FUNCTION, 154 cec.CEC_USER_CONTROL_CODE_F1_BLUE, 155 cec.CEC_USER_CONTROL_CODE_F2_RED, 156 cec.CEC_USER_CONTROL_CODE_F3_GREEN, 157 cec.CEC_USER_CONTROL_CODE_F4_YELLOW, 158 cec.CEC_USER_CONTROL_CODE_F5, 159 cec.CEC_USER_CONTROL_CODE_DATA, 160 cec.CEC_USER_CONTROL_CODE_AN_RETURN, 161 cec.CEC_USER_CONTROL_CODE_AN_CHANNELS_LIST, 162 cec.CEC_USER_CONTROL_CODE_MAX, 163 cec.CEC_USER_CONTROL_CODE_UNKNOWN, 164] 165 166 167class _UserControlCodes(object): 168 _control_codes = {} 169 170 def __init__(self): 171 cec_lib = cec.ICECAdapter.Create(cec.libcec_configuration()) 172 173 for code in _CONTROL_CODES: 174 code_name = cec_lib.UserControlCodeToString(code).title() 175 self._control_codes[code_name.replace(' (Function)', '')] = code 176 cec_lib.Close() 177 178 def __iter__(self): 179 for key in sorted(self._control_codes.keys()): 180 yield key 181 182 def __contains__(self, item): 183 return item in self._control_codes 184 185 def __getattr__(self, item): 186 if item in self.__dict__: 187 return self.__dict__[item] 188 189 if item in self._control_codes: 190 return self._control_codes[item] 191 192 for key in self._control_codes: 193 if '(%s)' % item in key: 194 return self._control_codes[key] 195 196 raise AttributeError 197 198 199UserControlCodes = _UserControlCodes() 200 201 202class CECDevice(object): 203 def __init__(self, adapter, name, device_const): 204 self.adapter = adapter 205 self.sla = adapter.LogicalAddressToString(device_const) 206 self.pa = adapter.GetDevicePhysicalAddress(device_const) 207 self.la = device_const 208 self._osd_event = threading.Event() 209 self._osd_thread = None 210 self._osd_string = None 211 self.name = name 212 213 @property 214 def osd_name(self): 215 return self.adapter.GetDeviceOSDName(self.la) 216 217 @property 218 def osd_string(self): 219 return self._osd_string 220 221 @osd_string.setter 222 def osd_string(self, (msg, duration)): 223 if self._osd_thread is not None: 224 self._osd_event.set() 225 self._osd_thread.join(1.0) 226 227 self._osd_event.clear() 228 229 def clear_osd(): 230 self._osd_event.wait(duration) 231 self._osd_string = None 232 self._osd_thread = None 233 234 self._osd_string = msg 235 self._osd_thread = threading.Thread(target=clear_osd()) 236 self.adapter.SetOSDString(self.la, duration, msg) 237 self._osd_thread.start() 238 239 @property 240 def menu_language(self): 241 return self.adapter.GetDeviceMenuLanguage(self.la) 242 243 @property 244 def cec_version(self): 245 cec_version = self.adapter.GetDeviceCecVersion(self.la) 246 return self.adapter.CecVersionToString(cec_version) 247 248 @property 249 def vendor(self): 250 vendor_id = self.adapter.GetDeviceVendorId(self.la) 251 return self.adapter.VendorIdToString(vendor_id) 252 253 @property 254 def power(self): 255 return CEC_POWER_CONSTANTS[self.adapter.GetDevicePowerStatus(self.la)] 256 257 @power.setter 258 def power(self, flag): 259 if flag: 260 self.adapter.PowerOnDevices(self.la) 261 else: 262 self.adapter.StandbyDevices(self.la) 263 264 @property 265 def active_device(self): 266 return self.adapter.IsActiveDevice(self.la) 267 268 @property 269 def active_source(self): 270 return self.adapter.IsActiveSource(self.la) 271 272 @active_source.setter 273 def active_source(self, flag=True): 274 if flag: 275 self.adapter.SetActiveSource(self.la) 276 277 def __getattr__(self, item): 278 if item in self.__dict__: 279 return self.__dict__[item] 280 281 if item in UserControlCodes: 282 adapter = self.adapter 283 284 class Wrapper: 285 def __init__(self): 286 pass 287 288 @staticmethod 289 def send_key_press(): 290 code = getattr(UserControlCodes, item) 291 print code 292 adapter.SendKeypress( 293 self.la, 294 code 295 ) 296 297 @staticmethod 298 def send_key_release(): 299 adapter.SendKeyRelease(self.la) 300 return Wrapper 301 return None 302 303 304class AdapterError(Exception): 305 pass 306 307 308class CECAdapter(object): 309 @eg.LogIt 310 def __init__(self, com_port, adapter_name, hdmi_port, use_avr, poll_interval): 311 self.name = adapter_name 312 self.com_port = com_port 313 self._log_level = None 314 self._menu_state = False 315 self._key_event = None 316 self._last_key = 255 317 self._restart_params = (com_port, adapter_name, hdmi_port, use_avr) 318 self._poll_event = threading.Event() 319 self._poll_interval = poll_interval 320 self._poll_thread = threading.Thread( 321 name='PulseEightCEC-' + adapter_name, 322 target=self._run_poll 323 ) 324 325 self.cec_config = cec_config = cec.libcec_configuration() 326 cec_config.clientVersion = cec.LIBCEC_VERSION_CURRENT 327 cec_config.deviceTypes.Add( 328 cec.CEC_DEVICE_TYPE_RECORDING_DEVICE 329 ) 330 cec_config.SetLogCallback(self._log_callback) 331 cec_config.SetKeyPressCallback(self._key_callback) 332 cec_config.iHDMIPort = hdmi_port 333 cec_config.strDeviceName = str(adapter_name) 334 cec_config.bActivateSource = 0 335 336 if use_avr: 337 cec_config.baseDevice = cec.CECDEVICE_AUDIOSYSTEM 338 else: 339 cec_config.baseDevice = cec.CECDEVICE_TV 340 341 self.adapter = adapter = cec.ICECAdapter.Create(cec_config) 342 343 if adapter.Open(com_port): 344 eg.Print('CEC: connection opened on ' + com_port) 345 else: 346 eg.PrintError( 347 'CEC Error: connection failed on ' + com_port 348 ) 349 raise AdapterError 350 351 self.tv = CECDevice(adapter, 'TV', cec.CECDEVICE_TV) 352 self.tuner1 = CECDevice(adapter, 'Tuner 1', cec.CECDEVICE_TUNER1) 353 self.tuner2 = CECDevice(adapter, 'Tuner 2', cec.CECDEVICE_TUNER2) 354 self.tuner3 = CECDevice(adapter, 'Tuner 3', cec.CECDEVICE_TUNER3) 355 self.tuner4 = CECDevice(adapter, 'Tuner 4', cec.CECDEVICE_TUNER4) 356 self.audiosystem = CECDevice(adapter, 'AVR', cec.CECDEVICE_AUDIOSYSTEM) 357 self.freeuse = CECDevice(adapter, 'Free Use', cec.CECDEVICE_FREEUSE) 358 self.unknown = CECDevice(adapter, 'Unknown', cec.CECDEVICE_UNKNOWN) 359 self.broadcast = CECDevice( 360 adapter, 361 'Broadcast', 362 cec.CECDEVICE_BROADCAST 363 ) 364 self.reserved1 = CECDevice( 365 adapter, 366 'Reserved 1', 367 cec.CECDEVICE_RESERVED1 368 ) 369 self.reserved2 = CECDevice( 370 adapter, 371 'Reserved 2', 372 cec.CECDEVICE_RESERVED2 373 ) 374 self.recordingdevice1 = CECDevice( 375 adapter, 376 'Recording Device 1', 377 cec.CECDEVICE_RECORDINGDEVICE1 378 ) 379 self.playbackdevice1 = CECDevice( 380 adapter, 381 'Playback Device 1', 382 cec.CECDEVICE_PLAYBACKDEVICE1 383 ) 384 self.recordingdevice2 = CECDevice( 385 adapter, 386 'Recording Device 2', 387 cec.CECDEVICE_RECORDINGDEVICE2 388 ) 389 self.playbackdevice2 = CECDevice( 390 adapter, 391 'Playback Device 2', 392 cec.CECDEVICE_PLAYBACKDEVICE2 393 ) 394 self.recordingdevice3 = CECDevice( 395 adapter, 396 'Recording Device 3', 397 cec.CECDEVICE_RECORDINGDEVICE3 398 ) 399 self.playbackdevice3 = CECDevice( 400 adapter, 401 'Playback Device 3', 402 cec.CECDEVICE_PLAYBACKDEVICE3 403 ) 404 405 self.devices = [ 406 self.tv, 407 self.audiosystem, 408 self.tuner1, 409 self.tuner2, 410 self.tuner3, 411 self.tuner4, 412 self.recordingdevice1, 413 self.recordingdevice2, 414 self.recordingdevice3, 415 self.playbackdevice1, 416 self.playbackdevice2, 417 self.playbackdevice3, 418 self.reserved1, 419 self.reserved2, 420 self.freeuse, 421 self.broadcast, 422 self.unknown, 423 ] 424 self._poll_thread.start() 425 426 def _run_poll(self): 427 devices = [] 428 429 volume = self.volume 430 mute = self.mute 431 menu = self.menu 432 433 for device in self.devices: 434 try: 435 devices.append([ 436 device.active_device, 437 device.active_source, 438 device.power, 439 device.menu_language 440 ]) 441 except: 442 devices.append([None] * 4) 443 444 while not self._poll_event.isSet(): 445 new_volume = self.volume 446 new_mute = self.mute 447 new_menu = self.menu 448 449 if volume != new_volume: 450 volume = new_volume 451 if volume is not None: 452 eg.TriggerEvent( 453 prefix=self.name, 454 suffix='Volume.' + str(volume) 455 ) 456 457 if mute != new_mute: 458 mute = new_mute 459 if mute is not None: 460 if mute: 461 suffix = 'On' 462 else: 463 suffix = 'Off' 464 465 eg.TriggerEvent( 466 prefix=self.name, 467 suffix='Mute.' + suffix 468 ) 469 470 if menu != new_menu: 471 menu = new_menu 472 if menu is not None: 473 if menu: 474 suffix = 'Opened' 475 else: 476 suffix = 'Closed' 477 478 eg.TriggerEvent( 479 prefix=self.name, 480 suffix='Menu.' + suffix 481 ) 482 483 for i, device in enumerate(self.devices): 484 active, source, power, language = devices[i] 485 486 new_active = device.active_device 487 new_source = device.active_source 488 new_power = device.power 489 new_language = device.menu_language 490 491 if active != new_active: 492 active = new_active 493 if active: 494 suffix = 'Active' 495 else: 496 suffix = 'Inactive' 497 498 eg.TriggerEvent( 499 prefix=self.name, 500 suffix=device.name + '.' + suffix 501 ) 502 503 if source != new_source: 504 source = new_source 505 if source: 506 eg.TriggerEvent( 507 prefix=self.name, 508 suffix='Source.' + device.name 509 ) 510 511 if power != new_power: 512 if power is None: 513 eg.TriggerEvent( 514 prefix=self.name, 515 suffix=device.name + '.Connected' 516 ) 517 power = new_power 518 if power is None: 519 eg.TriggerEvent( 520 prefix=self.name, 521 suffix=device.name + '.Disconnected' 522 ) 523 else: 524 if power: 525 suffix = 'On' 526 else: 527 suffix = 'Off' 528 eg.TriggerEvent( 529 prefix=self.name, 530 suffix=device.name + '.Power.' + suffix 531 ) 532 533 if language != new_language: 534 language = new_language 535 eg.TriggerEvent( 536 prefix=self.name, 537 suffix=device.name + '.MenuLanguage.' + str(language) 538 ) 539 540 devices[i] = [active, source, power, language] 541 self._poll_event.wait(self._poll_interval) 542 543 def transmit_command(self, command): 544 return self.adapter.Transmit(self.adapter.CommandFromString(command)) 545 546 def _log_callback(self, level, time, message): 547 if ( 548 self._log_level is not None and 549 level <= self._log_level and 550 level in CEC_LOG_CONSTANTS 551 ): 552 level_str = CEC_LOG_CONSTANTS[level] 553 eg.PrintDebugNotice( 554 "CEC %s: %s [%s] %s" % 555 (self.name, level_str, str(time), message) 556 ) 557 return 0 558 559 def _key_callback(self, key, duration): 560 str_key = lib.UserControlCodeToString(key).title() 561 if duration == 0 and self._last_key != key: 562 self._last_key = key 563 self._key_event = eg.TriggerEnduringEvent( 564 prefix=self._name, 565 suffix='KeyPressed.' + str_key 566 ) 567 elif duration > 0 and self._last_key == key: 568 self._last_key = 255 569 self._key_event.SetShouldEnd() 570 self._key_event = None 571 elif self._last_key != key: 572 self._last_key = 255 573 eg.TriggerEvent( 574 prefix=self._name, 575 suffix='KeyPressed.' + str_key 576 ) 577 return 0 578 579 @property 580 def log_level(self): 581 return self._log_level 582 583 @log_level.setter 584 def log_level(self, level): 585 if level is not None and level not in CEC_LOG_CONSTANTS: 586 return 587 self._log_level = level 588 589 @property 590 def vendor(self): 591 vendor_id = self.adapter.GetAdapterVendorId() 592 return self.adapter.VendorIdToString(vendor_id) 593 594 @property 595 def menu(self): 596 return self._menu_state 597 598 @menu.setter 599 def menu(self, state): 600 self._menu_state = state 601 self.adapter.SetMenuState(state) 602 603 def set_interactive_view(self): 604 self.adapter.SetInactiveView() 605 606 @property 607 def volume(self): 608 res = self.adapter.AudioStatus() ^ cec.CEC_AUDIO_MUTE_STATUS_MASK 609 if res == 255: 610 return None 611 return res 612 613 @volume.setter 614 def volume(self, volume): 615 if volume < self.volume: 616 while volume < self.volume: 617 self.volume_down() 618 619 elif volume > self.volume: 620 while volume > self.volume: 621 self.volume_up() 622 623 def volume_up(self): 624 self.adapter.VolumeUp() 625 return self.volume 626 627 def volume_down(self): 628 self.adapter.VolumeDown() 629 return self.volume 630 631 @property 632 def mute(self): 633 return ( 634 self.adapter.AudioStatus() & cec.CEC_AUDIO_MUTE_STATUS_MASK == 635 cec.CEC_AUDIO_MUTE_STATUS_MASK 636 ) 637 638 @mute.setter 639 def mute(self, flag): 640 if flag and not self.mute: 641 self.adapter.AudioMute() 642 elif not flag and self.mute: 643 self.adapter.AudioUnmute() 644 645 def toggle_mute(self): 646 self.adapter.AudioToggleMute() 647 648 def restart(self): 649 self.close() 650 return CECAdapter(*self._restart_params) 651 652 def close(self): 653 self._poll_event.set() 654 self._poll_thread.join(3) 655 self.adapter.Close() 656 eg.Print('CEC: connection closed on ' + self.com_port) 657