pykefcontrol 0.7.0__3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pykefcontrol/__init__.py
ADDED
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import aiohttp
|
|
3
|
+
import time
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KefConnector:
|
|
8
|
+
def __init__(self, host):
|
|
9
|
+
self.host = host
|
|
10
|
+
self.previous_volume = self.volume
|
|
11
|
+
self.last_polled = None
|
|
12
|
+
self.polling_queue = None
|
|
13
|
+
self._previous_poll_song_status = False
|
|
14
|
+
|
|
15
|
+
def power_on(self):
|
|
16
|
+
self.status = "powerOn"
|
|
17
|
+
|
|
18
|
+
def shutdown(self):
|
|
19
|
+
self.source = "standby"
|
|
20
|
+
|
|
21
|
+
def mute(self):
|
|
22
|
+
self.previous_volume = self.volume
|
|
23
|
+
self.volume = 0
|
|
24
|
+
|
|
25
|
+
def unmute(self):
|
|
26
|
+
"""
|
|
27
|
+
unmute speaker
|
|
28
|
+
"""
|
|
29
|
+
self.volume = self.previous_volume
|
|
30
|
+
|
|
31
|
+
def toggle_play_pause(self):
|
|
32
|
+
"""
|
|
33
|
+
Toogle play/pause
|
|
34
|
+
"""
|
|
35
|
+
self._track_control("pause")
|
|
36
|
+
|
|
37
|
+
def next_track(self):
|
|
38
|
+
"""
|
|
39
|
+
Next track
|
|
40
|
+
"""
|
|
41
|
+
self._track_control("next")
|
|
42
|
+
|
|
43
|
+
def previous_track(self):
|
|
44
|
+
"""
|
|
45
|
+
Previous track
|
|
46
|
+
"""
|
|
47
|
+
self._track_control("previous")
|
|
48
|
+
|
|
49
|
+
def _track_control(self, command):
|
|
50
|
+
"""
|
|
51
|
+
toogle play/pause
|
|
52
|
+
"""
|
|
53
|
+
payload = {
|
|
54
|
+
"path": "player:player/control",
|
|
55
|
+
"roles": "activate",
|
|
56
|
+
"value": """{{"control":"{command}"}}""".format(command=command),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
with requests.get(
|
|
60
|
+
"http://" + self.host + "/api/setData", params=payload
|
|
61
|
+
) as response:
|
|
62
|
+
json_output = response.json()
|
|
63
|
+
|
|
64
|
+
def set_volume(self, volume):
|
|
65
|
+
"""
|
|
66
|
+
Set volume
|
|
67
|
+
"""
|
|
68
|
+
self.volume = volume
|
|
69
|
+
|
|
70
|
+
def _get_player_data(self):
|
|
71
|
+
"""
|
|
72
|
+
Is the speaker currently playing
|
|
73
|
+
"""
|
|
74
|
+
payload = {
|
|
75
|
+
"path": "player:player/data",
|
|
76
|
+
"roles": "value",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
with requests.get(
|
|
80
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
81
|
+
) as response:
|
|
82
|
+
json_output = response.json()
|
|
83
|
+
|
|
84
|
+
return json_output[0]
|
|
85
|
+
|
|
86
|
+
def get_song_information(self, song_data=None):
|
|
87
|
+
"""
|
|
88
|
+
Get song title, album and artist
|
|
89
|
+
"""
|
|
90
|
+
if song_data == None:
|
|
91
|
+
song_data = self._get_player_data()
|
|
92
|
+
info_dict = dict()
|
|
93
|
+
info_dict["title"] = song_data.get("trackRoles", {}).get("title")
|
|
94
|
+
info_dict["artist"] = (
|
|
95
|
+
song_data.get("trackRoles", {})
|
|
96
|
+
.get("mediaData", {})
|
|
97
|
+
.get("metaData", {})
|
|
98
|
+
.get("artist")
|
|
99
|
+
)
|
|
100
|
+
info_dict["album"] = (
|
|
101
|
+
song_data.get("trackRoles", {})
|
|
102
|
+
.get("mediaData", {})
|
|
103
|
+
.get("metaData", {})
|
|
104
|
+
.get("album")
|
|
105
|
+
)
|
|
106
|
+
info_dict["cover_url"] = song_data.get("trackRoles", {}).get("icon", None)
|
|
107
|
+
|
|
108
|
+
return info_dict
|
|
109
|
+
|
|
110
|
+
def _get_polling_queue(self, song_status=False, poll_song_status=False):
|
|
111
|
+
"""
|
|
112
|
+
Get the polling queue uuid, and subscribe to all relevant topics
|
|
113
|
+
"""
|
|
114
|
+
payload = {
|
|
115
|
+
"subscribe": [
|
|
116
|
+
{"path": "settings:/mediaPlayer/playMode", "type": "itemWithValue"},
|
|
117
|
+
{"path": "playlists:pq/getitems", "type": "rows"},
|
|
118
|
+
{"path": "notifications:/display/queue", "type": "rows"},
|
|
119
|
+
{"path": "settings:/kef/host/maximumVolume", "type": "itemWithValue"},
|
|
120
|
+
{"path": "player:volume", "type": "itemWithValue"},
|
|
121
|
+
{"path": "kef:fwupgrade/info", "type": "itemWithValue"},
|
|
122
|
+
{"path": "settings:/kef/host/volumeStep", "type": "itemWithValue"},
|
|
123
|
+
{"path": "settings:/kef/host/volumeLimit", "type": "itemWithValue"},
|
|
124
|
+
{"path": "settings:/mediaPlayer/mute", "type": "itemWithValue"},
|
|
125
|
+
{"path": "settings:/kef/host/speakerStatus", "type": "itemWithValue"},
|
|
126
|
+
{"path": "settings:/kef/play/physicalSource", "type": "itemWithValue"},
|
|
127
|
+
{"path": "player:player/data", "type": "itemWithValue"},
|
|
128
|
+
{"path": "kef:speedTest/status", "type": "itemWithValue"},
|
|
129
|
+
{"path": "network:info", "type": "itemWithValue"},
|
|
130
|
+
{"path": "kef:eqProfile", "type": "itemWithValue"},
|
|
131
|
+
{"path": "settings:/kef/host/modelName", "type": "itemWithValue"},
|
|
132
|
+
{"path": "settings:/version", "type": "itemWithValue"},
|
|
133
|
+
{"path": "settings:/deviceName", "type": "itemWithValue"},
|
|
134
|
+
],
|
|
135
|
+
"unsubscribe": [],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if song_status:
|
|
139
|
+
payload["subscribe"].append(
|
|
140
|
+
{"path": "player:player/data/playTime", "type": "itemWithValue"}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
with requests.post(
|
|
144
|
+
"http://" + self.host + "/api/event/modifyQueue", json=payload
|
|
145
|
+
) as response:
|
|
146
|
+
json_output = response.json()
|
|
147
|
+
|
|
148
|
+
# Update polling_queue property with queue uuid
|
|
149
|
+
self.polling_queue = json_output[1:-1]
|
|
150
|
+
|
|
151
|
+
# Update last polled time
|
|
152
|
+
self.last_polled = time.time()
|
|
153
|
+
|
|
154
|
+
return self.polling_queue
|
|
155
|
+
|
|
156
|
+
def parse_events(self, events):
|
|
157
|
+
"""Parse events"""
|
|
158
|
+
parsed_events = dict()
|
|
159
|
+
|
|
160
|
+
for event in events:
|
|
161
|
+
if event == "settings:/kef/play/physicalSource":
|
|
162
|
+
parsed_events["source"] = events[event].get("kefPhysicalSource")
|
|
163
|
+
elif event == "player:player/data/playTime":
|
|
164
|
+
parsed_events["song_status"] = events[event].get("i64_")
|
|
165
|
+
elif event == "player:volume":
|
|
166
|
+
parsed_events["volume"] = events[event].get("i32_")
|
|
167
|
+
elif event == "player:player/data":
|
|
168
|
+
parsed_events["song_info"] = self.get_song_information(events[event])
|
|
169
|
+
parsed_events["song_length"] = (
|
|
170
|
+
events[event].get("status", {}).get("duration")
|
|
171
|
+
)
|
|
172
|
+
parsed_events["status"] = events[event].get("state")
|
|
173
|
+
elif event == "settings:/kef/host/speakerStatus":
|
|
174
|
+
parsed_events["speaker_status"] = events[event].get("kefSpeakerStatus")
|
|
175
|
+
elif event == "settings:/deviceName":
|
|
176
|
+
parsed_events["device_name"] = events[event].get("string_")
|
|
177
|
+
elif event == "settings:/mediaPlayer/mute":
|
|
178
|
+
parsed_events["mute"] = events[event].get("bool_")
|
|
179
|
+
else:
|
|
180
|
+
if parsed_events.get("other") == None:
|
|
181
|
+
parsed_events["other"] = {}
|
|
182
|
+
parsed_events["other"].update({event: events[event]})
|
|
183
|
+
|
|
184
|
+
return parsed_events
|
|
185
|
+
|
|
186
|
+
def poll_speaker(self, timeout=10, song_status=False, poll_song_status=False):
|
|
187
|
+
"""poll speaker for info"""
|
|
188
|
+
|
|
189
|
+
if song_status:
|
|
190
|
+
warnings.warn(
|
|
191
|
+
"The 'song_status' parameter is deprecated and will be removed in version 0.8.0. "
|
|
192
|
+
"Please use 'poll_song_status' instead.",
|
|
193
|
+
DeprecationWarning,
|
|
194
|
+
stacklevel=2,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# check if it is necessary to get a new queue
|
|
198
|
+
# recreate a new queue if polling_queue is None
|
|
199
|
+
# or if last poll was more than 50 seconds ago
|
|
200
|
+
if (
|
|
201
|
+
(self.polling_queue == None)
|
|
202
|
+
or ((time.time() - self.last_polled) > 50)
|
|
203
|
+
or (song_status != self._previous_poll_song_status)
|
|
204
|
+
):
|
|
205
|
+
self._previous_poll_song_status = song_status
|
|
206
|
+
self._get_polling_queue(poll_song_status=poll_song_status)
|
|
207
|
+
|
|
208
|
+
payload = {
|
|
209
|
+
"queueId": "{{{}}}".format(self.polling_queue),
|
|
210
|
+
"timeout": timeout,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
with requests.get(
|
|
214
|
+
"http://" + self.host + "/api/event/pollQueue",
|
|
215
|
+
params=payload,
|
|
216
|
+
timeout=timeout + 0.5, # add 0.5 seconds to timeout to allow for processing
|
|
217
|
+
) as response:
|
|
218
|
+
json_output = response.json()
|
|
219
|
+
|
|
220
|
+
# Process all events
|
|
221
|
+
|
|
222
|
+
events = dict()
|
|
223
|
+
# fill events lists
|
|
224
|
+
for j in json_output:
|
|
225
|
+
if events.get(j["path"], False):
|
|
226
|
+
events[j["path"]].append(j)
|
|
227
|
+
else:
|
|
228
|
+
events[j["path"]] = [j]
|
|
229
|
+
# prune events lists
|
|
230
|
+
for k in events:
|
|
231
|
+
events[k] = events[k][-1].get("itemValue", "updated")
|
|
232
|
+
|
|
233
|
+
return self.parse_events(events)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def mac_address(self):
|
|
237
|
+
"""Get the mac address of the Speaker"""
|
|
238
|
+
"""http://192.168.124.46/api/getData?path=settings:/system/primaryMacAddress&roles=value"""
|
|
239
|
+
payload = {"path": "settings:/system/primaryMacAddress", "roles": "value"}
|
|
240
|
+
|
|
241
|
+
with requests.get(
|
|
242
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
243
|
+
) as response:
|
|
244
|
+
json_output = response.json()
|
|
245
|
+
|
|
246
|
+
return json_output[0]["string_"]
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def speaker_name(self):
|
|
250
|
+
"""Get the friendly name of the Speaker"""
|
|
251
|
+
payload = {"path": "settings:/deviceName", "roles": "value"}
|
|
252
|
+
|
|
253
|
+
with requests.get(
|
|
254
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
255
|
+
) as response:
|
|
256
|
+
json_output = response.json()
|
|
257
|
+
|
|
258
|
+
return json_output[0]["string_"]
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def status(self):
|
|
262
|
+
"""Status of the speaker : standby or poweredOn"""
|
|
263
|
+
payload = {"path": "settings:/kef/host/speakerStatus", "roles": "value"}
|
|
264
|
+
|
|
265
|
+
with requests.get(
|
|
266
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
267
|
+
) as response:
|
|
268
|
+
json_output = response.json()
|
|
269
|
+
|
|
270
|
+
return json_output[0]["kefSpeakerStatus"]
|
|
271
|
+
|
|
272
|
+
@status.setter
|
|
273
|
+
def status(self, status):
|
|
274
|
+
payload = {
|
|
275
|
+
"path": "settings:/kef/play/physicalSource",
|
|
276
|
+
"roles": "value",
|
|
277
|
+
"value": """{{"type":"kefPhysicalSource","kefPhysicalSource":"{status}"}}""".format(
|
|
278
|
+
status=status
|
|
279
|
+
),
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
with requests.get(
|
|
283
|
+
"http://" + self.host + "/api/setData", params=payload
|
|
284
|
+
) as response:
|
|
285
|
+
json_output = response.json()
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def source(self):
|
|
289
|
+
"""
|
|
290
|
+
Speaker source : standby (not powered on),
|
|
291
|
+
wifi, bluetooth, tv, optic, coaxial or analog
|
|
292
|
+
"""
|
|
293
|
+
payload = {
|
|
294
|
+
"path": "settings:/kef/play/physicalSource",
|
|
295
|
+
"roles": "value",
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
with requests.get(
|
|
299
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
300
|
+
) as response:
|
|
301
|
+
json_output = response.json()
|
|
302
|
+
|
|
303
|
+
return json_output[0]["kefPhysicalSource"]
|
|
304
|
+
|
|
305
|
+
@source.setter
|
|
306
|
+
def source(self, source):
|
|
307
|
+
"""
|
|
308
|
+
Set spaker source, if speaker in standby, it powers on the speaker.
|
|
309
|
+
Possible sources : wifi, bluetooth, tv, optic, coaxial or analog
|
|
310
|
+
"""
|
|
311
|
+
payload = {
|
|
312
|
+
"path": "settings:/kef/play/physicalSource",
|
|
313
|
+
"roles": "value",
|
|
314
|
+
"value": """{{"type":"kefPhysicalSource","kefPhysicalSource":"{source}"}}""".format(
|
|
315
|
+
source=source
|
|
316
|
+
),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
with requests.get(
|
|
320
|
+
"http://" + self.host + "/api/setData", params=payload
|
|
321
|
+
) as response:
|
|
322
|
+
json_output = response.json()
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def volume(self):
|
|
326
|
+
"""
|
|
327
|
+
Speaker volume (1 to 100, 0 = muted)
|
|
328
|
+
"""
|
|
329
|
+
payload = {
|
|
330
|
+
"path": "player:volume",
|
|
331
|
+
"roles": "value",
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
with requests.get(
|
|
335
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
336
|
+
) as response:
|
|
337
|
+
json_output = response.json()
|
|
338
|
+
|
|
339
|
+
return json_output[0]["i32_"]
|
|
340
|
+
|
|
341
|
+
@volume.setter
|
|
342
|
+
def volume(self, volume):
|
|
343
|
+
payload = {
|
|
344
|
+
"path": "player:volume",
|
|
345
|
+
"roles": "value",
|
|
346
|
+
"value": """{{"type":"i32_","i32_":{volume}}}""".format(volume=volume),
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
with requests.get(
|
|
350
|
+
"http://" + self.host + "/api/setData", params=payload
|
|
351
|
+
) as response:
|
|
352
|
+
json_output = response.json()
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def is_playing(self):
|
|
356
|
+
"""
|
|
357
|
+
Is the speaker currently playing
|
|
358
|
+
"""
|
|
359
|
+
return self._get_player_data()["state"] == "playing"
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def song_length(self):
|
|
363
|
+
"""
|
|
364
|
+
Song length in ms
|
|
365
|
+
"""
|
|
366
|
+
if self.is_playing:
|
|
367
|
+
return self._get_player_data()["status"]["duration"]
|
|
368
|
+
else:
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def song_status(self):
|
|
373
|
+
"""
|
|
374
|
+
Progression of song
|
|
375
|
+
"""
|
|
376
|
+
payload = {
|
|
377
|
+
"path": "player:player/data/playTime",
|
|
378
|
+
"roles": "value",
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
with requests.get(
|
|
382
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
383
|
+
) as response:
|
|
384
|
+
json_output = response.json()
|
|
385
|
+
|
|
386
|
+
return json_output[0]["i64_"]
|
|
387
|
+
|
|
388
|
+
def _get_speaker_firmware_version(self):
|
|
389
|
+
"""
|
|
390
|
+
Get speaker firmware "release text"
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
payload = {
|
|
394
|
+
"path": "settings:/releasetext",
|
|
395
|
+
"roles": "value",
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
with requests.get(
|
|
399
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
400
|
+
) as response:
|
|
401
|
+
json_output = response.json()
|
|
402
|
+
|
|
403
|
+
return json_output[0]["string_"]
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def speaker_model(self):
|
|
407
|
+
"""
|
|
408
|
+
Speaker model
|
|
409
|
+
"""
|
|
410
|
+
speaker_model = self._get_speaker_firmware_version().split("_")[0]
|
|
411
|
+
return speaker_model
|
|
412
|
+
|
|
413
|
+
@property
|
|
414
|
+
def firmware_version(self):
|
|
415
|
+
"""
|
|
416
|
+
Speaker firmware version
|
|
417
|
+
"""
|
|
418
|
+
speaker_firmware_version = self._get_speaker_firmware_version().split("_")[1]
|
|
419
|
+
return speaker_firmware_version
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class KefAsyncConnector:
|
|
423
|
+
def __init__(self, host, session=None):
|
|
424
|
+
self.host = host
|
|
425
|
+
self._session = session
|
|
426
|
+
self.previous_volume = (
|
|
427
|
+
15 # Hardcoded previous volume, in case unmute is used before mute
|
|
428
|
+
)
|
|
429
|
+
self.last_polled = None
|
|
430
|
+
self.polling_queue = None
|
|
431
|
+
self._previous_poll_song_status = False
|
|
432
|
+
|
|
433
|
+
async def close_session(self):
|
|
434
|
+
"""close session"""
|
|
435
|
+
if self._session is not None:
|
|
436
|
+
await self._session.close()
|
|
437
|
+
self._session = None
|
|
438
|
+
|
|
439
|
+
async def resurect_session(self):
|
|
440
|
+
if self._session is None:
|
|
441
|
+
self._session = aiohttp.ClientSession()
|
|
442
|
+
|
|
443
|
+
async def power_on(self):
|
|
444
|
+
"""power on speaker"""
|
|
445
|
+
await self.set_status("powerOn")
|
|
446
|
+
|
|
447
|
+
async def shutdown(self):
|
|
448
|
+
"""Shutdown speaker"""
|
|
449
|
+
await self.set_source("standby")
|
|
450
|
+
|
|
451
|
+
async def mute(self):
|
|
452
|
+
"""mute speaker"""
|
|
453
|
+
self.previous_volume = await self.volume
|
|
454
|
+
await self.set_volume(0)
|
|
455
|
+
|
|
456
|
+
async def unmute(self):
|
|
457
|
+
"""unmute speaker"""
|
|
458
|
+
await self.set_volume(self.previous_volume)
|
|
459
|
+
|
|
460
|
+
async def toggle_play_pause(self):
|
|
461
|
+
"""Toogle play/pause"""
|
|
462
|
+
await self._track_control("pause")
|
|
463
|
+
|
|
464
|
+
async def next_track(self):
|
|
465
|
+
"""Next track"""
|
|
466
|
+
await self._track_control("next")
|
|
467
|
+
|
|
468
|
+
async def previous_track(self):
|
|
469
|
+
"""Previous track"""
|
|
470
|
+
await self._track_control("previous")
|
|
471
|
+
|
|
472
|
+
async def _track_control(self, command):
|
|
473
|
+
"""toogle play/pause"""
|
|
474
|
+
payload = {
|
|
475
|
+
"path": "player:player/control",
|
|
476
|
+
"roles": "activate",
|
|
477
|
+
"value": """{{"control":"{command}"}}""".format(command=command),
|
|
478
|
+
}
|
|
479
|
+
await self.resurect_session()
|
|
480
|
+
async with self._session.get(
|
|
481
|
+
"http://" + self.host + "/api/setData", params=payload
|
|
482
|
+
) as response:
|
|
483
|
+
json_output = await response.json()
|
|
484
|
+
|
|
485
|
+
async def _get_player_data(self):
|
|
486
|
+
"""get data about currently playing media"""
|
|
487
|
+
payload = {
|
|
488
|
+
"path": "player:player/data",
|
|
489
|
+
"roles": "value",
|
|
490
|
+
}
|
|
491
|
+
await self.resurect_session()
|
|
492
|
+
async with self._session.get(
|
|
493
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
494
|
+
) as response:
|
|
495
|
+
json_output = await response.json()
|
|
496
|
+
|
|
497
|
+
return json_output[0]
|
|
498
|
+
|
|
499
|
+
async def set_source(self, source):
|
|
500
|
+
"""Set spaker source, if speaker in standby, it powers on the speaker.
|
|
501
|
+
Possible sources : wifi, bluetooth, tv, optic, coaxial or analog"""
|
|
502
|
+
payload = {
|
|
503
|
+
"path": "settings:/kef/play/physicalSource",
|
|
504
|
+
"roles": "value",
|
|
505
|
+
"value": """{{"type":"kefPhysicalSource","kefPhysicalSource":"{source}"}}""".format(
|
|
506
|
+
source=source
|
|
507
|
+
),
|
|
508
|
+
}
|
|
509
|
+
await self.resurect_session()
|
|
510
|
+
async with self._session.get(
|
|
511
|
+
"http://" + self.host + "/api/setData", params=payload
|
|
512
|
+
) as response:
|
|
513
|
+
json_output = await response.json()
|
|
514
|
+
|
|
515
|
+
async def set_volume(self, volume):
|
|
516
|
+
"""Set speaker volume (between 0 and 100)"""
|
|
517
|
+
payload = {
|
|
518
|
+
"path": "player:volume",
|
|
519
|
+
"roles": "value",
|
|
520
|
+
"value": """{{"type":"i32_","i32_":{volume}}}""".format(volume=volume),
|
|
521
|
+
}
|
|
522
|
+
await self.resurect_session()
|
|
523
|
+
async with self._session.get(
|
|
524
|
+
"http://" + self.host + "/api/setData", params=payload
|
|
525
|
+
) as response:
|
|
526
|
+
json_output = await response.json()
|
|
527
|
+
|
|
528
|
+
async def set_status(self, status):
|
|
529
|
+
payload = {
|
|
530
|
+
"path": "settings:/kef/play/physicalSource",
|
|
531
|
+
"roles": "value",
|
|
532
|
+
"value": """{{"type":"kefPhysicalSource","kefPhysicalSource":"{status}"}}""".format(
|
|
533
|
+
status=status
|
|
534
|
+
),
|
|
535
|
+
}
|
|
536
|
+
await self.resurect_session()
|
|
537
|
+
async with self._session.get(
|
|
538
|
+
"http://" + self.host + "/api/setData", params=payload
|
|
539
|
+
) as response:
|
|
540
|
+
json_output = await response.json()
|
|
541
|
+
|
|
542
|
+
async def get_song_information(self, song_data=None):
|
|
543
|
+
"""Get song title, album and artist"""
|
|
544
|
+
if song_data == None:
|
|
545
|
+
song_data = await self._get_player_data()
|
|
546
|
+
info_dict = dict()
|
|
547
|
+
info_dict["title"] = song_data.get("trackRoles", {}).get("title")
|
|
548
|
+
info_dict["artist"] = (
|
|
549
|
+
song_data.get("trackRoles", {})
|
|
550
|
+
.get("mediaData", {})
|
|
551
|
+
.get("metaData", {})
|
|
552
|
+
.get("artist")
|
|
553
|
+
)
|
|
554
|
+
info_dict["album"] = (
|
|
555
|
+
song_data.get("trackRoles", {})
|
|
556
|
+
.get("mediaData", {})
|
|
557
|
+
.get("metaData", {})
|
|
558
|
+
.get("album")
|
|
559
|
+
)
|
|
560
|
+
info_dict["cover_url"] = song_data.get("trackRoles", {}).get("icon", None)
|
|
561
|
+
|
|
562
|
+
return info_dict
|
|
563
|
+
|
|
564
|
+
async def get_polling_queue(self, song_status=False, poll_song_status=False):
|
|
565
|
+
"""Get the polling queue uuid, and subscribe to all relevant topics"""
|
|
566
|
+
payload = {
|
|
567
|
+
"subscribe": [
|
|
568
|
+
{"path": "settings:/mediaPlayer/playMode", "type": "itemWithValue"},
|
|
569
|
+
{"path": "playlists:pq/getitems", "type": "rows"},
|
|
570
|
+
{"path": "notifications:/display/queue", "type": "rows"},
|
|
571
|
+
{"path": "settings:/kef/host/maximumVolume", "type": "itemWithValue"},
|
|
572
|
+
{"path": "player:volume", "type": "itemWithValue"},
|
|
573
|
+
{"path": "kef:fwupgrade/info", "type": "itemWithValue"},
|
|
574
|
+
{"path": "settings:/kef/host/volumeStep", "type": "itemWithValue"},
|
|
575
|
+
{"path": "settings:/kef/host/volumeLimit", "type": "itemWithValue"},
|
|
576
|
+
{"path": "settings:/mediaPlayer/mute", "type": "itemWithValue"},
|
|
577
|
+
{"path": "settings:/kef/host/speakerStatus", "type": "itemWithValue"},
|
|
578
|
+
{"path": "settings:/kef/play/physicalSource", "type": "itemWithValue"},
|
|
579
|
+
{"path": "player:player/data", "type": "itemWithValue"},
|
|
580
|
+
{"path": "kef:speedTest/status", "type": "itemWithValue"},
|
|
581
|
+
{"path": "network:info", "type": "itemWithValue"},
|
|
582
|
+
{"path": "kef:eqProfile", "type": "itemWithValue"},
|
|
583
|
+
{"path": "settings:/kef/host/modelName", "type": "itemWithValue"},
|
|
584
|
+
{"path": "settings:/version", "type": "itemWithValue"},
|
|
585
|
+
{"path": "settings:/deviceName", "type": "itemWithValue"},
|
|
586
|
+
],
|
|
587
|
+
"unsubscribe": [],
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if song_status:
|
|
591
|
+
payload["subscribe"].append(
|
|
592
|
+
{"path": "player:player/data/playTime", "type": "itemWithValue"}
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
await self.resurect_session()
|
|
596
|
+
async with self._session.post(
|
|
597
|
+
"http://" + self.host + "/api/event/modifyQueue", json=payload
|
|
598
|
+
) as response:
|
|
599
|
+
json_output = await response.json()
|
|
600
|
+
|
|
601
|
+
# Update polling_queue property with queue uuid
|
|
602
|
+
self.polling_queue = json_output[1:-1]
|
|
603
|
+
|
|
604
|
+
# Update last polled time
|
|
605
|
+
self.last_polled = time.time()
|
|
606
|
+
|
|
607
|
+
return self.polling_queue
|
|
608
|
+
|
|
609
|
+
async def parse_events(self, events):
|
|
610
|
+
"""Parse events"""
|
|
611
|
+
parsed_events = dict()
|
|
612
|
+
|
|
613
|
+
for event in events:
|
|
614
|
+
if event == "settings:/kef/play/physicalSource":
|
|
615
|
+
parsed_events["source"] = events[event].get("kefPhysicalSource")
|
|
616
|
+
elif event == "player:player/data/playTime":
|
|
617
|
+
parsed_events["song_status"] = events[event].get("i64_")
|
|
618
|
+
elif event == "player:volume":
|
|
619
|
+
parsed_events["volume"] = events[event].get("i32_")
|
|
620
|
+
elif event == "player:player/data":
|
|
621
|
+
parsed_events["song_info"] = await self.get_song_information(
|
|
622
|
+
events[event]
|
|
623
|
+
)
|
|
624
|
+
parsed_events["song_length"] = (
|
|
625
|
+
events[event].get("status", {}).get("duration")
|
|
626
|
+
)
|
|
627
|
+
parsed_events["status"] = events[event].get("state")
|
|
628
|
+
elif event == "settings:/kef/host/speakerStatus":
|
|
629
|
+
parsed_events["speaker_status"] = events[event].get("kefSpeakerStatus")
|
|
630
|
+
elif event == "settings:/deviceName":
|
|
631
|
+
parsed_events["device_name"] = events[event].get("string_")
|
|
632
|
+
elif event == "settings:/mediaPlayer/mute":
|
|
633
|
+
parsed_events["mute"] = events[event].get("bool_")
|
|
634
|
+
else:
|
|
635
|
+
if parsed_events.get("other") == None:
|
|
636
|
+
parsed_events["other"] = {}
|
|
637
|
+
parsed_events["other"].update({event: events[event]})
|
|
638
|
+
|
|
639
|
+
return parsed_events
|
|
640
|
+
|
|
641
|
+
async def poll_speaker(self, timeout=10, song_status=False, poll_song_status=False):
|
|
642
|
+
"""poll speaker for info"""
|
|
643
|
+
|
|
644
|
+
if song_status:
|
|
645
|
+
warnings.warn(
|
|
646
|
+
"The 'song_status' parameter is deprecated and will be removed in version 0.8.0. "
|
|
647
|
+
"Please use 'poll_song_status' instead.",
|
|
648
|
+
DeprecationWarning,
|
|
649
|
+
stacklevel=2,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# check if it is necessary to get a new queue
|
|
653
|
+
if (
|
|
654
|
+
(self.polling_queue == None)
|
|
655
|
+
or ((time.time() - self.last_polled) > 50)
|
|
656
|
+
or (song_status != self._previous_polling_song_status)
|
|
657
|
+
):
|
|
658
|
+
await self.get_polling_queue(poll_song_status=poll_song_status)
|
|
659
|
+
|
|
660
|
+
payload = {"queueId": "{{{}}}".format(self.polling_queue), "timeout": timeout}
|
|
661
|
+
|
|
662
|
+
await self.resurect_session()
|
|
663
|
+
async with self._session.get(
|
|
664
|
+
"http://" + self.host + "/api/event/pollQueue",
|
|
665
|
+
params=payload,
|
|
666
|
+
timeout=10 + 0.5, # add 0.5 seconds to timeout to allow for processing
|
|
667
|
+
) as response:
|
|
668
|
+
json_output = await response.json()
|
|
669
|
+
|
|
670
|
+
# Process all events
|
|
671
|
+
|
|
672
|
+
events = dict()
|
|
673
|
+
# fill events lists
|
|
674
|
+
for j in json_output:
|
|
675
|
+
if events.get(j["path"], False):
|
|
676
|
+
events[j["path"]].append(j)
|
|
677
|
+
else:
|
|
678
|
+
events[j["path"]] = [j]
|
|
679
|
+
# prune events lists
|
|
680
|
+
for k in events:
|
|
681
|
+
events[k] = events[k][-1].get("itemValue", "updated")
|
|
682
|
+
|
|
683
|
+
return await self.parse_events(events)
|
|
684
|
+
|
|
685
|
+
@property
|
|
686
|
+
async def mac_address(self):
|
|
687
|
+
"""Get the mac address of the Speaker"""
|
|
688
|
+
payload = {"path": "settings:/system/primaryMacAddress", "roles": "value"}
|
|
689
|
+
await self.resurect_session()
|
|
690
|
+
async with self._session.get(
|
|
691
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
692
|
+
) as response:
|
|
693
|
+
json_output = await response.json()
|
|
694
|
+
|
|
695
|
+
return json_output[0]["string_"]
|
|
696
|
+
|
|
697
|
+
@property
|
|
698
|
+
async def speaker_name(self):
|
|
699
|
+
"""Get the friendly name of the Speaker"""
|
|
700
|
+
payload = {"path": "settings:/deviceName", "roles": "value"}
|
|
701
|
+
await self.resurect_session()
|
|
702
|
+
async with self._session.get(
|
|
703
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
704
|
+
) as response:
|
|
705
|
+
json_output = await response.json()
|
|
706
|
+
|
|
707
|
+
return json_output[0]["string_"]
|
|
708
|
+
|
|
709
|
+
@property
|
|
710
|
+
async def status(self):
|
|
711
|
+
"""Status of the speaker : standby or poweredOn"""
|
|
712
|
+
payload = {"path": "settings:/kef/host/speakerStatus", "roles": "value"}
|
|
713
|
+
await self.resurect_session()
|
|
714
|
+
async with self._session.get(
|
|
715
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
716
|
+
) as response:
|
|
717
|
+
json_output = await response.json()
|
|
718
|
+
|
|
719
|
+
return json_output[0]["kefSpeakerStatus"]
|
|
720
|
+
|
|
721
|
+
@property
|
|
722
|
+
async def is_playing(self):
|
|
723
|
+
"""Is the speaker currently playing"""
|
|
724
|
+
json_output = await self._get_player_data()
|
|
725
|
+
return json_output["state"] == "playing"
|
|
726
|
+
|
|
727
|
+
@property
|
|
728
|
+
async def song_length(self):
|
|
729
|
+
"""Song length in ms"""
|
|
730
|
+
if await self.is_playing:
|
|
731
|
+
json_output = await self._get_player_data()
|
|
732
|
+
return json_output["status"]["duration"]
|
|
733
|
+
else:
|
|
734
|
+
return None
|
|
735
|
+
|
|
736
|
+
@property
|
|
737
|
+
async def song_status(self):
|
|
738
|
+
"""Progression of song"""
|
|
739
|
+
payload = {
|
|
740
|
+
"path": "player:player/data/playTime",
|
|
741
|
+
"roles": "value",
|
|
742
|
+
}
|
|
743
|
+
await self.resurect_session()
|
|
744
|
+
async with self._session.get(
|
|
745
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
746
|
+
) as response:
|
|
747
|
+
json_output = await response.json()
|
|
748
|
+
|
|
749
|
+
return json_output[0]["i64_"]
|
|
750
|
+
|
|
751
|
+
@property
|
|
752
|
+
async def source(self):
|
|
753
|
+
"""Speaker soe : standby (not powered on), wifi, bluetooth, tv, optic,
|
|
754
|
+
coaxial or analog"""
|
|
755
|
+
payload = {
|
|
756
|
+
"path": "settings:/kef/play/physicalSource",
|
|
757
|
+
"roles": "value",
|
|
758
|
+
}
|
|
759
|
+
await self.resurect_session()
|
|
760
|
+
async with self._session.get(
|
|
761
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
762
|
+
) as response:
|
|
763
|
+
json_output = await response.json()
|
|
764
|
+
|
|
765
|
+
return json_output[0]["kefPhysicalSource"]
|
|
766
|
+
|
|
767
|
+
@property
|
|
768
|
+
async def volume(self):
|
|
769
|
+
"""Speaker volume (1 to 100, 0 = muted)"""
|
|
770
|
+
payload = {
|
|
771
|
+
"path": "player:volume",
|
|
772
|
+
"roles": "value",
|
|
773
|
+
}
|
|
774
|
+
await self.resurect_session()
|
|
775
|
+
async with self._session.get(
|
|
776
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
777
|
+
) as response:
|
|
778
|
+
json_output = await response.json()
|
|
779
|
+
|
|
780
|
+
return json_output[0]["i32_"]
|
|
781
|
+
|
|
782
|
+
async def _get_speaker_firmware_version(self):
|
|
783
|
+
"""
|
|
784
|
+
Get speaker firmware "release text"
|
|
785
|
+
"""
|
|
786
|
+
|
|
787
|
+
payload = {
|
|
788
|
+
"path": "settings:/releasetext",
|
|
789
|
+
"roles": "value",
|
|
790
|
+
}
|
|
791
|
+
await self.resurect_session()
|
|
792
|
+
async with self._session.get(
|
|
793
|
+
"http://" + self.host + "/api/getData", params=payload
|
|
794
|
+
) as response:
|
|
795
|
+
json_output = await response.json()
|
|
796
|
+
|
|
797
|
+
return json_output[0]["string_"]
|
|
798
|
+
|
|
799
|
+
async def get_speaker_model(self):
|
|
800
|
+
"""
|
|
801
|
+
Speaker model
|
|
802
|
+
"""
|
|
803
|
+
speaker_model = await self._get_speaker_firmware_version().split("_")[0]
|
|
804
|
+
return speaker_model
|
|
805
|
+
|
|
806
|
+
async def get_firmware_version(self):
|
|
807
|
+
"""
|
|
808
|
+
Speaker firmware version
|
|
809
|
+
"""
|
|
810
|
+
speaker_firmware_version = await self._get_speaker_firmware_version().split(
|
|
811
|
+
"_"
|
|
812
|
+
)[1]
|
|
813
|
+
return speaker_firmware_version
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 rodupont
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pykefcontrol
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Python library for controling KEF speakers
|
|
5
|
+
Home-page: https://github.com/rodupont/pykefcontrol
|
|
6
|
+
Author: Robin Dupont
|
|
7
|
+
Author-email: robin.dpnt@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: Kef,KEF,Speaker,LS50,LS50W2,Wireless II,Wireless 2,LSX,LSX2,LS60,LS60W
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Topic :: Home Automation
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: requests (>=2.26.0)
|
|
20
|
+
Requires-Dist: aiohttp (>=3.7.4)
|
|
21
|
+
|
|
22
|
+
Python library for controling KEF speakers (LS50W2, LSX2, LS60). It supports basic commands for setting the volume, the source, and getting the media playing informations
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pykefcontrol/__init__.py,sha256=AtLPzkmBoQPtss2rJBqe0l5CwNIRt_mQuScOzB3Kq4A,94
|
|
2
|
+
pykefcontrol/kef_connector.py,sha256=og-lmM0jaXausOCwrA4cYJScSnZ7Iz9ZKeZQjRUtvJQ,27553
|
|
3
|
+
pykefcontrol-0.7.0.dist-info/LICENSE,sha256=bHMz7H_RDkXelud5gGBaEyLn_xv7Moc0jeEvMzgaAxQ,1065
|
|
4
|
+
pykefcontrol-0.7.0.dist-info/METADATA,sha256=8Y8Kjeit0HT2cOnRh1s734kKuvNZ5IwuQ5-GuiW4YUI,952
|
|
5
|
+
pykefcontrol-0.7.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
6
|
+
pykefcontrol-0.7.0.dist-info/top_level.txt,sha256=Mp2tOSOUkq5lUkWsWBaX8Yvmjy4IduQWyJy_L5QEeus,13
|
|
7
|
+
pykefcontrol-0.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pykefcontrol
|