controler.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. #
  2. # Copyright (C) 2015-2017 Savoir-faire Linux Inc
  3. #
  4. # Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  19. #
  20. """DRing controling class through DBUS"""
  21. import sys
  22. import os
  23. import random
  24. import time
  25. import hashlib
  26. from threading import Thread
  27. from functools import partial
  28. try:
  29. from gi.repository import GObject
  30. except ImportError as e:
  31. import gobject as GObject
  32. except Exception as e:
  33. print(str(e))
  34. exit(1)
  35. from errors import *
  36. try:
  37. import dbus
  38. from dbus.mainloop.glib import DBusGMainLoop
  39. except ImportError as e:
  40. raise DRingCtrlError("No python-dbus module found")
  41. DBUS_DEAMON_OBJECT = 'cx.ring.Ring'
  42. DBUS_DEAMON_PATH = '/cx/ring/Ring'
  43. class DRingCtrl(Thread):
  44. def __init__(self, name, autoAnswer):
  45. if sys.version_info[0] < 3:
  46. super(DRingCtrl, self).__init__()
  47. else:
  48. super().__init__()
  49. self.activeCalls = {} # list of active calls (known by the client)
  50. self.activeConferences = {} # list of active conferences
  51. self.account = None # current active account
  52. self.name = name # client name
  53. self.autoAnswer = autoAnswer
  54. self.currentCallId = ""
  55. self.currentConfId = ""
  56. self.isStop = False
  57. # Glib MainLoop for processing callbacks
  58. self.loop = GObject.MainLoop()
  59. GObject.threads_init()
  60. # client registered to sflphoned ?
  61. self.registered = False
  62. self.register()
  63. def __del__(self):
  64. if self.registered:
  65. self.unregister()
  66. self.loop.quit()
  67. def stopThread(self):
  68. self.isStop = True
  69. def register(self):
  70. if self.registered:
  71. return
  72. try:
  73. # register the main loop for d-bus events
  74. DBusGMainLoop(set_as_default=True)
  75. bus = dbus.SessionBus()
  76. except dbus.DBusException as e:
  77. raise DRingCtrlDBusError("Unable to connect DBUS session bus")
  78. if not bus.name_has_owner(DBUS_DEAMON_OBJECT) :
  79. raise DRingCtrlDBusError(("Unable to find %s in DBUS." % DBUS_DEAMON_OBJECT)
  80. + " Check if dring is running")
  81. try:
  82. proxy_instance = bus.get_object(DBUS_DEAMON_OBJECT,
  83. DBUS_DEAMON_PATH+'/Instance', introspect=False)
  84. proxy_callmgr = bus.get_object(DBUS_DEAMON_OBJECT,
  85. DBUS_DEAMON_PATH+'/CallManager', introspect=False)
  86. proxy_confmgr = bus.get_object(DBUS_DEAMON_OBJECT,
  87. DBUS_DEAMON_PATH+'/ConfigurationManager', introspect=False)
  88. proxy_videomgr = bus.get_object(DBUS_DEAMON_OBJECT,
  89. DBUS_DEAMON_PATH+'/VideoManager', introspect=False)
  90. self.instance = dbus.Interface(proxy_instance,
  91. DBUS_DEAMON_OBJECT+'.Instance')
  92. self.callmanager = dbus.Interface(proxy_callmgr,
  93. DBUS_DEAMON_OBJECT+'.CallManager')
  94. self.configurationmanager = dbus.Interface(proxy_confmgr,
  95. DBUS_DEAMON_OBJECT+'.ConfigurationManager')
  96. if proxy_videomgr:
  97. self.videomanager = dbus.Interface(proxy_videomgr,
  98. DBUS_DEAMON_OBJECT+'.VideoManager')
  99. except dbus.DBusException as e:
  100. raise DRingCtrlDBusError("Unable to bind to dring DBus API")
  101. try:
  102. self.instance.Register(os.getpid(), self.name)
  103. self.registered = True
  104. except dbus.DBusException as e:
  105. raise DRingCtrlDeamonError("Client registration failed")
  106. try:
  107. proxy_callmgr.connect_to_signal('incomingCall', self.onIncomingCall)
  108. proxy_callmgr.connect_to_signal('callStateChanged', self.onCallStateChanged)
  109. proxy_callmgr.connect_to_signal('conferenceCreated', self.onConferenceCreated)
  110. proxy_confmgr.connect_to_signal('accountsChanged', self.onAccountsChanged)
  111. except dbus.DBusException as e:
  112. raise DRingCtrlDBusError("Unable to connect to dring DBus signals")
  113. def unregister(self):
  114. if not self.registered:
  115. return
  116. try:
  117. self.instance.Unregister(os.getpid())
  118. self.registered = False
  119. except:
  120. raise DRingCtrlDeamonError("Client unregistration failed")
  121. def isRegistered(self):
  122. return self.registered
  123. #
  124. # Signal handling
  125. #
  126. def onIncomingCall_cb(self, callId):
  127. if self.autoAnswer:
  128. self.Accept(callId)
  129. pass
  130. def onCallHangup_cb(self, callId):
  131. pass
  132. def onCallRinging_cb(self):
  133. pass
  134. def onCallHold_cb(self):
  135. pass
  136. def onCallCurrent_cb(self):
  137. pass
  138. def onCallBusy_cb(self):
  139. pass
  140. def onCallFailure_cb(self):
  141. pass
  142. def onIncomingCall(self, account, callid, to):
  143. """ On incoming call event, add the call to the list of active calls """
  144. self.activeCalls[callid] = {'Account': account,
  145. 'To': to,
  146. 'State': ''}
  147. self.currentCallId = callid
  148. self.onIncomingCall_cb(callid)
  149. def onCallHangUp(self, callid):
  150. """ Remove callid from call list """
  151. self.onCallHangup_cb(callid)
  152. self.currentCallId = ""
  153. del self.activeCalls[callid]
  154. def onCallRinging(self, callid, state):
  155. """ Update state for this call to Ringing """
  156. self.activeCalls[callid]['State'] = state
  157. self.onCallRinging_cb(callid)
  158. def onCallHold(self, callid, state):
  159. """ Update state for this call to Hold """
  160. self.activeCalls[callid]['State'] = state
  161. self.onCallHold_cb()
  162. def onCallCurrent(self, callid, state):
  163. """ Update state for this call to current """
  164. self.activeCalls[callid]['State'] = state
  165. self.onCallCurrent_cb()
  166. def onCallBusy(self, callid, state):
  167. """ Update state for this call to busy """
  168. self.activeCalls[callid]['State'] = state
  169. self.onCallBusy_cb()
  170. def onCallFailure(self, callid, state):
  171. """ Handle call failure """
  172. self.onCallFailure_cb()
  173. del self.activeCalls[callid]
  174. def onCallStateChanged(self, callid, state, code):
  175. """ On call state changed event, set the values for new calls,
  176. or delete the call from the list of active calls
  177. """
  178. print(("On call state changed " + callid + " " + state))
  179. if callid not in self.activeCalls:
  180. print("This call didn't exist!: " + callid + ". Adding it to the list.")
  181. callDetails = self.getCallDetails(callid)
  182. self.activeCalls[callid] = {'Account': callDetails['ACCOUNTID'],
  183. 'To': callDetails['PEER_NUMBER'],
  184. 'State': state,
  185. 'Code': code }
  186. self.currentCallId = callid
  187. if state == "HUNGUP":
  188. self.onCallHangUp(callid)
  189. elif state == "RINGING":
  190. self.onCallRinging(callid, state)
  191. elif state == "CURRENT":
  192. self.onCallCurrent(callid, state)
  193. elif state == "HOLD":
  194. self.onCallHold(callid, state)
  195. elif state == "BUSY":
  196. self.onCallBusy(callid, state)
  197. elif state == "FAILURE":
  198. self.onCallFailure(callid, state)
  199. else:
  200. print("unknown state:" + str(state))
  201. def onConferenceCreated_cb(self):
  202. pass
  203. def onConferenceCreated(self, confId):
  204. self.currentConfId = confId
  205. self.onConferenceCreated_cb()
  206. #
  207. # Account management
  208. #
  209. def _valid_account(self, account):
  210. account = account or self.account
  211. if account is None:
  212. raise DRingCtrlError("No provided or current account!")
  213. return account
  214. def isAccountExists(self, account):
  215. """ Checks if the account exists"""
  216. return account in self.getAllAccounts()
  217. def isAccountEnable(self, account=None):
  218. """Return True if the account is enabled. If no account is provided, active account is used"""
  219. return self.getAccountDetails(self._valid_account(account))['Account.enable'] == "true"
  220. def isAccountRegistered(self, account=None):
  221. """Return True if the account is registered. If no account is provided, active account is used"""
  222. return self.getVolatileAccountDetails(self._valid_account(account))['Account.registrationStatus'] in ('READY', 'REGISTERED')
  223. def isAccountOfType(self, account_type, account=None):
  224. """Return True if the account type is the given one. If no account is provided, active account is used"""
  225. return self.getAccountDetails(self._valid_account(account))['Account.type'] == account_type
  226. def getAllAccounts(self, account_type=None):
  227. """Return a list with all accounts"""
  228. acclist = map(str, self.configurationmanager.getAccountList())
  229. if account_type:
  230. acclist = filter(partial(self.isAccountOfType, account_type), acclist)
  231. return list(acclist)
  232. def getAllEnabledAccounts(self):
  233. """Return a list with all enabled-only accounts"""
  234. return [x for x in self.getAllAccounts() if self.isAccountEnable(x)]
  235. def getAllRegisteredAccounts(self):
  236. """Return a list with all registered-only accounts"""
  237. return [x for x in self.getAllAccounts() if self.isAccountRegistered(x)]
  238. def getAccountDetails(self, account=None):
  239. """Return a list of string. If no account is provided, active account is used"""
  240. account = self._valid_account(account)
  241. if self.isAccountExists(account):
  242. return self.configurationmanager.getAccountDetails(account)
  243. return []
  244. def getVolatileAccountDetails(self, account=None):
  245. """Return a list of string. If no account is provided, active account is used"""
  246. account = self._valid_account(account)
  247. if self.isAccountExists(account):
  248. return self.configurationmanager.getVolatileAccountDetails(account)
  249. return []
  250. def setActiveCodecList(self, account=None, codec_list=''):
  251. """Activate given codecs on an account. If no account is provided, active account is used"""
  252. account = self._valid_account(account)
  253. if self.isAccountExists(account):
  254. codec_list = [dbus.UInt32(x) for x in codec_list.split(',')]
  255. self.configurationmanager.setActiveCodecList(account, codec_list)
  256. def addAccount(self, details=None):
  257. """Add a new account account
  258. Add a new account to the Ring-daemon. Default parameters are \
  259. used for missing account configuration field.
  260. Required parameters are type, alias, hostname, username and password
  261. input details
  262. """
  263. if details is None:
  264. raise DRingCtrlAccountError("Must specifies type, alias, hostname, \
  265. username and password in \
  266. order to create a new account")
  267. return str(self.configurationmanager.addAccount(details))
  268. def removeAccount(self, accountID=None):
  269. """Remove an account from internal list"""
  270. if accountID is None:
  271. raise DRingCtrlAccountError("Account ID must be specified")
  272. self.configurationmanager.removeAccount(accountID)
  273. def setAccountByAlias(self, alias):
  274. """Define as active the first account who match with the alias"""
  275. for testedaccount in self.getAllAccounts():
  276. details = self.getAccountDetails(testedaccount)
  277. if (details['Account.enable'] == 'true' and
  278. details['Account.alias'] == alias):
  279. self.account = testedaccount
  280. return
  281. raise DRingCtrlAccountError("No enabled account matched with alias")
  282. def getAccountByAlias(self, alias):
  283. """Get account name having its alias"""
  284. for account in self.getAllAccounts():
  285. details = self.getAccountDetails(account)
  286. if details['Account.alias'] == alias:
  287. return account
  288. raise DRingCtrlAccountError("No account matched with alias")
  289. def setAccount(self, account):
  290. """Define the active account
  291. The active account will be used when sending a new call
  292. """
  293. if account in self.getAllAccounts():
  294. self.account = account
  295. else:
  296. print(account)
  297. raise DRingCtrlAccountError("Not a valid account")
  298. def setFirstRegisteredAccount(self):
  299. """Find the first enabled account and define it as active"""
  300. rAccounts = self.getAllRegisteredAccounts()
  301. if 0 == len(rAccounts):
  302. raise DRingCtrlAccountError("No registered account !")
  303. self.account = rAccounts[0]
  304. def setFirstActiveAccount(self):
  305. """Find the first enabled account and define it as active"""
  306. aAccounts = self.getAllEnabledAccounts()
  307. if 0 == len(aAccounts):
  308. raise DRingCtrlAccountError("No active account !")
  309. self.account = aAccounts[0]
  310. def getAccount(self):
  311. """Return the active account"""
  312. return self.account
  313. def setAccountEnable(self, account=None, enable=False):
  314. """Set account enabled"""
  315. account = self._valid_account(account)
  316. if enable == True:
  317. details = self.getAccountDetails(account)
  318. details['Account.enable'] = "true"
  319. self.configurationmanager.setAccountDetails(account, details)
  320. else:
  321. details = self.getAccountDetails(account)
  322. details['Account.enable'] = "false"
  323. self.configurationmanager.setAccountDetails(account, details)
  324. def setAccountRegistered(self, account=None, register=False):
  325. """ Tries to register the account"""
  326. account = self._valid_account(account)
  327. self.configurationmanager.sendRegister(account, register)
  328. def onAccountsChanged(self):
  329. print("Accounts changed")
  330. #
  331. # Codec manager
  332. #
  333. def getAllCodecs(self):
  334. """ Return all codecs"""
  335. return [int(x) for x in self.configurationmanager.getCodecList()]
  336. def getCodecDetails(self, account, codecId):
  337. """ Return codec details"""
  338. codecId=dbus.UInt32(codecId)
  339. return self.configurationmanager.getCodecDetails(account, codecId)
  340. def getActiveCodecs(self, account=None):
  341. """ Return all active codecs on given account"""
  342. account = self._valid_account(account)
  343. return [int(x) for x in self.configurationmanager.getActiveCodecList(account)]
  344. def setVideoCodecBitrate(self, account, bitrate):
  345. """ Change bitrate for all codecs on given account"""
  346. for codecId in self.configurationmanager.getActiveCodecList(account):
  347. details = self.configurationmanager.getCodecDetails(account, codecId)
  348. details['CodecInfo.bitrate'] = str(bitrate)
  349. if details['CodecInfo.type'] == 'VIDEO':
  350. self.configurationmanager.setCodecDetails(account, codecId, details)
  351. #
  352. # Call management
  353. #
  354. def getAllCalls(self):
  355. """Return all calls handled by the daemon"""
  356. return [str(x) for x in self.callmanager.getCallList()]
  357. def getCallDetails(self, callid):
  358. """Return informations on this call if exists"""
  359. return self.callmanager.getCallDetails(callid)
  360. def printClientCallList(self):
  361. print("Client active call list:")
  362. print("------------------------")
  363. for call in self.activeCalls:
  364. print("\t" + call)
  365. def Call(self, dest):
  366. """Start a call and return a CallID
  367. Use the current account previously set using setAccount().
  368. If no account specified, first registered one in account list is used.
  369. return callID Newly generated callidentifier for this call
  370. """
  371. if dest is None or dest == "":
  372. raise SflPhoneError("Invalid call destination")
  373. # Set the account to be used for this call
  374. if not self.account:
  375. self.setFirstRegisteredAccount()
  376. if self.account is not "IP2IP" and not self.isAccountRegistered():
  377. raise DRingCtrlAccountError("Can't place a call without a registered account")
  378. # Send the request to the CallManager
  379. callid = self.callmanager.placeCall(self.account, dest)
  380. if callid:
  381. # Add the call to the list of active calls and set status to SENT
  382. self.activeCalls[callid] = {'Account': self.account, 'To': dest, 'State': 'SENT' }
  383. return callid
  384. def HangUp(self, callid):
  385. """End a call identified by a CallID"""
  386. if not self.account:
  387. self.setFirstRegisteredAccount()
  388. if callid is None or callid == "":
  389. pass # just to see
  390. self.callmanager.hangUp(callid)
  391. def Transfer(self, callid, to):
  392. """Transfert a call identified by a CallID"""
  393. if callid is None or callid == "":
  394. raise DRingCtrlError("Invalid callID")
  395. self.callmanager.transfert(callid, to)
  396. def Refuse(self, callid):
  397. """Refuse an incoming call identified by a CallID"""
  398. print("Refuse call " + callid)
  399. if callid is None or callid == "":
  400. raise DRingCtrlError("Invalid callID")
  401. self.callmanager.refuse(callid)
  402. def Accept(self, callid):
  403. """Accept an incoming call identified by a CallID"""
  404. print("Accept call " + callid)
  405. if not self.account:
  406. self.setFirstRegisteredAccount()
  407. if not self.isAccountRegistered():
  408. raise DRingCtrlAccountError("Can't accept a call without a registered account")
  409. if callid is None or callid == "":
  410. raise DRingCtrlError("Invalid callID")
  411. self.callmanager.accept(callid)
  412. def Hold(self, callid):
  413. """Hold a call identified by a CallID"""
  414. if callid is None or callid == "":
  415. raise DRingCtrlError("Invalid callID")
  416. self.callmanager.hold(callid)
  417. def UnHold(self, callid):
  418. """Unhold an incoming call identified by a CallID"""
  419. if callid is None or callid == "":
  420. raise DRingCtrlError("Invalid callID")
  421. self.callmanager.unhold(callid)
  422. def Dtmf(self, key):
  423. """Send a DTMF"""
  424. self.callmanager.playDTMF(key)
  425. def _GenerateCallID(self):
  426. """Generate Call ID"""
  427. m = hashlib.md5()
  428. t = int( time.time() * 1000 )
  429. r = int( random.random()*100000000000000000 )
  430. m.update(str(t) + str(r))
  431. callid = m.hexdigest()
  432. return callid
  433. def createConference(self, call1Id, call2Id):
  434. """ Create a conference given the two call ids """
  435. self.callmanager.joinParticipant(call1Id, call2Id)
  436. def hangupConference(self, confId):
  437. """ Hang up each call for this conference """
  438. self.callmanager.hangUpConference(confId)
  439. def switchInput(self, callid, inputName):
  440. """switch to input if exist"""
  441. return self.callmanager.switchInput(callid, inputName)
  442. def interruptHandler(self, signum, frame):
  443. print('Signal handler called with signal ' + str(signum))
  444. self.stopThread()
  445. def printAccountDetails(self, account):
  446. details = self.getAccountDetails(account)
  447. print(account)
  448. for k in sorted(details.keys()):
  449. print(" %s: %s" % (k, details[k]))
  450. print()
  451. def run(self):
  452. """Processing method for this thread"""
  453. context = self.loop.get_context()
  454. while True:
  455. context.iteration(True)
  456. if self.isStop:
  457. print("++++++++++++++++++ EXIT ++++++++++++++++++++++")
  458. return