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