
import struct
import time

# Header Byte Types
addr = 0x04
func = 0x06
fullBright = 0x7e;

funccode = {
    "All Off": 0x0,
    "All On": 0x1,
    "On": 0x2,
    "Off": 0x3,
    "Dim": 0x4,
    "Bright": 0x5,
    "All Off": 0x6,
    "ExtCode": 0x7,
    "HailReq": 0x8,
    "HailAck": 0x9,
    "Preset1": 0xA,
    "Preset2": 0xB,
    "ExtDataXfer": 0xC,
    "StatusOn": 0xD,
    "StatusOff": 0xE,
    "StatusReq": 0xF
    }


housecode = {
    "A": 0x6,
    "B": 0xE,
    "C": 0x2,
    "D": 0xA,
    "E": 0x1,
    "F": 0x9,
    "G": 0x5,
    "H": 0xD,
    "I": 0x7,
    "J": 0xF,
    "K": 0x3,
    "L": 0xB,
    "M": 0x0,
    "N": 0x8,
    "O": 0x4,
    "P": 0xC
    }

unitcode = {
    "1": 0x6,
    "2": 0xE,
    "3": 0x2,
    "4": 0xA,
    "5": 0x1,
    "6": 0x9,
    "7": 0x5,
    "8": 0xD,
    "9": 0x7,
    "10": 0xF,
    "11": 0x3,
    "12": 0xB,
    "13": 0x0,
    "14": 0x8,
    "15": 0x4,
    "16": 0xC
    }

# Given a byte string, calculate the checksum by adding all the
# bytes and then returning the bitwise AND with 0xFF.
#==============================================================
def X10_Checksum( s ):
    sum = 0
    for c in s:
        sum = sum + ord(c)
    return sum & 0xFF


# Send a string to the X10 CM11a module.  The module replies with
# a checksum.  On a good checksum, we send an ACK.  After the ACK,
# the module replies with a READY.  Note, the ACK value is zero and
# the READY value is 0x55.  Also note, it make take up to two
# seconds for the module to respond on some commands.
#==============================================================
def X10_Send( ser, s ):
    ret = True  # Init return code.

    ser.flushInput()
    ser.write( s )
    c = ser.read( 1 )
    cs = X10_Checksum( s )  # Compute checksum of the bytes.

    if (len(c) == 1) and (ord(c) == cs):  # Good Checksum
        ser.write( chr(0x00) )  # Send ACK
        c = ser.read(1)  # Look for X10 ready.
        if (len(c) == 1) and (ord(c) == 0x55):
            ret = True  # All Good
        else:
            print("Err: Missing X10 ready response.")
            ret = False
    else:
        print(f"Checksum Err / Len = {len(c)}")
        if len(c) == 1:
            print(f"Checksum -> {ord(c)} expecting {cs}")
        ret = False

    return ret  # Return True on Good & False on Bad



#==============================================================
def X10_On( ser, h, u ):

    ser.flushInput()
    b = struct.pack( 'BB', addr, (h << 4 ) | (u & 0x0F) )
    if not X10_Send( ser, b ):
        print("X10 Error send first ON string.")
        return False
    b = struct.pack( 'BB', func, (h << 4 ) | funccode['On'] )
    if not X10_Send( ser, b ):
        print("X10 Error send second ON string.")
        return False

    return True  # Everything must be OK.

#==============================================================
def X10_Off( ser, h, u ):

    ser.flushInput()
    b = struct.pack( 'BB', addr, (h << 4 ) | (u & 0x0F) )
    if not X10_Send( ser, b ):
        print("X10 Error send first OFF string.")
        return False
    b = struct.pack( 'BB', func, (h << 4 ) | funccode['Off'] )
    if not X10_Send( ser, b ):
        print("X10 Error send second OFF string.")
        return False

    return True  # Everything must be OK.

#==============================================================
def X10_Bright( ser, h, u ):
    ret = True
    to = ser.timeout  # Save timeout
    ser.timeout = 5
    ser.flushInput()
    b = struct.pack( 'BB', addr, (h << 4 ) | (u & 0x0F) )
    if not X10_Send( ser, b ):
        print("X10 Error send first Bright string.")
        ret = False
    if ret == True:
        b = struct.pack( 'BB', fullBright, (h << 4 ) | funccode['Bright'] )
        if not X10_Send( ser, b ):
            print("X10 Error send second Bright string.")
            ret = False

    ser.timeout = to  # Restore timeout to orginal value.
    return ret  # True = OK / False = Error.



# Ask the CM11A for its status.  In short order, the CM11A should
# respond with 14 bytes of status.
#==============================================================
def X10_Status( ser ):
    to = ser.timeout  # Save current timeout.
    ser.timeout = 0.1  # Status bytes should be quick.
    ret = False

    ser.flushInput()
    time.sleep( 0.1 )
    ser.write( chr(0x8b) )
    c = ser.read( 14 )  # The module should return 14 bytes of info.
    if len(c) >= 13:
        i = 0
        for a in c: 
            print(f"{i} : {hex(ord(a))}")
            i = i + 1
        print("X10 status OK.")
        ret = True
    else:
        print("X10 status is BAD.")
        print(f"X10 string len: {len(c)}")
        i = 0
        for a in c: 
            print(f"{i} : {hex(ord(a))}")
            i = i + 1
        ret = False

    ser.write( chr(0x00) )  # Send an ACK.
    ser.timeout = to  # Restore timeout value.
    return (ret, c)


# The CM11A X10 will NOT do anything until its clock is set.
# The following code just sets the clock to something so the 
# interface can be used.
#==============================================================
def X10_SetClock( ser ):
    # Since I don't use the clock in the CM11A this code just
    # sets the clock to some junk so the module can start working.
    s = b"\x9b\x32\x66\x07\xf4\x04\x60"

    ser.flushInput()
    time.sleep( 0.5 )  # Wait a bit after getting a 0xA5.
    ser.flushInput()
    ser.write( s )
    print("Resetting X10 clock.")
    c = ser.read(1)  # Readback checksum.
    cs = X10_Checksum( s[1:] )
    if (len(c) == 1) and (ord(c) == cs):
        ser.write( chr(0x00) )
        c = ser.read( 1 )
        if (len(c) == 1) and (ord(c) == 0x55):
            print("X10 Clock set.")
        else:
            print("X10 final 0x55 marker missing.")
    else:
        ser.write( chr(0x00) )
        print("Bad checksum from X10 interface.")
        print(f"X10 returned: {hex(ord(c))}")
        print(f"Expected Checksum: {hex(cs)}")


