Part 2: Reviving Microsoft Agent using PyWin32 - Run the Agent Hourly

 
Now the agent can run in specified time interval

Now the agent can run in a specified time interval

If you inspect closely our last article, you will realize that the Agent although having the ability to speak the time when you need it, it still unable to speak the time in specified interval, say, hourly. In this part of the article we are going to inspect whether we can easily add that ability.

Let's find out!

Quick Note: COM Threading Models

Long story short: Microsoft Agent is COM Object having Single Threading Apartment Model. You won't be able using MS Agent instance that have been instantiated in a different thread. For example, consider our current Agent application in previous article. As the Agent was instantiated in the main thread, seen below, 

1
2
3
4
if __name__ == '__main__':
    ...
    agent = MsAgent()
    ...

You won't be able to use this agent object outside of this main thread. And why does it important? Because I want to display this Agent in a certain time interval (hourly, to be exact), meaning it must be run by a Python thread. The simple solution would be to instantiate and use this Agent entirely in another Thread, not separate its instantiation in main thread and use it in different thread. That's not going to work with COM Object having Single Threading Apartment Model as this MS Agent things.

How to run Python code in certain time interval?

To run Python code in certain time interval, you can easily use Python Timer class from threading module. The function below will run a thread that will wake up at exactly +1 o'clock from current time. 

1
2
3
4
5
6
7
8
9
10
    def wakeup_next_hour(self):
        '''
        Run a thread that will wake up exactly n-o'clock
        '''
        now = datetime.datetime.now()
        next_hour = now + datetime.timedelta(hours = 1)
        next_hour_oclock = datetime.datetime(next_hour.year, next_hour.month, next_hour.day, next_hour.hour, 0, 0)
        seconds = next_hour_oclock - now
        self.thread = threading.Timer(seconds.total_seconds(), self.say_the_time_hourly)
        self.thread.start()

Observe that, first, we get the value of what time is it now. And then, we calculate the next hour using timedelta from datetime module, later we calculate total seconds from current time until exactly next hour at H:0 o'clock. And finally, we run the thread using thread.start(). Don't forget to store this new thread object (using the statement self.thread = threading.Timer(..)) as object variable/property, for later dispose/cancellation of this running thread. 

The thread will run our new method say_the_time_hourly() that will call say_the_time() to  speak up the current time and schedule new thread for another hour. Below is the say_the_time_hourly() method:

1
2
3
4
5
6
    def say_the_time_hourly(self):
        '''
        say the time and then schedule for another hour at exactly n-th o'clock
        '''
        self.say_the_time(None)
        self.wakeup_next_hour()

Great. Now, what is the different between the previous say_the_time() method  with this new one? Let's inspect it in another section.

Safely Running MS Agent in Different Thread

As already been said, you can not use an instance of MsAgent that have been created from another thread. Previously, this is how we instantiate our MsAgent object:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MsAgent(object):
    def __init__(self):
        self.agent=win32com.client.Dispatch('Agent.Control')
        self.charId = 'James'
        self.agent.Connected=1
        self.agent.Characters.Load(self.charId)
 
    def say_the_time(self, sysTrayIcon):
        now = datetime.datetime.now()
        str_now = '%s:%s:%s' % (now.hour, now.minute, now.second)
        self.agent.Characters(self.charId).Show()
        self.agent.Characters(self.charId).Speak('The time is %s' % str_now)
        self.agent.Characters(self.charId).Hide()

If you don't modify the above method, and forcefully try to run say_the_time() from another Thread (using threading.Timer() object), Python will gives this exception: CoInitialize has not been called. This is a clue to indicate that to use COM object in a new thread, except that the object support multi-threading concurrency model, we must instantiate it here. Actually, Pythoncom, which is the core module of PyWin32 that dealing with any sort of Python <-> COM communication, silently call CoInitialize() when first imported in main thread. Therefore, if we would like to use COM object in different thread, we have to initialize it ourselves.

Using the above explanation, below is the code that will schedule the Agent to run hourly at exactly x-o'clock and speak up the time. Got to say, it is quite a fun! Laughing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class MsAgent(object):
    '''
    Construct an MS Agent object, display it and let it says the time exactly every hour
    '''
    def __init__(self):
        self.charId = 'James'
 
    def say_the_time(self, sysTrayIcon):
        '''
        Speak up the time!
        '''
        pythoncom.CoInitialize()
        agent = win32com.client.Dispatch('Agent.Control')
        charId = 'James'
        agent.Connected=1
        try:
            agent.Characters.Load(charId)
        except Exception as ex:
            print ex
 
        now = datetime.datetime.now()
 
        if now.hour == 0 and now.minute == 0:
            str_now = ' exactly %s o''clock' % now.hour
        else:
            str_now = '%s:%s:%s' % (now.hour, now.minute, now.second)
        agent.Characters(charId).Show()
 
        agent.Characters(charId).Speak('The time is %s' % str_now)
        time.sleep(3)
        agent.Characters(charId).Hide()
        time.sleep(5)
        agent.Characters.Unload(charId)
        pythoncom.CoUninitialize()

Observe from the code above, that we initialize our MS Agent object completely in the function say_the_time(), that will be run by a thread, making it as a one way to a thread safe COM application. We begin and end the function by calling pythoncom.CoInitialize() and pythoncom.CoUninitialize() respectively, in order to have a clean instantiation and cleanup of our MS Agent object. Observe that we also pause the running code several seconds after each MS Agent animation, to let the animation finished. Without doing so, you won't see anything!

What's Next?

As always, you can get the current source code here: oclockreminder-version-2.zip.

Or, follow its public Github repository here: pythonthusiast/oclockreminder.

You maybe wondering, why I keep pursuing this application. "It is a dead technology!", as RJ implied in the comment section of the previous article. Well, it is. But I visioned that it will be a great starting point for a new productivity application that integrated seamlessly in a Windows environment!

What is it? 

Stay tuned for my next article!

 

 

 

 




Leave comments

authimage

Copyright(c) 2014 - PythonBlogs.com
By using this website, you signify your acceptance of Terms and Conditions and Privacy Policy
All rights reserved