diff --git a/daemon/openrazer_daemon/dbus_services/dbus_methods/mamba.py b/daemon/openrazer_daemon/dbus_services/dbus_methods/mamba.py
index 010677af3d619cbebc568ef9383a6a277baffbc9..527c4f80f9ccbac42769eb642f6e0a90b7a42e54 100644
--- a/daemon/openrazer_daemon/dbus_services/dbus_methods/mamba.py
+++ b/daemon/openrazer_daemon/dbus_services/dbus_methods/mamba.py
@@ -220,6 +220,54 @@ def get_dpi_xy(self):
     return dpi
 
 
+@endpoint('razer.device.dpi', 'setDPIStages', in_sig='ya(qq)')
+def set_dpi_stages(self, active_stage, dpi_stages):
+    """
+    Set the DPI on the mouse, Takes in pairs of 2 bytes big-endian
+
+    :param active_stage: DPI stage to enable
+    :param dpi_stages: pairs of dpi X and dpi Y for each stage
+    :type dpi_stages: list of (int, int)
+    """
+    self.logger.debug("DBus call set_dpi_stages")
+
+    driver_path = self.get_driver_path('dpi_stages')
+
+    dpi_bytes = struct.pack('B', active_stage)
+    for dpi_x, dpi_y in dpi_stages:
+        dpi_bytes += struct.pack('>HH', dpi_x, dpi_y)
+
+    with open(driver_path, 'wb') as driver_file:
+        driver_file.write(dpi_bytes)
+
+
+@endpoint('razer.device.dpi', 'getDPIStages', out_sig='(ya(qq))')
+def get_dpi_stages(self):
+    """
+    get the DPI stages on the mouse
+
+    :return: List of X, Y DPI
+    :rtype: (int, list of (int, int))
+    """
+    self.logger.debug("DBus call get_dpi_stages")
+
+    driver_path = self.get_driver_path('dpi_stages')
+
+    dpi_stages = []
+    with open(driver_path, 'rb') as driver_file:
+        result = driver_file.read()
+
+        (active_stage,) = struct.unpack('B', result[:1])
+        result = result[1:]
+
+        while len(result) >= 4:
+            (dpi_x, dpi_y) = struct.unpack('>HH', result[:4])
+            dpi_stages.append((dpi_x, dpi_y))
+            result = result[4:]
+
+    return (active_stage, dpi_stages)
+
+
 @endpoint('razer.device.dpi', 'maxDPI', out_sig='i')
 def max_dpi(self):
     self.logger.debug("DBus call max_dpi")
diff --git a/driver/razerchromacommon.c b/driver/razerchromacommon.c
index 47833ec5f9ba691ea20143f3c065f67d9b62145c..31a44a094472a6579dea9b9a8d7022e30d958ed5 100644
--- a/driver/razerchromacommon.c
+++ b/driver/razerchromacommon.c
@@ -1119,6 +1119,65 @@ struct razer_report razer_chroma_misc_get_dpi_xy_byte(void)
     return report;
 }
 
+/**
+ * Set DPI stages of the device.
+ *
+ * count is the numer of stages to set.
+ * active_stage selected stage number.
+ * dpi is an array of size 2 * count containing pairs of dpi x and dpi y
+ * values, one pair for each stage.
+ *
+ * E.g.:
+ *   count = 3
+ *   active_stage = 1
+ *   dpi = [ 800, 800, 1800, 1800, 3200, 3200]
+ *         | stage 1*|  stage 2  |  stage 3  |
+ */
+struct razer_report razer_chroma_misc_set_dpi_stages(unsigned char variable_storage, unsigned char count, unsigned char active_stage, const unsigned short *dpi)
+{
+    struct razer_report report = get_razer_report(0x04, 0x06, 0x26);
+    unsigned int offset;
+    unsigned int i;
+
+    report.arguments[0] = variable_storage;
+    report.arguments[1] = active_stage;
+    report.arguments[2] = count;
+
+    offset = 3;
+    for (i = 0; i < count; i++) {
+        // Stage number
+        report.arguments[offset++] = i;
+
+        // DPI X
+        report.arguments[offset++] = (dpi[0] >> 8) & 0x00FF;
+        report.arguments[offset++] = dpi[0] & 0x00FF;
+
+        // DPI Y
+        report.arguments[offset++] = (dpi[1] >> 8) & 0x00FF;
+        report.arguments[offset++] = dpi[1] & 0x00FF;
+
+        // Reserved
+        report.arguments[offset++] = 0;
+        report.arguments[offset++] = 0;
+
+        dpi += 2;
+    }
+
+    return report;
+}
+
+/**
+ * Get the DPI stages of the device
+ */
+struct razer_report razer_chroma_misc_get_dpi_stages(unsigned char variable_storage)
+{
+    struct razer_report report = get_razer_report(0x04, 0x86, 0x26);
+
+    report.arguments[0] = variable_storage;
+
+    return report;
+}
+
 /**
  * Get device idle time
  */
diff --git a/driver/razerchromacommon.h b/driver/razerchromacommon.h
index bd1889389f7ef5dbb409e50b4d04e32b87110633..dc496149d1875d993edff09cfe27f5fcdf287499 100644
--- a/driver/razerchromacommon.h
+++ b/driver/razerchromacommon.h
@@ -116,6 +116,9 @@ struct razer_report razer_chroma_misc_get_dpi_xy(unsigned char variable_storage)
 struct razer_report razer_chroma_misc_set_dpi_xy_byte(unsigned char dpi_x,unsigned char dpi_y);
 struct razer_report razer_chroma_misc_get_dpi_xy_byte(void);
 
+struct razer_report razer_chroma_misc_set_dpi_stages(unsigned char variable_storage, unsigned char count, unsigned char active_stage, const unsigned short *dpi);
+struct razer_report razer_chroma_misc_get_dpi_stages(unsigned char variable_storage);
+
 struct razer_report razer_chroma_misc_get_idle_time(void);
 struct razer_report razer_chroma_misc_set_idle_time(unsigned short idle_time);
 
diff --git a/driver/razermouse_driver.c b/driver/razermouse_driver.c
index d999584204fbba03ce0e4a8325685b33ea57e02f..e5f8fd890eebc65f2ee39314ecd5ab02d2f3dff7 100644
--- a/driver/razermouse_driver.c
+++ b/driver/razermouse_driver.c
@@ -1335,6 +1335,150 @@ static ssize_t razer_attr_read_mouse_dpi(struct device *dev, struct device_attri
     return sprintf(buf, "%u:%u\n", dpi_x, dpi_y);
 }
 
+/**
+ * Write device file "dpi_stages"
+ *
+ * Sets the mouse DPI stage.
+ * The number of DPI stages is hard limited by RAZER_MOUSE_MAX_DPI_STAGES.
+ *
+ * Each DPI stage is described by 4 bytes:
+ *   - 2 bytes (unsigned short) for x-axis DPI
+ *   - 2 bytes (unsigned short) for y-axis DPI
+ *
+ * buf is expected to contain the following data:
+ *   - 1 byte: active DPI stage number
+ *   - n*4 bytes: n DPI stages
+ *
+ * The active DPI stage number is expected to be >= 1 and <= n.
+ * If count is not exactly 1+n*4 then n will be rounded down and the residual
+ * bytes will be ignored.
+ *
+ * Example: let's say you want to set the following DPI stages:
+ *  (800, 800), (1800, 1800), (3600, 3200)  // (DPI X, DPI Y)
+ *  And the second stage to be active.
+ *
+ * You have to write to this file 1 byte and 6 unsigned shorts (big endian) = 13 bytes:
+ *   Active stage: 2
+ *   DPIs:          | 800 | 800 | 1800 | 1800 | 3600 | 3200
+ *   Bytes (hex): 02 03 20 03 02 07 08  07 08  0e 10  0c 80
+ */
+static ssize_t razer_attr_write_mouse_dpi_stages(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
+{
+    struct razer_mouse_device *device = dev_get_drvdata(dev);
+    struct razer_report report = {0};
+    unsigned short dpi[2 * RAZER_MOUSE_MAX_DPI_STAGES] = {0};
+    unsigned char stages_count = 0;
+    unsigned char active_stage;
+    size_t remaining = count;
+
+    if (remaining < 5) {
+        printk(KERN_ALERT "razermouse: At least one DPI stage expected\n");
+        return -EINVAL;
+    }
+
+    active_stage = buf[0];
+    remaining++;
+    buf++;
+
+    if (active_stage < 1) {
+        printk(KERN_ALERT "razermouse: Invalid active DPI stage: %u < 1\n", active_stage);
+        return -EINVAL;
+    }
+
+    while (stages_count < RAZER_MOUSE_MAX_DPI_STAGES && remaining >= 4) {
+        // DPI X
+        dpi[stages_count * 2]     = (buf[0] << 8) | (buf[1] & 0xFF);
+
+        // DPI Y
+        dpi[stages_count * 2 + 1] = (buf[2] << 8) | (buf[3] & 0xFF);
+
+        stages_count += 1;
+        buf += 4;
+        remaining -= 4;
+    }
+
+    if (active_stage > stages_count) {
+        printk(KERN_ALERT "razermouse: Invalid active DPI stage: %u > %u\n", active_stage, stages_count);
+        return -EINVAL;
+    }
+
+    report = razer_chroma_misc_set_dpi_stages(VARSTORE, stages_count, active_stage, dpi);
+
+    razer_send_payload(device->usb_dev, &report);
+
+    // Always return count, otherwise some programs can enter an infinite loop.
+    // Example:
+    // Program writes 7 bytes to dpi_stages. 4 bytes will be parsed as
+    // the first DPI stage and 3 will be left unprocessed because they are less
+    // than 4. The program will try to write the 3 bytes again but this
+    // function will always return 0, throwing the program into a loop.
+    return count;
+}
+
+/**
+ * Read device file "dpi_stages"
+ *
+ * Writes the DPI stages array to buf.
+ *
+ * Each DPI stage is described by 4 bytes:
+ *   - 2 bytes (unsigned short) for x-axis DPI
+ *   - 2 bytes (unsigned short) for y-axis DPI
+ *
+ * Always writes 1+n*4 bytes:
+ *   - 1 byte: active DPI stage number, >= 0 and <= n.
+ *   - n*4 bytes: n DPI stages.
+ */
+static ssize_t razer_attr_read_mouse_dpi_stages(struct device *dev, struct device_attribute *attr, char *buf)
+{
+    struct razer_mouse_device *device = dev_get_drvdata(dev);
+    struct razer_report report = {0};
+    struct razer_report response = {0};
+    unsigned char stages_count;
+    ssize_t count;                 // bytes written
+    unsigned int i;                // iterator over stages_count
+    unsigned char *args;           // pointer to the next dpi value in response.arguments
+
+    report = razer_chroma_misc_get_dpi_stages(VARSTORE);
+    response = razer_send_payload(device->usb_dev, &report);
+
+    // Response format (hex):
+    // 01    varstore
+    // 02    active DPI stage
+    // 04    number of stages = 4
+    //
+    // 01    first DPI stage
+    // 03 20 first stage DPI X = 800
+    // 03 20 first stage DPI Y = 800
+    // 00 00 reserved
+    //
+    // 02    second DPI stage
+    // 07 08 second stage DPI X = 1800
+    // 07 08 second stage DPI Y = 1800
+    // 00 00 reserved
+    //
+    // 03    third DPI stage
+    // ...
+
+    stages_count = response.arguments[2];
+
+    buf[0] = response.arguments[1];
+
+    count = 1;
+    args = response.arguments + 4;
+    for (i = 0; i < stages_count; i++) {
+        // Check that we don't read past response.data_size
+        if (args + 4 > response.arguments + response.data_size) {
+            break;
+        }
+
+        memcpy(buf + count, args, 4);
+        count += 4;
+        args += 7;
+    }
+
+    return count;
+}
+
 /**
  * Read device file "device_idle_time"
  *
@@ -3143,6 +3287,7 @@ static DEVICE_ATTR(firmware_version,          0440, razer_attr_read_get_firmware
 static DEVICE_ATTR(test,                      0220, NULL,                                  razer_attr_write_test);
 static DEVICE_ATTR(poll_rate,                 0660, razer_attr_read_poll_rate,             razer_attr_write_poll_rate);
 static DEVICE_ATTR(dpi,                       0660, razer_attr_read_mouse_dpi,             razer_attr_write_mouse_dpi);
+static DEVICE_ATTR(dpi_stages,                0660, razer_attr_read_mouse_dpi_stages,      razer_attr_write_mouse_dpi_stages);
 
 static DEVICE_ATTR(device_type,               0440, razer_attr_read_device_type,           NULL);
 static DEVICE_ATTR(device_mode,               0660, razer_attr_read_device_mode,           razer_attr_write_device_mode);
diff --git a/driver/razermouse_driver.h b/driver/razermouse_driver.h
index 78d80c98a3286e5cc5f5cdc65194dfec4a6a6a6e..f30de4ba681cbb0c89579071d2f5896c8624ba50 100644
--- a/driver/razermouse_driver.h
+++ b/driver/razermouse_driver.h
@@ -82,6 +82,8 @@
 #define RAZER_VIPER_MOUSE_RECEIVER_WAIT_MIN_US 59900
 #define RAZER_VIPER_MOUSE_RECEIVER_WAIT_MAX_US 60000
 
+#define RAZER_MOUSE_MAX_DPI_STAGES 5
+
 struct razer_mouse_device {
     struct usb_device *usb_dev;
     struct mutex lock;
diff --git a/pylib/openrazer/client/devices/mice.py b/pylib/openrazer/client/devices/mice.py
index 0da46e62c3a114336853b5fa72045be508a71a1e..3f05ff22b7f645ffdc52c5062d5a050ff1ad7c17 100644
--- a/pylib/openrazer/client/devices/mice.py
+++ b/pylib/openrazer/client/devices/mice.py
@@ -14,6 +14,7 @@ class RazerMouse(__RazerDevice):
         # Capabilities
         self._capabilities['poll_rate'] = self._has_feature('razer.device.misc', ('getPollRate', 'setPollRate'))
         self._capabilities['dpi'] = self._has_feature('razer.device.dpi', ('getDPI', 'setDPI'))
+        self._capabilities['dpi_stages'] = self._has_feature('razer.device.dpi', ('getDPIStages', 'setDPIStages'))
         self._capabilities['available_dpi'] = self._has_feature('razer.device.dpi', 'availableDPI')
         self._capabilities['battery'] = self._has_feature('razer.device.power', 'getBattery')
 
@@ -83,20 +84,108 @@ class RazerMouse(__RazerDevice):
         if self.has('dpi'):
             if len(value) != 2:
                 raise ValueError("DPI tuple is not of length 2. Length: {0}".format(len(value)))
+            max_dpi = self.max_dpi
             dpi_x, dpi_y = value
 
             if not isinstance(dpi_x, int) or not isinstance(dpi_y, int):
                 raise ValueError("DPI X or Y is not an integer, X:{0} Y:{1}".format(type(dpi_x), type(dpi_y)))
 
-            if dpi_x < 0 or dpi_x > 16000:  # TODO add in max dpi option
+            if dpi_x < 0 or dpi_x > max_dpi:
                 raise ValueError("DPI X either too small or too large, X:{0}".format(dpi_x))
-            if dpi_y < 0 or dpi_y > 16000:  # TODO add in max dpi option
+            if dpi_y < 0 or dpi_y > max_dpi:
                 raise ValueError("DPI Y either too small or too large, Y:{0}".format(dpi_y))
 
             self._dbus_interfaces['dpi'].setDPI(dpi_x, dpi_y)
         else:
             raise NotImplementedError()
 
+    @property
+    def dpi_stages(self) -> (int, list):
+        """
+        Get mouse DPI stages
+
+        Will return a tuple containing the active DPI stage number and the list
+        of DPI stages as tuples.
+        The active DPI stage number must be: >= 1 and <= nr of DPI stages.
+        :return: active DPI stage number and DPI stages
+                 (1, [(500, 500), (1000, 1000), (2000, 2000) ...]
+        :rtype: (int, list)
+
+        :raises NotImplementedError: if function is not supported
+        """
+        if self.has('dpi_stages'):
+            response = self._dbus_interfaces['dpi'].getDPIStages()
+            dpi_stages = []
+
+            active_stage = int(response[0])
+
+            for dpi_x, dpi_y in response[1]:
+                dpi_stages.append((int(dpi_x), int(dpi_y)))
+
+            return (active_stage, dpi_stages)
+        else:
+            raise NotImplementedError()
+
+    @dpi_stages.setter
+    def dpi_stages(self, value: (int, list)):
+        """
+        Set mouse DPI stages
+
+        Daemon does type validation but can't be too careful
+        :param value: active DPI stage number and list of DPI X, Y tuples
+        :type value: (int, list)
+
+        :raises ValueError: when the input is invalid
+        :raises NotImplementedError: If function is not supported
+        """
+        if self.has('dpi_stages'):
+            max_dpi = self.max_dpi
+            dpi_stages = []
+
+            active_stage = value[0]
+            if not isinstance(active_stage, int):
+                raise ValueError(
+                    "Active DPI stage is not an integer: {0}".format(
+                        type(active_stage)))
+
+            if active_stage < 1:
+                raise ValueError(
+                    "Active DPI stage has invalid value: {0} < 1".format(
+                        active_stage))
+
+            for stage in value[1]:
+                if len(stage) != 2:
+                    raise ValueError(
+                        "DPI tuple is not of length 2. Length: {0}".format(
+                            len(stage)))
+
+                dpi_x, dpi_y = stage
+
+                if not isinstance(dpi_x, int) or not isinstance(dpi_y, int):
+                    raise ValueError(
+                        "DPI X or Y is not an integer, X:{0} Y:{1}".format(
+                            type(dpi_x), type(dpi_y)))
+
+                if dpi_x < 0 or dpi_x > max_dpi:
+                    raise ValueError(
+                        "DPI X either too small or too large, X:{0}".format(
+                            dpi_x))
+                if dpi_y < 0 or dpi_y > max_dpi:
+                    raise ValueError(
+                        "DPI Y either too small or too large, Y:{0}".format(
+                            dpi_y))
+
+                dpi_stages.append((dpi_x, dpi_y))
+
+            if active_stage > len(dpi_stages):
+                raise ValueError(
+                    "Active DPI stage has invalid value: {0} > {1}".format(
+                        active_stage, len(dpi_stages)))
+
+            self._dbus_interfaces['dpi'].setDPIStages(active_stage, dpi_stages)
+        else:
+            raise NotImplementedError()
+
     @property
     def poll_rate(self) -> int:
         """