class Log(object):
    """
    Logging module

    Call get() to get a cached instance of a specific logger.
    Colored output can optionally be enabled.
    """


    _LOGGERS = {}
    _use_color = False

    @synchronized
    @classmethod
    def get(cls, tag):
        """
        Get the global logger instance for the given tag

        :param tag: the log tag
        :return: the logger instance
        """
        if tag not in cls._LOGGERS:
            if cls._use_color:
                handler = colorlog.StreamHandler()
                handler.setFormatter(colorlog.ColoredFormatter( \
                    ' %(log_color)s%(name)s/%(levelname)-8s%(reset)s |'
                    ' %(log_color)s%(message)s%(reset)s'))
            else:
                handler = logging.StreamHandler()
                handler.setFormatter(logging.Formatter( \
                    ' %(name)s/%(levelname)-8s | %(message)s'))

            logger = logging.getLogger(tag)
            logger.addHandler(handler)

            cls._LOGGERS[tag] = logger

        return cls._LOGGERS[tag]


    @classmethod
    def enable_color(cls, enable):
        """
        Enable colored output for loggers. Must be called before
        any loggers are initialized with get()
        """
        cls._use_color = enable def ensure_future(coro, loop=None):
    """
    Wrapper for asyncio.ensure_future which dumps exceptions
    """
    if loop is None:
        loop = asyncio.get_event_loop()
    fut = asyncio.ensure_future(coro, loop=loop)
    def exception_logging_done_cb(fut):
        try:
            e = fut.exception()
        except asyncio.CancelledError:
            return
        if e is not None:
            loop.call_exception_handler({
                'message': 'Unhandled exception in async future',
                'future': fut,
                'exception': e,
            })
    fut.add_done_callback(exception_logging_done_cb)
    return fut class Singleton(type):
    """
    Metaclass for creating singletons
    """
    def __call__(cls, *args, **kwargs):
        try:
            return cls.__instance
        except AttributeError:
            cls.__instance = super(Singleton, cls).__call__(*args, **kwargs)
            return cls.__instance class Signal(object):
    """
    A simple signalling construct.

    Listeners may connect() to this signal, and their handlers will
    be invoked when fire() is called.
    """
    def __init__(self):
        self._handlers = set()


    def connect(self, handler):
        """
        Connect a handler to this signal

        :param handler: Function to invoke when the signal fires
        """
        self._handlers.add(handler)


    def fire(self, *args, **kwargs):
        """
        Fire the signal, invoking all connected handlers

        :params args: Arguments to call handlers with
        """
        for handler in self._handlers:
            handler(*args, **kwargs) class UChromaDevice(BaseUChromaDevice):
    """
    Class encapsulating all functionality available on standard Chroma devices
    """

    def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo, devindex: int,
                 sys_path: str, input_devices=None, *args, **kwargs):

        super(UChromaDevice, self).__init__(hardware, devinfo, devindex,
                                            sys_path, input_devices,
                                            *args, **kwargs)

        self._fx_manager = FXManager(self, StandardFX(self))
        self._led_manager = LEDManager(self)

        self._frame_control = None


    def get_led(self, led_type: LEDType) -> LED:
        """
        Fetches the requested LED interface on this device

        :param led_type: The LED type to fetch

        :return: The LED interface, if available
        """
        if self.led_manager is None:
            return None
        return self.led_manager.get(led_type)


    @property
    def frame_control(self) -> Frame:
        """
        Gets the Frame object for creating custom effects on this device

        :param base_color: Background color for the Frame (defaults to black)

        :return: The Frame interface
        """
        if self.width == 0 or self.height == 0:
            return None

        if self._frame_control is None:
            self._frame_control = Frame(self, self.width, self.height)

        return self._frame_control


    def _set_brightness(self, level: float) -> bool:
        if self.has_quirk(Quirks.SCROLL_WHEEL_BRIGHTNESS):
            self.get_led(LEDType.SCROLL_WHEEL).brightness = level

        elif self.has_quirk(Quirks.LOGO_LED_BRIGHTNESS):
            self.get_led(LEDType.LOGO).brightness = level

        else:
            self.get_led(LEDType.BACKLIGHT).brightness = level

        return True


    def _get_brightness(self) -> float:
        if self.has_quirk(Quirks.SCROLL_WHEEL_BRIGHTNESS):
            return self.get_led(LEDType.SCROLL_WHEEL).brightness

        if self.has_quirk(Quirks.LOGO_LED_BRIGHTNESS):
            return self.get_led(LEDType.LOGO).brightness

        return self.get_led(LEDType.BACKLIGHT).brightness


    @property
    def supported_leds(self) -> tuple:
        return self.hardware.supported_leds


    @property
    def led_manager(self) -> LEDManager:
        return self._led_manager


    def reset(self) -> bool:
        """
        Clear all effects and custom frame

        :return: True if successful
        """
        if self._frame_control is not None:
            self.frame_control.background_color = None
            self.frame_control.reset()

        if hasattr(self, 'fx_manager'):
            self.fx_manager.disable() class BaseUChromaDevice:
    """
    Base class for device objects
    """

    class Command(BaseCommand):
        """
        Standard commands used by all Chroma devices
        """
        # info queries, class 0
        GET_FIRMWARE_VERSION = (0x00, 0x81, 0x02)
        GET_SERIAL = (0x00, 0x82, 0x16)


    def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo, index: int,
                 sys_path: str, input_devices=None, *args, **kwargs):

        self._hardware = hardware
        self._devinfo = devinfo
        self._devindex = index
        self._sys_path = sys_path

        self.logger = Log.get('uchroma.driver-%d' % index)

        # needed for mixins
        super(BaseUChromaDevice, self).__init__(*args, **kwargs)

        self._dev = None
        self._serial_number = None
        self._firmware_version = None
        self._last_cmd_time = None
        self._prefs = None

        self._offline = False
        self._suspended = False

        self.power_state_changed = Signal()
        self.restore_prefs = Signal()

        self._input_manager = None
        if input_devices is not None:
            self._input_manager = InputManager(self, input_devices)

        self._animation_manager = None
        if self.width > 0 and self.height > 0:
            self._animation_manager = AnimationManager(self)

        self._brightness_animator = ValueAnimator(self._update_brightness)

        self._fx_manager = None

        self._ref_count = 0
        self._executor = futures.ThreadPoolExecutor(max_workers=1)


    async def shutdown(self):
        """
        Shuts down all services associated with the device and closes the HID instance.
        """
        if asyncio.get_event_loop().is_running():
            if hasattr(self, '_animation_manager') and self.animation_manager is not None:
                await self.animation_manager.shutdown()

            if hasattr(self, '_input_manager') and self._input_manager is not None:
                await self._input_manager.shutdown()

        self.close(True)


    def close(self, force: bool = False):
        if not force:
            if self.animation_manager is not None and self.is_animating:
                return

            if self._ref_count > 0:
                return

        if hasattr(self, '_dev') and self._dev is not None:
            try:
                self._dev.close()
            except Exception:
                pass

            self._dev = None


    def has_fx(self, fx_type: str) -> bool:
        """
        Test if the device supports a particular built-in effect

        :param fx_type: the effect to test
        :return: true if the effect is supported
        """
        if self.fx_manager is None:
            return False
        return fx_type in self.fx_manager.available_fx


    @property
    def animation_manager(self):
        """
        Animation manager for this device
        """
        if hasattr(self, '_animation_manager'):
            return self._animation_manager
        return None


    @property
    def is_animating(self):
        """
        True if an animation is currently running
        """
        if self.animation_manager is not None:
            return self.animation_manager.running
        return False


    @property
    def fx_manager(self):
        """
        Built-in effects manager for this device
        """
        return self._fx_manager


    @property
    def input_manager(self):
        """
        Input manager service for this device
        """
        return self._input_manager


    @property
    def input_devices(self):
        """
        Input devices associated with this instance
        """
        if self._input_manager is None:
            return None
        return self._input_manager.input_devices


    @property
    def hid(self):
        """
        The lower-layer hidapi device
        """
        return self._dev


    @property
    def last_cmd_time(self):
        """
        Timestamp of the last command sent to the hardware, used for delay enforcement
        """
        return self._last_cmd_time


    @last_cmd_time.setter
    def last_cmd_time(self, last_cmd_time):
        self._last_cmd_time = last_cmd_time


    def _set_brightness(self, level: float) -> bool:
        return False


    def _get_brightness(self) -> float:
        return 0.0


    async def _update_brightness(self, level):
        await ensure_future(asyncio.get_event_loop().run_in_executor( \
                self._executor, functools.partial(self._set_brightness, level)))

        suspended = self.suspended and level == 0
        self.power_state_changed.fire(level, suspended)


    @property
    def suspended(self):
        """
        The power state of the device, true if suspended
        """
        return self._suspended


    def suspend(self, fast=False):
        """
        Suspend the device

        Performs any actions necessary to suspend the device. By default,
        the current brightness level is saved and set to zero.
        """
        if self._suspended:
            return

        self.preferences.brightness = self.brightness
        if fast:
            self._set_brightness(0)
        else:
            if self._device_open():
                self._brightness_animator.animate(self.brightness, 0,
                                                  done_cb=self._done_cb)

        self._suspended = True


    def resume(self):
        """
        Resume the device

        Performs any actions necessary to resume the device. By default,
        the saved brightness level is restored.
        """
        if not self._suspended:
            return

        self._suspended = False
        self.brightness = self.preferences.brightness


    @property
    def brightness(self):
        """
        The current brightness level of the device lighting
        """
        if self._suspended:
            return self.preferences.brightness

        return self._get_brightness()



    @brightness.setter
    def brightness(self, level: float):
        """
        Set the brightness level of the main device lighting

        :param level: Brightness level, 0-100
        """
        if not self._suspended:
            if self._device_open():
                self._brightness_animator.animate(self.brightness, level,
                                                  done_cb=self._done_cb)

        self.preferences.brightness = level


    def _ensure_open(self) -> bool:
        try:
            if self._dev is None:
                self._dev = hidapi.Device(self._devinfo, blocking=False)
        except Exception as err:
            self.logger.exception("Failed to open connection", exc_info=err)
            return False

        return True


    def get_report(self, command_class: int, command_id: int, data_size: int,
                   *args, transaction_id: int, remaining_packets: int = 0x00) -> RazerReport:
        """
        Create and initialize a new RazerReport on this device
        """
        if transaction_id is None:
            if self.has_quirk(Quirks.TRANSACTION_CODE_3F):
                transaction_id = 0x3F
            else:
                transaction_id = 0xFF

        self.logger.debug('Transaction id: %d quirks: %s' % (transaction_id, self.hardware.quirks))

        report = RazerReport(self, command_class, command_id, data_size,
                             transaction_id=transaction_id,
                             remaining_packets=remaining_packets)

        if args is not None:
            for arg in args:
                if arg is not None:
                    report.args.put(arg)

        return report


    def _get_timeout_cb(self):
        """
        Getter for report timeout handler
        """
        return None


    def run_with_result(self, command: BaseCommand, *args,
                        transaction_id: int = None, delay: float = None,
                        remaining_packets: int = 0x00) -> bytes:
        """
        Run a command and return the result

        Executes the given command with the provided list of arguments, returning
        the result report.

        Transaction id is only necessary for specialized commands or hardware.

        The connection to the device will be automatically closed by default.

        :param command: The command to run

        :param args: The list of arguments to call the command with
        :type args: varies
        :param transaction_id: Transaction identified, defaults to 0xFF

        :return: The result report from the hardware
        """
        report = self.get_report(*command.value, *args, transaction_id=transaction_id,
                                 remaining_packets=remaining_packets)
        result = None

        if self.run_report(report, delay=delay):
            result = report.result

        return result


    @synchronized
    def run_report(self, report: RazerReport, delay: float = None) -> bool:
        """
        Runs a previously initialized RazerReport on the device

        :param report: the report to run
        :param delay: custom delay to enforce between commands
        :return: True if successful
        """
        with self.device_open():
            return report.run(delay=delay, timeout_cb=self._get_timeout_cb())


    def run_command(self, command: BaseCommand, *args, transaction_id: int = None,
                    delay: float = None, remaining_packets: int = 0x00) -> bool:
        """
        Run a command

        Executes the given command with the provided list of arguments.
        Transaction id is only necessary for specialized commands or hardware.

        The connection to the device will be automatically closed by default.

        :param command: The command to run
        :param args: The list of arguments to call the command with
        :type args: varies
        :param transaction_id: Transaction identified, defaults to 0xFF
        :param timeout_cb: Callback to invoke on a timeout

        :return: True if the command was successful
        """
        report = self.get_report(*command.value, *args, transaction_id=transaction_id,
                                 remaining_packets=remaining_packets)

        return self.run_report(report, delay=delay)


    def _decode_serial(self, value: bytes) -> str:
        if value is not None:
            try:
                return value.decode()
            except UnicodeDecodeError:
                return self.key

        return None


    def _get_serial_number(self) -> str:
        """
        Get the serial number from the hardware directly

        Laptops don't return a serial number for their devices,
        so we return the model name.
        """
        value = self.run_with_result(BaseUChromaDevice.Command.GET_SERIAL)
        return self._decode_serial(value)


    @property
    def serial_number(self) -> str:
        """
        The hardware serial number of this device

        On laptops, this is not available.
        """
        if self._serial_number is not None:
            return self._serial_number

        serial = self._get_serial_number()

        if serial is not None:
            self._serial_number = re.sub(r'\W+', r'', serial)

        return self._serial_number


    def _get_firmware_version(self) -> str:
        """
        Get the firmware version from the hardware directly
        """
        return self.run_with_result(BaseUChromaDevice.Command.GET_FIRMWARE_VERSION)


    @property
    def firmware_version(self) -> str:
        """
        The firmware version present on this device
        """
        if self._firmware_version is None:
            version = self._get_firmware_version()

            if version is None:
                self._firmware_version = '(unknown)'
            else:
                self._firmware_version = 'v%d.%d' % (int(version[0]), int(version[1]))

        return self._firmware_version


    @property
    def is_offline(self) -> bool:
        """
        Some devices (such as wireless models) might be "offline" in that
        the dock or other USB receiver might be plugged in, but the actual
        device is switched off. In this case, we can't interact with it
        but it will still enumerate.
        """
        return self._offline


    @property
    def name(self) -> str:
        """
        The name of this device
        """
        return self.hardware.name


    @property
    def device_index(self) -> int:
        """
        The internal index of this device in the device manager
        """
        return self._devindex


    @property
    def sys_path(self) -> str:
        """
        The sysfs path of this device
        """
        return self._sys_path


    @property
    def key(self) -> str:
        """
        Unique key which identifies this device to the device manager
        """
        return '%04x:%04x.%02d' % (self.vendor_id, self.product_id, self.device_index)


    @property
    def hardware(self) -> Hardware:
        """
        The sub-enumeration of Hardware
        """
        return self._hardware


    @property
    def product_id(self) -> int:
        """
        The USB product identifier of this device
        """
        return self._devinfo.product_id


    @property
    def vendor_id(self) -> int:
        """
        The USB vendor identifier of this device
        """
        return self._devinfo.vendor_id


    @property
    def manufacturer(self) -> str:
        """
        The manufacturer of this device
        """
        return self._hardware.manufacturer


    @property
    def device_type(self) -> Hardware.Type:
        """
        The type of this device, from the Hardware.Type enumeration
        """
        return self.hardware.type


    @property
    def driver_version(self):
        """
        Get the uChroma version
        """
        return __version__


    @property
    def width(self) -> int:
        """
        Gets the width of the key matrix (if applicable)
        """
        if self.hardware.dimensions is None:
            return 0

        return self.hardware.dimensions.x


    @property
    def height(self) -> int:
        """
        Gets the height of the key matrix (if applicable)
        """
        if self.hardware.dimensions is None:
            return 0

        return self.hardware.dimensions.y


    @property
    def has_matrix(self) -> bool:
        """
        True if the device supports matrix control
        """
        return self.hardware.has_matrix


    def has_quirk(self, quirk) -> bool:
        """
        True if the quirk is required for this device
        """
        return self.hardware.has_quirk(quirk)


    @property
    def key_mapping(self):
        """
        The mapping between keycodes and lighting matrix coordinates
        """
        return self.hardware.key_mapping


    @property
    def preferences(self):
        """
        Saved preferences for this device
        """
        if self._prefs is None:
            self._prefs = PreferenceManager().get(self.serial_number)
        return self._prefs


    def reset(self) -> bool:
        """
        Reset effects and other configuration to defaults
        """
        return True


    def fire_restore_prefs(self):
        """
        Restore saved preferences
        """
        with self.preferences.observers_paused():
            if hasattr(self, 'brightness') and self.preferences.brightness is not None:
                setattr(self, 'brightness', self.preferences.brightness)

            self.restore_prefs.fire(self.preferences)


    def __repr__(self):
        return "%s(name=%s, type=%s, product_id=0x%04x, index=%d, quirks=%s)" % \
            (self.__class__.__name__, self.name, self.device_type.value,
             self.product_id, self.device_index, self.hardware.quirks)


    def _device_open(self):
        self._ref_count += 1
        return self._ensure_open()


    def _device_close(self):
        self._ref_count -= 1
        self.close()


    def _done_cb(self, future):
        self._device_close()


    @contextmanager
    def device_open(self):
        try:
            if self._device_open():
                yield
        finally:
            self._device_close()


    def __del__(self):
        self.close(force=True) class Hardware(BaseHardware):
    """
    Static hardware configuration data

    Loaded by Configuration from YAML.
    """

    class Type(Enum):
        HEADSET = 'Headset'
        KEYBOARD = 'Keyboard'
        KEYPAD = 'Keypad'
        LAPTOP = 'Laptop'
        MOUSE = 'Mouse'
        MOUSEPAD = 'Mousepad'

    @property
    def has_matrix(self) -> bool:
        """
        True if the device has an addressable key matrix
        """
        return self.dimensions is not None and self.dimensions.x > 1 and self.dimensions.y > 1


    def has_quirk(self, *quirks) -> bool:
        """
        True if quirk is required for the device

        :param: quirks The quirks to check (varargs)

        :return: True if the quirk is required
        """
        if self.quirks is None:
            return False

        for quirk in quirks:
            if isinstance(self.quirks, (list, tuple)) and quirk in self.quirks:
                return True
            if self.quirks == quirk:
                return True

        return False


    @classmethod
    def get_type(cls, hw_type) -> 'Hardware':
        if hw_type is None:
            return None

        config_path = os.path.join(os.path.dirname(__file__), 'data')
        yaml_path = os.path.join(config_path, '%s.yaml' % hw_type.name.lower())

        config = cls.load_yaml(yaml_path)
        assert config is not None

        return config


    @classmethod
    def _get_device(cls, product_id: int, hw_type) -> 'Hardware':
        if product_id is None:
            return None

        config = cls.get_type(hw_type)

        result = config.search('product_id', product_id)
        if not result:
            return None

        if isinstance(result, list) and len(result) == 1:
            return result[0]

        return result


    @classmethod
    def get_device(cls, product_id: int, hw_type=None) -> 'Hardware':
        if hw_type is not None:
            return cls._get_device(product_id, hw_type)

        for hw in Hardware.Type:
            device = cls._get_device(product_id, hw)
            if device is not None:
                return device

        return None


    def _yaml_header(self) -> str:
        header = '#\n#  uChroma device configuration\n#\n'
        if self.name is not None:
            header += '#  Model: %s (%s)\n' % (self.name, self.type.value)
        elif self.type is not None:
            header += '#  Type: %s\n' % self.type.name.title()
        header += '#  Updated on: %s\n' % datetime.now().isoformat(' ')
        header += '#\n'

        return header class Quirks(IntEnum):
    """
    Various "quirks" that are found across hardware models.
    """

    # Always use transaction code 0x3F
    TRANSACTION_CODE_3F = 1

    # Use "extended" commands
    EXTENDED_FX_CMDS = 2

    # Control device brightness with the scroll wheel LED
    SCROLL_WHEEL_BRIGHTNESS = 3

    # Device has charge and dock controls
    WIRELESS = 4

    # Needs transaction code 0x80 for custom frame data
    CUSTOM_FRAME_80 = 5

    # Control device brightness with the logo LED
    LOGO_LED_BRIGHTNESS = 6

    # Device has individual "profile" LEDs
    PROFILE_LEDS = 7

    # Device only supports spectrum effect on the backlight LED
    BACKLIGHT_LED_FX_ONLY = 8 RAZER_VENDOR_ID = 0x1532 class UChromaHeadset(BaseUChromaDevice):
    """
    Additional functionality for Chroma Headsets
    The protocol on these devices is different than
    the rest of the Chroma line.
    """

    class Command(BaseCommand):
        """
        Commands used for headset features

        0: Destination
        1: Length
        2-3: Address
        """
        GET_SERIAL = (READ_EEPROM, 0x16, ADDR_SERIAL_NUMBER)
        GET_FIRMWARE_VERSION = (READ_EEPROM, 0x02, ADDR_FIRMWARE_VERSION)

        KYLIE_GET_LED_MODE = (READ_RAM, 0x01, ADDR_KYLIE_LED_MODE)
        KYLIE_SET_LED_MODE = (WRITE_RAM, 0x01, ADDR_KYLIE_LED_MODE)

        KYLIE_GET_RGB_1 = (READ_RAM, 0x04, ADDR_KYLIE_BREATHING1_START)
        KYLIE_GET_RGB_2 = (READ_RAM, 0x08, ADDR_KYLIE_BREATHING2_START)
        KYLIE_GET_RGB_3 = (READ_RAM, 0x0C, ADDR_KYLIE_BREATHING3_START)
        KYLIE_SET_RGB_1 = (WRITE_RAM, 0x04, ADDR_KYLIE_BREATHING1_START)
        KYLIE_SET_RGB_2 = (WRITE_RAM, 0x08, ADDR_KYLIE_BREATHING2_START)
        KYLIE_SET_RGB_3 = (WRITE_RAM, 0x0C, ADDR_KYLIE_BREATHING3_START)

        RAINIE_GET_LED_MODE = (READ_RAM, 0x01, ADDR_RAINIE_LED_MODE)
        RAINIE_SET_LED_MODE = (WRITE_RAM, 0x01, ADDR_RAINIE_LED_MODE)

        RAINIE_GET_RGB = (WRITE_RAM, 0x04, ADDR_RAINIE_BREATHING1_START)
        RAINIE_SET_RGB = (WRITE_RAM, 0x04, ADDR_RAINIE_BREATHING1_START)


        def __init__(self, destination, length, address):
            self._destination = destination
            self._length = length
            self._address = address


        @property
        def destination(self):
            return self._destination


        @property
        def length(self):
            return self._length


        @property
        def address(self):
            return self._address



    def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo,
                 devindex: int, sys_path: str, *args, **kwargs):
        super(UChromaHeadset, self).__init__(hardware, devinfo, devindex,
                                             sys_path, *args, **kwargs)

        self._last_cmd_time = None

        if self.hardware.revision == 1:
            self._cmd_get_led = UChromaHeadset.Command.RAINIE_GET_LED_MODE
            self._cmd_set_led = UChromaHeadset.Command.RAINIE_SET_LED_MODE
            self._cmd_get_rgb = [UChromaHeadset.Command.RAINIE_GET_RGB]
            self._cmd_set_rgb = [UChromaHeadset.Command.RAINIE_SET_RGB]

        elif self.hardware.revision == 2:
            self._cmd_get_led = UChromaHeadset.Command.KYLIE_GET_LED_MODE
            self._cmd_set_led = UChromaHeadset.Command.KYLIE_SET_LED_MODE
            self._cmd_get_rgb = [UChromaHeadset.Command.KYLIE_GET_RGB_1,
                                 UChromaHeadset.Command.KYLIE_GET_RGB_2,
                                 UChromaHeadset.Command.KYLIE_GET_RGB_3]
            self._cmd_set_rgb = [UChromaHeadset.Command.KYLIE_SET_RGB_1,
                                 UChromaHeadset.Command.KYLIE_SET_RGB_2,
                                 UChromaHeadset.Command.KYLIE_SET_RGB_3]
        else:
            raise ValueError('Incompatible model (%s)' % repr(self.hardware))

        self._fx_manager = FXManager(self, KrakenFX(self))
        self._rgb = None
        self._mode = None


    @staticmethod
    def _pack_request(command: BaseCommand, *args) -> bytes:
        req = ByteArgs(REPORT_LENGTH_OUT)
        req.put(command.destination)
        req.put(command.length)
        req.put(command.address, packing='>H')

        if args is not None:
            for arg in args:
                if arg is not None:
                    req.put(arg)

        return req.data


    def _hexdump(self, data: bytes, tag=""):
        if data is not None and self.logger.isEnabledFor(LOG_PROTOCOL_TRACE):
            self.logger.debug('%s%s', tag, "".join('%02x ' % b for b in data))


    def _run_command(self, command: BaseCommand, *args) -> bool:
        try:
            data = UChromaHeadset._pack_request(command, *args)
            self._hexdump(data, '--> ')
            self._last_cmd_time = smart_delay(DELAY_TIME, self._last_cmd_time, 0)
            self._dev.write(data, report_id=to_byte(REPORT_ID_OUT))
            return True

        except (OSError, IOError) as err:
            self.logger.exception('Caught exception running command', exc_info=err)

        return False


    @synchronized
    def run_command(self, command: BaseCommand, *args) -> bool:
        """
        Run a command against the Kraken hardware

        :param command: The command tuple
        :param args: Argument list (varargs)

        :return: True if successful
        """
        with self.device_open():
            return self._run_command(command, *args)


    @synchronized
    def run_with_result(self, command: BaseCommand, *args) -> bytes:
        """
        Run a command against the Kraken hardware and fetch the result

        :param command: The command tuple
        :param args: Argument list (varargs)

        :return: Raw response bytes
        """
        try:
            with self.device_open():
                if not self._run_command(command, *args):
                    return None

                self._last_cmd_time = smart_delay(DELAY_TIME, self._last_cmd_time, 0)
                resp = self._dev.read(REPORT_LENGTH_IN, timeout_ms=500)
                self._hexdump(resp, '<-- ')

                if not resp:
                    return None

                assert resp[0] == REPORT_ID_IN, \
                    'Inbound report should have id %02x (was %02x)' % \
                    (REPORT_ID_IN, resp[0])

                return resp[1:command.length+1]

        except (OSError, IOError) as err:
            self.logger.exception('Caught exception running command', exc_info=err)

        return None


    def _get_serial_number(self) -> str:
        """
        Get the serial number from the hardware directly
        """
        return self._decode_serial(
            self.run_with_result(UChromaHeadset.Command.GET_SERIAL))


    def _get_firmware_version(self) -> str:
        """
        Get the firmware version from the hardware directly
        """
        return self.run_with_result(UChromaHeadset.Command.GET_FIRMWARE_VERSION)


    def _get_led_mode(self) -> 'UChromaHeadset.EffectBits':
        if self._mode is None:
            with self.device_open():
                value = self.run_with_result(self._cmd_get_led)
                bits = 0
                if value:
                    bits = value[0]
                self._mode = EffectBits(bits)
        return self._mode


    def _set_led_mode(self, bits: EffectBits) -> bool:
        status = self.run_command(self._cmd_set_led, bits.value)
        if status:
            self._mode = bits

        return status


    def _get_rgb(self) -> list:
        with self.device_open():
            bits = self._get_led_mode()
            if bits.color_count == 0:
                return None

            value = self.run_with_result(self._cmd_get_rgb[bits.color_count - 1])
            if value is None:
                return None

            it = iter(value)
            values = list(iter(zip(it, it, it, it)))

            return [to_color(x) for x in values]


    def _set_rgb(self, *colors, brightness: float = None) -> bool:
        if not colors:
            self.logger.error('RGB group out of range')
            return False

        # only allow what the hardware permits
        colors = colors[0:len(self._cmd_set_rgb)]

        with self.device_open():
            if brightness is None:
                brightness = self._get_brightness()
                if brightness == 0.0:
                    brightness = 80.0

            brightness = scale_brightness(brightness)

            args = []
            for color in colors:
                args.append(to_color(color))
                args.append(brightness)

            return self.run_command(self._cmd_set_rgb[len(colors) - 1], *args)


    def get_current_effect(self) -> EffectBits:
        """
        Gets the current effects configuration

        :return: EffectBits populated from the hardware
        """
        return self._get_led_mode()


    def get_current_colors(self) -> list:
        """
        Gets the colors currently in use

        :return: List of RGB tuples
        """
        colors = self._get_rgb()
        if not colors:
            return None

        return [to_rgb(color) for color in colors]


    def _get_brightness(self) -> float:
        """
        The current brightness level
        """
        with self.device_open():
            bits = self._get_led_mode()
            if bits.color_count == 0:
                if bits.on:
                    return 100.0
                return 0.0

            value = self.run_with_result(self._cmd_get_rgb[bits.color_count - 1])
            if value is None:
                return 0.0

            return scale_brightness(value[3], True)


    def _set_brightness(self, brightness: float) -> bool:
        """
        Set the brightness level
        """
        with self.device_open():
            bits = self._get_led_mode()
            if bits.color_count == 0:
                return False

            value = self.run_with_result(self._cmd_get_rgb[bits.color_count - 1])
            if value is None:
                return False

            level = scale_brightness(brightness)

            data = bytearray(value)
            for num in range(0, bits.color_count):
                data[(num * 4) + 3] = level

            return self.run_command(self._cmd_set_rgb[bits.color_count -1], data) class UChromaKeyboard(UChromaDevice, KeyboardFixup):
    """
    Additional functionality for Chroma Keyboards
    """

    class Command(BaseCommand):
        """
        Commands used for keyboard features
        """
        SET_DEVICE_MODE = (0x00, 0x04, 0x02)

        GET_DEVICE_MODE = (0x00, 0x84, 0x02)


    def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo, devindex: int,
                 sys_path: str, input_devices=None, *args, **kwargs):
        super(UChromaKeyboard, self).__init__(hardware, devinfo, devindex,
                                              sys_path, input_devices,
                                              *args, **kwargs)


    def get_device_mode(self) -> tuple:
        """
        Gets the current device mode
        """
        if self.device_type == Hardware.Type.LAPTOP:
            return None

        value = self.run_with_result(UChromaKeyboard.Command.GET_DEVICE_MODE)
        if value is None:
            return None

        return (value[0], value[1])


    def set_device_mode(self, mode, param=0) -> bool:
        """
        Sets the requested device mode
        """
        if self.device_type == Hardware.Type.LAPTOP:
            return None

        return self.run_command(UChromaKeyboard.Command.SET_DEVICE_MODE, mode, param) class UChromaKeypad(UChromaKeyboard):

    def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo, devindex: int,
                 sys_path: str, input_devices=None, *args, **kwargs):
        super(UChromaKeypad, self).__init__(hardware, devinfo, devindex,
                                            sys_path, input_devices,
                                            *args, **kwargs)

        self._macrodev = None
        if hardware.macro_keys is not None:
            self._macrodev = MacroDevice(self)


    @property
    def macro_manager(self):
        return self._macrodev class UChromaLaptop(UChromaKeyboard):
    """
    Commands required for Blade laptops
    """

    # commands
    class Command(BaseCommand):
        """
        Enumeration of standard commands not handled elsewhere
        """
        SET_BRIGHTNESS = (0x0e, 0x04, 0x02)
        GET_BRIGHTNESS = (0x0e, 0x84, 0x02)


    def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo, devindex: int,
                 sys_path: str, input_devices=None, *args, **kwargs):
        super(UChromaLaptop, self).__init__(hardware, devinfo, devindex,
                                            sys_path, input_devices,
                                            *args, **kwargs)


    def _get_serial_number(self):
        return self.name


    def _set_brightness(self, level: float):
        return self.run_command(UChromaLaptop.Command.SET_BRIGHTNESS, 0x01, scale_brightness(level))


    def _get_brightness(self) -> float:
        value = self.run_with_result(UChromaLaptop.Command.GET_BRIGHTNESS)
        if value is None:
            return 0.0
        return scale_brightness(int(value[1]), True) class UChromaMouse(UChromaDevice):
    """
    Additional functionality for Chroma Mice
    """

    class Command(BaseCommand):
        """
        Commands used for mouse features
        """
        SET_POLLING_RATE = (0x00, 0x05, 0x01)
        SET_DPI_XY = (0x04, 0x05, 0x07)
        SET_IDLE_TIME = (0x07, 0x03, 0x02)

        GET_POLLING_RATE = (0x00, 0x85, 0x01)
        GET_DPI_XY = (0x04, 0x85, 0x07)


    def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo, devindex: int,
                 sys_path: str, input_devices=None, *args, **kwargs):
        super(UChromaMouse, self).__init__(hardware, devinfo, devindex,
                                           sys_path, input_devices,
                                           *args, **kwargs)


    @property
    def polling_rate(self) -> PollingRate:
        """
        Get the current polling rate
        """
        value = self.run_with_result(UChromaMouse.Command.GET_POLLING_RATE)
        if value is None:
            return PollingRate.INVALID

        return PollingRate(value[0])


    @polling_rate.setter
    def polling_rate(self, rate: PollingRate):
        """
        Set the polling rate
        """
        if isinstance(rate, str):
            rate = PollingRate.__members__[rate.upper()]

        self.run_command(UChromaMouse.Command.SET_POLLING_RATE, rate.value)



    @property
    def dpi_xy(self) -> tuple:
        """
        Get an (x, y) tuple of the current device DPI
        """
        value = self.run_with_result(UChromaMouse.Command.GET_DPI_XY)
        if value is None:
            return (-1, -1)

        return struct.unpack('>HH', value[1:5])


    @dpi_xy.setter
    def dpi_xy(self, dpi: tuple):
        """
        Set the (x, y) device DPI
        """
        args = None
        if len(dpi) == 2:
            args = struct.pack('>HH', dpi[0], dpi[1])
        elif len(dpi == 1):
            args = struct.pack('>H', dpi[0])
        else:
            raise ValueError("Must specify one (x) or two (x, y) integers to set DPI")

        self.run_with_result(UChromaMouse.Command.SET_DPI_XY, 0x01, args)


    def set_idle_time(self, idle_time: int):
        """
        Sets the idle time in seconds. The device will enter powersave
        mode after the timeout expires.

        :param idle_time: Timeout in seconds. Must be between 60 and 900.

        :return: True if successful
        """
        idle_time = clamp(idle_time, 60, 900)
        arg = struct.pack('>H', idle_time)

        return self.run_command(UChromaMouse.Command.SET_IDLE_TIME, arg)


    @property
    def is_wireless(self):
        """
        True if this device has battery and dock settings
        """
        return False class UChromaWirelessMouse(UChromaMouse):
    """
    A mouse with dock and battery functions
    """
    class Command(BaseCommand):
        """
        Commands used for mouse features
        """
        SET_DOCK_CHARGE_EFFECT = (0x03, 0x10, 0x01)
        SET_DOCK_BRIGHTNESS = (0x07, 0x02, 0x01)
        SET_LOW_BATTERY_THRESHOLD = (0x07, 0x01, 0x01)

        GET_BATTERY_LEVEL = (0x07, 0x80, 0x02)
        GET_DOCK_BRIGHTNESS = (0x07, 0x82, 0x01)
        GET_CHARGING_STATUS = (0x07, 0x84, 0x02)


    def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo, devindex: int,
                 sys_path: str, input_devices=None, *args, **kwargs):
        super(UChromaWirelessMouse, self).__init__(hardware, devinfo, devindex,
                                                   sys_path, input_devices,
                                                   *args, **kwargs)


    @property
    def is_wireless(self):
        """
        This mouse has wireless controls
        """
        return True


    def _timeout_cb(self, status, data):
        if self._offline and status == Status.OK:
            self._offline = False
            self.close(True)
        self._offline = True


    def _get_timeout_cb(self):
        return self._timeout_cb


    @property
    def dock_brightness(self) -> float:
        value = self.run_with_result(UChromaWirelessMouse.Command.GET_DOCK_BRIGHTNESS)
        if value is None:
            return 0.0
        return scale_brightness(int(value[0]), True)


    @dock_brightness.setter
    def dock_brightness(self, brightness: float) -> bool:
        return self.run_command(UChromaWirelessMouse.Command.SET_DOCK_BRIGHTNESS,
                                scale_brightness(brightness))


    @property
    def battery_level(self) -> float:
        """
        The current battery level
        """
        value = self.run_with_result(UChromaWirelessMouse.Command.GET_BATTERY_LEVEL)
        if value is None:
            return -1.0
        return (value[1] / 255) * 100


    @property
    def is_charging(self) -> bool:
        """
        Is the device currently charging?
        """
        value = self.run_with_result(UChromaWirelessMouse.Command.GET_CHARGING_STATUS)
        if value is None:
            return False
        return value[1] == 1


    def enable_dock_charge_effect(self, enable: bool) -> bool:
        """
        If enabled, a special charge effect will be shown on the device lighting.

        :param enable: True to enable the charge effect

        :return: True if successful
        """
        return self.run_command(UChromaWirelessMouse.Command.SET_DOCK_CHARGE_EFFECT, int(enable))


    @property
    def dock_charge_color(self) -> Color:
        """
        The color of the dock LEDs while charging
        """
        return self.get_led(LEDType.BATTERY).color


    @dock_charge_color.setter
    @colorarg
    def dock_charge_color(self, color: ColorType):
        """
        Set the color of the dock while charging. None to disable
        """
        if color is None or (color.rgb[0] == 0.0 and \
                             color.rgb[1] == 0.0 and \
                             color.rgb[2] == 0.0):
            self.enable_dock_charge_effect(False)
        else:
            self.enable_dock_charge_effect(True)
            self.get_led(LEDType.BATTERY).color = color


    def set_low_battery_threshold(self, threshold: float) -> bool:
        """
        Sets the low battery warning threshold as a percentage

        :param threshold: Threshold percentage, must be between 5 and 25

        :return: True if successful
        """
        arg = scale(threshold, 5.0, 25.0, 0x0C, 0x3F, True)
        return self.run_command(UChromaWirelessMouse.Command.SET_LOW_BATTERY_THRESHOLD, arg)
import asyncio
import hidapi
from collections import OrderedDict
from concurrent import futures
from pyudev import Context, Monitor, MonitorObserver
from pyudev._os import pipe
from uchroma.log import Log
from uchroma.util import ensure_future, Singleton, Signal
from .device import UChromaDevice
from .device_base import BaseUChromaDevice
from .hardware import Hardware, Quirks, RAZER_VENDOR_ID
from .headset import UChromaHeadset
from .keyboard import UChromaKeyboard
from .keypad import UChromaKeypad
from .laptop import UChromaLaptop
from .mouse import UChromaMouse, UChromaWirelessMouse
        self._udev_observer = None
        self._callbacks = []

        if callbacks is not None:
            self._callbacks.extend(callbacks)

        self._loop = asyncio.get_event_loop()

        self.device_added = Signal()
        self.device_removed = Signal()

        self.discover()


    async def _fire_callbacks(self, action: str, device: BaseUChromaDevice):
        # delay for udev setup
        await asyncio.sleep(0.2)

        for callback in self._callbacks:
            await callback(action, device)


    def discover(self):
        """
        Perform HID device discovery

        Iterates over all connected HID devices with RAZER_VENDOR_ID
        and checks the product ID against the Hardware descriptor.

        Interface endpoint restrictions are currently hard-coded. In
        the future this should be done by checking the HID report
        descriptor of the endpoint, however this functionality in
        HIDAPI is broken (report_descriptor returns garbage) on
        Linux in the current release.

        Discovery is automatically performed when the object is
        constructed, so this should only need to be called if the
        list of devices changes (monitoring for changes is beyond
        the scope of this API).
        """
        devinfos = sorted(list(hidapi.enumerate(vendor_id=RAZER_VENDOR_ID)), key=lambda x: x.path)

        for devinfo in devinfos:
            parent = self._get_parent(devinfo.product_id)
            if parent is None:
                continue

            if self._key_for_path(parent.sys_path) is not None:
                continue

            hardware = Hardware.get_device(devinfo.product_id)
            if hardware is None:
                continue

            if hardware.type == Hardware.Type.HEADSET:
                if devinfo.interface_number != 3:
                    continue
            elif hardware.type in (Hardware.Type.KEYBOARD,
                                   Hardware.Type.KEYPAD,
                                   Hardware.Type.LAPTOP):
                if devinfo.interface_number != 2:
                    continue
            elif hardware.type in (Hardware.Type.MOUSE,
                                   Hardware.Type.MOUSEPAD):
                if devinfo.interface_number != 1:
                    continue
            else:
                if devinfo.interface_number != 0:
                    continue

            device = self._create_device(parent, hardware, devinfo)
            if device is not None:
                self._devices[device.key] = device

                if hardware.type == Hardware.Type.KEYBOARD:
                    device.set_device_mode(0)

                if self._monitor and self._callbacks:
                    ensure_future(self._fire_callbacks('add', device), loop=self._loop)


    def _next_index(self):
        if not self._devices:
            return 0

        indexes = [device.device_index for device in self._devices.values()]
        indexes.sort()
        for idx, _ in enumerate(indexes):
            if idx + 1 == len(indexes):
                return _ + 1
            if _ + 1 == indexes[idx + 1]:
                continue
            return _ + 1
        raise ValueError('should not be here')


    def _create_device(self, parent, hardware, devinfo):
        input_devs = self._get_input_devices(parent)
        sys_path = parent.sys_path
        index = self._next_index()

        if hardware.type == Hardware.Type.MOUSE:
            if hardware.has_quirk(Quirks.WIRELESS):
                return UChromaWirelessMouse(hardware, devinfo, index, sys_path, input_devs)
            return UChromaMouse(hardware, devinfo, index, sys_path, input_devs)

        if hardware.type == Hardware.Type.LAPTOP:
            return UChromaLaptop(hardware, devinfo, index, sys_path, input_devs)

        if hardware.type == Hardware.Type.KEYBOARD:
            input_devs = self._get_input_devices(parent)
            return UChromaKeyboard(hardware, devinfo, index, sys_path, input_devs)

        if hardware.type == Hardware.Type.KEYPAD:
            input_devs = self._get_input_devices(parent)
            return UChromaKeypad(hardware, devinfo, index, sys_path, input_devs)

        if hardware.type == Hardware.Type.HEADSET:
            return UChromaHeadset(hardware, devinfo, index, sys_path)


        return UChromaDevice(hardware, devinfo, index, sys_path)