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