Skip to content

Commit 857378b

Browse files
committed
initial commit
1 parent 90e554f commit 857378b

4 files changed

Lines changed: 558 additions & 0 deletions

File tree

afsapi/__init__.py

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
"""
2+
Implements an asynchronous interface for a Frontier Silicon device.
3+
For example internet radios from: Medion, Hama, Auna, ...
4+
"""
5+
import asyncio
6+
import aiohttp
7+
from lxml import objectify
8+
9+
# pylint: disable=R0904
10+
class AFSAPI():
11+
"""Builds the interface to a Frontier Silicon device."""
12+
13+
# states
14+
PLAY_STATES = {
15+
0: 'stopped',
16+
1: 'unknown',
17+
2: 'playing',
18+
3: 'paused',
19+
}
20+
21+
# implemented API calls
22+
API = {
23+
# sys
24+
'friendly_name': 'netRemote.sys.info.friendlyName',
25+
'power': 'netRemote.sys.power',
26+
'mode': 'netRemote.sys.mode',
27+
'valid_modes': 'netRemote.sys.caps.validModes',
28+
'equalisers': 'netRemote.sys.caps.eqPresets',
29+
'sleep': 'netRemote.sys.sleep',
30+
# volume
31+
'volume_steps': 'netRemote.sys.caps.volumeSteps',
32+
'volume': 'netRemote.sys.audio.volume',
33+
'mute': 'netRemote.sys.audio.mute',
34+
# play
35+
'status': 'netRemote.play.status',
36+
'name': 'netRemote.play.info.name',
37+
'control': 'netRemote.play.control',
38+
# info
39+
'text': 'netRemote.play.info.text',
40+
'artist': 'netRemote.play.info.artist',
41+
'album': 'netRemote.play.info.album',
42+
'graphic_uri': 'netRemote.play.info.graphicUri',
43+
'duration': 'netRemote.play.info.duration',
44+
}
45+
46+
def __init__(self, fsapi_device_url, pin):
47+
self.fsapi_device_url = fsapi_device_url
48+
self.pin = pin
49+
self.sid = None
50+
self.__webfsapi = None
51+
self.__modes = None
52+
self.__volume_steps = None
53+
self.__equalisers = None
54+
self.__session = aiohttp.ClientSession()
55+
56+
def __del__(self):
57+
self.call('DELETE_SESSION')
58+
self.__session.close()
59+
60+
# http request helpers
61+
62+
@asyncio.coroutine
63+
def get_fsapi_endpoint(self):
64+
"""Parse the fsapi endpoint from the device url."""
65+
endpoint = yield from self.__session.get(self.fsapi_device_url)
66+
text = yield from endpoint.text(encoding='utf-8')
67+
doc = objectify.fromstring(text)
68+
return doc.webfsapi.text
69+
70+
@asyncio.coroutine
71+
def create_session(self):
72+
"""Create a session on the frontier silicon device."""
73+
req_url = '%s/%s' % (self.__webfsapi, 'CREATE_SESSION')
74+
sid = yield from self.__session.get(req_url, params=dict(pin=self.pin))
75+
text = yield from sid.text(encoding='utf-8')
76+
doc = objectify.fromstring(text)
77+
return doc.sessionId.text
78+
79+
@asyncio.coroutine
80+
def call(self, path, extra=None):
81+
"""Execute a frontier silicon API call."""
82+
if not self.__webfsapi:
83+
self.__webfsapi = yield from self.get_fsapi_endpoint()
84+
85+
if not self.sid:
86+
self.sid = yield from self.create_session()
87+
88+
if not isinstance(extra, dict):
89+
extra = dict()
90+
91+
params = dict(pin=self.pin, sid=self.sid)
92+
params.update(**extra)
93+
94+
req_url = ('%s/%s' % (self.__webfsapi, path))
95+
result = yield from self.__session.get(req_url, params=params)
96+
if result.status == 200:
97+
text = yield from result.text(encoding='utf-8')
98+
else:
99+
self.sid = yield from self.create_session()
100+
params = dict(pin=self.pin, sid=self.sid)
101+
params.update(**extra)
102+
result = yield from self.__session.get(req_url, params=params)
103+
text = yield from result.text(encoding='utf-8')
104+
105+
return objectify.fromstring(text)
106+
107+
# Helper methods
108+
109+
# Handlers
110+
@asyncio.coroutine
111+
def handle_get(self, item):
112+
"""Helper method for reading a value by using the fsapi API."""
113+
res = yield from self.call('GET/{}'.format(item))
114+
return res
115+
116+
@asyncio.coroutine
117+
def handle_set(self, item, value):
118+
"""Helper method for setting a value by using the fsapi API."""
119+
doc = yield from self.call('SET/{}'.format(item), dict(value=value))
120+
if doc is None:
121+
return None
122+
123+
return doc.status == 'FS_OK'
124+
125+
@asyncio.coroutine
126+
def handle_text(self, item):
127+
"""Helper method for fetching a text value."""
128+
doc = yield from self.handle_get(item)
129+
if doc is None:
130+
return None
131+
132+
return doc.value.c8_array.text or None
133+
134+
@asyncio.coroutine
135+
def handle_int(self, item):
136+
"""Helper method for fetching a integer value."""
137+
doc = yield from self.handle_get(item)
138+
if doc is None:
139+
return None
140+
141+
return int(doc.value.u8.text) or None
142+
143+
# returns an int, assuming the value does not exceed 8 bits
144+
@asyncio.coroutine
145+
def handle_long(self, item):
146+
"""Helper method for fetching a long value. Result is integer."""
147+
doc = yield from self.handle_get(item)
148+
if doc is None:
149+
return None
150+
151+
return int(doc.value.u32.text) or None
152+
153+
@asyncio.coroutine
154+
def handle_list(self, item):
155+
"""Helper method for fetching a list(map) value."""
156+
doc = yield from self.call('LIST_GET_NEXT/'+item+'/-1', dict(
157+
maxItems=100,
158+
))
159+
160+
if doc is None:
161+
return []
162+
163+
if not doc.status == 'FS_OK':
164+
return []
165+
166+
ret = list()
167+
for index, item in enumerate(list(doc.iterchildren('item'))):
168+
temp = dict(band=index)
169+
for field in list(item.iterchildren()):
170+
temp[field.get('name')] = list(field.iterchildren()).pop()
171+
ret.append(temp)
172+
173+
return ret
174+
175+
@asyncio.coroutine
176+
def collect_labels(self, items):
177+
"""Helper methods for extracting the labels from a list with maps."""
178+
if items is None:
179+
return []
180+
181+
return [str(item['label']) for item in items if item['label']]
182+
183+
# API implementation starts here
184+
185+
# sys
186+
@asyncio.coroutine
187+
def get_friendly_name(self):
188+
"""Get the friendly name of the device."""
189+
return (yield from self.handle_text(self.API.get('friendly_name')))
190+
191+
@asyncio.coroutine
192+
def set_friendly_name(self, value):
193+
"""Set the friendly name of the device."""
194+
return (yield from self.handle_set(
195+
self.API.get('friendly_name'), value))
196+
197+
@asyncio.coroutine
198+
def get_power(self):
199+
"""Check if the device is on."""
200+
power = (yield from self.handle_int(self.API.get('power')))
201+
return bool(power)
202+
203+
@asyncio.coroutine
204+
def set_power(self, value=False):
205+
"""Power on or off the device."""
206+
power = (yield from self.handle_set(
207+
self.API.get('power'), int(value)))
208+
return bool(power)
209+
210+
@asyncio.coroutine
211+
def get_modes(self):
212+
"""Get the modes supported by this device."""
213+
if not self.__modes:
214+
self.__modes = yield from self.handle_list(
215+
self.API.get('valid_modes'))
216+
217+
return self.__modes
218+
219+
@asyncio.coroutine
220+
def get_mode_list(self):
221+
"""Get the label list of the supported modes."""
222+
self.__modes = yield from self.get_modes()
223+
return (yield from self.collect_labels(self.__modes))
224+
225+
@asyncio.coroutine
226+
def get_mode(self):
227+
"""Get the currently active mode on the device (DAB, FM, Spotify)."""
228+
mode = None
229+
int_mode = (yield from self.handle_long(self.API.get('mode')))
230+
modes = yield from self.get_modes()
231+
for temp_mode in modes:
232+
if temp_mode['band'] == int_mode:
233+
mode = temp_mode['label']
234+
235+
return str(mode)
236+
237+
@asyncio.coroutine
238+
def set_mode(self, value):
239+
"""Set the currently active mode on the device (DAB, FM, Spotify)."""
240+
mode = -1
241+
modes = yield from self.get_modes()
242+
for temp_mode in modes:
243+
if temp_mode['label'] == value:
244+
mode = temp_mode['band']
245+
246+
return (yield from self.handle_set(self.API.get('mode'), mode))
247+
248+
@asyncio.coroutine
249+
def get_volume_steps(self):
250+
"""Read the maximum volume level of the device."""
251+
if not self.__volume_steps:
252+
self.__volume_steps = yield from self.handle_int(
253+
self.API.get('volume_steps'))
254+
255+
return self.__volume_steps
256+
257+
# Volume
258+
@asyncio.coroutine
259+
def get_volume(self):
260+
"""Read the volume level of the device."""
261+
return (yield from self.handle_int(self.API.get('volume')))
262+
263+
@asyncio.coroutine
264+
def set_volume(self, value):
265+
"""Set the volume level of the device."""
266+
return (yield from self.handle_set(self.API.get('volume'), value))
267+
268+
# Mute
269+
@asyncio.coroutine
270+
def get_mute(self):
271+
"Check if the device is muted."
272+
mute = (yield from self.handle_int(self.API.get('mute')))
273+
return bool(mute)
274+
275+
@asyncio.coroutine
276+
def set_mute(self, value=False):
277+
"""Mute or unmute the device."""
278+
mute = (yield from self.handle_set(self.API.get('mute'), int(value)))
279+
return bool(mute)
280+
281+
@asyncio.coroutine
282+
def get_play_status(self):
283+
"""Get the play status of the device."""
284+
status = yield from self.handle_int(self.API.get('status'))
285+
return self.PLAY_STATES.get(status)
286+
287+
@asyncio.coroutine
288+
def get_play_name(self):
289+
"""Get the name of the played item."""
290+
return (yield from self.handle_text(self.API.get('name')))
291+
292+
@asyncio.coroutine
293+
def get_play_text(self):
294+
"""Get the text associated with the played media."""
295+
return (yield from self.handle_text(self.API.get('text')))
296+
297+
@asyncio.coroutine
298+
def get_play_artist(self):
299+
"""Get the artists of the current media(song)."""
300+
return (yield from self.handle_text(self.API.get('artist')))
301+
302+
@asyncio.coroutine
303+
def get_play_album(self):
304+
"""Get the songs's album."""
305+
return (yield from self.handle_text(self.API.get('album')))
306+
307+
@asyncio.coroutine
308+
def get_play_graphic(self):
309+
"""Get the album art associated with the song/album/artist."""
310+
return (yield from self.handle_text(self.API.get('graphic_uri')))
311+
312+
@asyncio.coroutine
313+
def get_play_duration(self):
314+
"""Get the duration of the played media."""
315+
return (yield from self.handle_long(self.API.get('duration')))
316+
317+
# play controls
318+
319+
@asyncio.coroutine
320+
def play_control(self, value):
321+
"""
322+
Controls the player of the device.
323+
1=Play; 2=Pause; 3=Next; 4=Previous (song/station)
324+
"""
325+
return (yield from self.handle_set(self.API.get('control'), value))
326+
327+
@asyncio.coroutine
328+
def play(self):
329+
"""Play media."""
330+
return (yield from self.play_control(1))
331+
332+
@asyncio.coroutine
333+
def pause(self):
334+
"""Pause media."""
335+
return (yield from self.play_control(2))
336+
337+
@asyncio.coroutine
338+
def next(self):
339+
"""Next media."""
340+
return (yield from self.play_control(3))
341+
342+
@asyncio.coroutine
343+
def prev(self):
344+
"""Previous media."""
345+
return (yield from self.play_control(4))
346+
347+
@asyncio.coroutine
348+
def get_equalisers(self):
349+
"""Get the equaliser modes supported by this device."""
350+
if not self.__equalisers:
351+
self.__equalisers = yield from self.handle_list(
352+
self.API.get('equalisers'))
353+
354+
return self.__equalisers
355+
356+
@asyncio.coroutine
357+
def get_equaliser_list(self):
358+
"""Get the label list of the supported modes."""
359+
self.__equalisers = yield from self.get_equalisers()
360+
return (yield from self.collect_labels(self.__equalisers))
361+
362+
# Sleep
363+
@asyncio.coroutine
364+
def get_sleep(self):
365+
"Check when and if the device is going to sleep."
366+
return (yield from self.handle_long(self.API.get('sleep')))
367+
368+
@asyncio.coroutine
369+
def set_sleep(self, value=False):
370+
"""Set device sleep timer."""
371+
return (yield from self.handle_set(self.API.get('sleep'), int(value)))

0 commit comments

Comments
 (0)