From 03df9f3bde2a3e7e47153c2e142d942da9d68564 Mon Sep 17 00:00:00 2001
From: tildearrow <tildearrow@protonmail.com>
Date: Mon, 29 Jul 2019 15:38:14 -0500
Subject: [PATCH] Add persistent storage of effects in daemon

This patch adds several D-Bus methods for accessing the current state of
the devices (plus pylib API for the new functionality):
* get*Effect
* get*EffectColors
* get*EffectSpeed
* get*WaveDir
* restoreLastEffect

The star means e.g. getEffect, getLogoEffect, getScrollEffect, etc.

The last one can be used to reset to the previous effect after setting a
custom effect (which won't be persisted).

Co-authored-by: Luke Horwell <code@horwell.me>
Co-authored-by: Luca Weiss <luca@z3ntu.xyz>
---
 daemon/openrazer_daemon/daemon.py             |  87 ++-
 .../dbus_services/dbus_methods/bw2013.py      |  23 +-
 .../dbus_methods/chroma_keyboard.py           |  78 ++-
 .../dbus_methods/deathadder_chroma.py         | 132 ++---
 .../dbus_services/dbus_methods/kraken.py      |  66 ---
 .../dbus_services/dbus_methods/lanceheadte.py |  94 ++-
 .../dbus_services/dbus_methods/mamba.py       |  39 +-
 .../dbus_services/dbus_methods/nagahexv2.py   |  50 ++
 .../openrazer_daemon/hardware/device_base.py  | 538 +++++++++++++++++-
 daemon/openrazer_daemon/hardware/headsets.py  |  93 +--
 daemon/openrazer_daemon/hardware/keyboards.py |  22 +-
 daemon/openrazer_daemon/hardware/mouse.py     |  16 +-
 .../misc/autosave_persistence.py              |  36 ++
 daemon/resources/man/openrazer-daemon.8       |  10 +-
 daemon/resources/man/openrazer-daemon.8.scd   |   3 +
 daemon/run_openrazer_daemon.py                |  21 +
 examples/custom_starlight.py                  |  25 +-
 pylib/openrazer/client/fx.py                  |  86 +++
 18 files changed, 1127 insertions(+), 292 deletions(-)
 create mode 100644 daemon/openrazer_daemon/misc/autosave_persistence.py

diff --git a/daemon/openrazer_daemon/daemon.py b/daemon/openrazer_daemon/daemon.py
index 51cc88de..e67974fa 100644
--- a/daemon/openrazer_daemon/daemon.py
+++ b/daemon/openrazer_daemon/daemon.py
@@ -29,6 +29,7 @@ import openrazer_daemon.hardware
 from openrazer_daemon.dbus_services.service import DBusService
 from openrazer_daemon.device import DeviceCollection
 from openrazer_daemon.misc.screensaver_monitor import ScreensaverMonitor
+from openrazer_daemon.misc.autosave_persistence import PersistenceAutoSave
 
 
 class RazerDaemon(DBusService):
@@ -46,7 +47,7 @@ class RazerDaemon(DBusService):
 
     BUS_NAME = 'org.razer'
 
-    def __init__(self, verbose=False, log_dir=None, console_log=False, run_dir=None, config_file=None, test_dir=None):
+    def __init__(self, verbose=False, log_dir=None, console_log=False, run_dir=None, config_file=None, persistence_file=None, test_dir=None):
 
         setproctitle.setproctitle('openrazer-daemon')  # pylint: disable=no-member
 
@@ -68,12 +69,24 @@ class RazerDaemon(DBusService):
                 print("Config file {} does not exist.".format(config_file), file=sys.stderr)
                 sys.exit(1)
 
+        if persistence_file is not None:
+            persistence_file = os.path.expanduser(persistence_file)
+            if not os.path.exists(persistence_file):
+                print("Persistence file {} does not exist.".format(persistence_file), file=sys.stderr)
+                sys.exit(1)
+
         self._test_dir = test_dir
         self._run_dir = run_dir
+
         self._config_file = config_file
         self._config = configparser.ConfigParser()
         self.read_config(config_file)
 
+        self._persistence_file = persistence_file
+        self._persistence = configparser.ConfigParser()
+        self._persistence.status = {"changed": False}
+        self.read_persistence(persistence_file)
+
         # Logging
         log_level = logging.INFO
         if verbose or self._config.getboolean('General', 'verbose_logging'):
@@ -126,6 +139,8 @@ class RazerDaemon(DBusService):
         self._collecting_udev = False
         self._collecting_udev_devices = []
 
+        self._init_autosave_persistence()
+
         # TODO remove
         self.sync_effects(self._config.getboolean('Startup', 'sync_effects_enabled'))
         # TODO ======
@@ -200,6 +215,16 @@ class RazerDaemon(DBusService):
         except dbus.exceptions.DBusException as e:
             self.logger.error("Failed to init ScreensaverMonitor: {}".format(e))
 
+    def _init_autosave_persistence(self):
+        if not self._persistence:
+            self.logger.debug("Persistence unspecified. Will not create auto save thread")
+            return
+
+        self._autosave_persistence = PersistenceAutoSave(self._persistence, self._persistence_file, self._persistence.status, self.logger, 10, self.write_persistence)
+        self._autosave_persistence.thread = threading.Thread(target=self._autosave_persistence.watch)
+        self._autosave_persistence.thread.daemon = True
+        self._autosave_persistence.thread.start()
+
     def _init_signals(self):
         """
         Heinous hack to properly handle signals on the mainloop. Necessary
@@ -254,16 +279,64 @@ class RazerDaemon(DBusService):
         for section in ('General', 'Startup', 'Statistics'):
             self._config[section] = {}
 
-        self._config['DEFAULT'] = {
+        self._config['General'] = {
             'verbose_logging': False,
+        }
+        self._config['Startup'] = {
             'sync_effects_enabled': True,
             'devices_off_on_screensaver': True,
-            'key_statistics': False,
+            'mouse_battery_notifier': True,
+        }
+        self._config['Statistics'] = {
+            'key_statistics': True,
         }
 
         if config_file is not None and os.path.exists(config_file):
             self._config.read(config_file)
 
+    def read_persistence(self, persistence_file):
+        """
+        Read the persistence file and set states into memory
+
+        :param persistence_file: Persistence file
+        :type persistence_file: str or None
+        """
+        if persistence_file is not None and os.path.exists(persistence_file):
+            self._persistence.read(persistence_file)
+
+    def write_persistence(self, persistence_file):
+        """
+        Write in the persistence file
+
+        :param persistence_file: Persistence file
+        :type persistence_file: str or None
+        """
+        if not persistence_file:
+            return
+
+        self.logger.debug('Writing persistence config')
+
+        for device in self._razer_devices:
+            self._persistence[device.dbus.storage_name] = {}
+            if 'set_dpi_xy' in device.dbus.METHODS:
+                self._persistence[device.dbus.storage_name]['dpi_x'] = str(device.dbus.dpi[0])
+                self._persistence[device.dbus.storage_name]['dpi_y'] = str(device.dbus.dpi[1])
+
+            if 'set_poll_rate' in device.dbus.METHODS:
+                self._persistence[device.dbus.storage_name]['poll_rate'] = str(device.dbus.poll_rate)
+
+            for i in device.dbus.ZONES:
+                if device.dbus.zone[i]["present"]:
+                    self._persistence[device.dbus.storage_name][i + '_active'] = str(device.dbus.zone[i]["active"])
+                    self._persistence[device.dbus.storage_name][i + '_brightness'] = str(device.dbus.zone[i]["brightness"])
+                    self._persistence[device.dbus.storage_name][i + '_effect'] = device.dbus.zone[i]["effect"]
+                    self._persistence[device.dbus.storage_name][i + '_colors'] = ' '.join(str(i) for i in device.dbus.zone[i]["colors"])
+                    self._persistence[device.dbus.storage_name][i + '_speed'] = str(device.dbus.zone[i]["speed"])
+                    self._persistence[device.dbus.storage_name][i + '_wave_dir'] = str(device.dbus.zone[i]["wave_dir"])
+
+        with open(persistence_file, 'w') as cf:
+            self._persistence.write(cf)
+
     def get_off_on_screensaver(self):
         """
         Returns if turn off on screensaver
@@ -406,7 +479,7 @@ class RazerDaemon(DBusService):
                         self.logger.critical("Could not access {0}/device_type, file is not owned by plugdev".format(sys_path))
                         break
 
-                    razer_device = device_class(sys_path, device_number, self._config, testing=self._test_dir is not None, additional_interfaces=sorted(additional_interfaces))
+                    razer_device = device_class(sys_path, device_number, self._config, self._persistence, testing=self._test_dir is not None, additional_interfaces=sorted(additional_interfaces))
 
                     # Wireless devices sometimes don't listen
                     count = 0
@@ -442,7 +515,7 @@ class RazerDaemon(DBusService):
 
             if device_class.match(sys_name, sys_path):  # Check it matches sys/ ID format and has device_type file
                 self.logger.info('Found valid device.%d: %s', device_number, sys_name)
-                razer_device = device_class(sys_path, device_number, self._config, testing=self._test_dir is not None)
+                razer_device = device_class(sys_path, device_number, self._config, self._persistence, testing=self._test_dir is not None)
 
                 # Its a udev event so currently the device hasn't been chmodded yet
                 time.sleep(0.2)
@@ -479,6 +552,7 @@ class RazerDaemon(DBusService):
 
             device.dbus.close()
             device.dbus.remove_from_connection()
+            self.write_persistence(self._persistence_file)
             self.logger.warning("Removing %s", device_id)
 
             # Delete device
@@ -555,3 +629,6 @@ class RazerDaemon(DBusService):
 
         for device in self._razer_devices:
             device.dbus.close()
+
+        # Write config
+        self.write_persistence(self._persistence_file)
diff --git a/daemon/openrazer_daemon/dbus_services/dbus_methods/bw2013.py b/daemon/openrazer_daemon/dbus_services/dbus_methods/bw2013.py
index bd67c730..3fc7e457 100644
--- a/daemon/openrazer_daemon/dbus_services/dbus_methods/bw2013.py
+++ b/daemon/openrazer_daemon/dbus_services/dbus_methods/bw2013.py
@@ -4,23 +4,6 @@ BlackWidow Ultimate 2013 effects
 from openrazer_daemon.dbus_services import endpoint
 
 
-@endpoint('razer.device.lighting.bw2013', 'getEffect', out_sig='y')
-def bw_get_effect(self):
-    """
-    Get current effect
-
-    :return: Brightness
-    :rtype: int
-    """
-    self.logger.debug("DBus call bw_get_effect")
-
-    driver_path = self.get_driver_path('matrix_effect_pulsate')
-
-    with open(driver_path, 'r') as driver_file:
-        brightness = int(driver_file.read().strip())
-        return brightness
-
-
 @endpoint('razer.device.lighting.bw2013', 'setPulsate')
 def bw_set_pulsate(self):
     """
@@ -30,6 +13,9 @@ def bw_set_pulsate(self):
 
     driver_path = self.get_driver_path('matrix_effect_pulsate')
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'pulsate')
+
     with open(driver_path, 'w') as driver_file:
         driver_file.write('1')
 
@@ -46,6 +32,9 @@ def bw_set_static(self):
 
     driver_path = self.get_driver_path('matrix_effect_static')
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'static')
+
     with open(driver_path, 'w') as driver_file:
         driver_file.write('1')
 
diff --git a/daemon/openrazer_daemon/dbus_services/dbus_methods/chroma_keyboard.py b/daemon/openrazer_daemon/dbus_services/dbus_methods/chroma_keyboard.py
index cdee1185..7acbc51c 100644
--- a/daemon/openrazer_daemon/dbus_services/dbus_methods/chroma_keyboard.py
+++ b/daemon/openrazer_daemon/dbus_services/dbus_methods/chroma_keyboard.py
@@ -15,14 +15,7 @@ def get_brightness(self):
     """
     self.logger.debug("DBus call get_brightness")
 
-    driver_path = self.get_driver_path('matrix_brightness')
-
-    with open(driver_path, 'r') as driver_file:
-        brightness = round(float(driver_file.read()) * (100.0 / 255.0), 2)
-
-        self.method_args['brightness'] = brightness
-
-        return brightness
+    return self.zone["backlight"]["brightness"]
 
 
 @endpoint('razer.device.lighting.brightness', 'setBrightness', in_sig='d')
@@ -39,12 +32,15 @@ def set_brightness(self, brightness):
 
     self.method_args['brightness'] = brightness
 
-    brightness = int(round(brightness * (255.0 / 100.0)))
-    if brightness > 255:
-        brightness = 255
+    if brightness > 100:
+        brightness = 100
     elif brightness < 0:
         brightness = 0
 
+    self.set_persistence("backlight", "brightness", brightness)
+
+    brightness = int(round(brightness * (255.0 / 100.0)))
+
     with open(driver_path, 'w') as driver_file:
         driver_file.write(str(brightness))
 
@@ -181,6 +177,10 @@ def set_wave_effect(self, direction):
     # Notify others
     self.send_effect_event('setWave', direction)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'wave')
+    self.set_persistence("backlight", "wave_dir", int(direction))
+
     driver_path = self.get_driver_path('matrix_effect_wave')
 
     if direction not in self.WAVE_DIRS:
@@ -209,6 +209,10 @@ def set_static_effect(self, red, green, blue):
     # Notify others
     self.send_effect_event('setStatic', red, green, blue)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'static')
+    self.zone["backlight"]["colors"][0:3] = int(red), int(green), int(blue)
+
     driver_path = self.get_driver_path('matrix_effect_static')
 
     payload = bytes([red, green, blue])
@@ -236,6 +240,10 @@ def set_blinking_effect(self, red, green, blue):
     # Notify others
     self.send_effect_event('setBlinking', red, green, blue)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'blinking')
+    self.zone["backlight"]["colors"][0:3] = int(red), int(green), int(blue)
+
     driver_path = self.get_driver_path('matrix_effect_blinking')
 
     payload = bytes([red, green, blue])
@@ -254,6 +262,9 @@ def set_spectrum_effect(self):
     # Notify others
     self.send_effect_event('setSpectrum')
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'spectrum')
+
     driver_path = self.get_driver_path('matrix_effect_spectrum')
 
     with open(driver_path, 'w') as driver_file:
@@ -270,6 +281,9 @@ def set_none_effect(self):
     # Notify others
     self.send_effect_event('setNone')
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'none')
+
     driver_path = self.get_driver_path('matrix_effect_none')
 
     with open(driver_path, 'w') as driver_file:
@@ -316,9 +330,15 @@ def set_reactive_effect(self, red, green, blue, speed):
     # Notify others
     self.send_effect_event('setReactive', red, green, blue, speed)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'reactive')
+    self.zone["backlight"]["colors"][0:3] = int(red), int(green), int(blue)
+
     if speed not in (1, 2, 3, 4):
         speed = 4
 
+    self.set_persistence("backlight", "speed", int(speed))
+
     payload = bytes([speed, red, green, blue])
 
     with open(driver_path, 'wb') as driver_file:
@@ -335,6 +355,9 @@ def set_breath_random_effect(self):
     # Notify others
     self.send_effect_event('setBreathRandom')
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'breathRandom')
+
     driver_path = self.get_driver_path('matrix_effect_breath')
 
     payload = b'1'
@@ -362,6 +385,10 @@ def set_breath_single_effect(self, red, green, blue):
     # Notify others
     self.send_effect_event('setBreathSingle', red, green, blue)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'breathSingle')
+    self.zone["backlight"]["colors"][0:3] = int(red), int(green), int(blue)
+
     driver_path = self.get_driver_path('matrix_effect_breath')
 
     payload = bytes([red, green, blue])
@@ -398,6 +425,10 @@ def set_breath_dual_effect(self, red1, green1, blue1, red2, green2, blue2):
     # Notify others
     self.send_effect_event('setBreathDual', red1, green1, blue1, red2, green2, blue2)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'breathDual')
+    self.zone["backlight"]["colors"][0:6] = int(red1), int(green1), int(blue1), int(red2), int(green2), int(blue2)
+
     driver_path = self.get_driver_path('matrix_effect_breath')
 
     payload = bytes([red1, green1, blue1, red2, green2, blue2])
@@ -443,6 +474,10 @@ def set_breath_triple_effect(self, red1, green1, blue1, red2, green2, blue2, red
     # Notify others
     self.send_effect_event('setBreathTriple', red1, green1, blue1, red2, green2, blue2, red3, green3, blue3)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'breathTriple')
+    self.zone["backlight"]["colors"][0:9] = int(red1), int(green1), int(blue1), int(red2), int(green2), int(blue2), int(red3), int(green3), int(blue3)
+
     driver_path = self.get_driver_path('matrix_effect_breath')
 
     payload = bytes([red1, green1, blue1, red2, green2, blue2, red3, green3, blue3])
@@ -513,6 +548,10 @@ def set_ripple_effect(self, red, green, blue, refresh_rate):
     # Notify others
     self.send_effect_event('setRipple', red, green, blue, refresh_rate)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'ripple')
+    self.zone["backlight"]["colors"][0:3] = int(red), int(green), int(blue)
+
 
 @endpoint('razer.device.lighting.custom', 'setRippleRandomColour', in_sig='d')
 def set_ripple_effect_random_colour(self, refresh_rate):
@@ -527,6 +566,9 @@ def set_ripple_effect_random_colour(self, refresh_rate):
     # Notify others
     self.send_effect_event('setRipple', None, None, None, refresh_rate)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'rippleRandomColour')
+
 
 @endpoint('razer.device.lighting.chroma', 'setStarlightRandom', in_sig='y')
 def set_starlight_random_effect(self, speed):
@@ -543,6 +585,10 @@ def set_starlight_random_effect(self, speed):
     # Notify others
     self.send_effect_event('setStarlightRandom')
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'starlightRandom')
+    self.set_persistence("backlight", "speed", speed)
+
 
 @endpoint('razer.device.lighting.chroma', 'setStarlightSingle', in_sig='yyyy')
 def set_starlight_single_effect(self, red, green, blue, speed):
@@ -559,6 +605,11 @@ def set_starlight_single_effect(self, red, green, blue, speed):
     # Notify others
     self.send_effect_event('setStarlightSingle', red, green, blue, speed)
 
+    # remember effect
+    self.set_persistence("backlight", "effect", 'starlightSingle')
+    self.set_persistence("backlight", "speed", speed)
+    self.zone["backlight"]["colors"][0:3] = int(red), int(green), int(blue)
+
 
 @endpoint('razer.device.lighting.chroma', 'setStarlightDual', in_sig='yyyyyyy')
 def set_starlight_dual_effect(self, red1, green1, blue1, red2, green2, blue2, speed):
@@ -574,3 +625,8 @@ def set_starlight_dual_effect(self, red1, green1, blue1, red2, green2, blue2, sp
 
     # Notify others
     self.send_effect_event('setStarlightDual', red1, green1, blue1, red2, green2, blue2, speed)
+
+    # remember effect
+    self.set_persistence("backlight", "effect", 'starlightDual')
+    self.set_persistence("backlight", "speed", speed)
+    self.zone["backlight"]["colors"][0:6] = int(red1), int(green1), int(blue1), int(red2), int(green2), int(blue2)
diff --git a/daemon/openrazer_daemon/dbus_services/dbus_methods/deathadder_chroma.py b/daemon/openrazer_daemon/dbus_services/dbus_methods/deathadder_chroma.py
index 65e627b2..68883734 100644
--- a/daemon/openrazer_daemon/dbus_services/dbus_methods/deathadder_chroma.py
+++ b/daemon/openrazer_daemon/dbus_services/dbus_methods/deathadder_chroma.py
@@ -29,11 +29,7 @@ def get_backlight_active(self):
     """
     self.logger.debug("DBus call get_backlight_active")
 
-    driver_path = self.get_driver_path('backlight_led_state')
-
-    with open(driver_path, 'r') as driver_file:
-        active = int(driver_file.read().strip())
-        return active == 1
+    return self.zone["backlight"]["active"]
 
 
 @endpoint('razer.device.lighting.backlight', 'setBacklightActive', in_sig='b')
@@ -46,6 +42,9 @@ def set_backlight_active(self, active):
     """
     self.logger.debug("DBus call set_backlight_active")
 
+    # remember status
+    self.set_persistence("backlight", "active", bool(active))
+
     driver_path = self.get_driver_path('backlight_led_state')
 
     with open(driver_path, 'w') as driver_file:
@@ -65,11 +64,7 @@ def get_logo_active(self):
     """
     self.logger.debug("DBus call get_logo_active")
 
-    driver_path = self.get_driver_path('logo_led_state')
-
-    with open(driver_path, 'r') as driver_file:
-        active = int(driver_file.read().strip())
-        return active == 1
+    return self.zone["logo"]["active"]
 
 
 @endpoint('razer.device.lighting.logo', 'setLogoActive', in_sig='b')
@@ -82,29 +77,15 @@ def set_logo_active(self, active):
     """
     self.logger.debug("DBus call set_logo_active")
 
+    # remember status
+    self.set_persistence("logo", "active", bool(active))
+
     driver_path = self.get_driver_path('logo_led_state')
 
     with open(driver_path, 'w') as driver_file:
         driver_file.write('1' if active else '0')
 
 
-@endpoint('razer.device.lighting.logo', 'getLogoEffect', out_sig='y')
-def get_logo_effect(self):
-    """
-    Get logo effect
-
-    :return: Active
-    :rtype: bool
-    """
-    self.logger.debug("DBus call get_logo_effect")
-
-    driver_path = self.get_driver_path('logo_led_effect')
-
-    with open(driver_path, 'r') as driver_file:
-        effect = int(driver_file.read().strip())
-        return effect
-
-
 @endpoint('razer.device.lighting.logo', 'getLogoBrightness', out_sig='d')
 def get_logo_brightness(self):
     """
@@ -115,12 +96,7 @@ def get_logo_brightness(self):
     """
     self.logger.debug("DBus call get_logo_brightness")
 
-    driver_path = self.get_driver_path('logo_led_brightness')
-
-    with open(driver_path, 'r') as driver_file:
-        brightness = round(float(driver_file.read()) * (100.0 / 255.0), 2)
-
-        return brightness
+    return self.zone["logo"]["brightness"]
 
 
 @endpoint('razer.device.lighting.logo', 'setLogoBrightness', in_sig='d')
@@ -137,12 +113,15 @@ def set_logo_brightness(self, brightness):
 
     self.method_args['brightness'] = brightness
 
-    brightness = int(round(brightness * (255.0 / 100.0)))
     if brightness > 255:
         brightness = 255
     elif brightness < 0:
         brightness = 0
 
+    self.set_persistence("logo", "brightness", brightness)
+
+    brightness = int(round(brightness * (255.0 / 100.0)))
+
     with open(driver_path, 'w') as driver_file:
         driver_file.write(str(brightness))
 
@@ -169,6 +148,10 @@ def set_logo_static(self, red, green, blue):
     # Notify others
     self.send_effect_event('setStatic', red, green, blue)
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'static')
+    self.zone["logo"]["colors"][0:3] = int(red), int(green), int(blue)
+
     set_led_effect_color_common(self, 'logo', '0', red, green, blue)
 
 
@@ -188,7 +171,7 @@ def set_logo_static_mono(self):
 @endpoint('razer.device.lighting.logo', 'setLogoBlinking', in_sig='yyy')
 def set_logo_blinking(self, red, green, blue):
     """
-    Set the device to pulsate
+    Set the device to blinking
 
     :param red: Red component
     :type red: int
@@ -204,6 +187,10 @@ def set_logo_blinking(self, red, green, blue):
     # Notify others
     self.send_effect_event('setLogoBlinking', red, green, blue)
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'blinking')
+    self.zone["logo"]["colors"][0:3] = int(red), int(green), int(blue)
+
     set_led_effect_color_common(self, 'logo', '1', red, green, blue)
 
 
@@ -226,6 +213,10 @@ def set_logo_pulsate(self, red, green, blue):
     # Notify others
     self.send_effect_event('setPulsate', red, green, blue)
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'pulsate')
+    self.zone["logo"]["colors"][0:3] = int(red), int(green), int(blue)
+
     set_led_effect_color_common(self, 'logo', '2', red, green, blue)
 
 
@@ -245,7 +236,7 @@ def set_logo_pulsate_mono(self):
 @endpoint('razer.device.lighting.logo', 'setLogoSpectrum')
 def set_logo_spectrum(self):
     """
-    Set the device to pulsate
+    Set the device to spectrum
 
     :param red: Red component
     :type red: int
@@ -261,6 +252,9 @@ def set_logo_spectrum(self):
     # Notify others
     self.send_effect_event('setSpectrum')
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'spectrum')
+
     set_led_effect_common(self, 'logo', '4')
 
 
@@ -274,11 +268,7 @@ def get_scroll_active(self):
     """
     self.logger.debug("DBus call get_scroll_active")
 
-    driver_path = self.get_driver_path('scroll_led_state')
-
-    with open(driver_path, 'r') as driver_file:
-        active = int(driver_file.read().strip())
-        return active == 1
+    return self.zone["scroll"]["active"]
 
 
 @endpoint('razer.device.lighting.scroll', 'setScrollActive', in_sig='b')
@@ -291,29 +281,15 @@ def set_scroll_active(self, active):
     """
     self.logger.debug("DBus call set_scroll_active")
 
+    # remember status
+    self.set_persistence("scroll", "active", bool(active))
+
     driver_path = self.get_driver_path('scroll_led_state')
 
     with open(driver_path, 'w') as driver_file:
         driver_file.write('1' if active else '0')
 
 
-@endpoint('razer.device.lighting.scroll', 'getScrollEffect', out_sig='y')
-def get_scroll_effect(self):
-    """
-    Get scroll effect
-
-    :return: Active
-    :rtype: bool
-    """
-    self.logger.debug("DBus call get_scroll_effect")
-
-    driver_path = self.get_driver_path('scroll_led_effect')
-
-    with open(driver_path, 'r') as driver_file:
-        effect = int(driver_file.read().strip())
-        return effect
-
-
 @endpoint('razer.device.lighting.scroll', 'getScrollBrightness', out_sig='d')
 def get_scroll_brightness(self):
     """
@@ -324,12 +300,7 @@ def get_scroll_brightness(self):
     """
     self.logger.debug("DBus call get_scroll_brightness")
 
-    driver_path = self.get_driver_path('scroll_led_brightness')
-
-    with open(driver_path, 'r') as driver_file:
-        brightness = round(float(driver_file.read()) * (100.0 / 255.0), 2)
-
-        return brightness
+    return self.zone["scroll"]["brightness"]
 
 
 @endpoint('razer.device.lighting.scroll', 'setScrollBrightness', in_sig='d')
@@ -346,12 +317,15 @@ def set_scroll_brightness(self, brightness):
 
     self.method_args['brightness'] = brightness
 
-    brightness = int(round(brightness * (255.0 / 100.0)))
     if brightness > 255:
         brightness = 255
     elif brightness < 0:
         brightness = 0
 
+    self.set_persistence("scroll", "brightness", brightness)
+
+    brightness = int(round(brightness * (255.0 / 100.0)))
+
     with open(driver_path, 'w') as driver_file:
         driver_file.write(str(brightness))
 
@@ -378,6 +352,10 @@ def set_scroll_static(self, red, green, blue):
     # Notify others
     self.send_effect_event('setStatic', red, green, blue)
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'static')
+    self.zone["scroll"]["colors"][0:3] = int(red), int(green), int(blue)
+
     set_led_effect_color_common(self, 'scroll', '0', red, green, blue)
 
 
@@ -397,7 +375,7 @@ def set_scroll_static_mono(self):
 @endpoint('razer.device.lighting.scroll', 'setScrollBlinking', in_sig='yyy')
 def set_scroll_blinking(self, red, green, blue):
     """
-    Set the device to pulsate
+    Set the device to blinking
 
     :param red: Red component
     :type red: int
@@ -413,6 +391,10 @@ def set_scroll_blinking(self, red, green, blue):
     # Notify others
     self.send_effect_event('setPulsate', red, green, blue)
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'blinking')
+    self.zone["scroll"]["colors"][0:3] = int(red), int(green), int(blue)
+
     set_led_effect_color_common(self, 'scroll', '1', red, green, blue)
 
 
@@ -435,6 +417,10 @@ def set_scroll_pulsate(self, red, green, blue):
     # Notify others
     self.send_effect_event('setPulsate', red, green, blue)
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'pulsate')
+    self.zone["scroll"]["colors"][0:3] = int(red), int(green), int(blue)
+
     set_led_effect_color_common(self, 'scroll', '2', red, green, blue)
 
 
@@ -454,20 +440,14 @@ def set_scroll_pulsate_mono(self):
 @endpoint('razer.device.lighting.scroll', 'setScrollSpectrum')
 def set_scroll_spectrum(self):
     """
-    Set the device to pulsate
-
-    :param red: Red component
-    :type red: int
-
-    :param green: Green component
-    :type green: int
-
-    :param blue: Blue component
-    :type blue: int
+    Set the device to spectrum
     """
     self.logger.debug("DBus call set_scroll_spectrum")
 
     # Notify others
     self.send_effect_event('setSpectrum')
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'spectrum')
+
     set_led_effect_common(self, 'scroll', '4')
diff --git a/daemon/openrazer_daemon/dbus_services/dbus_methods/kraken.py b/daemon/openrazer_daemon/dbus_services/dbus_methods/kraken.py
index d234b60c..f137ea41 100644
--- a/daemon/openrazer_daemon/dbus_services/dbus_methods/kraken.py
+++ b/daemon/openrazer_daemon/dbus_services/dbus_methods/kraken.py
@@ -4,72 +4,6 @@ Module for kraken methods
 from openrazer_daemon.dbus_services import endpoint
 
 
-@endpoint('razer.device.lighting.kraken', 'getCurrentEffect', out_sig='y')
-def get_current_effect_kraken(self):
-    """
-    Get the device's current effect
-
-    :return: The internal bitfield like 05 (in hex)
-    :rtype: int
-    """
-    self.logger.debug("DBus call matrix_current_effect")
-
-    driver_path = self.get_driver_path('matrix_current_effect')
-
-    with open(driver_path, 'r') as driver_file:
-        return int(driver_file.read().strip(), 16)
-
-
-@endpoint('razer.device.lighting.kraken', 'getStaticArgs', out_sig='ai')
-def get_static_effect_args_kraken(self):
-    """
-    Get the static effect arguments
-
-    :return: List of args used for static effect
-    :rtype: int
-    """
-    self.logger.debug("DBus call get_static_effect_args")
-
-    driver_path = self.get_driver_path('matrix_effect_static')
-
-    with open(driver_path, 'rb') as driver_file:
-        bytestring = driver_file.read()
-        if len(bytestring) != 4:
-            raise ValueError("Response from driver is not valid, should be length 4 got: {0}".format(len(bytestring)))
-        else:
-            return list(bytestring[:3])  # We cut off the intensity value in the end, aint letting people mess with that.
-
-
-@endpoint('razer.device.lighting.kraken', 'getBreathArgs', out_sig='ai')
-def get_breath_effect_args_kraken(self):
-    """
-    Get the breath effect arguments
-
-    :return: List of args used for breathing effect
-    :rtype: int
-    """
-    self.logger.debug("DBus call get_breath_effect_args")
-
-    driver_path = self.get_driver_path('matrix_effect_breath')
-
-    with open(driver_path, 'rb') as driver_file:
-        bytestring = driver_file.read()
-        if len(bytestring) % 4 != 0:
-            raise ValueError("Response from driver is not valid, should be length 4 got: {0}".format(len(bytestring)))
-        else:
-            result = []
-
-            # Result could be 4 bytes (breathing1), 8 bytes (breathing2), 12 bytes (breathing3) and we need to cut of the
-            # intensity value, so i thought it easier to cut it into chunks of 4, append the first 3 values to `result`
-            for chunk in [bytestring[i:i + 4] for i in range(0, len(bytestring), 4)]:
-                # Get first 3 values
-                values = list(chunk)[:3]
-                # Add those 3 values into the list
-                result.extend(values)
-
-            return result  # We cut off the intensity value in the end, aint letting people mess with that.
-
-
 @endpoint('razer.device.lighting.kraken', 'setCustom', in_sig='ai')
 def set_custom_kraken(self, rgbi):
     """
diff --git a/daemon/openrazer_daemon/dbus_services/dbus_methods/lanceheadte.py b/daemon/openrazer_daemon/dbus_services/dbus_methods/lanceheadte.py
index 4d224471..dc3fe108 100644
--- a/daemon/openrazer_daemon/dbus_services/dbus_methods/lanceheadte.py
+++ b/daemon/openrazer_daemon/dbus_services/dbus_methods/lanceheadte.py
@@ -13,6 +13,10 @@ def set_logo_wave(self, direction):
     # Notify others
     self.send_effect_event('setWave', direction)
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'wave')
+    self.set_persistence("logo", "wave_dir", int(direction))
+
     driver_path = self.get_driver_path('logo_matrix_effect_wave')
 
     if direction not in self.WAVE_DIRS:
@@ -34,6 +38,10 @@ def set_scroll_wave(self, direction):
     # Notify others
     self.send_effect_event('setWave', direction)
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'wave')
+    self.set_persistence("scroll", "wave_dir", int(direction))
+
     driver_path = self.get_driver_path('scroll_matrix_effect_wave')
 
     if direction not in self.WAVE_DIRS:
@@ -52,12 +60,7 @@ def get_left_brightness(self):
     """
     self.logger.debug("DBus call get_left_brightness")
 
-    driver_path = self.get_driver_path('left_led_brightness')
-
-    with open(driver_path, 'r') as driver_file:
-        brightness = round(float(driver_file.read()) * (100.0 / 255.0), 2)
-
-        return brightness
+    return self.zone["left"]["brightness"]
 
 
 @endpoint('razer.device.lighting.left', 'setLeftBrightness', in_sig='d')
@@ -73,12 +76,15 @@ def set_left_brightness(self, brightness):
 
     self.method_args['brightness'] = brightness
 
-    brightness = int(round(brightness * (255.0 / 100.0)))
     if brightness > 255:
         brightness = 255
     elif brightness < 0:
         brightness = 0
 
+    self.set_persistence("left", "brightness", brightness)
+
+    brightness = int(round(brightness * (255.0 / 100.0)))
+
     with open(driver_path, 'w') as driver_file:
         driver_file.write(str(brightness))
 
@@ -98,6 +104,10 @@ def set_left_wave(self, direction):
     # Notify others
     self.send_effect_event('setWave', direction)
 
+    # remember effect
+    self.set_persistence("left", "effect", 'wave')
+    self.set_persistence("left", "wave_dir", int(direction))
+
     driver_path = self.get_driver_path('left_matrix_effect_wave')
 
     if direction not in self.WAVE_DIRS:
@@ -126,6 +136,10 @@ def set_left_static(self, red, green, blue):
     # Notify others
     self.send_effect_event('setStatic', red, green, blue)
 
+    # remember effect
+    self.set_persistence("left", "effect", 'static')
+    self.zone["left"]["colors"][0:3] = int(red), int(green), int(blue)
+
     rgb_driver_path = self.get_driver_path('left_matrix_effect_static')
 
     payload = bytes([red, green, blue])
@@ -144,6 +158,9 @@ def set_left_spectrum(self):
     # Notify others
     self.send_effect_event('setSpectrum')
 
+    # remember effect
+    self.set_persistence("left", "effect", 'spectrum')
+
     effect_driver_path = self.get_driver_path('left_matrix_effect_spectrum')
 
     with open(effect_driver_path, 'w') as effect_driver_file:
@@ -160,6 +177,9 @@ def set_left_none(self):
     # Notify others
     self.send_effect_event('setNone')
 
+    # remember effect
+    self.set_persistence("left", "effect", 'none')
+
     driver_path = self.get_driver_path('left_matrix_effect_none')
 
     with open(driver_path, 'w') as driver_file:
@@ -190,9 +210,15 @@ def set_left_reactive(self, red, green, blue, speed):
     # Notify others
     self.send_effect_event('setReactive', red, green, blue, speed)
 
+    # remember effect
+    self.set_persistence("left", "effect", 'reactive')
+    self.zone["left"]["colors"][0:3] = int(red), int(green), int(blue)
+
     if speed not in (1, 2, 3, 4):
         speed = 4
 
+    self.set_persistence("left", "speed", int(speed))
+
     payload = bytes([speed, red, green, blue])
 
     with open(driver_path, 'wb') as driver_file:
@@ -209,6 +235,9 @@ def set_left_breath_random(self):
     # Notify others
     self.send_effect_event('setBreathRandom')
 
+    # remember effect
+    self.set_persistence("left", "effect", 'breathRandom')
+
     driver_path = self.get_driver_path('left_matrix_effect_breath')
 
     payload = b'1'
@@ -236,6 +265,10 @@ def set_left_breath_single(self, red, green, blue):
     # Notify others
     self.send_effect_event('setBreathSingle', red, green, blue)
 
+    # remember effect
+    self.set_persistence("left", "effect", 'breathSingle')
+    self.zone["left"]["colors"][0:3] = int(red), int(green), int(blue)
+
     driver_path = self.get_driver_path('left_matrix_effect_breath')
 
     payload = bytes([red, green, blue])
@@ -272,6 +305,10 @@ def set_left_breath_dual(self, red1, green1, blue1, red2, green2, blue2):
     # Notify others
     self.send_effect_event('setBreathDual', red1, green1, blue1, red2, green2, blue2)
 
+    # remember effect
+    self.set_persistence("left", "effect", 'breathDual')
+    self.zone["left"]["colors"][0:6] = int(red1), int(green1), int(blue1), int(red2), int(green2), int(blue2)
+
     driver_path = self.get_driver_path('left_matrix_effect_breath')
 
     payload = bytes([red1, green1, blue1, red2, green2, blue2])
@@ -289,12 +326,7 @@ def get_right_brightness(self):
     """
     self.logger.debug("DBus call get_right_brightness")
 
-    driver_path = self.get_driver_path('right_led_brightness')
-
-    with open(driver_path, 'r') as driver_file:
-        brightness = round(float(driver_file.read()) * (100.0 / 255.0), 2)
-
-        return brightness
+    return self.zone["right"]["brightness"]
 
 
 @endpoint('razer.device.lighting.right', 'setRightBrightness', in_sig='d')
@@ -310,12 +342,15 @@ def set_right_brightness(self, brightness):
 
     self.method_args['brightness'] = brightness
 
-    brightness = int(round(brightness * (255.0 / 100.0)))
     if brightness > 255:
         brightness = 255
     elif brightness < 0:
         brightness = 0
 
+    self.set_persistence("right", "brightness", brightness)
+
+    brightness = int(round(brightness * (255.0 / 100.0)))
+
     with open(driver_path, 'w') as driver_file:
         driver_file.write(str(brightness))
 
@@ -335,6 +370,10 @@ def set_right_wave(self, direction):
     # Notify others
     self.send_effect_event('setWave', direction)
 
+    # remember effect
+    self.set_persistence("right", "effect", 'wave')
+    self.set_persistence("right", "wave_dir", int(direction))
+
     driver_path = self.get_driver_path('right_matrix_effect_wave')
 
     if direction not in self.WAVE_DIRS:
@@ -363,6 +402,10 @@ def set_right_static(self, red, green, blue):
     # Notify others
     self.send_effect_event('setStatic', red, green, blue)
 
+    # remember effect
+    self.set_persistence("right", "effect", 'static')
+    self.zone["right"]["colors"][0:3] = int(red), int(green), int(blue)
+
     rgb_driver_path = self.get_driver_path('right_matrix_effect_static')
 
     payload = bytes([red, green, blue])
@@ -381,6 +424,9 @@ def set_right_spectrum(self):
     # Notify others
     self.send_effect_event('setSpectrum')
 
+    # remember effect
+    self.set_persistence("right", "effect", 'spectrum')
+
     effect_driver_path = self.get_driver_path('right_matrix_effect_spectrum')
 
     with open(effect_driver_path, 'w') as effect_driver_file:
@@ -397,6 +443,9 @@ def set_right_none(self):
     # Notify others
     self.send_effect_event('setNone')
 
+    # remember effect
+    self.set_persistence("right", "effect", 'none')
+
     driver_path = self.get_driver_path('right_matrix_effect_none')
 
     with open(driver_path, 'w') as driver_file:
@@ -427,9 +476,15 @@ def set_right_reactive(self, red, green, blue, speed):
     # Notify others
     self.send_effect_event('setReactive', red, green, blue, speed)
 
+    # remember effect
+    self.set_persistence("right", "effect", 'reactive')
+    self.zone["right"]["colors"][0:3] = int(red), int(green), int(blue)
+
     if speed not in (1, 2, 3, 4):
         speed = 4
 
+    self.set_persistence("right", "speed", int(speed))
+
     payload = bytes([speed, red, green, blue])
 
     with open(driver_path, 'wb') as driver_file:
@@ -446,6 +501,9 @@ def set_right_breath_random(self):
     # Notify others
     self.send_effect_event('setBreathRandom')
 
+    # remember effect
+    self.set_persistence("right", "effect", 'breathRandom')
+
     driver_path = self.get_driver_path('right_matrix_effect_breath')
 
     payload = b'1'
@@ -473,6 +531,10 @@ def set_right_breath_single(self, red, green, blue):
     # Notify others
     self.send_effect_event('setBreathSingle', red, green, blue)
 
+    # remember effect
+    self.set_persistence("right", "effect", 'breathSingle')
+    self.zone["right"]["colors"][0:3] = int(red), int(green), int(blue)
+
     driver_path = self.get_driver_path('right_matrix_effect_breath')
 
     payload = bytes([red, green, blue])
@@ -509,6 +571,10 @@ def set_right_breath_dual(self, red1, green1, blue1, red2, green2, blue2):
     # Notify others
     self.send_effect_event('setBreathDual', red1, green1, blue1, red2, green2, blue2)
 
+    # remember effect
+    self.set_persistence("right", "effect", 'breathDual')
+    self.zone["right"]["colors"][0:6] = int(red1), int(green1), int(blue1), int(red2), int(green2), int(blue2)
+
     driver_path = self.get_driver_path('right_matrix_effect_breath')
 
     payload = bytes([red1, green1, blue1, red2, green2, blue2])
diff --git a/daemon/openrazer_daemon/dbus_services/dbus_methods/mamba.py b/daemon/openrazer_daemon/dbus_services/dbus_methods/mamba.py
index 1fb7cb74..010677af 100644
--- a/daemon/openrazer_daemon/dbus_services/dbus_methods/mamba.py
+++ b/daemon/openrazer_daemon/dbus_services/dbus_methods/mamba.py
@@ -1,6 +1,3 @@
-"""
-BlackWidow Chroma Effects
-"""
 import math
 import struct
 from openrazer_daemon.dbus_services import endpoint
@@ -181,6 +178,19 @@ def set_dpi_xy(self, dpi_x, dpi_y):
     else:
         dpi_bytes = struct.pack('>HH', dpi_x, dpi_y)
 
+    self.dpi[0] = dpi_x
+    self.dpi[1] = dpi_y
+
+    self.set_persistence(None, "dpi_x", dpi_x)
+    self.set_persistence(None, "dpi_y", dpi_y)
+
+    # constrain DPI to maximum
+    if hasattr(self, 'DPI_MAX'):
+        if self.dpi[0] > self.DPI_MAX:
+            self.dpi[0] = self.DPI_MAX
+        if self.dpi[1] > self.DPI_MAX:
+            self.dpi[1] = self.DPI_MAX
+
     with open(driver_path, 'wb') as driver_file:
         driver_file.write(dpi_bytes)
 
@@ -197,9 +207,15 @@ def get_dpi_xy(self):
 
     driver_path = self.get_driver_path('dpi')
 
-    with open(driver_path, 'r') as driver_file:
-        result = driver_file.read()
-        dpi = [int(dpi) for dpi in result.strip().split(':')]
+    # try retrieving DPI from the hardware.
+    # if we can't (e.g. because the mouse has been disconnected)
+    # return the value in local storage.
+    try:
+        with open(driver_path, 'r') as driver_file:
+            result = driver_file.read()
+            dpi = [int(dpi) for dpi in result.strip().split(':')]
+    except FileNotFoundError:
+        return self.dpi
 
     return dpi
 
@@ -238,6 +254,9 @@ def set_poll_rate(self, rate):
     if rate in (1000, 500, 125):
         driver_path = self.get_driver_path('poll_rate')
 
+        # remember poll rate
+        self.poll_rate = rate
+
         with open(driver_path, 'w') as driver_file:
             driver_file.write(str(rate))
     else:
@@ -254,10 +273,4 @@ def get_poll_rate(self):
     """
     self.logger.debug("DBus call get_poll_rate")
 
-    driver_path = self.get_driver_path('poll_rate')
-
-    with open(driver_path, 'r') as driver_file:
-        result = driver_file.read()
-        result = int(result.strip())
-
-    return result
+    return int(self.poll_rate)
diff --git a/daemon/openrazer_daemon/dbus_services/dbus_methods/nagahexv2.py b/daemon/openrazer_daemon/dbus_services/dbus_methods/nagahexv2.py
index 7ae655f1..614b8963 100644
--- a/daemon/openrazer_daemon/dbus_services/dbus_methods/nagahexv2.py
+++ b/daemon/openrazer_daemon/dbus_services/dbus_methods/nagahexv2.py
@@ -20,6 +20,10 @@ def set_logo_static_naga_hex_v2(self, red, green, blue):
     # Notify others
     self.send_effect_event('setStatic', red, green, blue)
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'static')
+    self.zone["logo"]["colors"][0:3] = int(red), int(green), int(blue)
+
     rgb_driver_path = self.get_driver_path('logo_matrix_effect_static')
 
     payload = bytes([red, green, blue])
@@ -38,6 +42,9 @@ def set_logo_spectrum_naga_hex_v2(self):
     # Notify others
     self.send_effect_event('setSpectrum')
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'spectrum')
+
     effect_driver_path = self.get_driver_path('logo_matrix_effect_spectrum')
 
     with open(effect_driver_path, 'w') as effect_driver_file:
@@ -54,6 +61,9 @@ def set_logo_none_naga_hex_v2(self):
     # Notify others
     self.send_effect_event('setNone')
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'none')
+
     driver_path = self.get_driver_path('logo_matrix_effect_none')
 
     with open(driver_path, 'w') as driver_file:
@@ -84,6 +94,11 @@ def set_logo_reactive_naga_hex_v2(self, red, green, blue, speed):
     # Notify others
     self.send_effect_event('setReactive', red, green, blue, speed)
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'reactive')
+    self.zone["logo"]["colors"][0:3] = int(red), int(green), int(blue)
+    self.set_persistence("logo", "speed", int(speed))
+
     if speed not in (1, 2, 3, 4):
         speed = 4
 
@@ -103,6 +118,9 @@ def set_logo_breath_random_naga_hex_v2(self):
     # Notify others
     self.send_effect_event('setBreathRandom')
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'breathRandom')
+
     driver_path = self.get_driver_path('logo_matrix_effect_breath')
 
     payload = b'1'
@@ -130,6 +148,10 @@ def set_logo_breath_single_naga_hex_v2(self, red, green, blue):
     # Notify others
     self.send_effect_event('setBreathSingle', red, green, blue)
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'breathSingle')
+    self.zone["logo"]["colors"][0:3] = int(red), int(green), int(blue)
+
     driver_path = self.get_driver_path('logo_matrix_effect_breath')
 
     payload = bytes([red, green, blue])
@@ -166,6 +188,10 @@ def set_logo_breath_dual_naga_hex_v2(self, red1, green1, blue1, red2, green2, bl
     # Notify others
     self.send_effect_event('setBreathDual', red1, green1, blue1, red2, green2, blue2)
 
+    # remember effect
+    self.set_persistence("logo", "effect", 'breathDual')
+    self.zone["logo"]["colors"][0:6] = int(red1), int(green1), int(blue1), int(red2), int(green2), int(blue2)
+
     driver_path = self.get_driver_path('logo_matrix_effect_breath')
 
     payload = bytes([red1, green1, blue1, red2, green2, blue2])
@@ -193,6 +219,10 @@ def set_scroll_static_naga_hex_v2(self, red, green, blue):
     # Notify others
     self.send_effect_event('setStatic', red, green, blue)
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'static')
+    self.zone["scroll"]["colors"][0:3] = int(red), int(green), int(blue)
+
     rgb_driver_path = self.get_driver_path('scroll_matrix_effect_static')
 
     payload = bytes([red, green, blue])
@@ -211,6 +241,8 @@ def set_scroll_spectrum_naga_hex_v2(self):
     # Notify others
     self.send_effect_event('setSpectrum')
 
+    self.set_persistence("scroll", "effect", 'spectrum')
+
     effect_driver_path = self.get_driver_path('scroll_matrix_effect_spectrum')
 
     with open(effect_driver_path, 'w') as effect_driver_file:
@@ -227,6 +259,8 @@ def set_scroll_none_naga_hex_v2(self):
     # Notify others
     self.send_effect_event('setNone')
 
+    self.set_persistence("scroll", "effect", 'none')
+
     driver_path = self.get_driver_path('scroll_matrix_effect_none')
 
     with open(driver_path, 'w') as driver_file:
@@ -257,6 +291,11 @@ def set_scroll_reactive_naga_hex_v2(self, red, green, blue, speed):
     # Notify others
     self.send_effect_event('setReactive', red, green, blue, speed)
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'reactive')
+    self.zone["scroll"]["colors"][0:3] = int(red), int(green), int(blue)
+    self.set_persistence("scroll", "speed", int(speed))
+
     if speed not in (1, 2, 3, 4):
         speed = 4
 
@@ -276,6 +315,9 @@ def set_scroll_breath_random_naga_hex_v2(self):
     # Notify others
     self.send_effect_event('setBreathRandom')
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'breathRandom')
+
     driver_path = self.get_driver_path('scroll_matrix_effect_breath')
 
     payload = b'1'
@@ -303,6 +345,10 @@ def set_scroll_breath_single_naga_hex_v2(self, red, green, blue):
     # Notify others
     self.send_effect_event('setBreathSingle', red, green, blue)
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'breathSingle')
+    self.zone["scroll"]["colors"][0:3] = int(red), int(green), int(blue)
+
     driver_path = self.get_driver_path('scroll_matrix_effect_breath')
 
     payload = bytes([red, green, blue])
@@ -339,6 +385,10 @@ def set_scroll_breath_dual_naga_hex_v2(self, red1, green1, blue1, red2, green2,
     # Notify others
     self.send_effect_event('setBreathDual', red1, green1, blue1, red2, green2, blue2)
 
+    # remember effect
+    self.set_persistence("scroll", "effect", 'breathDual')
+    self.zone["scroll"]["colors"][0:6] = int(red1), int(green1), int(blue1), int(red2), int(green2), int(blue2)
+
     driver_path = self.get_driver_path('scroll_matrix_effect_breath')
 
     payload = bytes([red1, green1, blue1, red2, green2, blue2])
diff --git a/daemon/openrazer_daemon/hardware/device_base.py b/daemon/openrazer_daemon/hardware/device_base.py
index 10bf9baa..988e41f1 100644
--- a/daemon/openrazer_daemon/hardware/device_base.py
+++ b/daemon/openrazer_daemon/hardware/device_base.py
@@ -4,6 +4,7 @@ Hardware base class
 import re
 import os
 import types
+import inspect
 import logging
 import time
 import json
@@ -15,6 +16,8 @@ from openrazer_daemon.misc import effect_sync
 
 
 # pylint: disable=too-many-instance-attributes
+# pylint: disable=E1102
+# See https://github.com/PyCQA/pylint/issues/1493
 class RazerDevice(DBusService):
     """
     Base class
@@ -35,9 +38,11 @@ class RazerDevice(DBusService):
 
     WAVE_DIRS = (1, 2)
 
+    ZONES = ('backlight', 'logo', 'scroll', 'left', 'right')
+
     DEVICE_IMAGE = None
 
-    def __init__(self, device_path, device_number, config, testing=False, additional_interfaces=None, additional_methods=[]):
+    def __init__(self, device_path, device_number, config, persistence, testing=False, additional_interfaces=None, additional_methods=[]):
 
         self.logger = logging.getLogger('razer.device{0}'.format(device_number))
         self.logger.info("Initialising device.%d %s", device_number, self.__class__.__name__)
@@ -45,6 +50,9 @@ class RazerDevice(DBusService):
         # Serial cache
         self._serial = None
 
+        # Local storage key name
+        self.storage_name = "UnknownDevice"
+
         self._observer_list = []
         self._effect_sync_propagate_up = False
         self._disable_notifications = False
@@ -53,12 +61,40 @@ class RazerDevice(DBusService):
             self.additional_interfaces.extend(additional_interfaces)
 
         self.config = config
+        self.persistence = persistence
         self._testing = testing
         self._parent = None
         self._device_path = device_path
         self._device_number = device_number
         self.serial = self.get_serial()
 
+        if self.USB_PID == 0x0f07:
+            self.storage_name = "ChromaMug"
+        elif self.USB_PID == 0x0013:
+            self.storage_name = "Orochi2011"
+        elif self.USB_PID == 0x0016:
+            self.storage_name = "DeathAdder35G"
+        elif self.USB_PID == 0x0024 or self.USB_PID == 0x0025:
+            self.storage_name = "Mamba2012"
+        else:
+            self.storage_name = self.serial
+
+        self.zone = dict()
+
+        for i in self.ZONES:
+            self.zone[i] = {
+                "present": False,
+                "active": True,
+                "brightness": 75.0,
+                "effect": 'spectrum',
+                "colors": [0, 255, 0, 0, 255, 255, 0, 0, 255],
+                "speed": 1,
+                "wave_dir": 1,
+            }
+
+        self.dpi = [1800, 1800]
+        self.poll_rate = 500
+
         self._effect_sync = effect_sync.EffectSync(self, device_number)
 
         self._is_closed = False
@@ -98,18 +134,169 @@ class RazerDevice(DBusService):
             ('razer.device.misc', 'getVidPid', self.get_vid_pid, None, 'ai'),
             ('razer.device.misc', 'getDriverVersion', openrazer_daemon.dbus_services.dbus_methods.version, None, 's'),
             ('razer.device.misc', 'hasDedicatedMacroKeys', self.dedicated_macro_keys, None, 'b'),
-
             # Deprecated API, but kept for backwards compatibility
-            ('razer.device.misc', 'getRazerUrls', self.get_image_json, None, 's')
+            ('razer.device.misc', 'getRazerUrls', self.get_image_json, None, 's'),
+
+            ('razer.device.lighting.chroma', 'restoreLastEffect', self.restore_effect, None, None),
+        }
+
+        effect_methods = {
+            "backlight": {
+                ('razer.device.lighting.chroma', 'getEffect', self.get_current_effect, None, 's'),
+                ('razer.device.lighting.chroma', 'getEffectColors', self.get_current_effect_colors, None, 'ay'),
+                ('razer.device.lighting.chroma', 'getEffectSpeed', self.get_current_effect_speed, None, 'i'),
+                ('razer.device.lighting.chroma', 'getWaveDir', self.get_current_wave_dir, None, 'i'),
+            },
+
+            "logo": {
+                ('razer.device.lighting.logo', 'getLogoEffect', self.get_current_logo_effect, None, 's'),
+                ('razer.device.lighting.logo', 'getLogoEffectColors', self.get_current_logo_effect_colors, None, 'ay'),
+                ('razer.device.lighting.logo', 'getLogoEffectSpeed', self.get_current_logo_effect_speed, None, 'i'),
+                ('razer.device.lighting.logo', 'getLogoWaveDir', self.get_current_logo_wave_dir, None, 'i'),
+            },
+
+            "scroll": {
+                ('razer.device.lighting.scroll', 'getScrollEffect', self.get_current_scroll_effect, None, 's'),
+                ('razer.device.lighting.scroll', 'getScrollEffectColors', self.get_current_scroll_effect_colors, None, 'ay'),
+                ('razer.device.lighting.scroll', 'getScrollEffectSpeed', self.get_current_scroll_effect_speed, None, 'i'),
+                ('razer.device.lighting.scroll', 'getScrollWaveDir', self.get_current_scroll_wave_dir, None, 'i'),
+            },
+
+            "left": {
+                ('razer.device.lighting.left', 'getLeftEffect', self.get_current_left_effect, None, 's'),
+                ('razer.device.lighting.left', 'getLeftEffectColors', self.get_current_left_effect_colors, None, 'ay'),
+                ('razer.device.lighting.left', 'getLeftEffectSpeed', self.get_current_left_effect_speed, None, 'i'),
+                ('razer.device.lighting.left', 'getLeftWaveDir', self.get_current_left_wave_dir, None, 'i'),
+            },
+
+            "right": {
+                ('razer.device.lighting.right', 'getRightEffect', self.get_current_right_effect, None, 's'),
+                ('razer.device.lighting.right', 'getRightEffectColors', self.get_current_right_effect_colors, None, 'ay'),
+                ('razer.device.lighting.right', 'getRightEffectSpeed', self.get_current_right_effect_speed, None, 'i'),
+                ('razer.device.lighting.right', 'getRightWaveDir', self.get_current_right_wave_dir, None, 'i'),
+            }
         }
 
         for m in methods:
             self.logger.debug("Adding {}.{} method to DBus".format(m[0], m[1]))
             self.add_dbus_method(m[0], m[1], m[2], in_signature=m[3], out_signature=m[4])
 
+        # this check is separate from the rest because backlight effects don't have prefixes in their names
+        if 'set_static_effect' in self.METHODS or 'bw_set_static' in self.METHODS:
+            self.zone["backlight"]["present"] = True
+            for m in effect_methods["backlight"]:
+                self.logger.debug("Adding {}.{} method to DBus".format(m[0], m[1]))
+                self.add_dbus_method(m[0], m[1], m[2], in_signature=m[3], out_signature=m[4])
+
+        for i in self.ZONES[1:]:
+            if 'set_' + i + '_static' in self.METHODS or 'set_' + i + '_static_naga_hex_v2' in self.METHODS or '`set_' + i + 'active' in self.METHODS:
+                self.zone[i]["present"] = True
+                for m in effect_methods[i]:
+                    self.logger.debug("Adding {}.{} method to DBus".format(m[0], m[1]))
+                    self.add_dbus_method(m[0], m[1], m[2], in_signature=m[3], out_signature=m[4])
+
         # Load additional DBus methods
         self.load_methods()
 
+        # load last DPI/poll rate state
+        if self.persistence.has_section(self.storage_name):
+            if 'set_dpi_xy' in self.METHODS:
+                try:
+                    self.dpi[0] = int(self.persistence[self.storage_name]['dpi_x'])
+                    self.dpi[1] = int(self.persistence[self.storage_name]['dpi_y'])
+                except KeyError:
+                    pass
+
+            if 'set_poll_rate' in self.METHODS:
+                try:
+                    self.poll_rate = int(self.persistence[self.storage_name]['poll_rate'])
+                except KeyError:
+                    pass
+
+        dpi_func = getattr(self, "setDPI", None)
+        if dpi_func is not None:
+            dpi_func(self.dpi[0], self.dpi[1])
+
+        poll_rate_func = getattr(self, "setPollRate", None)
+        if poll_rate_func is not None:
+            poll_rate_func(self.poll_rate)
+
+        # load last effects
+        for i in self.ZONES:
+            if self.zone[i]["present"]:
+                # check if we have the device in the persistence file
+                if self.persistence.has_section(self.storage_name):
+                    # try reading the effect name from the persistence
+                    try:
+                        self.zone[i]["effect"] = self.persistence[self.storage_name][i + '_effect']
+                    except KeyError:
+                        pass
+
+                    # zone active status
+                    try:
+                        self.zone[i]["active"] = bool(self.persistence[self.storage_name][i + '_active'])
+                    except KeyError:
+                        pass
+
+                    # brightness
+                    try:
+                        self.zone[i]["brightness"] = float(self.persistence[self.storage_name][i + '_brightness'])
+                    except KeyError:
+                        pass
+
+                    # colors.
+                    # these are stored as a string that must contain 9 numbers, separated with spaces.
+                    try:
+                        for index, item in enumerate(self.persistence[self.storage_name][i + '_colors'].split(" ")):
+                            self.zone[i]["colors"][index] = int(item)
+                            # check if the color is in range
+                            if not 0 <= self.zone[i]["colors"][index] <= 255:
+                                raise ValueError('Color out of range')
+
+                        # check if we have exactly 9 colors
+                        if len(self.zone[i]["colors"]) != 9:
+                            raise ValueError('There must be exactly 9 colors')
+
+                    except ValueError:
+                        # invalid colors. reinitialize
+                        self.zone[i]["colors"] = [0, 255, 0, 0, 255, 255, 0, 0, 255]
+                        self.logger.info("%s: Invalid colors; restoring to defaults.", self.__class__.__name__)
+                        pass
+
+                    except KeyError:
+                        pass
+
+                    # speed
+                    try:
+                        self.zone[i]["speed"] = int(self.persistence[self.storage_name][i + '_speed'])
+
+                    except KeyError:
+                        pass
+
+                    # wave direction
+                    try:
+                        self.zone[i]["wave_dir"] = int(self.persistence[self.storage_name][i + '_wave_dir'])
+
+                    except KeyError:
+                        pass
+
+                if 'set_' + i + '_active' in self.METHODS:
+                    active_func = getattr(self, "set" + self.capitalize_first_char(i) + "Active", None)
+                    if active_func is not None:
+                        active_func(self.zone[i]["active"])
+
+                # load brightness level
+                bright_func = None
+                if i == "backlight":
+                    bright_func = getattr(self, "setBrightness", None)
+                elif 'set_' + i + '_brightness' in self.METHODS:
+                    bright_func = getattr(self, "set" + self.capitalize_first_char(i) + "Brightness", None)
+
+                if bright_func is not None:
+                    bright_func(self.zone[i]["brightness"])
+
+        self.restore_effect()
+
     def send_effect_event(self, effect_name, *args):
         """
         Send effect event
@@ -134,6 +321,317 @@ class RazerDevice(DBusService):
         """
         return self.DEDICATED_MACRO_KEYS
 
+    def restore_effect(self):
+        """
+        Set the device to the current effect
+
+        This is used at launch time and can be called by applications
+        that use custom matrix frames after they exit
+        """
+        for i in self.ZONES:
+            if self.zone[i]["present"]:
+                # prepare the effect method name
+                # yes, we need to handle the backlight zone separately too.
+                # the backlight effect methods don't have a prefix.
+                if i == "backlight":
+                    effect_func_name = 'set' + self.capitalize_first_char(self.zone[i]["effect"])
+                else:
+                    effect_func_name = 'set' + self.capitalize_first_char(i) + self.capitalize_first_char(self.zone[i]["effect"])
+
+                # find the effect method
+                effect_func = getattr(self, effect_func_name, None)
+
+                # check if the effect method exists only if we didn't look for spectrum (because resetting to Spectrum when the effect is Spectrum is in vain)
+                if effect_func == None and not self.zone[i]["effect"] == "spectrum":
+                    # not found. restoring to Spectrum
+                    self.logger.info("%s: Invalid effect name %s; restoring to Spectrum.", self.__class__.__name__, effect_func_name)
+                    self.zone[i]["effect"] = 'spectrum'
+                    if i == "backlight":
+                        effect_func_name = 'setSpectrum'
+                    else:
+                        effect_func_name = 'set' + self.capitalize_first_char(i) + 'Spectrum'
+                    effect_func = getattr(self, effect_func_name, None)
+
+                # we check again here because there is a possibility the device may not even have Spectrum
+                if effect_func is not None:
+                    effect = self.zone[i]["effect"]
+                    colors = self.zone[i]["colors"]
+                    speed = self.zone[i]["speed"]
+                    wave_dir = self.zone[i]["wave_dir"]
+                    if self.get_num_arguments(effect_func) == 0:
+                        effect_func()
+                    elif self.get_num_arguments(effect_func) == 1:
+                        # there are 2 effects which require 1 argument.
+                        # these are: Starlight (Random) and Wave.
+                        if effect == 'starlightRandom':
+                            effect_func(speed)
+                        elif effect == 'wave':
+                            effect_func(wave_dir)
+                        elif effect == 'rippleRandomColour':
+                            # do nothing. this is handled in the ripple manager.
+                            pass
+                        else:
+                            self.logger.error("%s: Effect requires 1 argument but don't know how to handle it!", self.__class__.__name__)
+                    elif self.get_num_arguments(effect_func) == 3:
+                        effect_func(colors[0], colors[1], colors[2])
+                    elif self.get_num_arguments(effect_func) == 4:
+                        # starlight/reactive have different arguments.
+                        if effect == 'starlightSingle' or effect == 'reactive':
+                            effect_func(colors[0], colors[1], colors[2], speed)
+                        elif effect == 'ripple':
+                            # do nothing. this is handled in the ripple manager.
+                            pass
+                        else:
+                            self.logger.error("%s: Effect requires 4 arguments but don't know how to handle it!", self.__class__.__name__)
+                    elif self.get_num_arguments(effect_func) == 6:
+                        effect_func(colors[0], colors[1], colors[2], colors[3], colors[4], colors[5])
+                    elif self.get_num_arguments(effect_func) == 7:
+                        effect_func(colors[0], colors[1], colors[2], colors[3], colors[4], colors[5], speed)
+                    elif self.get_num_arguments(effect_func) == 9:
+                        effect_func(colors[0], colors[1], colors[2], colors[3], colors[4], colors[5], colors[6], colors[7], colors[8])
+                    else:
+                        self.logger.error("%s: Couldn't detect effect argument count!", self.__class__.__name__)
+
+    def set_persistence(self, zone, key, value):
+        """
+        Set a device's current state for persisting across sessions.
+
+        :param zone: Zone
+        :type zone: string
+
+        :param key: Key
+        :type key: string
+
+        :param value: Value
+        :type value: string
+        """
+        self.persistence.status["changed"] = True
+
+        if zone:
+            self.zone[zone][key] = value
+        else:
+            self.zone[key] = value
+
+    def get_current_effect(self):
+        """
+        Get the device's current effect
+
+        :return: Effect
+        :rtype: string
+        """
+        self.logger.debug("DBus call get_current_effect")
+
+        return self.zone["backlight"]["effect"]
+
+    def get_current_effect_colors(self):
+        """
+        Get the device's current effect's colors
+
+        :return: 3 colors
+        :rtype: list of byte
+        """
+        self.logger.debug("DBus call get_current_effect_colors")
+
+        return self.zone["backlight"]["colors"]
+
+    def get_current_effect_speed(self):
+        """
+        Get the device's current effect's speed
+
+        :return: Speed
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_effect_speed")
+
+        return self.zone["backlight"]["speed"]
+
+    def get_current_wave_dir(self):
+        """
+        Get the device's current wave direction
+
+        :return: Direction
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_wave_dir")
+
+        return self.zone["backlight"]["wave_dir"]
+
+    def get_current_logo_effect(self):
+        """
+        Get the device's current logo effect
+
+        :return: Effect
+        :rtype: string
+        """
+        self.logger.debug("DBus call get_current_logo_effect")
+
+        return self.zone["logo"]["effect"]
+
+    def get_current_logo_effect_colors(self):
+        """
+        Get the device's current logo effect's colors
+
+        :return: 3 colors
+        :rtype: list of byte
+        """
+        self.logger.debug("DBus call get_current_logo_effect_colors")
+
+        return self.zone["logo"]["colors"]
+
+    def get_current_logo_effect_speed(self):
+        """
+        Get the device's current logo effect's speed
+
+        :return: Speed
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_logo_effect_speed")
+
+        return self.zone["logo"]["speed"]
+
+    def get_current_logo_wave_dir(self):
+        """
+        Get the device's current logo wave direction
+
+        :return: Direction
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_logo_wave_dir")
+
+        return self.zone["logo"]["wave_dir"]
+
+    def get_current_scroll_effect(self):
+        """
+        Get the device's current scroll effect
+
+        :return: Effect
+        :rtype: string
+        """
+        self.logger.debug("DBus call get_current_scroll_effect")
+
+        return self.zone["scroll"]["effect"]
+
+    def get_current_scroll_effect_colors(self):
+        """
+        Get the device's current scroll effect's colors
+
+        :return: 3 colors
+        :rtype: list of byte
+        """
+        self.logger.debug("DBus call get_current_scroll_effect_colors")
+
+        return self.zone["scroll"]["colors"]
+
+    def get_current_scroll_effect_speed(self):
+        """
+        Get the device's current scroll effect's speed
+
+        :return: Speed
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_scroll_effect_speed")
+
+        return self.zone["scroll"]["speed"]
+
+    def get_current_scroll_wave_dir(self):
+        """
+        Get the device's current scroll wave direction
+
+        :return: Direction
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_scroll_wave_dir")
+
+        return self.zone["scroll"]["wave_dir"]
+
+    def get_current_left_effect(self):
+        """
+        Get the device's current left effect
+
+        :return: Effect
+        :rtype: string
+        """
+        self.logger.debug("DBus call get_current_left_effect")
+
+        return self.zone["left"]["effect"]
+
+    def get_current_left_effect_colors(self):
+        """
+        Get the device's current left effect's colors
+
+        :return: 3 colors
+        :rtype: list of byte
+        """
+        self.logger.debug("DBus call get_current_left_effect_colors")
+
+        return self.zone["left"]["colors"]
+
+    def get_current_left_effect_speed(self):
+        """
+        Get the device's current left effect's speed
+
+        :return: Speed
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_left_effect_speed")
+
+        return self.zone["left"]["speed"]
+
+    def get_current_left_wave_dir(self):
+        """
+        Get the device's current left wave direction
+
+        :return: Direction
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_left_wave_dir")
+
+        return self.zone["left"]["wave_dir"]
+
+    def get_current_right_effect(self):
+        """
+        Get the device's current right effect
+
+        :return: Effect
+        :rtype: string
+        """
+        self.logger.debug("DBus call get_current_right_effect")
+
+        return self.zone["right"]["effect"]
+
+    def get_current_right_effect_colors(self):
+        """
+        Get the device's current right effect's colors
+
+        :return: 3 colors
+        :rtype: list of byte
+        """
+        self.logger.debug("DBus call get_current_right_effect_colors")
+
+        return self.zone["right"]["colors"]
+
+    def get_current_right_effect_speed(self):
+        """
+        Get the device's current right effect's speed
+
+        :return: Speed
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_right_effect_speed")
+
+        return self.zone["right"]["speed"]
+
+    def get_current_right_wave_dir(self):
+        """
+        Get the device's current right wave direction
+
+        :return: Direction
+        :rtype: int
+        """
+        self.logger.debug("DBus call get_current_right_wave_dir")
+
+        return self.zone["right"]["wave_dir"]
+
     @property
     def effect_sync(self):
         """
@@ -349,6 +847,14 @@ class RazerDevice(DBusService):
         Close any resources opened by subclasses
         """
         if not self._is_closed:
+            # If this is a mouse, retrieve current DPI for local storage
+            # in case the user has changed the DPI on-the-fly
+            # (e.g. the DPI buttons)
+            if 'get_dpi_xy' in self.METHODS:
+                dpi_func = getattr(self, "getDPI", None)
+                if dpi_func is not None:
+                    self.dpi = dpi_func()
+
             self._close()
 
             self._is_closed = True
@@ -434,6 +940,24 @@ class RazerDevice(DBusService):
 
         return False
 
+    @staticmethod
+    def get_num_arguments(func):
+        """
+        Get number of arguments in a function
+
+        :param func: Function
+        :type func: callable
+
+        :return: Number of arguments
+        :rtype: int
+        """
+        func_sig = inspect.signature(func)
+        return len(func_sig.parameters)
+
+    @staticmethod
+    def capitalize_first_char(string):
+        return string[0].upper() + string[1:]
+
     def __del__(self):
         self.close()
 
@@ -448,8 +972,8 @@ class RazerDeviceSpecialBrightnessSuspend(RazerDevice):
     Suspend functions
     """
 
-    def __init__(self, device_path, device_number, config, testing=False, additional_interfaces=None, additional_methods=[]):
-        super().__init__(device_path, device_number, config, testing, additional_interfaces, additional_methods)
+    def __init__(self, device_path, device_number, config, persistence, testing=False, additional_interfaces=None, additional_methods=[]):
+        super().__init__(device_path, device_number, config, persistence, testing, additional_interfaces, additional_methods)
 
     def _suspend_device(self):
         """
@@ -484,6 +1008,6 @@ class RazerDeviceBrightnessSuspend(RazerDeviceSpecialBrightnessSuspend):
     Inherits from RazerDeviceSpecialBrightnessSuspend
     """
 
-    def __init__(self, device_path, device_number, config, testing=False, additional_interfaces=None, additional_methods=[]):
+    def __init__(self, device_path, device_number, config, persistence, testing=False, additional_interfaces=None, additional_methods=[]):
         additional_methods.extend(['get_brightness', 'set_brightness'])
-        super().__init__(device_path, device_number, config, testing, additional_interfaces, additional_methods)
+        super().__init__(device_path, device_number, config, persistence, testing, additional_interfaces, additional_methods)
diff --git a/daemon/openrazer_daemon/hardware/headsets.py b/daemon/openrazer_daemon/hardware/headsets.py
index d6bebdac..591b4723 100644
--- a/daemon/openrazer_daemon/hardware/headsets.py
+++ b/daemon/openrazer_daemon/hardware/headsets.py
@@ -16,7 +16,7 @@ class RazerKraken71(__RazerDevice):
     USB_VID = 0x1532
     USB_PID = 0x0501
     METHODS = ['get_device_type_headset',
-               'set_static_effect', 'set_none_effect', 'get_current_effect_kraken']
+               'set_static_effect', 'set_none_effect']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/229/229_kraken_71.png"
 
@@ -39,13 +39,7 @@ class RazerKraken71(__RazerDevice):
         """
         self.suspend_args.clear()
 
-        current_effect = _dbus_kraken.get_current_effect_kraken(self)
-        dec = self.decode_bitfield(current_effect)
-
-        if dec['state'] == 0x00:
-            self.suspend_args['effect'] = 'none'
-        elif dec['state'] == 0x01:
-            self.suspend_args['effect'] = 'static'
+        self.suspend_args['effect'] = self.zone["backlight"]["effect"]
 
         self.disable_notify = True
         _dbus_chroma.set_none_effect(self)
@@ -84,7 +78,7 @@ class RazerKraken71Chroma(__RazerDevice):
     USB_PID = 0x0504
     METHODS = ['get_device_type_headset',
                'set_static_effect', 'set_spectrum_effect', 'set_none_effect', 'set_breath_single_effect',
-               'get_current_effect_kraken', 'get_static_effect_args_kraken', 'get_breath_effect_args_kraken', 'set_custom_kraken']
+               'set_custom_kraken']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/280/280_kraken_71_chroma.png"
 
@@ -107,17 +101,8 @@ class RazerKraken71Chroma(__RazerDevice):
         """
         self.suspend_args.clear()
 
-        current_effect = _dbus_kraken.get_current_effect_kraken(self)
-        dec = self.decode_bitfield(current_effect)
-
-        if dec['breathing1']:
-            self.suspend_args['effect'] = 'breathing1'
-            self.suspend_args['args'] = _dbus_kraken.get_breath_effect_args_kraken(self)
-        elif dec['spectrum']:
-            self.suspend_args['effect'] = 'spectrum'
-        elif dec['state']:
-            self.suspend_args['effect'] = 'static'
-            self.suspend_args['args'] = _dbus_kraken.get_static_effect_args_kraken(self)
+        self.suspend_args['effect'] = self.zone["backlight"]["effect"]
+        self.suspend_args['args'] = self.zone["backlight"]["colors"][0:3]
 
         self.disable_notify = True
         _dbus_chroma.set_none_effect(self)
@@ -138,7 +123,7 @@ class RazerKraken71Chroma(__RazerDevice):
             _dbus_chroma.set_spectrum_effect(self)
         elif effect == 'static':
             _dbus_chroma.set_static_effect(self, *args)
-        elif effect == 'breathing1':
+        elif effect == 'breathSingle':
             _dbus_chroma.set_breath_single_effect(self, *args)
 
         self.disable_notify = False
@@ -154,7 +139,7 @@ class RazerKraken71V2(__RazerDevice):
     USB_PID = 0x0510
     METHODS = ['get_device_type_headset',
                'set_static_effect', 'set_spectrum_effect', 'set_none_effect', 'set_breath_single_effect', 'set_breath_dual_effect', 'set_breath_triple_effect',
-               'get_current_effect_kraken', 'get_static_effect_args_kraken', 'get_breath_effect_args_kraken', 'set_custom_kraken']
+               'set_custom_kraken']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/729/729_kraken_71_v2.png"
 
@@ -177,23 +162,13 @@ class RazerKraken71V2(__RazerDevice):
         """
         self.suspend_args.clear()
 
-        current_effect = _dbus_kraken.get_current_effect_kraken(self)
-        dec = self.decode_bitfield(current_effect)
-
-        if dec['breathing1']:
-            self.suspend_args['effect'] = 'breathing1'
-            self.suspend_args['args'] = _dbus_kraken.get_breath_effect_args_kraken(self)
-        elif dec['breathing2']:
-            self.suspend_args['effect'] = 'breathing2'
-            self.suspend_args['args'] = _dbus_kraken.get_breath_effect_args_kraken(self)
-        elif dec['breathing3']:
-            self.suspend_args['effect'] = 'breathing3'
-            self.suspend_args['args'] = _dbus_kraken.get_breath_effect_args_kraken(self)
-        elif dec['spectrum']:
-            self.suspend_args['effect'] = 'spectrum'
-        elif dec['state']:
-            self.suspend_args['effect'] = 'static'
-            self.suspend_args['args'] = _dbus_kraken.get_static_effect_args_kraken(self)
+        self.suspend_args['effect'] = self.zone["backlight"]["effect"]
+        if self.suspend_args['effect'] == "breathDual":
+            self.suspend_args['args'] = self.zone["backlight"]["colors"][0:6]
+        elif self.suspend_args['effect'] == "breathTriple":
+            self.suspend_args['args'] = self.zone["backlight"]["colors"][0:9]
+        else:
+            self.suspend_args['args'] = self.zone["backlight"]["colors"][0:3]
 
         self.disable_notify = True
         _dbus_chroma.set_none_effect(self)
@@ -214,11 +189,11 @@ class RazerKraken71V2(__RazerDevice):
             _dbus_chroma.set_spectrum_effect(self)
         elif effect == 'static':
             _dbus_chroma.set_static_effect(self, *args)
-        elif effect == 'breathing1':
+        elif effect == 'breathSingle':
             _dbus_chroma.set_breath_single_effect(self, *args)
-        elif effect == 'breathing2':
+        elif effect == 'breathDual':
             _dbus_chroma.set_breath_dual_effect(self, *args)
-        elif effect == 'breathing3':
+        elif effect == 'breathTriple':
             _dbus_chroma.set_breath_triple_effect(self, *args)
 
         self.disable_notify = False
@@ -234,8 +209,8 @@ class RazerKrakenUltimate(__RazerDevice):
     USB_PID = 0x0527
     METHODS = ['get_device_type_headset',
                'set_static_effect', 'set_spectrum_effect', 'set_none_effect', 'set_breath_single_effect',
-               'set_breath_dual_effect', 'set_breath_triple_effect', 'get_current_effect_kraken',
-               'get_static_effect_args_kraken', 'get_breath_effect_args_kraken', 'set_custom_kraken']
+               'set_breath_dual_effect', 'set_breath_triple_effect',
+               'get_static_effect_args_kraken', 'set_custom_kraken']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/1603/rzr_kraken_ultimate_render01_2019_resized.png"
 
@@ -258,23 +233,13 @@ class RazerKrakenUltimate(__RazerDevice):
         """
         self.suspend_args.clear()
 
-        current_effect = _dbus_kraken.get_current_effect_kraken(self)
-        dec = self.decode_bitfield(current_effect)
-
-        if dec['breathing1']:
-            self.suspend_args['effect'] = 'breathing1'
-            self.suspend_args['args'] = _dbus_kraken.get_breath_effect_args_kraken(self)
-        elif dec['breathing2']:
-            self.suspend_args['effect'] = 'breathing2'
-            self.suspend_args['args'] = _dbus_kraken.get_breath_effect_args_kraken(self)
-        elif dec['breathing3']:
-            self.suspend_args['effect'] = 'breathing3'
-            self.suspend_args['args'] = _dbus_kraken.get_breath_effect_args_kraken(self)
-        elif dec['spectrum']:
-            self.suspend_args['effect'] = 'spectrum'
-        elif dec['state']:
-            self.suspend_args['effect'] = 'static'
-            self.suspend_args['args'] = _dbus_kraken.get_static_effect_args_kraken(self)
+        self.suspend_args['effect'] = self.zone["backlight"]["effect"]
+        if self.suspend_args['effect'] == "breathDual":
+            self.suspend_args['args'] = self.zone["backlight"]["colors"][0:6]
+        elif self.suspend_args['effect'] == "breathTriple":
+            self.suspend_args['args'] = self.zone["backlight"]["colors"][0:9]
+        else:
+            self.suspend_args['args'] = self.zone["backlight"]["colors"][0:3]
 
         self.disable_notify = True
         _dbus_chroma.set_none_effect(self)
@@ -295,11 +260,11 @@ class RazerKrakenUltimate(__RazerDevice):
             _dbus_chroma.set_spectrum_effect(self)
         elif effect == 'static':
             _dbus_chroma.set_static_effect(self, *args)
-        elif effect == 'breathing1':
+        elif effect == 'breathSingle':
             _dbus_chroma.set_breath_single_effect(self, *args)
-        elif effect == 'breathing2':
+        elif effect == 'breathDual':
             _dbus_chroma.set_breath_dual_effect(self, *args)
-        elif effect == 'breathing3':
+        elif effect == 'breathTriple':
             _dbus_chroma.set_breath_triple_effect(self, *args)
 
         self.disable_notify = False
diff --git a/daemon/openrazer_daemon/hardware/keyboards.py b/daemon/openrazer_daemon/hardware/keyboards.py
index a47a2431..5973169f 100644
--- a/daemon/openrazer_daemon/hardware/keyboards.py
+++ b/daemon/openrazer_daemon/hardware/keyboards.py
@@ -55,6 +55,18 @@ class _RippleKeyboard(_MacroKeyboard):
 
         self.ripple_manager = _RippleManager(self, self._device_number)
 
+        # we need to set the effect to ripple (if needed) after the ripple manager has started
+        # otherwise it doesn't work
+        if self.zone["backlight"]["effect"] == "ripple" or self.zone["backlight"]["effect"] == "rippleRandomColour":
+            effect_func_name = 'set' + self.capitalize_first_char(self.zone["backlight"]["effect"])
+            effect_func = getattr(self, effect_func_name, None)
+
+            if effect_func is not None:
+                if effect_func_name == 'setRipple':
+                    effect_func(self.zone["backlight"]["colors"][0], self.zone["backlight"]["colors"][1], self.zone["backlight"]["colors"][2], self.ripple_manager._ripple_thread._refresh_rate)
+                elif effect_func_name == 'setRippleRandomColour':
+                    effect_func(self.ripple_manager._ripple_thread._refresh_rate)
+
     def _close(self):
         super(_RippleKeyboard, self)._close()
 
@@ -285,7 +297,7 @@ class RazerBlackWidowUltimate2012(_MacroKeyboard):
     DEDICATED_MACRO_KEYS = True
     MATRIX_DIMS = [6, 22]
     METHODS = ['get_device_type_keyboard', 'get_game_mode', 'set_game_mode', 'set_macro_mode', 'get_macro_mode',
-               'get_macro_effect', 'set_macro_effect', 'bw_get_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
+               'get_macro_effect', 'set_macro_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/563/563_blackwidow_ultimate_classic.png"
 
@@ -301,7 +313,7 @@ class RazerBlackWidowStealth(_MacroKeyboard):
     DEDICATED_MACRO_KEYS = True
     MATRIX_DIMS = [6, 22]
     METHODS = ['get_device_type_keyboard', 'get_game_mode', 'set_game_mode', 'set_macro_mode', 'get_macro_mode',
-               'get_macro_effect', 'set_macro_effect', 'bw_get_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
+               'get_macro_effect', 'set_macro_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/products/17559/razer-blackwidow-gallery-01.png"
 
@@ -317,7 +329,7 @@ class RazerBlackWidowStealthEdition(_MacroKeyboard):
     DEDICATED_MACRO_KEYS = True
     MATRIX_DIMS = [6, 22]
     METHODS = ['get_device_type_keyboard', 'get_game_mode', 'set_game_mode', 'set_macro_mode', 'get_macro_mode',
-               'get_macro_effect', 'set_macro_effect', 'bw_get_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
+               'get_macro_effect', 'set_macro_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/products/17559/razer-blackwidow-gallery-01.png"
 
@@ -333,7 +345,7 @@ class RazerBlackWidowUltimate2013(_MacroKeyboard):
     DEDICATED_MACRO_KEYS = True
     MATRIX_DIMS = [6, 22]
     METHODS = ['get_device_type_keyboard', 'get_game_mode', 'set_game_mode', 'set_macro_mode', 'get_macro_mode',
-               'get_macro_effect', 'set_macro_effect', 'bw_get_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
+               'get_macro_effect', 'set_macro_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/245/438_blackwidow_ultimate_2014.png"
 
@@ -836,7 +848,7 @@ class RazerDeathStalkerExpert(_MacroKeyboard):
     USB_VID = 0x1532
     USB_PID = 0x0202
     METHODS = ['get_device_type_keyboard', 'get_game_mode', 'set_game_mode', 'set_macro_mode', 'get_macro_mode',
-               'get_macro_effect', 'set_macro_effect', 'bw_get_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
+               'get_macro_effect', 'set_macro_effect', 'bw_set_pulsate', 'bw_set_static', 'get_macros', 'delete_macro', 'add_macro']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/49/49_razer_deathstalker.png"
 
diff --git a/daemon/openrazer_daemon/hardware/mouse.py b/daemon/openrazer_daemon/hardware/mouse.py
index fdc64d69..d68885df 100644
--- a/daemon/openrazer_daemon/hardware/mouse.py
+++ b/daemon/openrazer_daemon/hardware/mouse.py
@@ -620,8 +620,8 @@ class RazerDeathAdderChroma(__RazerDeviceSpecialBrightnessSuspend):
     USB_VID = 0x1532
     USB_PID = 0x0043
     METHODS = ['get_device_type_mouse',
-               'set_logo_active', 'get_logo_active', 'get_logo_effect', 'get_logo_brightness', 'set_logo_brightness', 'set_logo_static', 'set_logo_pulsate', 'set_logo_blinking', 'set_logo_spectrum',
-               'set_scroll_active', 'get_scroll_active', 'get_scroll_effect', 'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_pulsate', 'set_scroll_blinking', 'set_scroll_spectrum',
+               'set_logo_active', 'get_logo_active', 'get_logo_brightness', 'set_logo_brightness', 'set_logo_static', 'set_logo_pulsate', 'set_logo_blinking', 'set_logo_spectrum',
+               'set_scroll_active', 'get_scroll_active', 'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_pulsate', 'set_scroll_blinking', 'set_scroll_spectrum',
                'max_dpi', 'get_dpi_xy', 'set_dpi_xy', 'get_poll_rate', 'set_poll_rate']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/278/278_deathadder_chroma.png"
@@ -1232,7 +1232,7 @@ class RazerMamba2012Wireless(__RazerDeviceSpecialBrightnessSuspend):
     USB_PID = 0x0025
     METHODS = ['get_device_type_mouse', 'get_battery', 'is_charging',
                'set_idle_time', 'set_low_battery_threshold', 'max_dpi', 'get_dpi_xy', 'set_dpi_xy', 'get_poll_rate', 'set_poll_rate',
-               'set_scroll_active', 'get_scroll_active', 'get_scroll_effect', 'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_spectrum']
+               'set_scroll_active', 'get_scroll_active', 'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_spectrum']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/192/192_mamba_2012.png"
 
@@ -1288,7 +1288,7 @@ class RazerMamba2012Wired(__RazerDevice):
     USB_PID = 0x0024
     METHODS = ['get_device_type_mouse',
                'set_idle_time', 'set_low_battery_threshold', 'max_dpi', 'get_dpi_xy', 'set_dpi_xy', 'get_poll_rate', 'set_poll_rate',
-               'set_scroll_active', 'get_scroll_active', 'get_scroll_effect', 'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_spectrum',
+               'set_scroll_active', 'get_scroll_active', 'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_spectrum',
                'get_battery', 'is_charging']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/192/192_mamba_2012.png"
@@ -1497,8 +1497,8 @@ class RazerAbyssusV2(__RazerDeviceSpecialBrightnessSuspend):
     USB_VID = 0x1532
     USB_PID = 0x005B
     METHODS = ['get_device_type_mouse',
-               'set_logo_active', 'get_logo_active', 'get_logo_effect', 'get_logo_brightness', 'set_logo_brightness', 'set_logo_static', 'set_logo_pulsate', 'set_logo_blinking', 'set_logo_spectrum',
-               'set_scroll_active', 'get_scroll_active', 'get_scroll_effect', 'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_pulsate', 'set_scroll_blinking', 'set_scroll_spectrum',
+               'set_logo_active', 'get_logo_active', 'get_logo_brightness', 'set_logo_brightness', 'set_logo_static', 'set_logo_pulsate', 'set_logo_blinking', 'set_logo_spectrum',
+               'set_scroll_active', 'get_scroll_active', 'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_pulsate', 'set_scroll_blinking', 'set_scroll_spectrum',
                'max_dpi', 'get_dpi_xy', 'set_dpi_xy', 'get_poll_rate', 'set_poll_rate']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/721/721_abyssusv2.png"
@@ -1631,8 +1631,8 @@ class RazerDeathAdder3500(__RazerDeviceSpecialBrightnessSuspend):
     USB_VID = 0x1532
     USB_PID = 0x0054
     METHODS = ['get_device_type_mouse',
-               'get_logo_effect', 'get_logo_brightness', 'set_logo_brightness', 'set_logo_static', 'set_logo_pulsate', 'set_logo_blinking',
-               'get_scroll_effect', 'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_pulsate', 'set_scroll_blinking',
+               'get_logo_brightness', 'set_logo_brightness', 'set_logo_static', 'set_logo_pulsate', 'set_logo_blinking',
+               'get_scroll_brightness', 'set_scroll_brightness', 'set_scroll_static', 'set_scroll_pulsate', 'set_scroll_blinking',
                'max_dpi', 'get_dpi_xy', 'set_dpi_xy', 'get_poll_rate', 'set_poll_rate']
 
     DEVICE_IMAGE = "https://assets.razerzone.com/eeimages/support/products/561/561_deathadder_classic.png"
diff --git a/daemon/openrazer_daemon/misc/autosave_persistence.py b/daemon/openrazer_daemon/misc/autosave_persistence.py
new file mode 100644
index 00000000..f1d1c155
--- /dev/null
+++ b/daemon/openrazer_daemon/misc/autosave_persistence.py
@@ -0,0 +1,36 @@
+"""
+A class that writes persistence data to disk when device state is updated.
+
+Due to the architecture of the daemon, it's not as simple to know when
+something has changed, so for now, an interval will periodically check for
+changes in memory and write them to disk. This also avoids excessive
+writes when lots of variables change at once.
+
+This is essential because many desktop environments actually kill off
+the daemon upon logout/shutdown, thereby persistence isn't retained across
+sessions.
+
+A known issue is that this doesn't monitor DPI changes via hardware buttons,
+so this won't be persisted until the state is updated via the API.
+"""
+import time
+
+
+class PersistenceAutoSave(object):
+    def __init__(self, persistence, persistence_file, persistence_status, logger, interval, persistence_save_fn):
+        self.persistence = persistence
+        self.persistence_file = persistence_file
+        self.persistence_status = persistence_status
+        self.persistence_save_fn = persistence_save_fn
+        self.logger = logger
+        self.interval = interval
+
+    def watch(self):
+        # Run indefinitely until process is terminated
+        while True:
+            time.sleep(self.interval)
+
+            if self.persistence_status["changed"]:
+                self.logger.debug("State recently changed, writing to disk")
+                self.persistence_status["changed"] = False
+                self.persistence_save_fn(self.persistence_file)
diff --git a/daemon/resources/man/openrazer-daemon.8 b/daemon/resources/man/openrazer-daemon.8
index 980a54d9..a42d7211 100644
--- a/daemon/resources/man/openrazer-daemon.8
+++ b/daemon/resources/man/openrazer-daemon.8
@@ -1,10 +1,11 @@
-.\" Generated by scdoc 1.8.1
+.\" Generated by scdoc 1.11.0
+.\" Complete documentation for this program is not available as a GNU info page
 .ie \n(.g .ds Aq \(aq
 .el       .ds Aq '
 .nh
 .ad l
 .\" Begin generated content:
-.TH "openrazer-daemon" "8" "2019-02-06"
+.TH "openrazer-daemon" "8" "2020-11-11"
 .P
 .SH NAME
 .P
@@ -54,6 +55,11 @@ Allow the daemon to be started as root.
 Specifies the location of the config file. If this is not provided it will default to ~/.config/openrazer/razer.conf and create it if needed from the example config.
 .P
 .RE
+\fB--persistence\fR=\fIpersistence_file\fR
+.RS 4
+Specifies the location of the persistence file. This will be created if non-existent. This file will store device states so they persist between reboots.
+.P
+.RE
 \fB--run-dir\fR=\fIrun_directory\fR
 .RS 4
 Tells the daemon what directory is its run directory, the directory it will change to once started. It will default to \fB$XDG_RUNTIME_DIR\fR, if not set it falls back to ~/.local/share/openrazer/.
diff --git a/daemon/resources/man/openrazer-daemon.8.scd b/daemon/resources/man/openrazer-daemon.8.scd
index 449fa6ea..1757dc99 100644
--- a/daemon/resources/man/openrazer-daemon.8.scd
+++ b/daemon/resources/man/openrazer-daemon.8.scd
@@ -36,6 +36,9 @@ Note, that all paths shown as defaults, eg ~/.local/share/ or ~/.config/ can be
 *--config*=_config\_file_
 	Specifies the location of the config file. If this is not provided it will default to ~/.config/openrazer/razer.conf and create it if needed from the example config.
 
+*--persistence*=_persistence\_file_
+	Specifies the location of the persistence file. This will be created if non-existent. This file will store device states so they persist between reboots.
+
 *--run-dir*=_run\_directory_
 	Tells the daemon what directory is its run directory, the directory it will change to once started. It will default to *$XDG_RUNTIME_DIR*, if not set it falls back to ~/.local/share/openrazer/.
 
diff --git a/daemon/run_openrazer_daemon.py b/daemon/run_openrazer_daemon.py
index fd64377b..fd368015 100755
--- a/daemon/run_openrazer_daemon.py
+++ b/daemon/run_openrazer_daemon.py
@@ -26,6 +26,7 @@ RAZER_RUNTIME_DIR = XDG_RUNTIME_DIR
 EXAMPLE_CONF_FILE = '/usr/share/openrazer/razer.conf.example'
 
 CONF_FILE = os.path.join(RAZER_CONFIG_HOME, 'razer.conf')
+PERSISTENCE_FILE = os.path.join(RAZER_CONFIG_HOME, 'persistence.conf')
 LOG_PATH = os.path.join(RAZER_DATA_HOME, 'logs')
 
 args = None
@@ -45,6 +46,7 @@ def parse_args():
     parser.add_argument('--as-root', action='store_true', help='Allow the daemon to be started as root')
 
     parser.add_argument('--config', type=str, help='Location of the config file', default=CONF_FILE)
+    parser.add_argument('--persistence', type=str, help='Location to file for storing device persistence data', default=PERSISTENCE_FILE)
     parser.add_argument('--run-dir', type=str, help='Location of the run directory', default=RAZER_RUNTIME_DIR)
     parser.add_argument('--log-dir', type=str, help='Location of the log directory', default=LOG_PATH)
 
@@ -116,12 +118,30 @@ def install_example_config_file(config_file):
         sys.exit(1)
 
 
+def init_persistence_config(persistence_file):
+    """
+    Creates a new file for persistence, if it does not exist.
+    """
+    if os.path.exists(persistence_file):
+        return
+
+    try:
+        os.makedirs(os.path.dirname(persistence_file), exist_ok=True)
+        with open(persistence_file, "w") as f:
+            f.writelines("")
+
+    except NotADirectoryError as e:
+        print("Failed to create {}".format(e.filename), file=sys.stderr)
+        sys.exit(1)
+
+
 def run_daemon():
     global args
     daemon = RazerDaemon(verbose=args.verbose,
                          log_dir=args.log_dir,
                          console_log=args.foreground,
                          config_file=args.config,
+                         persistence_file=args.persistence,
                          test_dir=args.test_dir)
     try:
         daemon.run()
@@ -161,6 +181,7 @@ def run():
             logger.setLevel(logging.DEBUG)
 
     install_example_config_file(args.config)
+    init_persistence_config(args.persistence)
 
     os.makedirs(args.run_dir, exist_ok=True)
     daemon = Daemonize(app="openrazer-daemon",
diff --git a/examples/custom_starlight.py b/examples/custom_starlight.py
index ca8f452a..36b20050 100644
--- a/examples/custom_starlight.py
+++ b/examples/custom_starlight.py
@@ -1,12 +1,16 @@
 from collections import defaultdict
 import colorsys
 import random
+import sys
 import time
 import threading
 
 from openrazer.client import DeviceManager
 from openrazer.client import constants as razer_constants
 
+# Set a quit flag that will be used when the user quits to restore effects.
+quit = False
+
 # Create a DeviceManager. This is used to get specific devices
 device_manager = DeviceManager()
 
@@ -76,6 +80,11 @@ def starlight_effect(device):
 
         time.sleep(0.1)
 
+        if quit:
+            break
+
+    device.fx.advanced.restore()
+
 
 # Spawn a manager thread for each device and wait on all of them.
 threads = []
@@ -86,7 +95,15 @@ for device in devices:
 
 
 # If there are still threads, update each device.
-while any(t.isAlive() for t in threads):
-    for device in devices:
-        device.fx.advanced.draw()
-    time.sleep(1 / 60)
+try:
+    while any(t.isAlive() for t in threads):
+        for device in devices:
+            device.fx.advanced.draw()
+        time.sleep(1 / 60)
+except KeyboardInterrupt:
+    quit = True
+
+    for t in threads:
+        t.join()
+
+    sys.exit(0)
diff --git a/pylib/openrazer/client/fx.py b/pylib/openrazer/client/fx.py
index e5fb90b1..9442ab53 100644
--- a/pylib/openrazer/client/fx.py
+++ b/pylib/openrazer/client/fx.py
@@ -68,6 +68,46 @@ class RazerFX(BaseRazerFX):
 
         self.misc = MiscLighting(serial, capabilities, self._dbus)
 
+    @property
+    def effect(self) -> str:
+        """
+        Get current effect
+
+        :return: Effect name ("static", "spectrum", etc.)
+        :rtype: str
+        """
+        return self._lighting_dbus.getEffect()
+
+    @property
+    def colors(self) -> bytearray:
+        """
+        Get current effect colors
+
+        :return: Effect colors (an array of 9 bytes, for 3 colors in RGB format)
+        :rtype: bytearray
+        """
+        return bytes(self._lighting_dbus.getEffectColors())
+
+    @property
+    def speed(self) -> int:
+        """
+        Get current effect speed
+
+        :return: Effect speed (a value between 0 and 3)
+        :rtype: int
+        """
+        return self._lighting_dbus.getEffectSpeed()
+
+    @property
+    def wave_dir(self) -> int:
+        """
+        Get current wave direction
+
+        :return: Wave direction (WAVE_LEFT or WAVE_RIGHT)
+        :rtype: int
+        """
+        return self._lighting_dbus.getWaveDir()
+
     def none(self) -> bool:
         """
         No effect
@@ -612,6 +652,12 @@ class RazerAdvancedFX(BaseRazerFX):
             else:
                 raise ValueError("RGB must be an RGB tuple")
 
+    def restore(self):
+        """
+        Restore the device to the last effect
+        """
+        self._lighting_dbus.restoreLastEffect()
+
 
 class SingleLed(BaseRazerFX):
     def __init__(self, serial: str, capabilities: dict, daemon_dbus=None, led_name='logo'):
@@ -644,6 +690,46 @@ class SingleLed(BaseRazerFX):
             else:
                 func(False)
 
+    @property
+    def effect(self) -> str:
+        """
+        Get current effect
+
+        :return: Effect name ("static", "spectrum", etc.)
+        :rtype: str
+        """
+        return str(self._getattr('get#Effect')())
+
+    @property
+    def colors(self) -> bytearray:
+        """
+        Get current effect colors
+
+        :return: Effect colors (an array of 9 bytes, for 3 colors in RGB format)
+        :rtype: bytearray
+        """
+        return bytes(self._getattr('get#EffectColors')())
+
+    @property
+    def speed(self) -> int:
+        """
+        Get current effect speed
+
+        :return: Effect speed (a value between 0 and 3)
+        :rtype: int
+        """
+        return int(self._getattr('get#EffectSpeed')())
+
+    @property
+    def wave_dir(self) -> int:
+        """
+        Get current wave direction
+
+        :return: Wave direction (WAVE_LEFT or WAVE_RIGHT)
+        :rtype: int
+        """
+        return int(self._getattr('get#WaveDir')())
+
     @property
     def brightness(self):
         if self._shas('brightness'):
-- 
GitLab