"""
x10control.py - 2/27/2005

Copyright (c) 2005 Robert Stone
x10.py Copyright (c) 2004 Jimmy Retzlaff

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

Overview:

This is a Python for Symbian Series 60 app for controlling x10 modules using a
X10 "firecracker" serial interface transmitter and the x10server.py "driver".
For those who don't know, X10 is a company that makes a variety of control
modules for home automation (turning lights on and off, etc.)

For more information on X10 and the "Firecracker" remote control see:
    
    http://www.x10.com/products/x10_cm17a.htm
    http://www.averdevelopment.com/python/x10.html
    http://www.geocities.com/ido_bartana/Firecracker_protocol.htm
    
To set your system up to use your s60 device as a bluetooth remote control,
you need to:

    - Have the following installed:
        - Python 2.3 (I'm using 2.4 but 2.2 or later is probably ok)
        - pySerial http://pyserial.sourceforge.net/
        - x10.py http://www.averdevelopment.com/python/x10.html

    - Create a bluetooth serial "port" on your pc which accepts input
      from a connected bluetooth device and sends the input out of a
      serial device in the OS. For example, under windows you might create
      a local serial service named "X10 Remote Control" that is associated
      with com7.  When something connects to this service (such as
      x10control.py), output from the device will show up as input from com7.

    - Attach your firecracker to a port (say com1 if you're using windows.)
    
    - Run: x10server.py com1 com7

    - Edit command_list below to include the modules you use and
      descriptive names, etc.

    - Install x10control.py on your Symbian Series 60 device (assuming you
      already have Python for Series 60 installed.)

    - Run x10control.py and select the system you ran x10server.py on.
      Then select "X10 Remote Control" as the service.

Now you're ready to turn your modules on and off.
"""

import socket, re
import appuifw, e32, key_codes

e32.ao_yield()

# Some utility stuff that I came up with.
# At some point I mean to experiment with creating some sort of
# template-method-based object oriented application framework that
# encapsulates all of the appuifw "view" and concurrency details.
class AppuifwStateStack(object):
    """
    Maintains a stack of AppuifwAppStates
    """
    def __init__(self):
        self.stack = []
    def push(self):
        """
        Push the current appuifw.app state onto the stack.
        """
        app = appuifw.app
        self.stack.append((app.title, app.body, app.menu, app.exit_key_handler))
    def restore(self, state):
        app = appuifw.app
        (app.title, app.body, app.menu, app.exit_key_handler) = state
    def pop(self):
        """
        Pop (restore) the appuifw.app state from the stack and delete the
        state object.
        """
        self.restore(self.stack.pop())
    def restore_original(self):
        """
        Restore to the first item and clear stack.
        """
        self.restore(self.stack[0])
        del self.stack[:]

def set_exit_if_standalone():
    """Only set_exit() if this wasn't invoked from the interpreter ui."""
    appname = appuifw.app.full_name()
    if appname[-10:] != u"Python.app":
        appuifw.app.set_exit()
        
def bt_socket_connect(target=''):
    """
    Stole this from Nokia's "bluetooth sockets" example (appendix F).
    Let the user select a device and serial service, then connect to that
    service and return the socket.
    """
    if not target:
        (address, services) = socket.bt_discover()
    if len(services) > 1:
        choices = services.keys()
        choices.sort()
        choice = appuifw.popup_menu(
            [unicode(services[x])+": "+x for x in choices], u'Choose port:')
        target = (address, services[choices[choice]])
    else:
        target = (address, services.values()[0])
    sock = socket.socket(socket.AF_BT,socket.SOCK_STREAM)
    sock.connect(target)
    return sock

class X10RemoteControlApp(object):
    """
    Remote control application for the X10 firecracker through x10gate.py.
    """
    STATE_OFF = 0
    STATE_ON = 1
    STATE_NONE = -1
    
    state_str = { 1: "On", 0: "Off", -1: "" }
    
    # This defines the modules and commands.
    # Fields are: 0 = menu item name, 1 = associated command, 2 = initial state
    # For STATE_OFF and STATE_ON items, clicking the list item will toggle
    # the off/on state by appending "on" or "off" to the command [1].
    # For STATE_NONE items, the command [1] will just be sent unchanged.
    # Strings get converted into unicode later.
    command_list = [
        ["Living Room", "A1", STATE_OFF],
        ["Bedroom", "A2", STATE_OFF],
        ["All On", "A All On", STATE_NONE],
        ["All Off", "A All Off", STATE_NONE],
        ["Lamps On", "A Lamps On", STATE_NONE],
        ["Lamps Off", "A Lamps Off", STATE_NONE],
    ]
    
    def __init__(self):
        app = appuifw.app
        self.app_lock = e32.Ao_lock()
        self.exit_flag = False
        # So far, in this app, this is only uses to restore the app state if
        # the app is run from within the interpreter ui.
        state = self.appstate = AppuifwStateStack()
        state.push()
        app.title = u"X10 Remote"
        app.exit_key_handler = self.handle_exit
        self.bt_connect()
        self.listbox_items = map(self.make_item_name, self.command_list)
        self.listbox = appuifw.Listbox(self.listbox_items, self.handle_list)
        app.body = self.listbox
        self.listbox.bind(key_codes.EKeyLeftArrow, self.handle_list_left)
        self.listbox.bind(key_codes.EKeyRightArrow, self.handle_list_right)


    def make_item_name(self, item):
        s = self.state_str[item[2]]
        if s:
            return unicode("%s [%s]" % (item[0], s))
        else:
            return unicode(item[0])
        
    def bt_connect(self):
        self.sock = bt_socket_connect()
    
    def send_command(self, command):
        sock = self.sock
        sock.send(command + "\r\n")

    def refresh_item_name(self, itemnum):
        item = self.command_list[itemnum]
        self.listbox_items[itemnum] = self.make_item_name(item)
        self.listbox.set_list(self.listbox_items, itemnum)
        
    def toggle_state(self, itemnum):
        item = self.command_list[itemnum]
        if item[2] == self.STATE_OFF:
            self.send_command("%s on" % (item[1]))
            item[2] = self.STATE_ON
        elif item[2] == self.STATE_ON:
            self.send_command("%s off" % (item[1]))
            item[2] = self.STATE_OFF
        else:
            raise RuntimeError("can't toggle this item")
        self.refresh_item_name(itemnum)
        

    def handle_list(self):
        lb = appuifw.app.body
        itemnum = lb.current()
        name, cmd, state = item = self.command_list[itemnum]
        if state > -1:
            self.toggle_state(itemnum)
        else:
            self.send_command(cmd)

    def lamp_brightness(self, direction):
        lb = appuifw.app.body
        itemnum = lb.current()
        item = self.command_list[itemnum]
        if item[2] < 0: return
        item[2] = self.STATE_ON
        cmd = item[1]
        houseletter = re.sub("[0-9]+","",cmd)
        self.send_command("%s on, %s %s" % (cmd, houseletter, direction))
        item[2] = self.STATE_ON
        self.refresh_item_name(itemnum)

    def handle_list_left(self):
        self.lamp_brightness("dim")
        
    def handle_list_right(self):
        self.lamp_brightness("bright")

    def handle_exit(self):
        """Clean up and exit."""
        self.sock.close()
        self.appstate.restore_original()
        self.exit_flag = True
        self.app_lock.signal()
        set_exit_if_standalone()
        
    def main(self):
        while not self.exit_flag:
            self.app_lock.wait()

if __name__ == "__main__":
    X10RemoteControlApp().main()