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