1
+ """
2
+ Media Player component to integrate TVs exposing the Joint Space API.
3
+ Updated to support Android-based Philips TVs manufactured from 2014 but before 2016.
4
+ """
5
+ import homeassistant .helpers .config_validation as cv
6
+ import argparse
7
+ import json
8
+ import random
9
+ import requests
10
+ import string
11
+ import sys
12
+ import voluptuous as vol
13
+ import time
14
+ import wakeonlan
15
+
16
+ from base64 import b64encode ,b64decode
17
+ from Crypto .Hash import SHA , HMAC
18
+ from datetime import timedelta , datetime
19
+ from homeassistant .components .media_player import (PLATFORM_SCHEMA , SUPPORT_TURN_ON , SUPPORT_TURN_OFF , SUPPORT_VOLUME_MUTE , SUPPORT_VOLUME_STEP , MediaPlayerDevice )
20
+ from homeassistant .const import (
21
+ CONF_HOST , CONF_MAC , CONF_NAME , CONF_USERNAME , CONF_PASSWORD , STATE_OFF , STATE_ON , STATE_UNKNOWN )
22
+ from homeassistant .util import Throttle
23
+ from requests .auth import HTTPDigestAuth
24
+
25
+ MIN_TIME_BETWEEN_UPDATES = timedelta (seconds = 30 )
26
+
27
+ SUPPORT_PHILIPS_2014 = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE
28
+
29
+ DEFAULT_DEVICE = 'default'
30
+ DEFAULT_HOST = '127.0.0.1'
31
+ DEFAULT_MAC = 'aa:aa:aa:aa:aa:aa'
32
+ DEFAULT_USER = 'user'
33
+ DEFAULT_PASS = 'pass'
34
+ DEFAULT_NAME = 'Philips TV'
35
+ BASE_URL = 'http://{0}:1925/5/{1}'
36
+ TIMEOUT = 5.0
37
+ CONNFAILCOUNT = 5
38
+
39
+ PLATFORM_SCHEMA = PLATFORM_SCHEMA .extend ({
40
+ vol .Required (CONF_HOST , default = DEFAULT_HOST ): cv .string ,
41
+ vol .Required (CONF_MAC , default = DEFAULT_MAC ): cv .string ,
42
+ vol .Optional (CONF_USERNAME , default = DEFAULT_USER ): cv .string ,
43
+ vol .Optional (CONF_PASSWORD , default = DEFAULT_PASS ): cv .string ,
44
+ vol .Optional (CONF_NAME , default = DEFAULT_NAME ): cv .string
45
+ })
46
+
47
+ # pylint: disable=unused-argument
48
+ def setup_platform (hass , config , add_devices , discovery_info = None ):
49
+ """Set up the Philips 2016+ TV platform."""
50
+ name = config .get (CONF_NAME )
51
+ host = config .get (CONF_HOST )
52
+ mac = config .get (CONF_MAC )
53
+ user = config .get (CONF_USERNAME )
54
+ password = config .get (CONF_PASSWORD )
55
+ tvapi = PhilipsTVBase (host , mac , user , password )
56
+ add_devices ([PhilipsTV (tvapi , name )])
57
+
58
+ class PhilipsTV (MediaPlayerDevice ):
59
+ """Representation of a 2014-2015 Philips TV exposing the JointSpace API but not authentication."""
60
+
61
+ def __init__ (self , tv , name ):
62
+ """Initialize the TV."""
63
+ self ._tv = tv
64
+ self ._name = name
65
+ self ._state = STATE_UNKNOWN
66
+ self ._min_volume = None
67
+ self ._max_volume = None
68
+ self ._volume = None
69
+ self ._muted = False
70
+ self ._connfail = 0
71
+
72
+ @property
73
+ def name (self ):
74
+ """Return the device name."""
75
+ return self ._name
76
+
77
+ @property
78
+ def should_poll (self ):
79
+ """Device should be polled."""
80
+ return True
81
+
82
+ @property
83
+ def supported_features (self ):
84
+ """Flag media player features that are supported."""
85
+ return SUPPORT_PHILIPS_2014
86
+
87
+ @property
88
+ def state (self ):
89
+ """Get the device state. An exception means OFF state."""
90
+ return self ._state
91
+
92
+ @property
93
+ def volume_level (self ):
94
+ """Volume level of the media player (0..1)."""
95
+ return self ._volume
96
+
97
+ @property
98
+ def is_volume_muted (self ):
99
+ """Boolean if volume is currently muted."""
100
+ return self ._muted
101
+
102
+ def turn_on (self ):
103
+ """Turn on the device."""
104
+ i = 0
105
+ while ((not self ._tv .on ) and (i < 15 )):
106
+ self ._tv .wol ()
107
+ self ._tv .sendKey ('Standby' )
108
+ time .sleep (2 )
109
+ i += 1
110
+ if self ._tv .on :
111
+ self ._state = STATE_OFF
112
+
113
+ def turn_off (self ):
114
+ """Turn off the device."""
115
+ i = 0
116
+ while ((self ._tv .on ) and (i < 15 )):
117
+ self ._tv .sendKey ('Standby' )
118
+ time .sleep (0.5 )
119
+ i += 1
120
+ if not self ._tv .on :
121
+ self ._state = STATE_OFF
122
+
123
+ def volume_up (self ):
124
+ """Send volume up command."""
125
+ self ._tv .sendKey ('VolumeUp' )
126
+ if not self ._tv .on :
127
+ self ._state = STATE_OFF
128
+
129
+ def volume_down (self ):
130
+ """Send volume down command."""
131
+ self ._tv .sendKey ('VolumeDown' )
132
+ if not self ._tv .on :
133
+ self ._state = STATE_OFF
134
+
135
+ def mute_volume (self , mute ):
136
+ """Send mute command."""
137
+ self ._tv .sendKey ('Mute' )
138
+ if not self ._tv .on :
139
+ self ._state = STATE_OFF
140
+
141
+ @property
142
+ def media_title (self ):
143
+ """Title of current playing media."""
144
+ return None
145
+
146
+ @Throttle (MIN_TIME_BETWEEN_UPDATES )
147
+ def update (self ):
148
+ """Get the latest data and update device state."""
149
+ self ._tv .update ()
150
+ self ._min_volume = self ._tv .min_volume
151
+ self ._max_volume = self ._tv .max_volume
152
+ self ._volume = self ._tv .volume
153
+ self ._muted = self ._tv .muted
154
+ if self ._tv .on :
155
+ self ._state = STATE_ON
156
+ else :
157
+ self ._state = STATE_OFF
158
+
159
+ class PhilipsTVBase (object ):
160
+ def __init__ (self , host , mac , user , password ):
161
+ self ._host = host
162
+ self ._mac = mac
163
+ self ._user = user
164
+ self ._password = password
165
+ self ._connfail = 0
166
+ self .on = None
167
+ self .name = None
168
+ self .min_volume = None
169
+ self .max_volume = None
170
+ self .volume = None
171
+ self .muted = None
172
+ self .sources = None
173
+ self .source_id = None
174
+ self .channels = None
175
+ self .channel_id = None
176
+
177
+ def _getReq (self , path ):
178
+ try :
179
+ if self ._connfail :
180
+ self ._connfail -= 1
181
+ return None
182
+ resp = requests .get (BASE_URL .format (self ._host , path ), timeout = TIMEOUT )
183
+ self .on = True
184
+ return json .loads (resp .text )
185
+ except requests .exceptions .RequestException as err :
186
+ self ._connfail = CONNFAILCOUNT
187
+ self .on = False
188
+ return None
189
+
190
+ def _postReq (self , path , data ):
191
+ try :
192
+ if self ._connfail :
193
+ self ._connfail -= 1
194
+ return False
195
+ resp = requests .post (BASE_URL .format (self ._host , path ), data = json .dumps (data ))
196
+ self .on = True
197
+ if resp .status_code == 200 :
198
+ return True
199
+ else :
200
+ return False
201
+ except requests .exceptions .RequestException as err :
202
+ self ._connfail = CONNFAILCOUNT
203
+ self .on = False
204
+ return False
205
+
206
+ def update (self ):
207
+ self .getName ()
208
+ self .getAudiodata ()
209
+
210
+ def getName (self ):
211
+ r = self ._getReq ('system/name' )
212
+ if r :
213
+ self .name = r ['name' ]
214
+
215
+ def getAudiodata (self ):
216
+ audiodata = self ._getReq ('audio/volume' )
217
+ if audiodata :
218
+ self .min_volume = int (audiodata ['min' ])
219
+ self .max_volume = int (audiodata ['max' ])
220
+ self .volume = audiodata ['current' ]
221
+ self .muted = audiodata ['muted' ]
222
+ else :
223
+ self .min_volume = None
224
+ self .max_volume = None
225
+ self .volume = None
226
+ self .muted = None
227
+
228
+ def setVolume (self , level ):
229
+ if level :
230
+ if self .min_volume != 0 or not self .max_volume :
231
+ self .getAudiodata ()
232
+ if not self .on :
233
+ return
234
+ try :
235
+ targetlevel = int (level )
236
+ except ValueError :
237
+ return
238
+ if targetlevel < self .min_volume + 1 or targetlevel > self .max_volume :
239
+ return
240
+ self ._postReq ('audio/volume' , {'current' : targetlevel , 'muted' : False })
241
+ self .volume = targetlevel
242
+
243
+ def sendKey (self , key ):
244
+ self ._postReq ('input/key' , {'key' : key })
245
+
246
+ def wol (self ):
247
+ wakeonlan .send_magic_packet (self ._mac )
0 commit comments