import asyncio
from contextlib import AbstractAsyncContextManager
import datetime
import socket
import struct
import email.utils
import uuid
from aiofile import async_open
import re
import sys
import logging
import logging.handlers
import json

M_SEARCH_REQ_MATCH = "M-SEARCH"

UPNP_BROADCAST = """NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=100
LOCATION: http://{}:{}/description.xml
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
NTS: ssdp:alive
NT: upnp:rootdevice
USN: uuid:2f402f80-da50-11e1-9b23-{}::upnp:rootdevice

""".replace("\n", "\r\n")


# IP, PORT, ST
UPNP_RESPOND_TEMPLATE = """HTTP/1.1 200 OK
CACHE-CONTROL: max-age=100
EXT:
LOCATION: http://{}:{}/description.xml
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
ST: {}
USN: uuid:2f402f80-da50-11e1-9b23-{}::upnp:rootdevice

""".replace("\n", "\r\n")

# TODO: jimboca: check others to see if they are specifying this?
# CACHE-CONTROL: max-age=86400
# EXT:
# LOCATION: http://{}:{}/description.xml
# SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
# ST: urn:schemas-upnp-org:device:basic:1
# USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1

# updated modelName and removed extra tabs and \r to match examples on web
DESCRIPTION_XML = """HTTP/1.1 200 OK
Content-type: text/xml
Connection: Keep-Alive

<?xml version="1.0" encoding="UTF-8" ?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://{}:{}/</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
<friendlyName>Philips hue ({})</friendlyName>
<manufacturer>Royal Philips Electronics</manufacturer>
<manufacturerURL>http://www.philips.com</manufacturerURL>
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
<modelName>Philips hue bridge 2012</modelName>
<modelNumber>929000226503</modelNumber>
<modelURL>http://www.meethue.com</modelURL>
<serialNumber>{}</serialNumber>
<UDN>uuid:2f402f80-da50-11e1-9b23-{}</UDN>
<serviceList>
<service>
<serviceType>(null)</serviceType>
<serviceId>(null)</serviceId>
<controlURL>(null)</controlURL>
<eventSubURL>(null)</eventSubURL>
<SCPDURL>(null)</SCPDURL>
</service>
</serviceList>
<presentationURL>index.html</presentationURL>
<iconList>
<icon>
<mimetype>image/png</mimetype>
<height>48</height>
<width>48</width>
<depth>24</depth>
<url>hue_logo_0.png</url>
</icon>
<icon>
<mimetype>image/png</mimetype>
<height>120</height>
<width>120</width>
<depth>24</depth>
<url>hue_logo_3.png</url>
</icon>
</iconList>
</device>
</root>
""".replace("\n", "\n")  # was \r\n

# 20150920-Added in case it is used for discovery
APICONFIG_JSON = """
[{"swversion":"01008227","apiversion":"1.2.1","name":"Smartbridge 1","mac":"%s",}]
"""

NEWDEVELOPERSYNC_JSON = """
[{"success":{"%s":"c6260f982b43a226b5542b967f612ce"}}]
""" % ("username")

# example template values: "success", "1", "on", "true"
PUTRESP_TEMPLATE_JSON = """
[{"%s":{"/lights/%s/state/%s":%s}}]
"""

JSON_HEADERS = """HTTP/1.1 200 OK
CONTENT-LENGTH: %d
CONTENT-TYPE: application/json charset="utf-8"
DATE: %s
EXT:
SERVER: Unspecified, UPnP/1.0, Unspecified
CONNECTION: close

%s"""


ICON_HEADERS = """HTTP/1.1 200 OK
Content-type: image/png

""".replace("\n", "\r\n")

ICON_SMALL = b"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"
ICON_BIG = b" "


class Broadcaster():
    def __init__(self, config, logger: logging.Logger) -> None:
        self.config = config
        self.logger = logger
        self.event_loop = asyncio.get_event_loop()

    async def run(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        
        self.sock.bind((self.config['IP'], 0))        
        self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)
        
        self.logger.info("Starting broadcast loop")
        await self.loop() # Start the loop broadcast task

    async def loop(self):
        while True:
            self.logger.debug("Sending broadcast")
            await self.event_loop.sock_sendto(self.sock, UPNP_BROADCAST.encode(), (self.config['BCAST_IP'], self.config['UPNP_PORT']))
            await asyncio.sleep(self.config['BROADCAST_INTERVAL'])

    async def stop(self):
        self.logger.debug("Stopping broadcast loop")
        await asyncio.to_thread(self.sock.close)

class Responder():
    def __init__(self, config, logger: logging.Logger) -> None:
        self.config = config
        self.logger = logger
        self.event_loop = asyncio.get_event_loop()

    async def run(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind(('', self.config['UPNP_PORT']))
        mreq = struct.pack("4sl", socket.inet_aton(
            self.config['BCAST_IP']), socket.INADDR_ANY)
        self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
        self.sock.setblocking(False)

        # Issue 9: create separate response socket bound to assigned interface
        self.sockresp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        self.sockresp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sockresp.bind((self.config['IP'], self.config['UPNP_PORT']))
        self.sock.setblocking(False)

        self.logger.info("Starting response loop")
        await self.loop() # Start the loop response task

    async def loop(self):
        while True:
            try:
                self.logger.debug("Waiting for M-SEARCH")
                data, addr = await self.event_loop.sock_recvfrom(self.sock, 1024)
                data = data.decode() # type: ignore
            # if socket closed by stop() method
            except ConnectionResetError:
                break
            except socket.error:
                self.logger.error("Socket error")
                continue
            else:
                if M_SEARCH_REQ_MATCH in data:
                    self.logger.debug("Received M-SEARCH from {}".format(addr))

                    if "urn:schemas-upnp-org:device:basic:1" in data:
                        self.logger.debug("received urn:schemas-upnp-org:device:basic:1")
                        
                        resp = UPNP_RESPOND_TEMPLATE.format(
                            self.config['IP'],
                            self.config['HTTP_PORT'],
                            "urn:schemas-upnp-org:device:basic:1",
                            self.config['SERIALNO'])

                        await self.event_loop.sock_sendto(self.sockresp, resp.encode(), addr) # type: ignore
                        #self.logger.debug("Response sent: "+resp)
                    elif "upnp:rootdevice" in data:
                        self.logger.debug("received upnp:rootdevice")
                        resp = UPNP_RESPOND_TEMPLATE.format(
                            self.config['IP'],
                            self.config['HTTP_PORT'],
                            "upnp:rootdevice",
                            self.config['SERIALNO']
                        )
                        await self.event_loop.sock_sendto(self.sockresp, resp.encode(), addr) # type: ignore
                        #self.logger.debug("Response sent: "+resp)
                    elif "ssdp:all" in data:
                        self.logger.debug(
                            "received ssdp:all responding with upnp:rootdevice")
                        resp = UPNP_RESPOND_TEMPLATE.format(
                            self.config['IP'],
                            self.config['HTTP_PORT'],
                            "upnp:rootdevice",
                            self.config['SERIALNO']
                        )
                        await self.event_loop.sock_sendto(self.sockresp, resp.encode(), addr) # type: ignore
                        #self.logger.debug("Response sent: "+resp)
                    else:
                        self.logger.debug("ignoring")
                    self.logger.debug("----------------------")
                    self.logger.debug("  ")

    async def stop(self):
        self.logger.debug("Stopping response loop")
        await asyncio.to_thread(self.sock.close)
        await asyncio.to_thread(self.sockresp.close)


class Httpd():
    def __init__(self, devices, config, logger: logging.Logger) -> None:
        self.config = config
        self.logger = logger
        self.devices = devices

        self.event_loop = asyncio.get_event_loop()

    async def run(self):
        try:
            self.logger.info("Starting HTTP server on {IP}:{HTTP_PORT}".format(**self.config))

            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.sock.bind((self.config['IP'], self.config['HTTP_PORT']))
            self.sock.listen(100)
            self.sock.setblocking(False)

            await self.loop()

        except socket.error as msg:
            self.logger.error("Http Socket Error: {}".format(msg))

    async def loop(self):
        while True:
            try:
                client, addr = await self.event_loop.sock_accept(self.sock)
            except ConnectionResetError:
                break
            except socket.error:
                self.logger.error("Socket error")
                continue
            else:
                self.logger.debug("Received connection from {}".format(addr))
                self.event_loop.create_task(self.handle(client, addr))

    async def handle(self, client:socket.socket, addr):
        try:
            
            # all data isnt always sent right away--try a couple more times
            # 2015-08: Logitech change the data flow slightly.  We seem to need to
            # return the payload for "GET /api/lights" sooner.  The 1 sec sleeps
            # have been removed and we only do another "request.recv" if the
            # content-length is found and greater than 0
            # 2015-10: Header size is now computed to determine exactly how much
            # data still needs to be pulled
            data = ""
            for i in range(3):
                data += (await self.event_loop.sock_recv(client, 1024)).decode()
                if "\r\n\r\n" in data:
                    break
        except socket.error:
            self.logger.error("Socket error")
        else:
            if not data:
                self.logger.debug("Connection closed by {}".format(addr))
            else:
                self.logger.debug("Received data from {}: {}".format(addr, data))
                await self.handle_request(client, data)
                client.close()


    async def handle_request(self, client, data):
        searchObj = re.search(r'content-length: (\d+)', data, re.I)
        if searchObj and int(searchObj.group(1)) > 0:
            contentLength = int(searchObj.group(1))
            headerLength = data.find("\r\n\r\n") + 4
            self.logger.debug("Header-Length={} Content-Length={}".format(headerLength, contentLength))
            # got the header--now grab the remaining content if any
            #if len(data) < headerLength + contentLength:
                #data += client.recv(headerLength + contentLength - len(data)).decode() #Cause of problem maybe?
        else:
            contentLength = 0

        self.logger.debug("Content-Length={}".format(contentLength))

        if "description.xml" in data:
            # send description.xml and end for get request
            await self.event_loop.sock_sendall(client, DESCRIPTION_XML.encode())
            self.logger.debug("{} Sent HTTP description.xml Response".format(client))
            # alexa discovery request
            self.logger.debug("Alexa, discover devices")

        elif "hue_logo_0.png" in data:
            await self.event_loop.sock_sendall(client, ICON_HEADERS.encode())
            await self.event_loop.sock_sendall(client, ICON_SMALL.decode('base64').encode())
        elif "hue_logo_3.png" in data:
            await self.event_loop.sock_sendall(client, ICON_HEADERS.encode())
            await self.event_loop.sock_sendall(client, ICON_BIG.decode('base64').encode())

        elif re.match(r'GET /api/.*lights ', data, re.I):
            resp = "\n{"
            i = 1
            for device in self.devices:
                # TODO: Force update of device? dst = device.st()
                resp += "\"%d\":" % (i)
                resp += await self.get_onelight_json(device)
                if i < len(self.devices):
                    resp += ","
                i += 1
            resp += "}\n"
            await self.send_json(client, resp)

        elif "PUT /api" in data:
            matchObj = re.match(r'PUT /api/(.*)lights/(\d+)/state', data, re.I)
            # if "/lights/" in data and "/state" in data:
            if matchObj:
                self.logger.debug("{} Got PUT request to do something".format(client))
                # reqId is what Alexa passes, the match will include the trailing / for now.
                reqId = matchObj.group(1)
                reqHueNo = matchObj.group(2)
                # Just the content
                # Examples:
                #   Harmony: {"on":true,"bri":254}
                #   Echo: {"on": true}
                self.logger.debug("%s Content data=---\n%s\n---" % (client, data[-contentLength:]))

                parsedContent = json.loads(data[-contentLength:])

                self.logger.debug("%s Parsed Content data=---\n%s\n---" % (client, str(parsedContent)))
                #
                # Update the specified device
                #

                deviceNum = int(reqHueNo) - 1

                self.logger.debug("device number:%d" % deviceNum)
                device = self.devices[deviceNum]

                erg = await device.set(parsedContent)

                await self.send_json(client, json.dumps(erg))

            # All other PUT /api/ send back a blank response
            else:
                await self.send_json(client, "")

        # Requesting the state of just one light
        elif re.match(r'GET /api/.*lights/(\d+) ', data, re.I):
            self.logger.debug("{} Got request for one light".format(client))
            reqHueNo = "1"
            matchObj = re.match(r'GET /api/.*lights/(\d+) ', data, re.I)
            if matchObj:
                reqHueNo = matchObj.group(1)
            device_num = int(reqHueNo) - 1
            device = self.devices[device_num]
            # TODO: Force update of device? dst = device.st()
            OneResp = await self.get_onelight_state_json(device)
            await self.send_json(client, OneResp)

        # Assuming this is a new device registration or config request
        elif "GET /api" in data:
            if "/config" in data:
                self.logger.debug("{} Got request for /config".format(client))
                await self.send_json(client, APICONFIG_JSON)
                self.logger.debug("{} Sent API Config".format(client))
            else:
                newDev = "newdeveloper"
                matchObj = re.match(r'GET /api/(.+) ', data, re.I)
                if matchObj:
                    newDev = matchObj.group(1)
                self.logger.debug("{} Got request for new dev: {}".format(client, newDev))
                json_resp  = """{"lights":{"""
                i = 1
                for device in self.devices:
                    json_resp += """"%d":""" % (i)
                    json_resp += await self.get_onelight_json(device)
                    if i < len(self.devices):
                        json_resp += ","
                    i += 1

                json_resp += """},"schedules":{"1":{"time":"2012-10-29T12:00:00","description":"","name":"schedule","command":{"body":{"on":true,"xy":null,"bri":null,"transitiontime":null},"address":"/api/newdeveloper/groups/0/action","method":"PUT"}}},"config":{"portalservices":false,"gateway":"%s","mac":"%s","swversion":"01005215","linkbutton":false,"ipaddress":"%s:%s","proxyport":0,"swupdate":{"text":"","notify":false,"updatestate":0,"url":""},"netmask":"255.255.255.0","name":"Philips hue","dhcp":true,"proxyaddress":"","whitelist":{"newdeveloper":{"name":"test user","last use date":"2015-02-04T21:35:18","create date":"2012-10-29T12:00:00"}},"UTC":"2012-10-29T12:05:00"},"groups":{"1":{"name":"Group 1","action":{"on":true,"bri":254,"hue":33536,"sat":144,"xy":[0.346,0.3568],"ct":201,"alert":null,"effect":"none","colormode":"xy","reachable":null},"lights":["1","2"]}},"scenes":{}}\n""" % (self.config['GATEWAYIP'], self.config['MACADDRESS'], self.config['IP'], self.config['HTTP_PORT'])
                await self.send_json(client, json_resp)
                self.logger.debug("{} Sent HTTP New Dev Response".format(client))

        # I only saw a POST when registering the username
        elif "POST /api" in data:
            await self.send_json(client, NEWDEVELOPERSYNC_JSON)
            self.logger.debug("{} Sent HTTP New Dev Sync Response".format(client))
        else:
            await self.event_loop.sock_sendall(client, "HTTP/1.1 404 Not Found".encode())

        self.logger.debug("-------------------------------")
        self.logger.debug("    ")

    async def get_onelight_json(self, device):
        # example template values: "on", "[0.0,0.0]", "Hue Lamp 1", "254", "201"
        # on, bri, xy, ct, name
        data = await self.get_json_att(device, "./all.json")
        data["uniqueid"] = self.gen_unique_id()
        return json.dumps(data)
    
    def gen_unique_id(self):
        # gen "00:11:22:33:44:55:66:77-88" like id
        serial = uuid.uuid4().hex[:18]
        return ":".join([serial[i:i+2] for i in range(0, len(serial)-2, 2)]) + "-" + str(serial[-2:])

    async def get_onelight_state_json(self, device):
        # example template values: "on", "[0.0,0.0]", "Hue Lamp 1", "254", "201"
        # on, bri, xy, ct, name
        return json.dumps(await self.get_json_att(device, "./get-state.json"))

    async def get_json_att(self, device, path):
        async with async_open(path, 'r') as f:
            json_resp = json.loads(await f.read())

            json_resp["state"]["on"] = device.on
            json_resp["state"]["hue"] = device.hue
            json_resp["state"]["sat"] = device.sat
            json_resp["state"]["bri"] = device.bri
            json_resp["state"]["ct"] = device.ct
            json_resp["state"]["xy"] = device.xy
            json_resp["state"]["colormode"] = device.colormode
            json_resp["name"] = device.name
            json_resp["swupdate"]["lastinstall"] = datetime.datetime.now().isoformat().split(".")[0]

            return json_resp

    async def send_json(self, client, resp):
        date_str = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
        full_resp = (JSON_HEADERS %(len(resp), date_str, resp)).replace("\n", "\r\n")
        await self.event_loop.sock_sendall(client, full_resp.encode())

    async def stop(self):
        self.logger.debug("Stopping HTTP Server")
        await asyncio.to_thread(self.sock.close)

#
# This is the main object which all other handlers inherit from:


class hue_upnp_super_handler(object):
    def __init__(self, name, id, logger, on=False, bri=1):
        self.name = name
        self.id = id
        self.logger = logger

        self.on = on
        self.bri = bri

        self.get_default()

    # Set default initial values
    # Can be overridden, or used as a super, or just use the defaults.
    def get_default(self):
        self.xy = [0.0, 0.0]
        self.ct = 199
        self.hue = 0
        self.sat = 254
        self.colormode = "ct"

    # Super set method, parses incomming data and runs the appropriate method.
    async def set(self, data):
        results = []
        
        for elm in data:
            match (elm):
                case 'on':
                    self.logger.debug("on received: {}".format(data['on']))
                    if data['on']:
                        ret = await self.set_on()
                    else:
                        ret = await self.set_off()
                case 'bri':
                    self.logger.debug("bri received: {}".format(data['bri']))
                    ret = await self.set_bri(data['bri'])
           
                case 'ct':
                    self.logger.debug("ct received: {}".format(data['ct']))
                    if (ret := await self.set_ct(data['ct'])):
                        self.colormode = "ct"

                case 'xy':
                    self.logger.debug("xy received: {}".format(data['xy']))

                    if (ret := await self.set_xy(data['xy'])):
                        self.colormode = "hs"

                case 'hue':
                    self.logger.debug("hue received: {}".format(data['hue']))

                    if (ret := await self.set_hue(data['hue'])):
                        self.colormode = "hs"

                case 'sat':
                    self.logger.debug("sat received: {}".format(data['sat']))

                    if (ret := await self.set_sat(data['sat'])):
                        self.colormode = "hs"

                case _: # default
                    self.logger.error("ERROR: Unknown command: {}".format(elm))
                    ret = False

            if ret:
                results.append({"success": {f"/lights/{self.id+1}/state/{elm}": data[elm]}})
            else:
                results.append({"error": {"type": 901, "address": f"/lights/{self.id+1}/state/{elm}", "description": "Internal error"}})

        return results

    # Default, should always be overridden
    async def set_on(self):
        self.logger.error("ERROR: Device " + self.name + " does not have an on command?")

    # Default, should always be overridden
    async def set_off(self):
        self.logger.error("ERROR: Device " + self.name +" does not have an off command?")

    # Default, should always be overridden
    async def set_bri(self, value):
        self.logger.error("ERROR: Device " + self.name + " does not have a bri command?")

    # Default, should always be overridden
    async def set_ct(self, value):
        self.logger.error("ERROR: Device " + self.name + " does not have a ct command?")

    # Default, should always be overridden
    async def set_xy(self, value):
        self.logger.error("ERROR: Device " + self.name + " does not have a xy command?")

    # Default, should always be overridden
    async def set_hue(self, value):
        self.logger.error("ERROR: Device " + self.name + " does not have a hue command?")

    # Default, should always be overridden
    async def set_sat(self, value):
        self.logger.error("ERROR: Device " + self.name + " does not have a sat command?")


class Device(hue_upnp_super_handler):
    def __init__(self, name:str, on = False, bri = 1) -> None:
        self.id = None
        self.name = name
        super().__init__(self.name, None, None, on, bri if bri <= 254 and bri >= 1 else 1)

    def init(self, id, logger):
        self.logger = logger
        self.id = id

    async def set_on(self):
        self.logger.debug(f"Device: {self.name} set ON!")

        if await self.on_on() != False:
            self.on = True
            return True
        return False

    async def set_off(self):
        self.logger.debug(f"Device: {self.name} set OFF!")

        if await self.on_off() != False:
            self.on = False
            return True
        return False

    async def set_bri(self, value):
        self.logger.debug(f"Device: {self.name} set BRI {self.bri}!")

        if await self.on_bri(value) != False:
            self.bri = value
            return True
        return False

    async def set_ct(self, value):
        self.logger.debug(f"Device: {self.name} set CT {self.ct}!")

        if await self.on_ct(value) != False:
            self.ct = value
            return True
        return False

    async def set_xy(self, value):
        self.logger.debug(f"Device: {self.name} set XY {self.xy}!")

        if await self.on_xy(value) != False:
            self.xy = value
            return True
        return False

    async def set_hue(self, value):
        self.logger.debug(f"Device: {self.name} set HUE {self.hue}!")

        if await self.on_hue(value) != False:
            self.hue = value
            return True
        return False

    async def set_sat(self, value):
        self.logger.debug(f"Device: {self.name} set SAT {self.sat}!")

        if await self.on_sat(value) != False:
            self.sat = value
            return True
        return False

    async def on_on(self):
        return True

    async def on_off(self):
        return True

    async def on_bri(self, value):
        return True
    
    async def on_ct(self, value):
        return True
    
    async def on_xy(self, value):
        return True

    async def on_hue(self, value):
        return True
    
    async def on_sat(self, value):
        return True
    

class Hub(AbstractAsyncContextManager):
    def __init__(self, debug = False) -> None:
        self.devices = []
        self.config = {}

        self.setup_debug(debug)
        self.gen_config()

    def gen_config(self):
        self.config["GATEWAYIP"] = "1.1.1.1"
        self.config["HTTP_PORT"] = 80 # only port 80 is supported
        self.config["BCAST_IP"] = "239.255.255.250"
        self.config["UPNP_PORT"] = 1900 # type: ignore
        self.config["BROADCAST_INTERVAL"] = 200 # type: ignore

        self.gen_uuids()

    def gen_uuids(self):
        serial = uuid.uuid4().hex[:12].upper()
        
        self.config["MACADDRESS"] = ":".join([serial[i:i+2] for i in range(0, len(serial), 2)])
        self.config["SERIALNO"] = serial

    def get_ip(self):
        try:
            self.logger.debug("Retrieving local IP address...")
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.settimeout(0)
            try:
                s.connect(('10.254.254.254', 1))
                ip = s.getsockname()[0]
            except:
                ip = "127.0.0.1"
            finally:
                s.close()

            self.logger.debug("Local IP address is: {}".format(ip))

            if ip != "127.0.0.1":
                return ip
            else:
                self.logger.exception("No valid IP address found.")
        except socket.error as e:
            self.logger.exception("Failed to retrieve local IP address: {}".format(e))


    def setup_debug(self, debug):
        self.logger = logging.getLogger("AlexaHue")
        if debug is True:
            self.logger.setLevel(logging.DEBUG)
        else:
            self.logger.setLevel(logging.INFO)
        consoleHandlerDebug = logging.StreamHandler(sys.stdout)
        consoleHandlerInfo = logging.StreamHandler(sys.stdout)
        consoleHandlerError = logging.StreamHandler(sys.stdout)
        #change color of debug messages
        consoleHandler = logging.StreamHandler(sys.stdout)
        consoleHandlerDebug.addFilter(lambda record: record.levelno == logging.DEBUG)
        consoleHandlerDebug.setFormatter(logging.Formatter(
            "\033[1;30m%(asctime)s [Alexa Hue Emulation] [%(levelname)-5.5s] %(message)s\033[0m"))
        #change color of info messages
        consoleHandlerInfo.addFilter(lambda record: record.levelno == logging.INFO)
        consoleHandlerInfo.setFormatter(logging.Formatter(
            "\033[1;32m%(asctime)s [Alexa Hue Emulation] [%(levelname)-5.5s] %(message)s\033[0m"))
        #change color of error messages
        consoleHandlerError.addFilter(lambda record: record.levelno == logging.ERROR)
        consoleHandlerError.setFormatter(logging.Formatter(
            "\033[1;31m%(asctime)s [Alexa Hue Emulation] [%(levelname)-5.5s] %(message)s\033[0m"))
        
        consoleHandler.addFilter(lambda record: record.levelno != logging.DEBUG and record.levelno != logging.INFO and record.levelno != logging.ERROR)
        
        self.logger.addHandler(consoleHandlerDebug)
        self.logger.addHandler(consoleHandlerInfo)
        self.logger.addHandler(consoleHandlerError)
        self.logger.addHandler(consoleHandler)

        
    def add(self, *devices: list[Device]):
        for device in devices:
            self.logger.debug('Adding device: ' + device.name)
            device.init(len(self.devices), self.logger)
            self.devices.append(device)

    async def run(self):
        global UPNP_BROADCAST, DESCRIPTION_XML, APICONFIG_JSON
        # same as the MACADDRESS with colons removed
        # Put our info in the responses
        self.config["IP"] = self.get_ip()

        UPNP_BROADCAST = UPNP_BROADCAST.format(self.config['IP'], self.config['HTTP_PORT'], self.config['SERIALNO'])
        DESCRIPTION_XML = DESCRIPTION_XML.format(self.config['IP'], self.config['HTTP_PORT'], self.config['IP'], self.config['SERIALNO'], self.config['SERIALNO'])
        APICONFIG_JSON = APICONFIG_JSON % (self.config['MACADDRESS'])

        self.responder = Responder(self.config, self.logger)
        self.broadcaster = Broadcaster(self.config, self.logger)
        self.httpd = Httpd(self.devices, self.config, self.logger)

        async with asyncio.TaskGroup() as tg:
            tg.create_task(self.responder.run())
            tg.create_task(self.broadcaster.run())
            tg.create_task(self.httpd.run())

    async def stop(self):
        self.logger.debug("Stopping hub...")
        try:
            async with asyncio.TaskGroup() as tg:
                tg.create_task(self.responder.stop())
                tg.create_task(self.broadcaster.stop())
                tg.create_task(self.httpd.stop())
        except Exception as e:
            self.logger.exception("Failed to stop hub: {}".format(e))
        self.logger.debug("Hub stopped.")

    async def __aenter__(self):
        return self
    
    async def __aexit__(self, exc_type, exc, tb):
        await self.stop()
    


async def test():
    async with Hub(True) as hub:
        hub.add(Device('Test Device', True, 254))
        await hub.run()

if "__main__" == __name__:
    asyncio.run(test())
