af2e701c176b6733b6e0fdae19aaa3cb92ea41a6
[mevemon] / package / src / mevemon.py
1 #!/usr/bin/env python
2 #
3 # mEveMon - A character monitor for EVE Online
4 # Copyright (c) 2010  Ryan and Danny Campbell, and the mEveMon Team
5 #
6 # mEveMon 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 # mEveMon 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, see <http://www.gnu.org/licenses/>.
18 #
19
20
21 import os.path
22 import traceback
23 import time
24 import sys
25 #import socket for handling socket exceptions
26 import socket
27 import logging
28 import logging.handlers
29
30 import hildon
31 import gtk
32 #conic is used for connection handling
33 import conic
34 # we will store our preferences in gconf
35 import gnome.gconf
36
37 from eveapi import eveapi
38 import fetchimg
39 import apicache
40
41 #ugly hack to check maemo version. any better way?
42 if hasattr(hildon, "StackableWindow"):
43     from ui.fremantle import gui
44 else:
45     from ui.diablo import gui
46
47 LOGNAME = "mevemon.log"
48 CONFIG_DIR = os.path.expanduser("~/.mevemon/")
49 LOGPATH = os.path.join(CONFIG_DIR, LOGNAME)
50
51
52 class mEveMon():
53     """ The controller class for mEvemon. The intent is to help
54         abstract the EVE API and settings code from the UI code.
55     """
56
57     about_name = 'mEveMon'
58     about_text = ('Mobile character monitor for EVE Online')
59     about_authors = ['Ryan Campbell <campbellr@gmail.com>',
60                      'Danny Campbell <danny.campbell@gmail.com>']
61
62     about_website = 'http://mevemon.garage.maemo.org'
63     app_version = '0.4-8'
64
65
66     GCONF_DIR = "/apps/maemo/mevemon"
67
68     def __init__(self):
69         self.program = hildon.Program()
70         self.program.__init__()
71         self.gconf = gnome.gconf.client_get_default()
72         #NOTE: remove this after a few releases
73         self.update_settings()
74         self.connect_to_network()
75         self.cached_api = eveapi.EVEAPIConnection( cacheHandler = \
76                 apicache.cache_handler(debug=False))
77         self.gui = gui.mEveMonUI(self)
78         self.gui.run()
79
80     def run(self):
81         gtk.main()
82     
83     def quit(self, *args):
84         gtk.main_quit()
85
86     def update_settings(self):
87         """ Update from the old pre 0.3 settings to the new settings layout.
88             We should remove this eventually, once no one is using pre-0.3 mEveMon
89         """
90         uid = self.gconf.get_string("%s/eve_uid" % self.GCONF_DIR)
91         
92         if uid:
93             key = self.gconf.get_string("%s/eve_api_key" % self.GCONF_DIR)
94             self.add_account(uid, key)
95             self.gconf.unset("%s/eve_uid" % self.GCONF_DIR)
96             self.gconf.unset("%s/eve_api_key" % self.GCONF_DIR)
97
98
99     def get_accounts(self):
100         """ Returns a dictionary containing uid:api_key pairs gathered from gconf
101         """
102         accounts = {}
103         entries = self.gconf.all_entries("%s/accounts" % self.GCONF_DIR)
104
105         for entry in entries:
106             key = os.path.basename(entry.get_key())
107             value = entry.get_value().to_string()
108             accounts[key] = value
109
110         return accounts
111         
112     def get_api_key(self, uid):
113         """ Returns the api key associated with the given uid.
114         """
115         return self.gconf.get_string("%s/accounts/%s" % (self.GCONF_DIR, uid)) or ''
116
117     def remove_account(self, uid):
118         """ Removes the provided uid key from gconf
119         """
120         self.gconf.unset("%s/accounts/%s" % (self.GCONF_DIR, uid))
121
122     def add_account(self, uid, api_key):
123         """
124         Adds the provided uid:api_key pair to gconf.
125         """
126         self.gconf.set_string("%s/accounts/%s" % (self.GCONF_DIR, uid), api_key)
127
128     def get_auth(self, uid):
129         """ Returns an authentication object to be used for eveapi calls
130             that require authentication.
131         """
132         api_key = self.get_api_key(uid)
133
134         try:
135             auth = self.cached_api.auth(userID=uid, apiKey=api_key)
136         except Exception, e:
137             self.gui.report_error(str(e))
138             traceback.print_exc()
139             return None
140
141         return auth
142
143     def get_char_sheet(self, uid, char_id):
144         """ Returns an object containing information about the character specified
145             by the provided character ID.
146         """
147         try:
148             sheet = self.get_auth(uid).character(char_id).CharacterSheet()
149         except Exception, e:
150             self.gui.report_error(str(e))
151             # TODO: we should really have a logger that logs this error somewhere
152             traceback.print_exc()
153             return None
154
155         return sheet
156
157     def charid2uid(self, char_id):
158         """ Takes a character ID and returns the user ID of the account containing
159             the character.
160
161             Returns None if the character isn't found in any of the registered accounts.
162
163         """
164         acct_dict = self.get_accounts()
165         
166         for uid, api_key in acct_dict.items():
167             auth = self.cached_api.auth(userID=uid, apiKey=api_key)
168             try:
169                 api_char_list = auth.account.Characters()
170                 characters = api_char_list.characters
171             except:
172                 characters = []
173
174             for character in characters:
175                 if character.characterID == char_id:
176                     return uid
177
178         
179         return None
180     
181     def char_id2name(self, char_id):
182         """ Takes a character ID and returns the character name associated with
183             that ID.
184             The EVE API accepts a comma-separated list of IDs, but for now we
185             will just handle a single ID.
186         """
187         try:
188             chars = self.cached_api.eve.CharacterName(ids=char_id).characters
189             name = chars[0].characterName
190         except Exception, e:
191             self.gui.report_error(str(e))
192             traceback.print_exc()
193             return None
194
195         return name
196
197     def char_name2id(self, name):
198         """ Takes the name of an EVE character and returns the characterID.
199         
200             The EVE api accepts a comma separated list of names, but for now
201             we will just handle single names/
202         """
203         try:
204             chars = self.cached_api.eve.CharacterID(names=name).characters
205             char_id = chars[0].characterID
206             char_name = chars[0].name
207         except Exception, e:
208             self.gui.report_error(str(e))
209             traceback.print_exc()
210             return None
211
212         return char_id
213
214     def get_chars_from_acct(self, uid):
215         """ Returns a list of characters associated with the provided user ID.
216         """
217         auth = self.get_auth(uid)
218         if not auth:
219             return None
220         else:
221             try:
222                 api_char_list = auth.account.Characters()
223                 char_list = [char.name for char in api_char_list.characters]
224             except Exception, e:
225                 self.gui.report_error(str(e))
226                 traceback.print_exc()
227                 return None
228
229         return char_list
230
231     def get_characters(self):
232         """ Returns a list of (character_name, image_path, uid) tuples from all the
233             accounts that are registered to mEveMon.
234         
235             If there is an authentication issue, then instead of adding a valid
236             pair to the list, it appends an 'error message' 
237         """
238
239         ui_char_list = []
240         err_img = "/usr/share/mevemon/imgs/error.jpg"
241         err_txt = "Problem fetching info for account"
242
243         placeholder_chars = (err_txt, err_img, None)
244         
245         acct_dict = self.get_accounts()
246         if not acct_dict:
247             return [placeholder_chars]
248
249         for uid in acct_dict.keys():
250             char_names = self.get_chars_from_acct(uid)
251             
252             if not char_names:
253                 ui_char_list.append((err_txt + "\t(UID: %s)" % uid, err_img, None))
254             else:
255                 # append each char we get to the list we'll return to the
256                 # UI --danny
257                 for char_name in char_names:
258                     ui_char_list.append((char_name, self.get_portrait(char_name, 64) , uid) )
259         
260         return ui_char_list
261
262     def get_portrait(self, char_name, size):
263         """ Returns the file path of the retrieved portrait
264         """
265         char_id = self.char_name2id(char_name)
266         
267         return fetchimg.portrait_filename(char_id, size)
268
269     def get_skill_tree(self):
270         """ Returns an object from eveapi containing skill tree info
271         """
272         try:
273             tree = self.cached_api.eve.SkillTree()
274         except Exception, e:
275             self.gui.report_error(str(e))
276             traceback.print_exc()
277             return None
278         
279         return tree
280
281     def get_skill_in_training(self, uid, char_id):
282         """ Returns an object from eveapi containing information about the
283             current skill in training
284         """
285         try:
286             skill = self.get_auth(uid).character(char_id).SkillInTraining()
287         except Exception, e:
288             self.gui.report_error(str(e))
289             traceback.print_exc()
290             return None
291
292         return skill
293
294     def connection_cb(self, connection, event, mgc):
295         """ I'm not sure why we need this, but connection.connect() won't work
296             without it, even empty.
297         """
298         pass    
299
300
301     def connect_to_network(self):
302         """ This will connect to the default network if avaliable, or pop up the
303             connection dialog to select a connection.
304             Running this when we start the program ensures we are connected to a
305             network.
306         """
307         connection = conic.Connection()
308         #why 0xAA55?
309         connection.connect("connection-event", self.connection_cb, 0xAA55)
310         assert(connection.request_connection(conic.CONNECT_FLAG_NONE))
311
312
313     def get_sp(self, uid, char_id):
314         """ Adds up the SP for all known skills, then calculates the SP gained
315             from an in-training skill.
316         """
317         actual_sp = 0
318         
319         sheet = self.get_char_sheet(uid, char_id)
320         for skill in sheet.skills:
321             actual_sp += skill.skillpoints
322
323         live_sp = actual_sp + self.get_training_sp(uid, char_id)
324
325         return live_sp
326
327     def get_spps(self, uid, char_id):
328         """ Calculate and returns the skill points per hour for the given character.
329         """
330         skill = self.get_skill_in_training(uid, char_id)
331         
332         if not skill.skillInTraining:
333             return (0, 0)
334
335         total_sp = skill.trainingDestinationSP - skill.trainingStartSP
336         total_time = skill.trainingEndTime - skill.trainingStartTime
337         
338         spps = float(total_sp) / total_time
339     
340         return (spps, skill.trainingStartTime)
341
342     def get_training_sp(self, uid, char_id):
343         """ returns the additional SP that the in-training skill has acquired
344         """
345         spps_tuple = self.get_spps(uid, char_id)
346         
347         if not spps_tuple:
348             return 0
349         spps, start_time = spps_tuple
350         eve_time = time.time() #evetime is utc, right?
351         time_diff =  eve_time - start_time
352
353         return (spps * time_diff) 
354
355
356 def excepthook(ex_type, value, tb):
357     """ a replacement for the default exception handler that logs errors"""
358     #tb2 = "".join(traceback.format_exception(ex_type, value, tb))
359     #print tb2
360     logging.getLogger('meEveMon').error('Uncaught exception:', 
361                       exc_info=(ex_type, value, tb))
362
363 def setupLogger():
364     """ sets up the logging """
365     MAXBYTES = 1 * 1000 * 1000 # 1MB
366     LOGCOUNT = 10
367
368     logger = logging.getLogger("mEveMon")
369     logger.setLevel(logging.DEBUG)
370     
371     fileHandler = logging.handlers.RotatingFileHandler(LOGPATH,
372                                                     maxBytes=MAXBYTES,
373                                                     backupCount=LOGCOUNT)
374     logger.addHandler(fileHandler)
375
376     #create console handler
377     console = logging.StreamHandler()
378     console.setLevel(logging.DEBUG)
379     #formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
380     #console.setFormatter(formatter)
381     logger.addHandler(console)
382
383
384 if __name__ == "__main__":
385     setupLogger()
386     sys.excepthook = excepthook
387     app = mEveMon()
388     try:
389         app.run()
390     except KeyboardInterrupt:
391         sys.exit(0)