diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5e49f2c0..01f3837dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,12 @@
 All notable changes to this project will be documented in this file.
 Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
 
+## [Unreleased]
+
+### Added
+
+- Added `sun` plugin for sunrise/sunset events.
+
 ## [0.21.2] - 2021-07-20
 
 ### Added
diff --git a/docs/source/events.rst b/docs/source/events.rst
index a96548743..b0bace683 100644
--- a/docs/source/events.rst
+++ b/docs/source/events.rst
@@ -54,6 +54,7 @@ Events
     platypush/events/serial.rst
     platypush/events/sound.rst
     platypush/events/stt.rst
+    platypush/events/sun.rst
     platypush/events/tensorflow.rst
     platypush/events/todoist.rst
     platypush/events/torrent.rst
diff --git a/docs/source/platypush/events/sun.rst b/docs/source/platypush/events/sun.rst
new file mode 100644
index 000000000..135e60d25
--- /dev/null
+++ b/docs/source/platypush/events/sun.rst
@@ -0,0 +1,5 @@
+``platypush.message.event.sun``
+===============================
+
+.. automodule:: platypush.message.event.sun
+    :members:
diff --git a/docs/source/platypush/plugins/sun.rst b/docs/source/platypush/plugins/sun.rst
new file mode 100644
index 000000000..716ff5c12
--- /dev/null
+++ b/docs/source/platypush/plugins/sun.rst
@@ -0,0 +1,5 @@
+``sun``
+=======
+
+.. automodule:: platypush.plugins.sun
+    :members:
diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst
index ae49c3919..72570625f 100644
--- a/docs/source/plugins.rst
+++ b/docs/source/plugins.rst
@@ -119,6 +119,7 @@ Plugins
     platypush/plugins/stt.deepspeech.rst
     platypush/plugins/stt.picovoice.hotword.rst
     platypush/plugins/stt.picovoice.speech.rst
+    platypush/plugins/sun.rst
     platypush/plugins/switch.rst
     platypush/plugins/switch.tplink.rst
     platypush/plugins/switch.wemo.rst
diff --git a/platypush/__init__.py b/platypush/__init__.py
index be88565ae..c8edf8339 100644
--- a/platypush/__init__.py
+++ b/platypush/__init__.py
@@ -12,7 +12,7 @@ import sys
 
 from .bus.redis import RedisBus
 from .config import Config
-from .context import register_backends
+from .context import register_backends, register_plugins
 from .cron.scheduler import CronScheduler
 from .event.processor import EventProcessor
 from .logger import Logger
@@ -20,8 +20,7 @@ from .message.event import Event
 from .message.event.application import ApplicationStartedEvent
 from .message.request import Request
 from .message.response import Response
-from .utils import set_thread_name
-
+from .utils import set_thread_name, get_enabled_plugins
 
 __author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
 __version__ = '0.21.2'
@@ -160,9 +159,15 @@ class Daemon:
 
     def stop_app(self):
         """ Stops the backends and the bus """
+        from .plugins import RunnablePlugin
+
         for backend in self.backends.values():
             backend.stop()
 
+        for plugin in get_enabled_plugins().values():
+            if isinstance(plugin, RunnablePlugin):
+                plugin.stop()
+
         self.bus.stop()
         if self.cron_scheduler:
             self.cron_scheduler.stop()
@@ -184,6 +189,9 @@ class Daemon:
         for backend in self.backends.values():
             backend.start()
 
+        # Initialize the plugins
+        register_plugins(bus=self.bus)
+
         # Start the cron scheduler
         if Config.get_cronjobs():
             self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())
diff --git a/platypush/context/__init__.py b/platypush/context/__init__.py
index ecb542d4a..16048fead 100644
--- a/platypush/context/__init__.py
+++ b/platypush/context/__init__.py
@@ -5,6 +5,7 @@ import logging
 from threading import RLock
 
 from ..config import Config
+from ..utils import get_enabled_plugins
 
 logger = logging.getLogger('platypush:context')
 
@@ -21,6 +22,7 @@ plugins_init_locks = {}
 # Reference to the main application bus
 main_bus = None
 
+
 def register_backends(bus=None, global_scope=False, **kwargs):
     """ Initialize the backend objects based on the configuration and returns
         a name -> backend_instance map.
@@ -59,6 +61,16 @@ def register_backends(bus=None, global_scope=False, **kwargs):
 
     return backends
 
+
+def register_plugins(bus=None):
+    from ..plugins import RunnablePlugin
+
+    for plugin in get_enabled_plugins().values():
+        if isinstance(plugin, RunnablePlugin):
+            plugin.bus = bus
+            plugin.start()
+
+
 def get_backend(name):
     """ Returns the backend instance identified by name if it exists """
 
diff --git a/platypush/message/event/sun.py b/platypush/message/event/sun.py
new file mode 100644
index 000000000..46755123d
--- /dev/null
+++ b/platypush/message/event/sun.py
@@ -0,0 +1,36 @@
+from datetime import datetime
+from typing import Optional
+
+from platypush.message.event import Event
+
+
+class SunEvent(Event):
+    """
+    Base class for sun related events (sunrise and sunset).
+    """
+    def __init__(self, latitude: Optional[float] = None, longitude: Optional[float] = None,
+                 time: Optional[datetime] = None, *args, **kwargs):
+        """
+        :param latitude: Latitude for the sun event.
+        :param longitude: Longitude for the sun event.
+        :param time: Event timestamp.
+        """
+        super().__init__(*args, latitude=latitude, longitude=longitude, time=time, **kwargs)
+        self.latitude = latitude
+        self.longitude = longitude
+        self.time = time
+
+
+class SunriseEvent(SunEvent):
+    """
+    Class for sunrise events.
+    """
+
+
+class SunsetEvent(SunEvent):
+    """
+    Class for sunset events.
+    """
+
+
+# vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/__init__.py b/platypush/plugins/__init__.py
index 0db4cadf1..fd0042aa7 100644
--- a/platypush/plugins/__init__.py
+++ b/platypush/plugins/__init__.py
@@ -1,10 +1,15 @@
 import logging
+import threading
+import time
 
+from abc import ABC
 from functools import wraps
+from typing import Optional
 
+from platypush.bus import Bus
 from platypush.event import EventGenerator
 from platypush.message.response import Response
-from platypush.utils import get_decorators, get_plugin_name_by_class
+from platypush.utils import get_decorators, get_plugin_name_by_class, set_thread_name
 
 
 def action(f):
@@ -53,4 +58,56 @@ class Plugin(EventGenerator):
         return getattr(self, method)(*args, **kwargs)
 
 
+class RunnablePlugin(ABC, Plugin):
+    """
+    Class for runnable plugins - i.e. plugins that have a start/stop method and can be started.
+    """
+    def __init__(self, poll_interval: Optional[float] = None, **kwargs):
+        """
+        :param poll_interval: How often the :meth:`.loop` function should be execute (default: None, no pause/interval).
+        """
+        super().__init__(**kwargs)
+        self.poll_interval = poll_interval
+        self.bus: Optional[Bus] = None
+        self._should_stop = threading.Event()
+        self._thread: Optional[threading.Thread] = None
+
+    def main(self):
+        raise NotImplementedError()
+
+    def should_stop(self):
+        return self._should_stop.is_set()
+
+    def start(self):
+        set_thread_name(self.__class__.__name__)
+        self._thread = threading.Thread(target=self._runner)
+        self._thread.start()
+
+    def stop(self):
+        self._should_stop.set()
+        if self._thread and self._thread.is_alive():
+            self.logger.info(f'Waiting for {self.__class__.__name__} to stop')
+            # noinspection PyBroadException
+            try:
+                self._thread.join()
+            except:
+                pass
+
+        self.logger.info(f'{self.__class__.__name__} stopped')
+
+    def _runner(self):
+        self.logger.info(f'Starting {self.__class__.__name__}')
+
+        while not self.should_stop():
+            try:
+                self.main()
+            except Exception as e:
+                self.logger.exception(e)
+
+            if self.poll_interval:
+                time.sleep(self.poll_interval)
+
+        self._thread = None
+
+
 # vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/sun.py b/platypush/plugins/sun.py
new file mode 100644
index 000000000..d757eb4c9
--- /dev/null
+++ b/platypush/plugins/sun.py
@@ -0,0 +1,87 @@
+import datetime
+import time
+from typing import Optional
+
+import requests
+from dateutil.tz import gettz, tzutc
+
+from platypush.message.event.sun import SunriseEvent, SunsetEvent
+from platypush.plugins import RunnablePlugin, action
+from platypush.schemas.sun import SunEventsSchema
+
+
+class SunPlugin(RunnablePlugin):
+    """
+    Plugin to get sunset/sunrise events and info for a certain location.
+
+    Triggers:
+
+        * :class:`platypush.message.event.sun.SunriseEvent` on sunrise.
+        * :class:`platypush.message.event.sun.SunsetEvent` on sunset.
+
+    """
+    _base_url = 'https://api.sunrise-sunset.org/json'
+    _attr_to_event_class = {
+        'sunrise': SunriseEvent,
+        'sunset': SunsetEvent,
+    }
+
+    def __init__(self, latitude: float, longitude: float, **kwargs):
+        """
+        :param latitude: Default latitude.
+        :param longitude: Default longitude.
+        """
+        super().__init__(**kwargs)
+        self.latitude = latitude
+        self.longitude = longitude
+
+    def main(self):
+        while not self.should_stop():
+            # noinspection PyUnresolvedReferences
+            next_events = self.get_events().output
+            next_events = sorted([
+                event_class(latitude=self.latitude, longitude=self.longitude, time=next_events[attr])
+                for attr, event_class in self._attr_to_event_class.items()
+                if next_events.get(attr)
+            ], key=lambda t: t.time)
+
+            for event in next_events:
+                # noinspection PyTypeChecker
+                dt = datetime.datetime.fromisoformat(event.time)
+                while (not self.should_stop()) and (dt > datetime.datetime.now(tz=gettz())):
+                    time.sleep(1)
+
+                if dt <= datetime.datetime.now(tz=gettz()):
+                    self.bus.post(event)
+
+    @staticmethod
+    def _convert_time(t: str) -> datetime.datetime:
+        now = datetime.datetime.now().replace(tzinfo=gettz())  # lgtm [py/call-to-non-callable]
+        dt = datetime.datetime.strptime(t, '%H:%M:%S %p')
+        dt = datetime.datetime(year=now.year, month=now.month, day=now.day,
+                               hour=dt.hour, minute=dt.minute, second=dt.second, tzinfo=tzutc())
+
+        if dt < now:
+            dt += datetime.timedelta(days=1)
+        return datetime.datetime.fromtimestamp(dt.timestamp(), tz=gettz())
+
+    @action
+    def get_events(self, latitude: Optional[float] = None, longitude: Optional[float] = None) -> dict:
+        """
+        Return the next sun events.
+
+        :param latitude: Default latitude override.
+        :param longitude: Default longitude override.
+        :return: .. schema:: sun.SunEventsSchema
+        """
+        response = requests.get(self._base_url, params={
+            'lat': latitude or self.latitude,
+            'lng': longitude or self.longitude,
+        }).json().get('results', {})
+
+        schema = SunEventsSchema()
+        return schema.dump({
+            attr: self._convert_time(t)
+            for attr, t in response.items()
+            if attr in schema.declared_fields.keys()
+        })
diff --git a/platypush/schemas/__init__.py b/platypush/schemas/__init__.py
index e69de29bb..c4c103f97 100644
--- a/platypush/schemas/__init__.py
+++ b/platypush/schemas/__init__.py
@@ -0,0 +1,10 @@
+from datetime import datetime
+from typing import Optional
+
+
+def normalize_datetime(dt: str) -> Optional[datetime]:
+    if not dt:
+        return
+    if dt.endswith('Z'):
+        dt = dt[:-1] + '+00:00'
+    return datetime.fromisoformat(dt)
diff --git a/platypush/schemas/spotify.py b/platypush/schemas/spotify.py
index 11c8c41f3..1a809ae75 100644
--- a/platypush/schemas/spotify.py
+++ b/platypush/schemas/spotify.py
@@ -1,11 +1,12 @@
 from datetime import datetime
-from typing import Union, Optional
+from typing import Union
 
 from marshmallow import fields, pre_dump
 from marshmallow.schema import Schema
 from marshmallow.validate import OneOf, Range
 
 from platypush.plugins.media import PlayerState
+from platypush.schemas import normalize_datetime
 
 device_types = [
     'Unknown',
@@ -20,14 +21,6 @@ device_types = [
 ]
 
 
-def normalize_datetime(dt: str) -> Optional[datetime]:
-    if not dt:
-        return
-    if dt.endswith('Z'):
-        dt = dt[:-1] + '+00:00'
-    return datetime.fromisoformat(dt)
-
-
 class SpotifySchema(Schema):
     @staticmethod
     def _normalize_timestamp(t: Union[str, datetime]) -> datetime:
diff --git a/platypush/schemas/sun.py b/platypush/schemas/sun.py
new file mode 100644
index 000000000..199733e6e
--- /dev/null
+++ b/platypush/schemas/sun.py
@@ -0,0 +1,14 @@
+from marshmallow import fields
+from marshmallow.schema import Schema
+
+
+class SunEventsSchema(Schema):
+    sunrise = fields.DateTime(metadata=dict(description='Next sunrise time'))
+    sunset = fields.DateTime(metadata=dict(description='Next sunset time'))
+    solar_noon = fields.DateTime(metadata=dict(description='Next solar noon time'))
+    civil_twilight_begin = fields.DateTime(metadata=dict(description='Next civil twilight start time'))
+    civil_twilight_end = fields.DateTime(metadata=dict(description='Next civil twilight end time'))
+    nautical_twilight_begin = fields.DateTime(metadata=dict(description='Next nautical twilight start time'))
+    nautical_twilight_end = fields.DateTime(metadata=dict(description='Next nautical twilight end time'))
+    astronomical_twilight_begin = fields.DateTime(metadata=dict(description='Next astronomical twilight start time'))
+    astronomical_twilight_end = fields.DateTime(metadata=dict(description='Next astronomical twilight end time'))