#!/usr/bin/python

"""A simple HTTP interface to the X10 home automation system.

Uses REST-style HTTP URLs to manipulate X10 units. This interface
is simple enough that most computers can already interact with it, 
either by human using a web browser or automated with 'curl'. 

Examples:

# turning a unit on via PUT:
echo 'on'|curl -T - http://localhost/unit/e1

# setting an alias:
echo 'main'|curl -T - http://localhost/unit/e1/alias

# getting an alias:
curl http://localhost/unit/e1/alias

# turning off by alias:
echo 'off'|curl -T - http://localhost/unit/main

See x10RestServer.X10HTTPRequestHandler for more.
"""

__version__ = '0.1'

import BaseHTTPServer
import re, string
import os,sys


class X10Interface:
    """An interface to the X10 firecracker device, using the
    'bottlerocket' program."""
    avail_housecodes = ('E')
    aliases_file = os.getenv('HOME')+'/.x10_rest_server.aliases'
    unit_aliases = {}
    unit_aliases_rev = {}
    
    def __init__(self):
        self.load_aliases(self.aliases_file)

    def get_unit_from_alias(self, alias):
        return X10Unit(self, self.unit_aliases_rev[alias])

    def valid_alias(self, alias):
        return self.unit_aliases_rev.has_key(alias)

    def get_alias(self, unit):
        return self.unit_aliases[unit.get_id()]
    
    def del_alias(self, unit):
        if self.has_alias(unit):
            alias = self.unit_aliases[unit.get_id()]
            del self.unit_aliases[unit.get_id()]
            del self.unit_aliases_rev[alias]
            self.save_aliases(self.aliases_file)
            return True
        else:
            return False

    def set_alias(self, unit, alias):
        self.unit_aliases[unit.get_id()] = alias
        self.unit_aliases_rev[alias] = unit.get_id()
        self.save_aliases(self.aliases_file)

    def has_alias(self, unit):
        return self.unit_aliases.has_key(unit.get_id())
    
    def send_command(self, unit, command, value=''):
        os.system("br "+unit.get_id()+" "+command+" "+value)
        print "br", unit.get_id(), command, value

    def load_aliases(self, filename):
        comment_filter = re.compile(r'\s*#.+$')
        alias_match    = re.compile(r'^\s*(\w+)\s*=\s*(\w+)')
        
        f = file(filename, 'r')
        for line in f.readlines():
            line = re.sub(comment_filter, '', line)
            m = re.match(alias_match, line)
            if m != None:
                print "loaded", m.group(1), "=", m.group(2)
                self.unit_aliases[m.group(1)] = m.group(2)
                self.unit_aliases_rev[m.group(2)] = m.group(1)
        f.close()

    def save_aliases(self, filename):
        f = file(filename, 'w')
        for unit in self.unit_aliases:
            f.write(unit + "=" + self.unit_aliases[unit] + "\n")
        f.close()
        
    def all_units(self):
        """Returns a list of all addressable units."""
        units = []
        for c in self.avail_housecodes:
            for i in range(1,17):
                units.append( X10Unit(self, c, i) )
        return units

class X10Unit:
    """An X10 unit."""

    housecode = ''
    number = 0
    alias  = None
    x10    = None
    
    def __init__(self, x10, housecode, number=None):
        self.x10 = x10
        
        if number == None:
            m = re.match(r'(\w+)(\d+)', housecode)
            if m != None:
                self.housecode = m.group(1)
                self.number = m.group(2)
            else:
                # the X10Unit object can represent an alias, too.
                self.alias  = housecode
        else:
            self.housecode = housecode
            self.number = number

    def get_id(self):
        if self.alias:
            if self.x10.valid_alias(self.alias):
                return string.lower(self.x10.get_unit_from_alias(self.alias).get_id())
            else:
                return None
        return string.lower(self.housecode + str(self.number))
    
    def __str__(self):
        return self.get_id()
            
class X10HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    """A REST-style remote control for X10 remote controls.

    Supported URLs and accompanying HTTP methods
    (prefix with 'http://HOST:PORT'):

    /
    GET: Gets an XHTML index suitable for use in a web browser.

    /unit/XX
    GET: shows the status of the given unit
    PUT: 'on', 'off' 'bright NN', 'dim NN', sets the state of a unit.
    POST: 'action=XX' same as the actions for PUT

    /unit/XX/alias
    GET: returns the alias for a given unit
    PUT: sets the alias for a given unit
    DELETE: removes an alias for a given unit
    """
        
    server_version = "X10RESTServer/" + __version__
    x10 = X10Interface()

    mappings = {re.compile(r"^/unit/([\w\s]+)$"):
               'handle_unit_url',

               re.compile(r"^/unit/([\w\s]+)/alias$"):
               'handle_alias_url',

               re.compile(r"^/$"):
               'handle_root_url'}
    
    # the big-three HTTP methods

    def handle_unit_url(self, method, m):
        unit = X10Unit(self.x10, m.group(1))

        if method == 'GET':
            self.send_plain_message(str(unit)+"\n")
            
        elif method == 'PUT':
            self.send_put_allow()
            command = self.read_simple_put().strip()
            
            self.x10.send_command(unit, command)
            self.send_plain_message("unit "+unit.get_id()+" is unknown\n")

        elif method == 'POST':
            unit = X10Unit(self.x10, m.group(1))
            
            if not self.headers.has_key('Content-Length'):
                self.send_response(400, 'Must specify content length')
                
            message = self.rfile.read(int(self.headers['Content-Length']))
            m2 = re.match(r'^(.+)=(.+)$', message)
            if m2 != None:
                if m2.group(1) == "action":
                    self.x10.send_command(unit, m2.group(2))
                self.send_response(302, 'Success')
                self.send_header('Location', self.url())
                self.end_headers()

            else:
                self.send_response(400, 'Malformed POST content')
        else:
            self.send_header('Accept', "GET PUT POST")
            self.send_error(405)


    def handle_alias_url(self, method, m):
        unit = X10Unit(self.x10, m.group(1))
        
        if method == 'GET':
            if self.x10.has_alias(unit):
                self.send_plain_message(self.x10.get_alias(unit)+"\n")
            else:
                self.send_error(404, 'Alias not defined for given unit')
                
        elif method == 'PUT':
            self.send_put_allow()
            alias = self.read_simple_put().strip()
            self.x10.set_alias(unit, alias)
            self.send_response(201)
            
        elif method == 'DELETE':
            if self.x10.del_alias(unit):
                self.send_response(200)
            else:
                self.send_error(404, 'Cannot delete nonexistant alias')
            
        else:
            self.send_header('Accept', "GET PUT DELETE")
            self.send_error(405)
            
    def handle_root_url(self, method, m):
        if method == 'GET':
            self.list_units()
        else:
            self.send_header('Accept', "GET")
            self.send_error(405)


    def match_handler(self, httpmethod):
        for regexp in self.mappings:
            m = re.match(regexp, self.path)
            if m != None:
                if hasattr(self, self.mappings[regexp]):
                    method = getattr(self, self.mappings[regexp])
                    method(httpmethod, m)
                else:
                    self.send_error(500, 'missing function for matched URL request')
                return
        self.send_error(404)

        
    def do_GET(self):
        self.match_handler('GET')
            
    def do_PUT(self):
        self.match_handler('PUT')

    def do_POST(self):
        self.match_handler('POST')
            
    def do_DELETE(self):
        self.match_handler('DELETE')

    def url(self):
        sa = self.server.socket.getsockname()
        return "http://"+sa[0]+":"+ str(sa[1])
    

    def send_put_allow(self):
        if self.headers.has_key('Expect'):
            self.wfile.write("%s %d %s\r\n" %
                             (self.protocol_version, 100, 'Continue'))
            self.end_headers()
            
    def read_simple_put(self):
        length = self.rfile.readline()
        length.strip()
        content = self.rfile.read(string.atoi(length, base=16))
        return content

    def send_plain_message(self, content, code=200, message=''):
        self.send_response(code, message)
        self.send_header('Content-Type', 'text/plain')
        self.send_header('Content-Length', len(content))
        self.end_headers()
        self.wfile.write(content)

    def send_xhtml_message(self, content, code=200, message=''):
        content=self.add_xhtml_skel(content)
        self.send_response(code, message)
        self.send_header('Content-Type', 'application/xhtml+xml')
        self.send_header('Content-Length', len(content))
        self.end_headers()
        self.wfile.write(content)

                
    def list_units(self):
        """Sends a reply with an XHTML list of all addressable units."""
        unit_list = "<ul>\n"
        
        for unit in self.x10.all_units():
            if self.x10.has_alias(unit):
                name = self.x10.get_alias(unit)
            else:
                name = unit.get_id()
            unit_list += "<li>"+ \
                         "<a href='"+ \
                         self.unit_url(unit)+"'>"+name+"</a>\n"
            
            unit_list += "<form action='"+self.unit_url(unit)+ \
                         "' method='POST'>\n" + \
                         "<input type='submit' name='action' value='on'/>\n"+\
                         "<input type='submit' name='action' value='off'/>\n"+\
                         "</form>\n"+\
                         "</li>\n"
        
        unit_list += "\n</ul>"
        
        self.send_xhtml_message(unit_list)

    def unit_url(self, unit):
        return self.url()+"/unit/"+unit.get_id()
    
    def add_xhtml_skel(self, content):
        retval = "<?xml version='1.0'?>\n"+ \
        '<html xmlns="http://www.w3.org/1999/xhtml">\n'+ \
        '<head><title>X10 REST Server</title></head>'+ \
        '<body>\n'+content+'\n</body></html>'
        
        return retval

class x10RestServer:
    """A simple HTTP interface to the X10 home automation system.

    
    """
    def __init__(self, port, hostname='localhost'):
        HandlerClass = X10HTTPRequestHandler
        ServerClass = BaseHTTPServer.HTTPServer
        
        server_address = (hostname, port)
        HandlerClass.protocol_version = "HTTP/1.0"
        httpd = ServerClass(server_address, HandlerClass)

        sa = httpd.socket.getsockname()
        print "Connect to http://"+sa[0]+":"+ str(sa[1])+ "/"
        httpd.serve_forever()
    
if __name__ == '__main__':
    if sys.argv[1:]:
        hostname = sys.argv[1]
    else:
        hostname = 'localhost'

    server = x10RestServer(8090, hostname)
