DEFAULT_ATTEMPTS = 5 DEFAULT_TIMEOUT = 1 #second class Device(object):
    # mac_addr is a string, with the ":" and everything.
    # service is an integer that maps to a service type. See SERVICE_IDS in msgtypes.py
    # source_id is a number unique to this client, will appear in responses to this client
    def __init__(self, mac_addr, ip_addr, service, port, source_id, verbose=False):
        self.verbose = verbose
        self.mac_addr = mac_addr
        self.port = port
        self.service = service
        self.source_id = source_id
        self.ip_addr = ip_addr # IP addresses can change, though...

        # The following attributes can be set by calling refresh(), but that
        # takes time so it is not done by default during initialization.
        # However, refresh() will be called each time __str__ is called.
        # Printing the device is therefore accurate but expensive.

        self.label = None
        self.location = None
        self.group = None
        self.power_level = None
        self.host_firmware_build_timestamp = None
        self.host_firmware_version = None
        self.wifi_firmware_build_timestamp = None
        self.wifi_firmware_version = None
        self.vendor = None
        self.product = None
        self.version = None
        self.product_name = None
        self.product_features = None

        # For completeness, the following are state attributes of the device
        # that become stale too fast to bother caching in the device object,
        # though they can be accessed directly from the real device using the
        # methods below:

        # wifi signal mw
        # wifi tx bytes
        # wifi rx bytes
        # time
        # uptime
        # downtime


    ############################################################################
    #                                                                          #
    #                            Device API Methods                            #
    #                                                                          #
    ############################################################################

    # update the device's (relatively) persistent attributes
    def refresh(self):
        self.label = self.get_label()
        self.location = self.get_location()
        self.group = self.get_group()
        self.power_level = self.get_power()
        self.host_firmware_build_timestamp, self.host_firmware_version = self.get_host_firmware_tuple()
        self.wifi_firmware_build_timestamp, self.wifi_firmware_version = self.get_wifi_firmware_tuple()
        self.vendor, self.product, self.version = self.get_version_tuple()
        self.product_name = self.get_product_name()
        self.product_features = self.get_product_features()

    def get_mac_addr(self):
        return self.mac_addr

    def get_service(self):
        return self.service

    def get_port(self):
        return self.port

    def get_ip_addr(self):
        return self.ip_addr

    def get_source_id(self):
        return self.source_id

    def get_label(self):
        try:
            response = self.req_with_resp(GetLabel, StateLabel)
            self.label = response.label
        except:
            raise
        return self.label

    def get_label(self):
         try:
             response = self.req_with_resp(GetLabel, StateLabel)
         except:
             return self.label
         try:
             self.label = response.label
         except:
            raise
         return self.label

    def get_location(self):
        try:
            response = self.req_with_resp(GetLocation, StateLocation)
            self.location = response.label
        except:
            raise
        return self.location

    def get_group(self):
        try:
            response = self.req_with_resp(GetGroup, StateGroup)
            self.group = response.label
        except:
            raise
        return self.group

    def set_label(self, label):
        if len(label) > 32:
            label = label[:32]
        self.req_with_ack(SetLabel, {"label": label})

    def get_power(self):
        try:
            response = self.req_with_resp(GetPower, StatePower)
            self.power_level = response.power_level
        except:
            raise
        return self.power_level

    def set_power(self, power, rapid=False):
        on = [True, 1, "on"]
        off = [False, 0, "off"]
        if power in on and not rapid:
            success = self.req_with_ack(SetPower, {"power_level": 65535})
        elif power in off and not rapid:
            success = self.req_with_ack(SetPower, {"power_level": 0})
        elif power in on and rapid:
            success = self.fire_and_forget(SetPower, {"power_level": 65535})
        elif power in off and rapid:
            success = self.fire_and_forget(SetPower, {"power_level": 0})

    def get_host_firmware_tuple(self):
        build = None
        version = None
        try:
            response = self.req_with_resp(GetHostFirmware, StateHostFirmware)
            build = response.build
            version = float(str(str(response.version >> 16) + "." + str(response.version & 0xff)))
        except:
            raise
        return build, version

    def get_host_firmware_build_timestamp(self):
        self.host_firmware_build_timestamp, self.host_firmware_version = self.get_host_firmware_tuple()
        return self.host_firmware_build_timestamp

    def get_host_firmware_version(self):
        self.host_firmware_build_timestamp, self.host_firmware_version = self.get_host_firmware_tuple()
        return self.host_firmware_version

    def get_wifi_info_tuple(self):
        signal = None
        tx = None
        rx = None
        try:
            response = self.req_with_resp(GetWifiInfo, StateWifiInfo)
            signal = response.signal
            tx = response.tx
            rx = response.rx
        except:
            raise
        return signal, tx, rx

    def get_wifi_signal_mw(self):
        signal, tx, rx = self.get_wifi_info_tuple()
        return signal

    def get_wifi_tx_bytes(self):
        signal, tx, rx = self.get_wifi_info_tuple()
        return tx

    def get_wifi_rx_bytes(self):
        signal, tx, rx = self.get_wifi_info_tuple()
        return rx

    def get_wifi_firmware_tuple(self):
        build = None
        version = None
        try:
            response = self.req_with_resp(GetWifiFirmware, StateWifiFirmware)
            build = response.build
            version = float(str(str(response.version >> 16) + "." + str(response.version & 0xff)))
        except:
            raise
        return build, version

    def get_wifi_firmware_build_timestamp(self):
        self.wifi_firmware_build_timestamp, self.wifi_firmware_version = self.get_wifi_firmware_tuple()
        return self.wifi_firmware_build_timestamp

    def get_wifi_firmware_version(self):
        self.wifi_firmware_build_timestamp, self.wifi_firmware_version = self.get_wifi_firmware_tuple()
        return self.wifi_firmware_version

    def get_version_tuple(self):
        vendor = None
        product = None
        version = None
        try:
            response = self.req_with_resp(GetVersion, StateVersion)
            vendor = response.vendor
            product = response.product
            version = response.version
        except:
            raise
        return vendor, product, version

    def get_product_name(self):
        product_name = None
        if self.product == None:
            self.vendor, self.product, self.version = self.get_version_tuple()
        if self.product in product_map:
            product_name = product_map[self.product]
        return product_name

    def get_product_features(self):
        product_features = None
        if self.product == None:
            self.vendor, self.product, self.version = self.get_version_tuple()
        if self.product in product_map:
            product_features = features_map[self.product]
        return product_features

    def get_vendor(self):
        self.vendor, self.product, self.version = self.get_version_tuple()
        return self.vendor

    def get_product(self):
        self.vendor, self.product, self.version = self.get_version_tuple()
        return self.product

    def get_version(self):
        self.vendor, self.product, self.version = self.get_version_tuple()
        return self.version

    def get_location_tuple(self):
        label = None
        updated_at = None
        try:
            response = self.req_with_resp(GetLocation, StateLocation)
            self.location = response.location
            label = response.label
            updated_at = response.updated_at
        except:
            raise
        return self.location, label, updated_at

    def get_location_label(self):
        self.location, label, updated_at = self.get_location_tuple()
        return label

    def get_location_updated_at(self):
        self.location, label, updated_at = self.get_location_tuple()
        return updated_at

    def get_group_tuple(self):
        try:
            response = self.req_with_resp(GetGroup, StateGroup)
            self.group = response.group
            label = response.label
            updated_at = response.updated_at
        except:
            raise
        return self.group, label, updated_at

    def get_group_label(self):
        self.group, label, updated_at = self.get_group_tuple()
        return label

    def get_group_updated_at(self):
        self.group, label, updated_at = self.get_group_tuple()
        return updated_at

    def get_info_tuple(self):
        time = None
        uptime = None
        downtime = None
        try:
            response = self.req_with_resp(GetInfo, StateInfo)
            time = response.time
            uptime = response.uptime
            downtime = response.downtime
        except:
            raise
        return time, uptime, downtime

    def get_time(self):
        time, uptime, downtime = self.get_info_tuple()
        return time

    def get_uptime(self):
        time, uptime, downtime = self.get_info_tuple()
        return uptime

    def get_downtime(self):
        time, uptime, downtime = self.get_info_tuple()
        return downtime

    def is_light(self):
        if self.product == None:
            self.vendor, self.product, self.version = self.get_version_tuple()
        return self.product in light_products

    def supports_color(self):
        if self.product_features == None:
            self.product_features = self.get_product_features()
        return self.product_features['color']

    def supports_temperature(self):
        if self.product_features == None:
            self.product_features = self.get_product_features()
        return self.product_features['temperature']

    def supports_multizone(self):
        if self.product_features == None:
            self.product_features = self.get_product_features()
        return self.product_features['multizone']

    def supports_infrared(self):
        if self.product_features == None:
            self.product_features = self.get_product_features()
        return self.product_features['infrared']

    ############################################################################
    #                                                                          #
    #                            String Formatting                             #
    #                                                                          #
    ############################################################################

    def device_characteristics_str(self, indent):
        s = "{}\n".format(self.label)
        s += indent + "MAC Address: {}\n".format(self.mac_addr)
        s += indent + "IP Address: {}\n".format(self.ip_addr)
        s += indent + "Port: {}\n".format(self.port)
        s += indent + "Service: {}\n".format(SERVICE_IDS[self.service])
        s += indent + "Power: {}\n".format(str_map(self.power_level))
        s += indent + "Location: {}\n".format(self.location)
        s += indent + "Group: {}\n".format(self.group)
        return s

    def device_firmware_str(self, indent):
        host_build_ns = self.host_firmware_build_timestamp
        host_build_s = datetime.utcfromtimestamp(host_build_ns/1000000000) if host_build_ns != None else None
        wifi_build_ns = self.wifi_firmware_build_timestamp
        wifi_build_s = datetime.utcfromtimestamp(wifi_build_ns/1000000000) if wifi_build_ns != None else None
        s = "Host Firmware Build Timestamp: {} ({} UTC)\n".format(host_build_ns, host_build_s)
        s += indent + "Host Firmware Build Version: {}\n".format(self.host_firmware_version)
        s += indent + "Wifi Firmware Build Timestamp: {} ({} UTC)\n".format(wifi_build_ns, wifi_build_s)
        s += indent + "Wifi Firmware Build Version: {}\n".format(self.wifi_firmware_version)
        return s

    def device_product_str(self, indent):
        s = "Vendor: {}\n".format(self.vendor)
        s += indent + "Product: {} ({})\n".format(self.product, self.product_name)  #### FIX
        s += indent + "Version: {}\n".format(self.version)
        s += indent + "Features: {}\n".format(self.product_features)
        return s

    def device_time_str(self, indent):
        time, uptime, downtime = self.get_info_tuple()
        time_s = datetime.utcfromtimestamp(time/1000000000) if time != None else None
        uptime_s = round(nanosec_to_hours(uptime), 2) if uptime != None else None
        downtime_s = round(nanosec_to_hours(downtime), 2) if downtime != None else None
        s = "Current Time: {} ({} UTC)\n".format(time, time_s)
        s += indent + "Uptime (ns): {} ({} hours)\n".format(uptime, uptime_s)
        s += indent + "Last Downtime Duration +/-5s (ns): {} ({} hours)\n".format(downtime, downtime_s)
        return s

    def device_radio_str(self, indent):
        signal, tx, rx = self.get_wifi_info_tuple()
        s = "Wifi Signal Strength (mW): {}\n".format(signal)
        s += indent + "Wifi TX (bytes): {}\n".format(tx)
        s += indent + "Wifi RX (bytes): {}\n".format(rx)
        return s

    def __str__(self):
        self.refresh()
        indent = "  "
        s = self.device_characteristics_str(indent)
        s += indent + self.device_firmware_str(indent)
        s += indent + self.device_product_str(indent)
        s += indent + self.device_time_str(indent)
        s += indent + self.device_radio_str(indent)
        return s

    ############################################################################
    #                                                                          #
    #                            Workflow Methods                              #
    #                                                                          #
    ############################################################################

    # Don't wait for Acks or Responses, just send the same message repeatedly as fast as possible
    def fire_and_forget(self, msg_type, payload={}, timeout_secs=DEFAULT_TIMEOUT, num_repeats=DEFAULT_ATTEMPTS):
        self.initialize_socket(timeout_secs)
        msg = msg_type(self.mac_addr, self.source_id, seq_num=0, payload=payload, ack_requested=False, response_requested=False)
        sent_msg_count = 0
        sleep_interval = 0.05 if num_repeats > 20 else 0
        while(sent_msg_count < num_repeats):
            if self.ip_addr:
                self.sock.sendto(msg.packed_message, (self.ip_addr, self.port))
            else:
                for ip_addr in UDP_BROADCAST_IP_ADDRS:
                    self.sock.sendto(msg.packed_message, (ip_addr, self.port))
            if self.verbose:
                print("SEND: " + str(msg))
            sent_msg_count += 1
            sleep(sleep_interval) # Max num of messages device can handle is 20 per second.
        self.close_socket()

    # Usually used for Set messages
    def req_with_ack(self, msg_type, payload, timeout_secs=DEFAULT_TIMEOUT, max_attempts=DEFAULT_ATTEMPTS):
        self.req_with_resp(msg_type, Acknowledgement, payload, timeout_secs, max_attempts)

    # Usually used for Get messages, or for state confirmation after Set (hence the optional payload)
    def req_with_resp(self, msg_type, response_type, payload={}, timeout_secs=DEFAULT_TIMEOUT, max_attempts=DEFAULT_ATTEMPTS):
        # Need to put error checking here for aguments
        if type(response_type) != type([]):
            response_type = [response_type]
        success = False
        device_response = None
        self.initialize_socket(timeout_secs)
        if len(response_type) == 1 and Acknowledgement in response_type:
            msg = msg_type(self.mac_addr, self.source_id, seq_num=0, payload=payload, ack_requested=True, response_requested=False)
        else:
            msg = msg_type(self.mac_addr, self.source_id, seq_num=0, payload=payload, ack_requested=False, response_requested=True)
        response_seen = False
        attempts = 0
        while not response_seen and attempts < max_attempts:
            sent = False
            start_time = time()
            timedout = False
            while not response_seen and not timedout:
                if not sent:
                    if self.ip_addr:
                        self.sock.sendto(msg.packed_message, (self.ip_addr, self.port))
                    else:
                        for ip_addr in UDP_BROADCAST_IP_ADDRS:
                            self.sock.sendto(msg.packed_message, (ip_addr, self.port))
                    sent = True
                    if self.verbose:
                        print("SEND: " + str(msg))
                try:
                    data, (ip_addr, port) = self.sock.recvfrom(1024)
                    response = unpack_lifx_message(data)
                    if self.verbose:
                        print("RECV: " + str(response))
                    if type(response) in response_type:
                        if response.source_id == self.source_id and response.target_addr == self.mac_addr:
                            response_seen = True
                            device_response = response
                            self.ip_addr = ip_addr
                            success = True
                except timeout:
                    pass
                elapsed_time = time() - start_time
                timedout = True if elapsed_time > timeout_secs else False
            attempts += 1
        if not success:
            self.close_socket()
            raise WorkflowException("WorkflowException: Did not receive {} from {} (Name: {}) in response to {}".format(str(response_type), str(self.mac_addr), str(self.label), str(msg_type)))
        else:
            self.close_socket()
        return device_response

    # Not currently implemented, although the LIFX LAN protocol supports this kind of workflow natively
    def req_with_ack_resp(self, msg_type, response_type, payload, timeout_secs=DEFAULT_TIMEOUT, max_attempts=DEFAULT_ATTEMPTS):
        pass

    ############################################################################
    #                                                                          #
    #                              Socket Methods                              #
    #                                                                          #
    ############################################################################

    def initialize_socket(self, timeout):
        self.sock = socket(AF_INET, SOCK_DGRAM)
        self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        self.sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
        self.sock.settimeout(timeout)
        try:
            self.sock.bind(("", 0))  # allow OS to assign next available source port
        except Exception as err:
            raise WorkflowException("WorkflowException: error {} while trying to open socket".format(str(err)))

    def close_socket(self):
        self.sock.close() UDP_BROADCAST_IP_ADDRS = get_broadcast_addrs() UDP_BROADCAST_PORT = 56700 class InvalidParameterException(Exception):
    def __init__(self, message):
        super(InvalidParameterException, self).__init__(message) class WorkflowException(Exception):
    def __init__(self, message):
        super(WorkflowException, self).__init__(message) class Light(Device):
    def __init__(self, mac_addr, ip_addr, service=1, port=56700, source_id=os.getpid(), verbose=False):
        mac_addr = mac_addr.lower()
        super(Light, self).__init__(mac_addr, ip_addr, service, port, source_id, verbose)
        self.color = None
        self.infrared_brightness = None

    ############################################################################
    #                                                                          #
    #                            Light API Methods                             #
    #                                                                          #
    ############################################################################

    # GetPower - power level
    def get_power(self):
        try:
            response = self.req_with_resp(LightGetPower, LightStatePower)
            self.power_level = response.power_level
        except WorkflowException as e:
            raise
        return self.power_level

    def set_power(self, power, duration=0, rapid=False):
        on = [True, 1, "on", 65535]
        off = [False, 0, "off"]
        try:
            if power in on and not rapid:
                self.req_with_ack(LightSetPower, {"power_level": 65535, "duration": duration})
            elif power in on and rapid:
                self.fire_and_forget(LightSetPower, {"power_level": 65535, "duration": duration}, num_repeats=5)
            elif power in off and not rapid:
                self.req_with_ack(LightSetPower, {"power_level": 0, "duration": duration})
            elif power in off and rapid:
                self.fire_and_forget(LightSetPower, {"power_level": 0, "duration": duration}, num_repeats=5)
            else:
                raise InvalidParameterException("{} is not a valid power level.".format(power))
        except WorkflowException as e:
            raise

    # color is [Hue, Saturation, Brightness, Kelvin]
    def set_waveform(self, is_transient, color, period, cycles, duty_cycle, waveform, rapid=False):
        if len(color) == 4:
            try:
                if rapid:
                    self.fire_and_forget(LightSetWaveform, {"transient": is_transient, "color": color, "period": period, "cycles": cycles, "duty_cycle": duty_cycle, "waveform": waveform}, num_repeats=5)
                else:
                    self.req_with_ack(LightSetWaveform, {"transient": is_transient, "color": color, "period": period, "cycles": cycles, "duty_cycle": duty_cycle, "waveform": waveform})
            except WorkflowException as e:
                raise

    # color is [Hue, Saturation, Brightness, Kelvin], duration in ms
    def set_color(self, color, duration=0, rapid=False):
        if len(color) == 4:
            try:
                if rapid:
                    self.fire_and_forget(LightSetColor, {"color": color, "duration": duration}, num_repeats=5)
                else:
                    self.req_with_ack(LightSetColor, {"color": color, "duration": duration})
            except WorkflowException as e:
                raise

    def get_color(self):
        try:
            response = self.req_with_resp(LightGet, LightState)
            self.color = response.color
            self.power_level = response.power_level
            self.label = response.label
        except WorkflowException as e:
            raise
        return self.color

    # hue in range [0 - 65535]
    def set_hue(self, hue, duration=0, rapid=False):
        """ hue to set
            duration in ms"""
        color = self.get_color()
        color2 = (hue, color[1], color[2], color[3])
        try:
            if rapid:
                self.fire_and_forget(LightSetColor, {"color": color2, "duration": duration}, num_repeats=5)
            else:
                self.req_with_ack(LightSetColor, {"color": color2, "duration": duration})
        except WorkflowException as e:
            raise

    # saturation in range [0 - 65535]
    def set_saturation(self, saturation, duration=0, rapid=False):
        """ saturation to set
            duration in ms"""
        color = self.get_color()
        color2 = (color[0], saturation, color[2], color[3])
        try:
            if rapid:
                self.fire_and_forget(LightSetColor, {"color": color2, "duration": duration}, num_repeats=5)
            else:
                self.req_with_ack(LightSetColor, {"color": color2, "duration": duration})
        except WorkflowException as e:
            raise

    # brightness in range [0 - 65535]
    def set_brightness(self, brightness, duration=0, rapid=False):
        """ brightness to set
            duration in ms"""
        color = self.get_color()
        color2 = (color[0], color[1], brightness, color[3])
        try:
            if rapid:
                self.fire_and_forget(LightSetColor, {"color": color2, "duration": duration}, num_repeats=5)
            else:
                self.req_with_ack(LightSetColor, {"color": color2, "duration": duration})
        except WorkflowException as e:
            raise

    # kelvin in range [2500 - 9000]
    def set_colortemp(self, kelvin, duration=0, rapid=False):
        """ kelvin: color temperature to set
            duration in ms"""
        color = self.get_color()
        color2 = (color[0], color[1], color[2], kelvin)
        try:
            if rapid:
                self.fire_and_forget(LightSetColor, {"color": color2, "duration": duration}, num_repeats=5)
            else:
                self.req_with_ack(LightSetColor, {"color": color2, "duration": duration})
        except WorkflowException as e:
            raise

    # Infrared get maximum brightness, infrared_brightness
    def get_infrared(self):
        if self.supports_infrared():
            try:
                response = self.req_with_resp(LightGetInfrared, LightStateInfrared)
                self.infrared_brightness = response.infrared_brightness
            except WorkflowException as e:
                raise
        return self.infrared_brightness

    # Infrared set maximum brightness, infrared_brightness
    def set_infrared(self, infrared_brightness, rapid=False):
        try:
            if rapid:
                self.fire_and_forget(LightSetInfrared, {"infrared_brightness": infrared_brightness}, num_repeats=5)
            else:
                self.req_with_ack(LightSetInfrared, {"infrared_brightness": infrared_brightness})
        except WorkflowException as e:
            raise

    ############################################################################
    #                                                                          #
    #                            String Formatting                             #
    #                                                                          #
    ############################################################################

    def __str__(self):
        self.refresh()
        indent = "  "
        s = self.device_characteristics_str(indent)
        s += indent + "Color (HSBK): {}\n".format(self.get_color())
        s += indent + self.device_firmware_str(indent)
        s += indent + self.device_product_str(indent)
        s += indent + self.device_time_str(indent)
        s += indent + self.device_radio_str(indent)
        return s BROADCAST_MAC = "00:00:00:00:00:00" class Acknowledgement(Message):
    def __init__(self, target_addr, source_id, seq_num, payload={}, ack_requested=False, response_requested=False):
        super(Acknowledgement, self).__init__(MSG_IDS[Acknowledgement], target_addr, source_id, seq_num, ack_requested, response_requested) class GetService(Message):
    def __init__(self, target_addr, source_id, seq_num, payload={}, ack_requested=False, response_requested=False):
        target_addr = BROADCAST_MAC
        super(GetService, self).__init__(MSG_IDS[GetService], target_addr, source_id, seq_num, ack_requested, response_requested) class LightGet(Message):
    def __init__(self, target_addr, source_id, seq_num, payload={}, ack_requested=False, response_requested=False):
        super(LightGet, self).__init__(MSG_IDS[LightGet], target_addr, source_id, seq_num, ack_requested, response_requested) class LightGetPower(Message):
    def __init__(self, target_addr, source_id, seq_num, payload={}, ack_requested=False, response_requested=False):
        super(LightGetPower, self).__init__(MSG_IDS[LightGetPower], target_addr, source_id, seq_num, ack_requested, response_requested) class LightSetColor(Message):
    def __init__(self, target_addr, source_id, seq_num, payload, ack_requested=False, response_requested=False):
        self.color = payload["color"]
        self.duration = payload["duration"]
        super(LightSetColor, self).__init__(MSG_IDS[LightSetColor], target_addr, source_id, seq_num, ack_requested, response_requested)

    def get_payload(self):
        self.payload_fields.append(("Color", self.color))
        self.payload_fields.append(("Duration", self.duration))
        reserved_8 = little_endian(bitstring.pack("8", self.reserved))
        color = b"".join(little_endian(bitstring.pack("16", field)) for field in self.color)
        duration = little_endian(bitstring.pack("32", self.duration))
        payload = reserved_8 + color + duration
        return payload class LightSetPower(Message):
    def __init__(self, target_addr, source_id, seq_num, payload, ack_requested=False, response_requested=False):
        self.power_level = payload["power_level"]
        self.duration = payload["duration"]
        super(LightSetPower, self).__init__(MSG_IDS[LightSetPower], target_addr, source_id, seq_num, ack_requested, response_requested)

    def get_payload(self):
        self.payload_fields.append(("Power Level", self.power_level))
        self.payload_fields.append(("Duration", self.duration))
        power_level = little_endian(bitstring.pack("16", self.power_level))
        duration = little_endian(bitstring.pack("32", self.duration))
        payload = power_level + duration
        return payload class LightSetWaveform(Message):
    def __init__(self, target_addr, source_id, seq_num, payload, ack_requested=False, response_requested=False):
        self.transient = payload["transient"]
        self.color = payload["color"]
        self.period = payload["period"]
        self.cycles = payload["cycles"]
        self.duty_cycle = payload["duty_cycle"]
        self.waveform = payload["waveform"]
        super(LightSetWaveform, self).__init__(MSG_IDS[LightSetWaveform], target_addr, source_id, seq_num, ack_requested, response_requested)

    def get_payload(self):
        self.payload_fields.append(("Is Transient", self.transient))
        self.payload_fields.append(("Color", self.color))
        self.payload_fields.append(("Period", self.period))
        self.payload_fields.append(("Cycles", self.cycles))
        self.payload_fields.append(("Duty Cycle", self.duty_cycle))
        self.payload_fields.append(("Waveform", self.waveform))
        reserved_8 = little_endian(bitstring.pack("8", self.reserved))
        transient = little_endian(bitstring.pack("uint:8", self.transient))
        color = b"".join(little_endian(bitstring.pack("16", field)) for field in self.color)
        period = little_endian(bitstring.pack("uint:32", self.period))
        cycles = little_endian(bitstring.pack("float:32", self.cycles))
        duty_cycle = little_endian(bitstring.pack("int:16", self.duty_cycle))
        waveform = little_endian(bitstring.pack("uint:8", self.waveform))
        payload = reserved_8 + transient + color + period + cycles + duty_cycle + waveform
        return payload class LightState(Message):
    def __init__(self, target_addr, source_id, seq_num, payload, ack_requested=False, response_requested=False):
        self.color = payload["color"]
        self.reserved1 = payload["reserved1"]
        self.power_level = payload["power_level"]
        self.label = payload["label"]
        self.reserved2 = payload["reserved2"]
        super(LightState, self).__init__(MSG_IDS[LightState], target_addr, source_id, seq_num, ack_requested, response_requested)

    def get_payload(self):
        self.payload_fields.append(("Color (HSBK)", self.color))
        self.payload_fields.append(("Reserved", self.reserved1))
        self.payload_fields.append(("Power Level", self.power_level))
        self.payload_fields.append(("Label", self.label))
        self.payload_fields.append(("Reserved", self.reserved2))
        color = b"".join(little_endian(bitstring.pack("16", field)) for field in self.color)
        reserved1 = little_endian(bitstring.pack("16", self.reserved1))
        power_level = little_endian(bitstring.pack("16", self.power_level))
        label = b"".join(little_endian(bitstring.pack("8", ord(c))) for c in self.label)
        label_padding = b"".join(little_endian(bitstring.pack("8", 0)) for i in range(32-len(self.label)))
        label += label_padding
        reserved2 = little_endian(bitstring.pack("64", self.reserved1))
        payload = color + reserved1 + power_level + label + reserved2
        return payload class LightStatePower(Message):
    def __init__(self, target_addr, source_id, seq_num, payload, ack_requested=False, response_requested=False):
        self.power_level = payload["power_level"]
        super(LightStatePower, self).__init__(MSG_IDS[LightStatePower], target_addr, source_id, seq_num, ack_requested, response_requested)

    def get_payload(self):
        self.payload_fields.append(("Power Level", self.power_level))
        power_level = little_endian(bitstring.pack("16", self.power_level))
        payload = power_level
        return payload class StateService(Message):
    def __init__(self, target_addr, source_id, seq_num, payload, ack_requested=False, response_requested=False):
        self.service = payload["service"]
        self.port = payload["port"]
        super(StateService, self).__init__(MSG_IDS[StateService], target_addr, source_id, seq_num, ack_requested, response_requested)

    def get_payload(self):
        self.payload_fields.append(("Service", self.service))
        self.payload_fields.append(("Port", self.port))
        service = little_endian(bitstring.pack("8", self.service))
        port = little_endian(bitstring.pack("32", self.port))
        payload = service + port
        return payload class MultiZoneLight(Light):
    def __init__(self, mac_addr, ip_addr, service=1, port=56700, source_id=os.getpid(), verbose=False):
        super(MultiZoneLight, self).__init__(mac_addr, ip_addr, service, port, source_id, verbose)

    # 0 indexed, NOT inclusive, works like python list indices
    def get_color_zones(self, start=None, end=None):
        response = self.req_with_resp(MultiZoneGetColorZones, [MultiZoneStateZone, MultiZoneStateMultiZone], {"start_index":0, "end_index":255})
        total_zones = response.count
        # validate indices
        if start != None and end != None:
            # automatically truncate if the end is too large
            if end > total_zones:
                end = total_zones
            if start >= total_zones:
                raise ValueError("In the function get_color_zones, starting index is greater than the total available zones (provided start = {}, end = {} for a device with {} total zones).".format(start, end, total_zones))
            if end <= start:
                raise ValueError("In the function get_color_zones, end must be greater than start (provided start = {}, end = {}).".format(start, end, total_zones))
        if (start != None and end == None) or (start == None and end != None):
            raise ValueError("In the function get_color_zones, start and end indices must both be provided, or neither provided.")

        # get all zones
        all_zones = []
        for i in range(total_zones):
            all_zones.append(None)
        for i in range(int(math.ceil(total_zones/8.0))):
            response = self.req_with_resp(MultiZoneGetColorZones, [MultiZoneStateZone, MultiZoneStateMultiZone], {"start_index":0+(i*8), "end_index":7+(i*8)})
            first_included_zone = response.index
            if first_included_zone + 8 > total_zones:
                last_included_zone = total_zones-1
                last_index = last_included_zone - first_included_zone
            else:
                last_included_zone = first_included_zone + 7
                last_index = 8
            all_zones[first_included_zone:last_included_zone+1] = response.color[:last_index+1]
        self.color = all_zones

        if start != None and end != None:
            self.color = all_zones[start:end]

        return self.color

    def set_zone_color(self, start_index, end_index, color, duration=0, rapid=False, apply=1):
        if len(color) == 4:
            try:
                if rapid:
                    self.fire_and_forget(MultiZoneSetColorZones,
                                         {"start_index": start_index, "end_index": end_index, "color": color,
                                          "duration": duration, "apply": apply}, num_repeats=1)
                else:
                    self.req_with_ack(MultiZoneSetColorZones,
                                      {"start_index": start_index, "end_index": end_index, "color": color,
                                       "duration": duration, "apply": apply})
            except WorkflowException as e:
                raise

    # Sets colors for all zones given a list of HSVK colors
    def set_zone_colors(self, colors, duration=0, rapid=False):
        for (i, color) in enumerate(colors):
            apply = 0
            if i == len(colors)-1:
                apply = 1
            self.set_zone_color(i, i+1, color, duration, rapid, apply) def unpack_lifx_message(packed_message):
    header_str = packed_message[0:HEADER_SIZE_BYTES]
    payload_str = packed_message[HEADER_SIZE_BYTES:]

    size = struct.unpack("H", header_str[0:2])[0]
    flags = struct.unpack("H", header_str[2:4])[0]
    origin = (flags >> 14) & 3
    tagged = (flags >> 13) & 1
    addressable = (flags >> 12) & 1
    protocol = flags & 4095
    source_id = struct.unpack("I", header_str[4:8])[0]
    target_addr = ":".join([('%02x' % b) for b in struct.unpack("B"*6, header_str[8:14])])
    response_flags = struct.unpack("B", header_str[22:23])[0]
    ack_requested = response_flags & 2
    response_requested = response_flags & 1
    seq_num = struct.unpack("B", header_str[23:24])[0]
    message_type = struct.unpack("H", header_str[32:34])[0]

    message = None
    if message_type == MSG_IDS[GetService]:
        message = GetService(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateService]:
        service = struct.unpack("B", payload_str[0:1])[0]
        port = struct.unpack("I", payload_str[1:5])[0]
        payload = {"service": service, "port": port}
        message = StateService(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetHostInfo]:
        message = GetHostInfo(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateHostInfo]:
        signal = struct.unpack("f", payload_str[0:4])[0]
        tx = struct.unpack("I", payload_str[4:8])[0]
        rx = struct.unpack("I", payload_str[8:12])[0]
        reserved1 = struct.unpack("h", payload_str[12:14])[0]
        payload = {"signal": signal, "tx": tx, "rx": rx, "reserved1": reserved1}
        message = StateHostInfo(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetHostFirmware]:
        message = GetHostFirmware(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateHostFirmware]:
        build = struct.unpack("Q", payload_str[0:8])[0]
        reserved1 = struct.unpack("Q", payload_str[8:16])[0]
        version = struct.unpack("I", payload_str[16:20])[0]
        payload = {"build": build, "reserved1": reserved1, "version": version}
        message = StateHostFirmware(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetWifiInfo]:
        message = GetWifiInfo(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateWifiInfo]:
        signal = struct.unpack("f", payload_str[0:4])[0]
        tx = struct.unpack("I", payload_str[4:8])[0]
        rx = struct.unpack("I", payload_str[8:12])[0]
        reserved1 = struct.unpack("h", payload_str[12:14])[0]
        payload = {"signal": signal, "tx": tx, "rx": rx, "reserved1": reserved1}
        message = StateWifiInfo(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetWifiFirmware]:
        message = GetWifiFirmware(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateWifiFirmware]:
        build = struct.unpack("Q", payload_str[0:8])[0]
        reserved1 = struct.unpack("Q", payload_str[8:16])[0]
        version = struct.unpack("I", payload_str[16:20])[0]
        payload = {"build": build, "reserved1": reserved1, "version": version}
        message = StateWifiFirmware(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetPower]:
        message = GetPower(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[SetPower]:
        power_level = struct.unpack("H", payload_str[0:2])[0]
        payload = {"power_level": power_level}
        message = SetPower(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[StatePower]:
        power_level = struct.unpack("H", payload_str[0:2])[0]
        payload = {"power_level": power_level}
        message = StatePower(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetLabel]:
        message = GetLabel(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[SetLabel]:
        label = binascii.unhexlify("".join(["%2.2x" % (b & 0x000000ff) for b in struct.unpack("b"*32, payload_str[0:32])])).replace(b'\x00', b'')
        label = label.decode('utf-8')
        payload = {"label": label}
        message = SetLabel(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateLabel]:
        label = binascii.unhexlify("".join(["%2.2x" % (b & 0x000000ff) for b in struct.unpack("b"*32, payload_str[0:32])])).replace(b'\x00', b'')
        label = label.decode('utf-8')
        payload = {"label": label}
        message = StateLabel(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetLocation]:
        message = GetLocation(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateLocation]:
        location = [b for b in struct.unpack("B"*16, payload_str[0:16])]
        label = binascii.unhexlify("".join(["%2.2x" % (b & 0x000000ff) for b in struct.unpack("b"*32, payload_str[16:48])])).replace(b'\x00', b'')
        label = label.decode('utf-8')
        updated_at = struct.unpack("Q", payload_str[48:56])[0]
        payload = {"location": location, "label": label, "updated_at": updated_at}
        message = StateLocation(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetGroup]:
        message = GetGroup(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateGroup]:
        group = [b for b in struct.unpack("B"*16, payload_str[0:16])]
        label = binascii.unhexlify("".join(["%2.2x" % (b & 0x000000ff) for b in struct.unpack("b"*32, payload_str[16:48])])).replace(b'\x00', b'')
        label = label.decode('utf-8')
        updated_at = struct.unpack("Q", payload_str[48:56])[0]
        payload = {"group": group, "label": label, "updated_at": updated_at}
        message = StateGroup(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetVersion]:
        message = GetVersion(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateVersion]:
        vendor = struct.unpack("I", payload_str[0:4])[0]
        product = struct.unpack("I", payload_str[4:8])[0]
        version = struct.unpack("I", payload_str[8:12])[0]
        payload = {"vendor": vendor, "product": product, "version": version}
        message = StateVersion(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[GetInfo]:
        message = GetInfo(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[StateInfo]:
        time = struct.unpack("Q", payload_str[0:8])[0]
        uptime = struct.unpack("Q", payload_str[8:16])[0]
        downtime = struct.unpack("Q", payload_str[16:24])[0]
        payload = {"time": time, "uptime": uptime, "downtime": downtime}
        message = StateInfo(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[Acknowledgement]:
        message = Acknowledgement(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[EchoRequest]:
        byte_array_len = len(payload_str)
        byte_array = [b for b in struct.unpack("B"*byte_array_len, payload_str[0:byte_array_len])]
        payload = {"byte_array": byte_array}
        message = EchoRequest(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[EchoResponse]:
        byte_array_len = len(payload_str)
        byte_array = [b for b in struct.unpack("B"*byte_array_len, payload_str[0:byte_array_len])]
        payload = {"byte_array": byte_array}
        message = EchoResponse(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[LightGet]:
        message = LightGet(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[LightSetColor]:
        color = struct.unpack("H"*4, payload_str[0:8])
        duration = struct.unpack("I", payload_str[8:12])[0]
        payload = {"color": color, "duration": duration}
        message = LightSetColor(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[LightState]:
        color = struct.unpack("H"*4, payload_str[0:8])
        reserved1 = struct.unpack("H", payload_str[8:10])[0]
        power_level = struct.unpack("H", payload_str[10:12])[0]
        label = binascii.unhexlify("".join(["%2.2x" % (b & 0x000000ff) for b in struct.unpack("b"*32, payload_str[12:44])])).replace(b'\x00', b'')
        label = label.decode('utf-8')
        reserved2 = struct.unpack("Q", payload_str[44:52])[0]
        payload = {"color": color, "reserved1": reserved1, "power_level": power_level, "label": label, "reserved2": reserved2}
        message = LightState(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[LightGetPower]:
        message = LightGetPower(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[LightSetPower]:
        power_level = struct.unpack("H", payload_str[0:2])[0]
        duration = struct.unpack("I", payload_str[2:6])[0]
        payload = {"power_level": power_level, "duration": duration}
        message = LightSetPower(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[LightStatePower]:
        power_level = struct.unpack("H", payload_str[0:2])[0]
        payload = {"power_level": power_level}
        message = LightStatePower(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[LightGetInfrared]:  # 120
        message = LightGetInfrared(target_addr, source_id, seq_num, {}, ack_requested, response_requested)

    elif message_type == MSG_IDS[LightStateInfrared]:  # 121
        infrared_brightness = struct.unpack("H", payload_str[0:2])[0]
        payload = {"infrared_brightness": infrared_brightness}
        message = LightStateInfrared(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[LightSetInfrared]:  # 122
        infrared_brightness = struct.unpack("H", payload_str[0:2])[0]
        payload = {"infrared_brightness": infrared_brightness}
        message = LightSetInfrared(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[MultiZoneSetColorZones]: #501
        start_index = struct.unpack("c", payload_str[0:1])[0]
        start_index = ord(start_index) # 8 bit
        end_index = struct.unpack("c", payload_str[1:2])[0]
        end_index = ord(end_index) #8 bit
        color = struct.unpack("H" * 4, payload_str[2:10])
        duration = struct.unpack("I", payload_str[10:14])[0]
        apply = struct.unpack("c", payload_str[14:15])[0]
        apply = ord(apply) #8 bit
        payload = {"start_index": start_index, "end_index": end_index, "color": color, "duration": duration, "apply": apply}
        message = MultiZoneSetColorZones(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[MultiZoneGetColorZones]: #502
        start_index = struct.unpack("c", payload_str[0:1])[0]
        start_index = ord(start_index) # 8 bit
        end_index = struct.unpack("c", payload_str[1:2])[0]
        end_index = ord(end_index) #8 bit
        payload = {"start_index": start_index, "end_index": end_index}
        message = MultiZoneGetColorZones(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[MultiZoneStateZone]: #503
        count = struct.unpack("c", payload_str[0:1])[0]
        count = ord(count) # 8 bit
        index = struct.unpack("c", payload_str[1:2])[0]
        index = ord(index) #8 bit
        color = struct.unpack("H" * 4, payload_str[2:10])
        payload = {"count": count, "index": index, "color": color}
        message = MultiZoneStateZone(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    elif message_type == MSG_IDS[MultiZoneStateMultiZone]: #506
        count = struct.unpack("c", payload_str[0:1])[0]
        count = ord(count) # 8 bit
        index = struct.unpack("c", payload_str[1:2])[0]
        index = ord(index) #8 bit
        colors = []
        for i in range(8):
            color = struct.unpack("H" * 4, payload_str[2+(i*8):10+(i*8)])
            colors.append(color)
        payload = {"count": count, "index": index, "color": colors}
        message = MultiZoneStateMultiZone(target_addr, source_id, seq_num, payload, ack_requested, response_requested)

    else:
        message = Message(message_type, target_addr, source_id, seq_num, ack_requested, response_requested)

    message.size = size
    message.origin = origin
    message.tagged = tagged
    message.addressable = addressable
    message.protocol = protocol
    message.source_id = source_id
    message.header = header_str
    message.payload = payload_str
    message.packed_message = packed_message

    return message class Group(object):

    def __init__(self, devices=[], verbose=False):
        self.devices = devices
        self.verbose = verbose

    def add_device(self, device_object):
        self.devices.append(device_object)

    def remove_device(self, device_object):
        new_devices = []
        for d in self.devices:
            if d != device_object:
                new_devices.append(d)
        self.devices = new_devices

    def remove_device_by_name(self, device_name):
        new_devices = []
        for d in self.devices:
            if d.get_name() != device_name:
                new_devices.append(d)
        self.devices = new_devices

    def get_device_list(self):
        return self.devices

    def set_power(self, power, duration=0, rapid=False):
        threads = []
        for d in self.devices:
            t = Thread(target = self.set_power_helper, args = (d, power, duration, rapid))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    def set_power_helper(self, device, power, duration, rapid):
        if device.is_light():
            device.set_power(power, duration, rapid) # Light::set_power(power, [duration], [rapid])
        else:
            device.set_power(power, rapid) # Device::set_power(power, [rapid])

    def set_color(self, color, duration=0, rapid=False):
        # pre-calculate which devices you'll operate on
        # it'll make the color change look more simultaneous
        color_supporting_devices = []
        for d in self.devices:
            if d.supports_color:
                color_supporting_devices.append(d)
        # multi-threaded color change
        threads = []
        for d in color_supporting_devices:
            t = Thread(target = d.set_color, args = (color, duration, rapid))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    # Hue, saturation, brightness, and colortemp are a little different than the
    # other functions. You can't just spawn a new thread with "set_saturation"
    # or whatever for each device, because each command will first request the
    # current color from the bulbs, which will take different amounts of time to
    # receive, which makes the color change take different amounts for each
    # bulb. So basically you gotta get all the colors up front and then make
    # a set_color() call for each bulb.

    def set_hue(self, hue, duration=0, rapid=False):
        # pre-calculate which devices to operate on
        color_supporting_devices = []
        for d in self.devices:
            if d.supports_color:
                color_supporting_devices.append(d)
        # get colors
        colors = []
        for d in color_supporting_devices:
            colors.append(d.get_color())
        # "simultaneous" change
        threads = []
        for (i, d) in enumerate(color_supporting_devices):
            _, saturation, brightness, kelvin = colors[i]
            color = [hue, saturation, brightness, kelvin]
            t = Thread(target = d.set_color, args = (color, duration, rapid))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    def set_brightness(self, brightness, duration=0, rapid=False):
        # pre-calculate which devices to operate on
        color_supporting_devices = []
        for d in self.devices:
            if d.supports_color:
                color_supporting_devices.append(d)
        # get colors
        colors = []
        for d in color_supporting_devices:
            colors.append(d.get_color())
        # "simultaneous" change
        threads = []
        for (i, d) in enumerate(color_supporting_devices):
            hue, saturation, _, kelvin = colors[i]
            color = [hue, saturation, brightness, kelvin]
            t = Thread(target = d.set_color, args = (color, duration, rapid))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    def set_saturation(self, saturation, duration=0, rapid=False):
        # pre-calculate which devices to operate on
        color_supporting_devices = []
        for d in self.devices:
            if d.supports_color:
                color_supporting_devices.append(d)
        # get colors
        colors = []
        for d in color_supporting_devices:
            colors.append(d.get_color())
        # "simultaneous" change
        threads = []
        for (i, d) in enumerate(color_supporting_devices):
            hue, _, brightness, kelvin = colors[i]
            color = [hue, saturation, brightness, kelvin]
            t = Thread(target = d.set_color, args = (color, duration, rapid))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    def set_colortemp(self, kelvin, duration=0, rapid=False):
        # pre-calculate which devices to operate on
        color_supporting_devices = []
        for d in self.devices:
            if d.supports_color:
                color_supporting_devices.append(d)
        # get colors
        colors = []
        for d in color_supporting_devices:
            colors.append(d.get_color())
        # "simultaneous" change
        threads = []
        for (i, d) in enumerate(color_supporting_devices):
            hue, saturation, brightness, _ = colors[i]
            color = [hue, saturation, brightness, kelvin]
            t = Thread(target = d.set_color, args = (color, duration, rapid))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    def set_infrared(self, infrared_brightness):
        # pre-calculate which devices to operate on
        infrared_supporting_devices = []
        for d in self.devices:
            if d.supports_infrared():
                infrared_supporting_devices.append(d)
        # "simultaneous" change
        threads = []
        for d in infrared_supporting_devices:
            t = Thread(target = d.set_infrared, args = (infrared_brightness))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    def set_zone_color(self, start, end, color, duration=0, rapid=False, apply=1):
        # pre-calculate which devices to operate on
        multizone_devices = []
        for d in self.devices:
            if d.supports_multizone():
                multizone_devices.append(d)
        # "simultaneous" change
        threads = []
        for d in multizone_devices:
            t = Thread(target = d.set_zone_color, args = (start, end, color, duration, rapid, apply))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    def set_zone_colors(self, colors, duration=0, rapid=False):
        # pre-calculate which devices to operate on
        multizone_devices = []
        for d in self.devices:
            if d.supports_multizone():
                multizone_devices.append(d)
        # "simultaneous" change
        threads = []
        for d in multizone_devices:
            t = Thread(target = d.set_zone_colors, args = (colors, duration, rapid, apply))
            threads.append(t)
            t.start()
        for t in threads:
            t.join()

    def __str__(self):
        s = "Group ({}):\n\n".format(len(self.devices))
        for d in self.devices:
            s += str(d) + "\n"
        return s
from random import randint
from socket import AF_INET, SOCK_DGRAM, SOL_SOCKET, SO_BROADCAST, SO_REUSEADDR, socket, timeout
from time import sleep, time
from .device import DEFAULT_ATTEMPTS, DEFAULT_TIMEOUT, Device, UDP_BROADCAST_IP_ADDRS, UDP_BROADCAST_PORT
from .errors import InvalidParameterException, WorkflowException
from .light import Light
from .message import BROADCAST_MAC
from .msgtypes import Acknowledgement, GetService, LightGet, LightGetPower, LightSetColor, LightSetPower, \
    LightSetWaveform, LightState, LightStatePower, StateService
from .multizonelight import MultiZoneLight
from .unpack import unpack_lifx_message
from .group import Group
import os
        color_lights = []
        all_lights = self.get_lights()
        for l in all_lights:
            if l.supports_color():
                color_lights.append(l)
        return color_lights

    def get_device_by_name(self, name):
        device = None
        all_devices = self.get_devices()
        for d in all_devices:
            if d.get_label() == name:
                device = d
        if device == None:               # didn't find it?
            self.discover_devices()      # update list in case it is out of date
            all_devices = self.get_devices()
            for d in all_devices:            # and try again
                if d.get_label() == name:
                    device = d
        return device

    # takes in list of strings, returns Group of devices
    def get_devices_by_name(self, names):
        devices = []
        all_devices = self.get_devices()
        for d in all_devices:
            if d.get_label() in names:
                devices.append(d)
        if len(devices) != len(names):  # didn't find everything?
            self.discover_devices()     # update list in case it is out of date
            all_devices = self.get_devices()
            for d in all_devices:       # and try again
                if d.get_label() in names:
                    devices.append(d)
        return Group(devices)

    def get_devices_by_group(self, group):
        devices = []
        all_devices = self.get_devices()
        for d in all_devices:
            if d.get_group() == group:
                devices.append(d)
        return Group(devices)

    def get_devices_by_location(self, location):
        devices = []
        all_devices = self.get_devices()
        for d in all_devices:
            if d.get_location() == location:
                devices.append(d)
        return Group(devices)

    # returns dict of Light: power_level pairs
    def get_power_all_lights(self):
        responses = self.broadcast_with_resp(LightGetPower, LightStatePower)
        power_states = {}
        for light in self.lights:
            for response in responses:
                if light.mac_addr == response.target_addr:
                    power_states[light] = response.power_level
        return power_states

    def set_power_all_lights(self, power_level, duration=0, rapid=False):
        on = [True, 1, "on", 65535]
        off = [False, 0, "off"]
        try:
            if power_level in on and not rapid:
                self.broadcast_with_ack(LightSetPower, {"power_level": 65535, "duration": duration})
            elif power_level in on and rapid:
                self.broadcast_fire_and_forget(LightSetPower, {"power_level": 65535, "duration": duration}, num_repeats=5)
            elif power_level in off and not rapid:
                self.broadcast_with_ack(LightSetPower, {"power_level": 0, "duration": duration})
            elif power_level in off and rapid:
                self.broadcast_fire_and_forget(LightSetPower, {"power_level": 0, "duration": duration}, num_repeats=5)
            else:
                raise InvalidParameterException("{} is not a valid power level.".format(power_level))
        except WorkflowException as e:
            raise

    def get_color_all_lights(self):
        responses = self.broadcast_with_resp(LightGet, LightState)
        colors = {}
        for light in self.lights:
            for response in responses:
                if light.mac_addr == response.target_addr:
                    colors[light] = response.color
        return colors

    def set_color_all_lights(self, color, duration=0, rapid=False):
        if len(color) == 4:
            try:
                if rapid:
                    self.broadcast_fire_and_forget(LightSetColor, {"color": color, "duration": duration}, num_repeats=5)
                else:
                    self.broadcast_with_ack(LightSetColor, {"color": color, "duration": duration})
            except WorkflowException as e:
                raise
        else:
            raise InvalidParameterException("{} is not a valid color.".format(color))

    def set_waveform_all_lights(self, is_transient, color, period, cycles, duty_cycle, waveform, rapid=False):
        if len(color) == 4:
            try:
                if rapid:
                    self.broadcast_fire_and_forget(LightSetWaveform, {"transient": is_transient, "color": color, "period": period, "cycles": cycles, "duty_cycle": duty_cycle, "waveform": waveform}, num_repeats=5)
                else:
                    self.broadcast_with_ack(LightSetWaveform, {"transient": is_transient, "color": color, "period": period, "cycles": cycles, "duty_cycle": duty_cycle, "waveform": waveform})
            except WorkflowException as e:
                raise
        else:
            raise InvalidParameterException("{} is not a valid color.".format(color))

    ############################################################################
    #                                                                          #
    #                            Workflow Methods                              #
    #                                                                          #
    ############################################################################

    def discover(self, timeout_secs=DEFAULT_TIMEOUT, num_repeats=3):
        self.initialize_socket(timeout_secs)

        msg = GetService(BROADCAST_MAC, self.source_id, seq_num=0, payload={}, ack_requested=False, response_requested=True)