1 from Screens.Screen import Screen
2 from Components.ConfigList import ConfigListScreen
3 from Components.config import config, ConfigSubsection, ConfigSelection, getConfigListEntry
4 from Components.ActionMap import ActionMap
5 from Screens.MessageBox import MessageBox
6 from Components.Sources.StaticText import StaticText
7 from Plugins.Plugin import PluginDescriptor
8 from Tools.Directories import fileExists
10 from Components.MenuList import MenuList
11 from Components.Sources.List import List
13 from enigma import eTimer
14 from Screens.Standby import TryQuitMainloop
15 from Components.Network import iNetwork
17 from Tools.LoadPixmap import LoadPixmap
18 from Tools.Directories import pathExists, fileExists, resolveFilename, SCOPE_CURRENT_SKIN
20 import xml.etree.cElementTree
21 from twisted.internet import reactor, task
22 from twisted.internet.protocol import DatagramProtocol
30 from Components.config import config, ConfigSubList, ConfigSelection, ConfigElement
37 for x in glob.glob('/dev/misc/vtuner*'):
38 x = x.strip('/dev/misc/vtuner')
43 VTUNER_IDX_LIST = getVtunerList()
45 SSDP_ADDR = '239.255.255.250'
49 ST = "urn:ses-com:device:SatIPServer:1"
50 MS = 'M-SEARCH * HTTP/1.1\r\nHOST: %s:%d\r\nMAN: "%s"\r\nMX: %d\r\nST: %s\r\n\r\n' % (SSDP_ADDR, SSDP_PORT, MAN, MX, ST)
52 class SSDPServerDiscovery(DatagramProtocol):
53 def __init__(self, callback, iface = None):
54 self.callback = callback
57 def send_msearch(self, iface):
61 self.port = reactor.listenUDP(0, self, interface=iface)
62 if self.port is not None:
63 print "Sending M-SEARCH..."
64 self.port.write(MS, (SSDP_ADDR, SSDP_PORT))
66 def stop_msearch(self):
67 if self.port is not None:
68 self.port.stopListening()
70 def datagramReceived(self, datagram, address):
71 # print "Received: (from %r)" % (address,)
72 # print "%s" % (datagram )
73 self.callback(datagram)
92 discoveryTimeoutMS = 2000;
96 self.discoveryStartTimer = eTimer()
97 self.discoveryStartTimer.callback.append(self.DiscoveryStart)
99 self.discoveryStopTimer = eTimer()
100 self.discoveryStopTimer.callback.append(self.DiscoveryStop)
102 self.ssdp = SSDPServerDiscovery(self.dataReceive)
103 self.updateCallback = []
105 def formatAddr(self, address):
109 return "%d.%d.%d.%d"%(address[0],address[1],address[2],address[3])
111 def getEthernetAddr(self):
112 return self.formatAddr(iNetwork.getAdapterAttribute("eth0", "ip") )
114 def DiscoveryTimerStart(self):
115 self.discoveryStartTimer.start(10, True)
117 def DiscoveryStart(self, stop_timeout = discoveryTimeoutMS):
118 self.discoveryStopTimer.stop()
119 self.ssdp.stop_msearch()
121 # print "Discovery Start!"
122 self.ssdp.send_msearch(self.getEthernetAddr())
123 self.discoveryStopTimer.start(stop_timeout, True)
125 def DiscoveryStop(self):
126 # print "Discovery Stop!"
127 self.ssdp.stop_msearch()
129 for x in self.updateCallback:
132 def dataReceive(self, data):
133 # print "dataReceive:\n", data
135 serverData = self.dataParse(data)
136 if serverData.has_key('LOCATION'):
137 self.xmlParse(serverData['LOCATION'])
139 def dataParse(self, data):
141 for line in data.splitlines():
142 # print "[*] line : ", line
143 if line.find(':') != -1:
144 (attr, value) = line.split(':', 1)
145 attr = attr.strip().upper()
146 if not serverData.has_key(attr):
147 serverData[attr] = value.strip()
149 # for (key, value) in serverData.items():
150 # print "[%s] %s" % (key, value)
154 def xmlParse(self, location):
155 def findChild(parent, tag, namespace):
156 return parent.find('{%s}%s' % (namespace, tag))
158 def getAttr(root, parent, tag, namespace):
160 pElem = findChild(root, parent, namespace)
161 if pElem is not None:
162 child = findChild(pElem, tag, namespace)
163 if child is not None:
170 def getAttrN2(root, parent, tag, namespace_1 , namespace_2):
172 pElem = findChild(root, parent, namespace_1)
173 if pElem is not None:
174 child = findChild(pElem, tag, namespace_2)
175 if child is not None:
183 print "\n######## SATIPSERVERDATA ########"
184 for (k, v) in SATIPSERVERDATA.items():
185 # prestr = "[%s]" % k
187 for (k2, v2) in v.items():
188 prestr2 = prestr + "[%s]" % k2
189 if not isinstance(v2, dict):
190 print "%s %s" % (prestr2, v2)
192 for (k3, v3) in v2.items():
193 prestr3 = prestr2 + "[%s]" % k3
194 print "%s %s" % (prestr3, v3)
197 print "[SATIPClient] Parsing %s" % location
204 location = location.strip().split("http://")[1]
205 AAA = location.find(':')
206 BBB = location.find('/')
208 address = location[:AAA]
209 port = int(location[AAA+1 : BBB])
210 request = location[BBB:]
211 # print "address : ", address
212 # print "port: " , port
213 # print "request : ", request
215 conn = httplib.HTTPConnection(address, port)
216 conn.request("GET", request)
217 res = conn.getresponse()
218 except Exception, ErrMsg:
219 print "http request error %s" % ErrMsg
222 if res.status != 200 or res.reason !="OK":
223 print "response error"
230 root = xml.etree.cElementTree.fromstring(data)
232 xmlns_dev = "urn:schemas-upnp-org:device-1-0"
233 xmlns_satip = "urn:ses-com:satip"
235 udn = getAttr(root, 'device', 'UDN', xmlns_dev)
239 uuid = udn.strip('uuid:')
240 SATIPSERVERDATA[uuid] = {}
242 SATIPSERVERDATA[uuid]['ipaddress'] = address
245 SATIPSERVERDATA[uuid][pTag] = {}
246 for tag in DEVICE_ATTR:
247 SATIPSERVERDATA[uuid][pTag][tag] = getAttr(root, pTag, tag, xmlns_dev)
249 tagList = ['X_SATIPCAP']
251 SATIPSERVERDATA[uuid][pTag][tag] = getAttrN2(root, pTag, tag, xmlns_dev, xmlns_satip)
254 SATIPSERVERDATA[uuid][pTag] = {}
255 tagList = ['major', 'minor']
257 SATIPSERVERDATA[uuid][pTag][tag] = getAttr(root, pTag, tag, xmlns_dev)
261 def isEmptyServerData(self):
262 return isEmpty(SATIPSERVERDATA)
264 def getServerData(self):
265 return SATIPSERVERDATA
267 def getServerKeys(self):
268 return SATIPSERVERDATA.keys()
270 def getServerInfo(self, uuid, attr):
271 if attr in ["ipaddress"]:
272 return SATIPSERVERDATA[uuid][attr]
274 elif attr in DEVICE_ATTR + ['X_SATIPCAP']:
275 return SATIPSERVERDATA[uuid]["device"][attr]
277 elif attr in ['major', 'minor']:
278 return SATIPSERVERDATA[uuid]["specVersion"][attr]
282 def getServerDescFromIP(self, ip):
283 for (uuid, data) in SATIPSERVERDATA.items():
284 if data.get('ipaddress') == ip:
285 return data['device'].get('modelName')
288 def getUUIDFromIP(self, ip):
289 for (uuid, data) in SATIPSERVERDATA.items():
290 if data.get('ipaddress') == ip:
294 satipdiscovery = SATIPDiscovery()
295 SATIP_CONF_CHANGED = False
297 class SATIPTuner(Screen, ConfigListScreen):
299 <screen position="center,center" size="590,370">
300 <ePixmap pixmap="skin_default/buttons/red.png" position="40,0" size="140,40" alphatest="on" />
301 <ePixmap pixmap="skin_default/buttons/green.png" position="230,0" size="140,40" alphatest="on" />
302 <ePixmap pixmap="skin_default/buttons/yellow.png" position="420,0" size="140,40" alphatest="on" />
303 <widget source="key_red" render="Label" position="40,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" backgroundColor="#9f1313" foregroundColor="#ffffff" transparent="1" />
304 <widget source="key_green" render="Label" position="230,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" backgroundColor="#1f771f" foregroundColor="#ffffff" transparent="1" />
305 <widget source="key_yellow" render="Label" position="420,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" backgroundColor="#a08500" foregroundColor="#ffffff" transparent="1" />
306 <widget name="config" zPosition="2" position="20,70" size="550,50" scrollbarMode="showOnDemand" transparent="1" />
307 <widget source="description" render="Label" position="20,120" size="550,210" font="Regular;20" halign="left" valign="center" />
308 <widget source="choices" render="Label" position="20,330" size="550,40" font="Regular;20" halign="center" valign="center" />
311 def __init__(self, session, vtuner_idx, vtuner_uuid, vtuner_type, current_satipConfig):
312 Screen.__init__(self, session)
313 self.setTitle(_("SAT>IP Client Tuner Setup"))
314 self.skin = SATIPTuner.skin
315 self.session = session
316 self.vtuner_idx = vtuner_idx
317 self.vtuner_uuid = vtuner_uuid
318 self.vtuner_type = vtuner_type
319 self.current_satipConfig = current_satipConfig
321 self["key_red"] = StaticText(_("Cancel"))
322 self["key_green"] = StaticText(_("Ok"))
323 self["key_yellow"] = StaticText(_("Discover"))
324 self["description"] = StaticText(_("Starting..."))
325 self["choices"] = StaticText(_(" "))
327 self["shortcuts"] = ActionMap(["SATIPCliActions" ],
330 "cancel": self.keyCancel,
331 "red": self.keyCancel,
332 "green": self.keySave,
333 "yellow" : self.DiscoveryStart,
337 ConfigListScreen.__init__(self, self.list, session = self.session)
338 self.satipconfig = ConfigSubsection()
339 self.satipconfig.server = None
341 if not self.discoveryEnd in satipdiscovery.updateCallback:
342 satipdiscovery.updateCallback.append(self.discoveryEnd)
344 self.onClose.append(self.OnClose)
346 if satipdiscovery.isEmptyServerData():
347 self.onLayoutFinish.append(self.DiscoveryStart)
349 self.createServerConfig()
353 if self.discoveryEnd in satipdiscovery.updateCallback:
354 satipdiscovery.updateCallback.remove(self.discoveryEnd)
356 satipdiscovery.DiscoveryStop()
358 def DiscoveryStart(self):
359 self["shortcuts"].setEnabled(False)
360 self["config_actions"].setEnabled(False)
361 self["description"].setText(_("SAT>IP server discovering for %d seconds..." % (discoveryTimeoutMS / 1000)))
362 satipdiscovery.DiscoveryStart()
364 def discoveryEnd(self):
365 self["shortcuts"].setEnabled(True)
366 self["config_actions"].setEnabled(True)
367 if not satipdiscovery.isEmptyServerData():
368 self.createServerConfig()
371 self["description"].setText(_("SAT>IP server is not detected."))
373 def createServerConfig(self):
374 if satipdiscovery.isEmptyServerData():
379 server_default = None
380 for uuid in satipdiscovery.getServerKeys():
381 description = satipdiscovery.getServerInfo(uuid, "modelName")
382 server_choices.append( (uuid, description) )
383 if self.vtuner_uuid == uuid:
384 server_default = uuid
386 if server_default is None:
387 server_default = server_choices[0][0]
389 self.satipconfig.server = ConfigSelection(default = server_default, choices = server_choices )
391 def createSetup(self):
392 if self.satipconfig.server is None:
396 self.server_entry = getConfigListEntry(_("SAT>IP Server : "), self.satipconfig.server)
397 self.list.append( self.server_entry )
399 self.createTypeConfig(self.satipconfig.server.value)
400 self.type_entry = getConfigListEntry(_("SAT>IP Tuner Type : "), self.satipconfig.tunertype)
401 self.list.append( self.type_entry )
403 self["config"].list = self.list
404 self["config"].l.setList(self.list)
406 if not self.showChoices in self["config"].onSelectionChanged:
407 self["config"].onSelectionChanged.append(self.showChoices)
409 self.selectionChanged()
411 def createTypeConfig(self, uuid):
412 # type_choices = [ ("DVB-S", _("DVB-S")), ("DVB-C", _("DVB-C")), ("DVB-T", _("DVB-T"))]
415 capability = self.getCapability(uuid)
417 for (t, n) in capability.items():
419 type_choices.append( (t, _(t)) )
420 if self.vtuner_type == t:
423 if isEmpty(type_choices):
424 type_choices = [ ("DVB-S", _("DVB-S")) ]
426 self.satipconfig.tunertype = ConfigSelection(default = type_default, choices = type_choices )
428 def selectionChanged(self):
429 if self.satipconfig.server is None:
432 uuid = self.satipconfig.server.value
434 # ipaddress = satipdiscovery.getServerInfo(uuid, "ipaddress")
435 modelDescription = satipdiscovery.getServerInfo(uuid, "modelDescription")
436 manufacturer = satipdiscovery.getServerInfo(uuid, "manufacturer")
437 # specversion = "%s.%s" % (satipdiscovery.getServerInfo(uuid, "major"), satipdiscovery.getServerInfo(uuid, "minor"))
438 modelURL = satipdiscovery.getServerInfo(uuid, "modelURL")
439 presentationURL = satipdiscovery.getServerInfo(uuid, "presentationURL")
440 # satipcap = satipdiscovery.getServerInfo(uuid, "X_SATIPCAP")
441 # serialNumber = satipdiscovery.getServerInfo(uuid, "serialNumber")
443 capability = self.getCapability(uuid)
445 for (t, n) in capability.items():
447 satipcap_list.append("%d x %s" % (n, t))
449 satipcap = ",".join(satipcap_list)
452 description += "Description : %s\n" % modelDescription
453 description += "Manufacture : %s\n" % manufacturer
454 description += "Model URL : %s\n" % modelURL
455 description += "Presentation URL : %s\n" % presentationURL
456 description += "UUID : %s\n" % uuid
457 description += "SAT>IP Capavility : %s" % satipcap
459 self["description"].setText(description)
461 def showChoices(self):
462 currentConfig = self["config"].getCurrent()[1]
464 for choice in currentConfig.choices.choices:
465 text_list.append(choice[1])
467 text = ",".join(text_list)
469 self["choices"].setText("Choices : \n%s" % (text))
471 def getCapability(self, uuid):
472 capability = { 'DVB-S' : 0, 'DVB-C' : 0, 'DVB-T' : 0}
473 data = satipdiscovery.getServerInfo(uuid, "X_SATIPCAP")
474 for x in data.split(','):
475 if x.upper().find("DVBS") != -1:
476 capability['DVB-S'] = int(x.split('-')[1])
477 elif x.upper().find("DVBC") != -1:
478 capability['DVB-C'] = int(x.split('-')[1])
479 elif x.upper().find("DVBT") != -1:
480 capability['DVB-T'] = int(x.split('-')[1])
484 def checkTunerCapacity(self, uuid, tunertype):
485 capability = self.getCapability(uuid)
486 t_cap = capability[tunertype]
490 for idx in VTUNER_IDX_LIST:
491 if self.vtuner_idx == idx:
494 vtuner = self.current_satipConfig[int(idx)]
495 if vtuner["vtuner_type"] == "satip_client" and vtuner["uuid"] == uuid and vtuner["tuner_type"] == tunertype:
496 # print "[checkTunerCapacity] tuner %d use type %s" % (int(idx), tunertype)
499 # print "[checkTunerCapacity] capability : ", capability
500 # print "[checkTunerCapacity] t_cap : %d, t_count %d" % (t_cap, t_count)
502 if int(t_cap) > t_count:
508 ConfigListScreen.keyLeft(self)
509 if self["config"].getCurrent() == self.server_entry:
511 self.selectionChanged()
514 ConfigListScreen.keyRight(self)
515 if self["config"].getCurrent() == self.server_entry:
517 self.selectionChanged()
520 if self.satipconfig.server is None:
524 uuid = self.satipconfig.server.value
525 tunertype = self.satipconfig.tunertype.value
527 if not self.checkTunerCapacity(uuid, tunertype):
528 self.session.open(MessageBox, _("Server capacity is fulled."), MessageBox.TYPE_ERROR)
532 data['idx'] = self.vtuner_idx
533 data['ip'] = satipdiscovery.getServerInfo(uuid, 'ipaddress');
534 data['desc'] = satipdiscovery.getServerInfo(uuid, "modelName")
535 data['tuner_type'] = tunertype
543 SATIP_CONFFILE = "/etc/vtuner.conf"
544 class SATIPClient(Screen):
546 <screen position="center,center" size="590,370">
547 <ePixmap pixmap="skin_default/buttons/red.png" position="20,0" size="140,40" alphatest="on" />
548 <ePixmap pixmap="skin_default/buttons/green.png" position="160,0" size="140,40" alphatest="on" />
549 <ePixmap pixmap="skin_default/buttons/yellow.png" position="300,0" size="140,40" alphatest="on" />
550 <ePixmap pixmap="skin_default/buttons/blue.png" position="440,0" size="140,40" alphatest="on" />
552 <widget source="key_red" render="Label" position="20,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" foregroundColor="#ffffff" backgroundColor="#9f1313" transparent="1" />
553 <widget source="key_green" render="Label" position="160,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" foregroundColor="#ffffff" backgroundColor="#1f771f" transparent="1" />
554 <widget source="key_yellow" render="Label" position="300,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" foregroundColor="#ffffff" backgroundColor="#a08500" transparent="1" />
555 <widget source="key_blue" render="Label" position="440,0" zPosition="1" size="140,40" font="Regular;20" halign="center" valign="center" foregroundColor="#ffffff" backgroundColor="#18188b" transparent="1" />
557 <widget source="vtunerList" render="Listbox" position="30,60" size="540,272" scrollbarMode="showOnDemand">
558 <convert type="TemplatedMultiContent">
561 MultiContentEntryText(pos = (20, 0), size = (180, 28), font=0, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER, text = 0),
562 MultiContentEntryText(pos = (50, 28), size = (140, 20), font=1, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER, text = 1),
563 MultiContentEntryText(pos = (210, 28), size = (140, 20), font=1, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER, text = 2),
564 MultiContentEntryText(pos = (370, 28), size = (140, 20), font=1, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER, text = 3),
565 MultiContentEntryText(pos = (50, 48), size = (490, 20), font=1, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER, text = 4),
570 "fonts": [gFont("Regular", 24),gFont("Regular", 16)],
575 <widget source="description" render="Label" position="0,340" size="590,30" font="Regular;20" halign="center" valign="center" />
578 def __init__(self, session):
579 Screen.__init__(self, session)
580 self.setTitle(_("SAT>IP Client Setup"))
581 self.skin = SATIPClient.skin
582 self.session = session
584 self["key_red"] = StaticText(_("Cancel"))
585 self["key_green"] = StaticText(_("Save"))
586 self["key_yellow"] = StaticText(_("Setup"))
587 self["key_blue"] = StaticText(_("Disable"))
588 self["description"] = StaticText(_("Select tuner and press setup key (Yellow)"))
591 self["vtunerList"] = List(self.configList)
593 self["shortcuts"] = ActionMap(["SATIPCliActions" ],
596 "cancel": self.keyCancel,
597 "red": self.keyCancel,
598 "green": self.KeySave,
599 "yellow": self.keySetup,
600 "blue": self.keyDisable,
603 self.vtunerIndex = VTUNER_IDX_LIST
604 self.vtunerConfig = self.loadConfig()
605 self.sortVtunerConfig()
606 self.old_vtunerConfig = copy.deepcopy(self.vtunerConfig)
610 for vtuner_idx in self.vtunerIndex:
611 vtuner = self.vtunerConfig[int(vtuner_idx)]
612 old_vtuner = self.old_vtunerConfig[int(vtuner_idx)]
613 if vtuner['vtuner_type'] != old_vtuner['vtuner_type']:
616 elif vtuner['vtuner_type'] == "satip_client":
617 for key in sorted(vtuner):
618 if vtuner[key] != old_vtuner[key]:
624 msg = "You should now reboot your STB to change SAT>IP Configuration.\n\nReboot now ?\n\n"
625 self.session.openWithCallback(self.keySaveCB, MessageBox, (_(msg) ) )
630 def keySaveCB(self, res):
636 self.session.open(TryQuitMainloop, 2)
638 def cancelConfirm(self, result):
646 self.session.openWithCallback(self.cancelConfirm, MessageBox, _("Really close without saving settings?"))
650 def createSetup(self):
651 # print "vtunerIndex : ", self.vtunerIndex
652 # print "vtunerConfig : ", self.vtunerConfig
654 for vtuner_idx in self.vtunerIndex:
655 vtuner = self.vtunerConfig[int(vtuner_idx)]
657 if vtuner['vtuner_type'] == "satip_client":
659 "VIRTUAL TUNER %s" % vtuner_idx,
660 "TYPE : %s" % vtuner['vtuner_type'].replace('_',' ').upper(),
661 "IP : %s" % vtuner['ipaddr'],
662 "TUNER TYPE : %s" % vtuner['tuner_type'],
663 "SAT>IP SERVER : %s" % vtuner['desc'],
665 vtuner['tuner_type'],
671 "VIRTUAL TUNER %s" % vtuner_idx,
672 "TYPE : %s" % vtuner['vtuner_type'].replace('_',' ').upper(),
681 self.configList.append(entry)
682 # self.configList.sort()
683 self["vtunerList"].setList(self.configList)
685 def keyDisable(self):
686 idx = self["vtunerList"].getCurrent()[5]
688 self.vtunerConfig[int(idx)] = copy.deepcopy(self.old_vtunerConfig[int(idx)])
689 if self.vtunerConfig[int(idx)] and self.vtunerConfig[int(idx)]['vtuner_type'] == "satip_client":
690 self.vtunerConfig[int(idx)] = {'vtuner_type':"usb_tuner"}
692 self.sortVtunerConfig()
696 vtuner_idx = self["vtunerList"].getCurrent()[5]
697 vtuner_type = self["vtunerList"].getCurrent()[6]
698 vtuner_uuid = self["vtunerList"].getCurrent()[7]
699 self.session.openWithCallback(self.SATIPTunerCB, SATIPTuner, vtuner_idx, vtuner_uuid, vtuner_type, self.vtunerConfig)
701 def SATIPTunerCB(self, data = None):
705 def setConfig(self, data):
706 if data['uuid'] is not None:
707 vtuner = self.vtunerConfig[int(data['idx'])]
708 vtuner['vtuner_type'] = "satip_client"
709 vtuner['ipaddr'] = data['ip']
710 vtuner['desc'] = data['desc']
711 vtuner['uuid'] = data['uuid']
712 vtuner['tuner_type'] = data['tuner_type']
714 self.sortVtunerConfig()
719 def sortVtunerConfig(self):
720 self.vtunerConfig.sort(reverse=True)
722 def saveConfig(self):
725 for idx in self.vtunerIndex:
726 conf = self.vtunerConfig[int(idx)]
730 # print "conf : ", conf
733 for k in sorted(conf):
734 attr.append("%s:%s" % (k, conf[k]))
736 data += idx + '=' + ",".join(attr)+"\n"
739 fd = open(SATIP_CONFFILE, 'w')
743 def loadConfig(self):
746 for idx in self.vtunerIndex:
747 vtunerConfig.append({'vtuner_type':"usb_tuner"})
749 if os.access(SATIP_CONFFILE, os.R_OK):
750 fd = open(SATIP_CONFFILE)
755 for line in confData.splitlines():
756 if len(line) == 0 or line[0] == '#':
760 data = line.split('=')
766 vtunerConfig[int(idx)]
770 data = data[1].split(',')
781 vtunerConfig[int(idx)][attr] = value
785 def main(session, **kwargs):
786 session.open(SATIPClient)
788 def Plugins(**kwargs):
790 pList.append( PluginDescriptor(name=_("SAT>IP Client"), description=_("SAT>IP Client attached to vtuner."), where = PluginDescriptor.WHERE_PLUGINMENU, needsRestart = False, fnc=main) )