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 53import eg 54 55 56eg.RegisterPlugin( 57 name='Pulse-Eight CEC adapter', 58 author='Lars Op den Kamp, K', 59 version='1.1b', 60 kind='remote', 61 # guid='{fd322eea-c897-470c-bef7-77bf15c52db4}', 62 guid='{81AC5776-0220-4D2A-B561-DD91F052FF7B}', 63 url='http://libcec.pulse-eight.com/', 64 description=( 65 '<rst>' 66 'Integration with libCEC, which adds support for Pulse-Eight\'s ' 67 '`CEC adapters <http://www.pulse-eight.com/>`_.\n\n' 68 '|\n\n' 69 '.. image:: cec.png\n\n' 70 '**Notice:** ' 71 'Make sure you select the correct HDMI port number on the device that ' 72 'the CEC adapter is connected to, ' 73 'or remote control input won\'t work.\n' 74 ), 75 createMacrosOnAdd=True, 76 canMultiLoad=False, 77 hardwareId="USB\\VID_2548&PID_1002", 78) 79 80from cec_classes import UserControlCodes, CECAdapter, AdapterError # NOQA 81from controls import DeviceCtrl, AdapterCtrl, AdapterListCtrl # NOQA 82from . import cec # NOQA 83import threading # NOQA 84import wx # NOQA 85 86 87class Text(eg.TranslatableStrings): 88 mute_group_lbl = 'Mute' 89 volume_group_lbl = 'Volume' 90 power_group_lbl = 'Power' 91 remote_group_lbl = 'Remote Keys' 92 volume_lbl = 'Volume:' 93 command_lbl = 'Command:' 94 key_lbl = 'Remote Key:' 95 96 class RawCommand: 97 name = 'Send command to an adapter' 98 description = 'Send a raw CEC command to an adapter' 99 100 class RestartAdapter: 101 name = 'Restart Adapter' 102 description = 'Restarts an adapter.' 103 104 class VolumeUp: 105 name = 'Volume Up' 106 description = 'Turns up the volume by one point.' 107 108 class VolumeDown: 109 name = 'Volume Down' 110 description = 'Turns down the volume by one point.' 111 112 class GetVolume: 113 name = 'Get Volume' 114 description = 'Returns the current volume level.' 115 116 class SetVolume: 117 name = 'Set Volume' 118 description = 'Sets the volume level.' 119 120 class GetMute: 121 name = 'Get Mute' 122 description = 'Returns the mute state.' 123 124 class ToggleMute: 125 name = 'Toggle Mute' 126 description = 'Toggles mute On and Off.' 127 128 class MuteOn: 129 name = 'Mute On' 130 description = 'Turns mute on.' 131 132 class MuteOff: 133 name = 'Mute Off' 134 description = 'Turns mute off.' 135 136 class PowerOnAll: 137 name = 'Power On All Devices' 138 description = 'Powers on all devices on a specific adapter.' 139 140 class StandbyAll: 141 name = 'Standby All Devices' 142 description = 'Powers off (standby) all devices in a specific adapter.' 143 144 class StandbyDevice: 145 name = 'Standby a Device' 146 description = 'Powers off (standby) a single device.' 147 148 class GetDevicePower: 149 name = 'Get Device Power' 150 description = 'Returns the power status of a device.' 151 152 class PowerOnDevice: 153 name = 'Power On a Device' 154 description = 'Powers on a single device.' 155 156 class GetDeviceVendor: 157 name = 'Get Device Vendor' 158 description = 'Returns the vendor of a device.' 159 160 class GetDeviceMenuLanguage: 161 name = 'Get Device Menu Language' 162 description = 'Returns the menu language of a device.' 163 164 class IsActiveSource: 165 name = 'Is Device Active Source' 166 description = 'Returns True/False if a device is the active source.' 167 168 class IsDeviceActive: 169 name = 'Is Device Active' 170 description = 'Returns True/False if a device is active.' 171 172 class GetDeviceOSDName: 173 name = 'Get Device OSD Name' 174 description = 'Returns the OSD text that is display for a device.' 175 176 class SetDeviceActiveSource: 177 name = 'Set Device as Active Source' 178 description = 'Sets a device as the active source.' 179 180 class SendRemoteKey: 181 name = 'Send Remote Key' 182 description = 'Send a Remote Keypress to a specific device.' 183 184 185class PulseEight(eg.PluginBase): 186 text = Text 187 188 def __init__(self): 189 self.adapters = [] 190 191 power_group = self.AddGroup(Text.power_group_lbl) 192 power_group.AddAction(GetDevicePower) 193 power_group.AddAction(PowerOnDevice) 194 power_group.AddAction(StandbyDevice) 195 power_group.AddAction(PowerOnAll) 196 power_group.AddAction(StandbyAll) 197 198 volume_group = self.AddGroup(Text.volume_group_lbl) 199 volume_group.AddAction(GetVolume) 200 volume_group.AddAction(VolumeUp) 201 volume_group.AddAction(VolumeDown) 202 volume_group.AddAction(SetVolume) 203 204 mute_group = self.AddGroup(Text.mute_group_lbl) 205 mute_group.AddAction(GetMute) 206 mute_group.AddAction(MuteOn) 207 mute_group.AddAction(MuteOff) 208 mute_group.AddAction(ToggleMute) 209 210 self.AddAction(SendRemoteKey) 211 self.AddAction(SetDeviceActiveSource) 212 self.AddAction(IsActiveSource) 213 self.AddAction(IsDeviceActive) 214 self.AddAction(GetDeviceVendor) 215 self.AddAction(GetDeviceMenuLanguage) 216 self.AddAction(GetDeviceOSDName) 217 self.AddAction(RestartAdapter) 218 self.AddAction(RawCommand) 219 220 remote_group = self.AddGroup(Text.remote_group_lbl) 221 remote_group.AddActionsFromList(REMOTE_ACTIONS) 222 223 def __start__(self, *adapters): 224 225 def start_connections(*adptrs): 226 while self.adapters: 227 pass 228 229 cec_lib = cec.ICECAdapter.Create(cec.libcec_configuration()) 230 available_coms = list( 231 a.strComName for a in cec_lib.DetectAdapters() 232 ) 233 cec_lib.Close() 234 235 for item in adptrs: 236 com_port = item[0] 237 238 if com_port in available_coms: 239 try: 240 self.adapters += [CECAdapter(*item)] 241 except AdapterError: 242 continue 243 else: 244 eg.PrintError( 245 'CEC Error: adapter on %s is not found' % com_port 246 ) 247 248 if not self.adapters: 249 eg.PrintError('CEC Error: no CEC adapters found') 250 self.__stop__() 251 252 for items in adapters: 253 if not isinstance(items, tuple): 254 eg.PrintError( 255 'You cannot upgrade to this version.\n' 256 'Delete the plugin from the plugins folder ' 257 'and then install this one' 258 ) 259 break 260 else: 261 threading.Thread(target=start_connections, args=adapters).start() 262 263 @eg.LogIt 264 def __stop__(self): 265 for adapter in self.adapters: 266 adapter.close() 267 268 del self.adapters[:] 269 270 def Configure(self, *adapters): 271 panel = eg.ConfigPanel() 272 273 loading_st = panel.StaticText( 274 'Populating CEC Adapters, Please Wait.....' 275 ) 276 list_ctrl = AdapterListCtrl(panel) 277 desc_st = panel.StaticText( 278 'Click on "ENTER NAME" and enter a name ' 279 'to register an adapter\n' 280 'To remove an adapter registration delete the adapter name.' 281 ) 282 283 ok_button = panel.dialog.buttonRow.okButton 284 cancel_button = panel.dialog.buttonRow.cancelButton 285 apply_button = panel.dialog.buttonRow.applyButton 286 287 ok_button.Enable(False) 288 cancel_button.Enable(False) 289 apply_button.Enable(False) 290 291 def populate(): 292 def on_close(_): 293 pass 294 295 panel.dialog.Bind(wx.EVT_CLOSE, on_close) 296 297 cec_lib = cec.ICECAdapter.Create(cec.libcec_configuration()) 298 m_adapters = () 299 300 for adapter in cec_lib.DetectAdapters(): 301 com = adapter.strComName 302 for settings in adapters: 303 com_port, adapter_name = settings[:2] 304 hdmi_port, use_avr, poll_interval = settings[2:] 305 if com_port == com: 306 m_adapters += (( 307 com_port, 308 adapter_name, 309 hdmi_port, 310 use_avr, 311 poll_interval, 312 True 313 ),) 314 wx.CallAfter(list_ctrl.add_cec_item, *m_adapters[-1]) 315 break 316 else: 317 wx.CallAfter( 318 list_ctrl.add_cec_item, 319 com, 320 'ENTER NAME', 321 1, 322 False, 323 0.5, 324 None 325 ) 326 327 for adapter in adapters: 328 for m_adapter in m_adapters: 329 if m_adapter[:-1] == adapter: 330 break 331 else: 332 m_adapters += (adapter + (False,),) 333 wx.CallAfter(list_ctrl.add_cec_item, *m_adapters[-1]) 334 335 cec_lib.Close() 336 ok_button.Enable(True) 337 cancel_button.Enable(True) 338 apply_button.Enable(True) 339 340 panel.dialog.Bind(wx.EVT_CLOSE, panel.dialog.OnCancel) 341 loading_st.SetLabel('') 342 343 loading_sizer = wx.BoxSizer(wx.HORIZONTAL) 344 loading_sizer.AddStretchSpacer() 345 loading_sizer.Add(loading_st, 0, wx.ALL | 5) 346 loading_sizer.AddStretchSpacer() 347 348 panel.sizer.Add(loading_sizer, 0, wx.EXPAND) 349 panel.sizer.Add(list_ctrl, 1, wx.EXPAND) 350 panel.sizer.Add(desc_st, 0, wx.EXPAND) 351 352 threading.Thread(target=populate).start() 353 354 while panel.Affirmed(): 355 panel.SetResult(*list_ctrl.GetValue()) 356 357 358class AdapterBase(eg.ActionBase): 359 360 def GetLabel(self, com_port=None, adapter_name=None, *_): 361 return '%s: %s on %s' % (self.name, adapter_name, com_port) 362 363 def _find_adapter(self, com_port, adapter_name): 364 if com_port is None and adapter_name is None: 365 return None 366 367 for adapter in self.plugin.adapters: 368 if com_port == adapter.com_port and adapter_name == adapter.name: 369 return adapter 370 if com_port == adapter.com_port: 371 return adapter 372 if adapter_name == adapter.name: 373 return adapter 374 375 def __call__(self, *args): 376 raise NotImplementedError 377 378 def Configure(self, com_port='', adapter_name=''): 379 panel = eg.ConfigPanel() 380 381 adapter_ctrl = AdapterCtrl( 382 panel, 383 com_port, 384 adapter_name, 385 self.plugin.adapters 386 ) 387 388 panel.sizer.Add(adapter_ctrl, 0, wx.EXPAND) 389 390 while panel.Affirmed(): 391 panel.SetResult(*adapter_ctrl.GetValue()) 392 393 394class DeviceBase(AdapterBase): 395 def _process_call(self, device): 396 raise NotImplementedError 397 398 def __call__(self, com_port=None, adapter_name=None, device='TV'): 399 adapter = self._find_adapter(com_port, adapter_name) 400 401 if adapter is None: 402 eg.PrintNotice( 403 'CEC: Adapter %s on com port %s not found' % 404 (adapter_name, com_port) 405 ) 406 else: 407 d = getattr(adapter, device.lower().replace(' ', ''), None) 408 if d is None: 409 eg.PrintNotice( 410 'CEC: Device %s not found in adapter %s' % 411 (device, adpater.name) 412 ) 413 else: 414 return self._process_call(d) 415 416 def Configure(self, com_port='', adapter_name='', device='TV'): 417 panel = eg.ConfigPanel() 418 419 adapter_ctrl = AdapterCtrl( 420 panel, 421 com_port, 422 adapter_name, 423 self.plugin.adapters 424 ) 425 426 device_ctrl = DeviceCtrl(panel, device) 427 if com_port and adapter_name: 428 device_ctrl.UpdateDevices( 429 self._find_adapter(com_port, adapter_name) 430 ) 431 432 def on_choice(evt): 433 device_ctrl.UpdateDevices( 434 self._find_adapter(*adapter_ctrl.GetValue()) 435 ) 436 437 evt.Skip() 438 439 device_ctrl.UpdateDevices( 440 self._find_adapter(*adapter_ctrl.GetValue()) 441 ) 442 443 adapter_ctrl.Bind(wx.EVT_CHOICE, on_choice) 444 panel.sizer.Add(adapter_ctrl, 0, wx.EXPAND) 445 panel.sizer.Add(device_ctrl, 0, wx.EXPAND) 446 447 while panel.Affirmed(): 448 com_port, adapter_name = adapter_ctrl.GetValue() 449 panel.SetResult( 450 com_port, 451 adapter_name, 452 device_ctrl.GetValue() 453 ) 454 455 456class RestartAdapter(AdapterBase): 457 458 def __call__(self, com_port=None, adapter_name=None): 459 adapter = self._find_adapter(com_port, adapter_name) 460 self.plugin.adapters[self.plugin.adapters.index(adapter)] = ( 461 adapter.restart() 462 ) 463 464 465class VolumeUp(AdapterBase): 466 467 def __call__(self, com_port=None, adapter_name=None): 468 adapter = self._find_adapter(com_port, adapter_name) 469 return adapter.volume_up() 470 471 472class VolumeDown(AdapterBase): 473 474 def __call__(self, com_port=None, adapter_name=None): 475 adapter = self._find_adapter(com_port, adapter_name) 476 return adapter.volume_down() 477 478 479class GetVolume(AdapterBase): 480 481 def __call__(self, com_port=None, adapter_name=None): 482 adapter = self._find_adapter(com_port, adapter_name) 483 return adapter.volume 484 485 486class GetMute(AdapterBase): 487 def __call__(self, com_port=None, adapter_name=None): 488 adapter = self._find_adapter(com_port, adapter_name) 489 return adapter.mute 490 491 492class ToggleMute(AdapterBase): 493 def __call__(self, com_port=None, adapter_name=None): 494 adapter = self._find_adapter(com_port, adapter_name) 495 return adapter.toggle_mute() 496 497 498class MuteOn(AdapterBase): 499 def __call__(self, com_port=None, adapter_name=None): 500 adapter = self._find_adapter(com_port, adapter_name) 501 adapter.mute = True 502 return adapter.mute 503 504 505class MuteOff(AdapterBase): 506 def __call__(self, com_port=None, adapter_name=None): 507 adapter = self._find_adapter(com_port, adapter_name) 508 adapter.mute = False 509 return adapter.mute 510 511 512class PowerOnAll(AdapterBase): 513 def __call__(self, com_port=None, adapter_name=None): 514 adapter = self._find_adapter(com_port, adapter_name) 515 for d in adapter.devices: 516 d.power = True 517 518 519class StandbyAll(AdapterBase): 520 def __call__(self, com_port=None, adapter_name=None): 521 adapter = self._find_adapter(com_port, adapter_name) 522 for d in adapter.devices: 523 d.power = False 524 525 526class StandbyDevice(DeviceBase): 527 def _process_call(self, device): 528 device.power = False 529 return device.power 530 531 532class GetDevicePower(DeviceBase): 533 def _process_call(self, device): 534 return device.power 535 536 537class PowerOnDevice(DeviceBase): 538 def _process_call(self, device): 539 device.power = True 540 return device.power 541 542 543class GetDeviceVendor(DeviceBase): 544 def _process_call(self, device): 545 return device.vendor 546 547 548class GetDeviceMenuLanguage(DeviceBase): 549 def _process_call(self, device): 550 return device.menu_language 551 552 553class IsActiveSource(DeviceBase): 554 def _process_call(self, device): 555 return device.active_source 556 557 558class IsDeviceActive(DeviceBase): 559 def _process_call(self, device): 560 return device.active_device 561 562 563class GetDeviceOSDName(DeviceBase): 564 def _process_call(self, device): 565 return device.osd_name 566 567 568class SetDeviceActiveSource(DeviceBase): 569 def _process_call(self, device): 570 device.active_source = True 571 return device.active_source 572 573 574class RawCommand(AdapterBase): 575 def __call__(self, com_port=None, adapter_name=None, command=""): 576 adapter = self._find_adapter(com_port, adapter_name) 577 return adapter.transmit_command(command) 578 579 def Configure(self, com_port='', adapter_name='', command=''): 580 panel = eg.ConfigPanel() 581 582 adapter_ctrl = AdapterCtrl( 583 panel, 584 com_port, 585 adapter_name, 586 self.plugin.adapters 587 ) 588 589 command_st = panel.StaticText(Text.command_lbl) 590 command_ctrl = panel.TextCtrl(command) 591 592 command_sizer = wx.BoxSizer(wx.HORIZONTAL) 593 command_sizer.Add(command_st, 0, wx.EXPAND | wx.ALL, 5) 594 command_sizer.Add(command_ctrl, 0, wx.EXPAND | wx.ALL, 5) 595 596 panel.sizer.Add(adapter_ctrl, 0, wx.EXPAND) 597 panel.sizer.Add(command_sizer, 0, wx.EXPAND) 598 599 while panel.Affirmed(): 600 com_port, adapter_name = adapter_ctrl.GetValue() 601 panel.SetResult(com_port, adapter_name, command_ctrl.GetValue()) 602 603 604class SetVolume(AdapterBase): 605 606 def __call__(self, com_port=None, adapter_name=None, volume=0): 607 adapter = self._find_adapter(com_port, adapter_name) 608 adapter.volume = volume 609 return adapter.volume 610 611 def Configure(self, com_port='', adapter_name='', volume=0): 612 panel = eg.ConfigPanel() 613 614 adapter_ctrl = AdapterCtrl( 615 panel, 616 com_port, 617 adapter_name, 618 self.plugin.adapters 619 ) 620 volume_st = panel.StaticText(Text.volume_lbl) 621 volume_ctrl = panel.SpinIntCtrl(volume, min=0, max=100) 622 sizer = wx.BoxSizer(wx.HORIZONTAL) 623 sizer.Add(volume_st, 0, wx.EXPAND | wx.ALL, 5) 624 sizer.Add(volume_ctrl, 0, wx.EXPAND | wx.ALL, 5) 625 626 panel.sizer.Add(adapter_ctrl, 0, wx.EXPAND) 627 panel.sizer.Add(sizer, 0, wx.EXPAND) 628 629 while panel.Affirmed(): 630 com_port, adapter_name = adapter_ctrl.GetValue() 631 panel.SetResult(com_port, adapter_name, volume_ctrl.GetValue()) 632 633 634class SendRemoteKey(AdapterBase): 635 636 def __call__( 637 self, 638 com_port=None, 639 adapter_name=None, 640 device='TV', 641 key=None 642 ): 643 if key is None: 644 key = getattr(self, 'value', None) 645 if key is None or (com_port is None and adapter_name is None): 646 eg.PrintNotice( 647 'CEC: This action needs to be configured before use.' 648 ) 649 return 650 651 adapter = self._find_adapter(com_port, adapter_name) 652 653 if adapter is None: 654 eg.PrintNotice( 655 'CEC: Adapter %s on com port %s not found' % 656 (adapter_name, com_port) 657 ) 658 else: 659 d = getattr(adapter, device.lower().replace(' ', ''), None) 660 if d is None: 661 eg.PrintNotice( 662 'CEC: Device %s not found in adapter %s' % 663 (device, adpater.name) 664 ) 665 else: 666 remote = getattr(d, key, None) 667 if remote is None: 668 eg.PrintError( 669 'CEC: Key %s not found for device %s on adapter %s' % 670 (key, device, adpater.name) 671 ) 672 else: 673 import time 674 remote.send_key_press() 675 time.sleep(0.1) 676 remote.send_key_release() 677 678 def Configure(self, com_port='', adapter_name='', device='TV', key=None): 679 680 panel = eg.ConfigPanel() 681 682 adapter_ctrl = AdapterCtrl( 683 panel, 684 com_port, 685 adapter_name, 686 self.plugin.adapters 687 ) 688 689 device_ctrl = DeviceCtrl(panel, device) 690 691 device_ctrl.UpdateDevices( 692 self._find_adapter(*adapter_ctrl.GetValue()) 693 ) 694 695 def on_choice(evt): 696 device_ctrl.UpdateDevices( 697 self._find_adapter(*adapter_ctrl.GetValue()) 698 ) 699 700 evt.Skip() 701 702 adapter_ctrl.Bind(wx.EVT_CHOICE, on_choice) 703 panel.sizer.Add(adapter_ctrl, 0, wx.EXPAND) 704 panel.sizer.Add(device_ctrl, 0, wx.EXPAND) 705 706 if key is None and not hasattr(self, 'value'): 707 key = '' 708 key_st = panel.StaticText(Text.key_lbl) 709 key_ctrl = panel.Choice( 710 0, 711 choices=list(key_name for key_name in UserControlCodes) 712 ) 713 714 key_ctrl.SetStringSelection(key) 715 716 key_sizer = wx.BoxSizer(wx.HORIZONTAL) 717 key_sizer.Add(key_st, 0, wx.EXPAND | wx.ALL, 5) 718 key_sizer.Add(key_ctrl, 0, wx.EXPAND | wx.ALL, 5) 719 panel.sizer.Add(key_sizer, 0, wx.EXPAND) 720 else: 721 key_ctrl = None 722 723 while panel.Affirmed(): 724 com_port, adapter_name = adapter_ctrl.GetValue() 725 panel.SetResult( 726 com_port, 727 adapter_name, 728 device_ctrl.GetValue(), 729 None if key_ctrl is None else key_ctrl.GetStringSelection() 730 ) 731 732REMOTE_ACTIONS = () 733 734for remote_key in UserControlCodes: 735 key_func = remote_key 736 for rep in ('Samsung', 'Blue', 'Red', 'Green', 'Yellow'): 737 key_func = key_func.replace(' (%s)' % rep, '') 738 key_func = key_func.replace('.', 'DOT').replace('+', '_').replace(' ', '_') 739 740 REMOTE_ACTIONS += (( 741 SendRemoteKey, 742 'fn' + key_func.upper(), 743 'Remote Key: ' + remote_key, 744 'Remote Key ' + remote_key, 745 remote_key 746 ),) 747