diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py
index 015c386e836367e732a8500ce60c74f1a7dc08ff..2a25de96edc1dff85a198164e8e91b1794073907 100644
--- a/homeassistant/components/http/ban.py
+++ b/homeassistant/components/http/ban.py
@@ -10,7 +10,6 @@ from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
 import voluptuous as vol
 
 from homeassistant.core import callback
-from homeassistant.components import persistent_notification
 from homeassistant.config import load_yaml_config_file
 from homeassistant.exceptions import HomeAssistantError
 import homeassistant.helpers.config_validation as cv
@@ -92,9 +91,10 @@ async def process_wrong_login(request):
     msg = ('Login attempt or request with invalid authentication '
            'from {}'.format(remote_addr))
     _LOGGER.warning(msg)
-    persistent_notification.async_create(
-        request.app['hass'], msg, 'Login attempt failed',
-        NOTIFICATION_ID_LOGIN)
+
+    hass = request.app['hass']
+    hass.components.persistent_notification.async_create(
+        msg, 'Login attempt failed', NOTIFICATION_ID_LOGIN)
 
     # Check if ban middleware is loaded
     if (KEY_BANNED_IPS not in request.app or
@@ -108,15 +108,13 @@ async def process_wrong_login(request):
         new_ban = IpBan(remote_addr)
         request.app[KEY_BANNED_IPS].append(new_ban)
 
-        hass = request.app['hass']
         await hass.async_add_job(
             update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban)
 
         _LOGGER.warning(
             "Banned IP %s for too many login attempts", remote_addr)
 
-        persistent_notification.async_create(
-            hass,
+        hass.components.persistent_notification.async_create(
             'Too many login attempts from {}'.format(remote_addr),
             'Banning IP address', NOTIFICATION_ID_BAN)
 
diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py
index 2850a5f96cd90b203fda56bfac9c1dd9a14fd6f1..6b8fd68bc26b01f945a7066e1511f7edcd44f2a9 100644
--- a/homeassistant/components/persistent_notification/__init__.py
+++ b/homeassistant/components/persistent_notification/__init__.py
@@ -6,10 +6,12 @@ https://home-assistant.io/components/persistent_notification/
 """
 import asyncio
 import logging
+from collections import OrderedDict
 from typing import Awaitable
 
 import voluptuous as vol
 
+from homeassistant.components import websocket_api
 from homeassistant.core import callback, HomeAssistant
 from homeassistant.exceptions import TemplateError
 from homeassistant.loader import bind_hass
@@ -20,13 +22,17 @@ from homeassistant.util import slugify
 ATTR_MESSAGE = 'message'
 ATTR_NOTIFICATION_ID = 'notification_id'
 ATTR_TITLE = 'title'
+ATTR_STATUS = 'status'
 
 DOMAIN = 'persistent_notification'
 
 ENTITY_ID_FORMAT = DOMAIN + '.{}'
 
+EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = 'persistent_notifications_updated'
+
 SERVICE_CREATE = 'create'
 SERVICE_DISMISS = 'dismiss'
+SERVICE_MARK_READ = 'mark_read'
 
 SCHEMA_SERVICE_CREATE = vol.Schema({
     vol.Required(ATTR_MESSAGE): cv.template,
@@ -38,11 +44,21 @@ SCHEMA_SERVICE_DISMISS = vol.Schema({
     vol.Required(ATTR_NOTIFICATION_ID): cv.string,
 })
 
+SCHEMA_SERVICE_MARK_READ = vol.Schema({
+    vol.Required(ATTR_NOTIFICATION_ID): cv.string,
+})
 
 DEFAULT_OBJECT_ID = 'notification'
 _LOGGER = logging.getLogger(__name__)
 
 STATE = 'notifying'
+STATUS_UNREAD = 'unread'
+STATUS_READ = 'read'
+
+WS_TYPE_GET_NOTIFICATIONS = 'persistent_notification/get'
+SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+    vol.Required('type'): WS_TYPE_GET_NOTIFICATIONS,
+})
 
 
 @bind_hass
@@ -76,7 +92,7 @@ def async_create(hass: HomeAssistant, message: str, title: str = None,
 
 @callback
 @bind_hass
-def async_dismiss(hass, notification_id):
+def async_dismiss(hass: HomeAssistant, notification_id: str) -> None:
     """Remove a notification."""
     data = {ATTR_NOTIFICATION_ID: notification_id}
 
@@ -86,6 +102,9 @@ def async_dismiss(hass, notification_id):
 @asyncio.coroutine
 def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]:
     """Set up the persistent notification component."""
+    persistent_notifications = OrderedDict()
+    hass.data[DOMAIN] = {'notifications': persistent_notifications}
+
     @callback
     def create_service(call):
         """Handle a create notification service call."""
@@ -98,6 +117,8 @@ def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]:
         else:
             entity_id = async_generate_entity_id(
                 ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, hass=hass)
+            notification_id = entity_id.split('.')[1]
+
         attr = {}
         if title is not None:
             try:
@@ -120,18 +141,72 @@ def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]:
 
         hass.states.async_set(entity_id, STATE, attr)
 
+        # Store notification and fire event
+        # This will eventually replace state machine storage
+        persistent_notifications[entity_id] = {
+            ATTR_MESSAGE: message,
+            ATTR_NOTIFICATION_ID: notification_id,
+            ATTR_STATUS: STATUS_UNREAD,
+            ATTR_TITLE: title,
+        }
+
+        hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)
+
     @callback
     def dismiss_service(call):
         """Handle the dismiss notification service call."""
         notification_id = call.data.get(ATTR_NOTIFICATION_ID)
         entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))
 
+        if entity_id not in persistent_notifications:
+            return
+
         hass.states.async_remove(entity_id)
 
+        del persistent_notifications[entity_id]
+        hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)
+
+    @callback
+    def mark_read_service(call):
+        """Handle the mark_read notification service call."""
+        notification_id = call.data.get(ATTR_NOTIFICATION_ID)
+        entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))
+
+        if entity_id not in persistent_notifications:
+            _LOGGER.error('Marking persistent_notification read failed: '
+                          'Notification ID %s not found.', notification_id)
+            return
+
+        persistent_notifications[entity_id][ATTR_STATUS] = STATUS_READ
+        hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)
+
     hass.services.async_register(DOMAIN, SERVICE_CREATE, create_service,
                                  SCHEMA_SERVICE_CREATE)
 
     hass.services.async_register(DOMAIN, SERVICE_DISMISS, dismiss_service,
                                  SCHEMA_SERVICE_DISMISS)
 
+    hass.services.async_register(DOMAIN, SERVICE_MARK_READ, mark_read_service,
+                                 SCHEMA_SERVICE_MARK_READ)
+
+    hass.components.websocket_api.async_register_command(
+        WS_TYPE_GET_NOTIFICATIONS, websocket_get_notifications,
+        SCHEMA_WS_GET
+    )
+
     return True
+
+
+@callback
+def websocket_get_notifications(
+        hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
+    """Return a list of persistent_notifications."""
+    connection.to_write.put_nowait(
+        websocket_api.result_message(msg['id'], [
+            {
+                key: data[key] for key in (ATTR_NOTIFICATION_ID, ATTR_MESSAGE,
+                                           ATTR_STATUS, ATTR_TITLE)
+            }
+            for data in hass.data[DOMAIN]['notifications'].values()
+        ])
+    )
diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py
index a609247b839d9c510598b3e5665a729658b84ba8..6acc796a108fe279daddd59ecd4d23b1bc408683 100644
--- a/tests/components/persistent_notification/test_init.py
+++ b/tests/components/persistent_notification/test_init.py
@@ -1,5 +1,6 @@
 """The tests for the persistent notification component."""
-from homeassistant.setup import setup_component
+from homeassistant.components import websocket_api
+from homeassistant.setup import setup_component, async_setup_component
 import homeassistant.components.persistent_notification as pn
 
 from tests.common import get_test_home_assistant
@@ -19,7 +20,9 @@ class TestPersistentNotification:
 
     def test_create(self):
         """Test creating notification without title or notification id."""
+        notifications = self.hass.data[pn.DOMAIN]['notifications']
         assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+        assert len(notifications) == 0
 
         pn.create(self.hass, 'Hello World {{ 1 + 1 }}',
                   title='{{ 1 + 1 }} beers')
@@ -27,54 +30,170 @@ class TestPersistentNotification:
 
         entity_ids = self.hass.states.entity_ids(pn.DOMAIN)
         assert len(entity_ids) == 1
+        assert len(notifications) == 1
 
         state = self.hass.states.get(entity_ids[0])
         assert state.state == pn.STATE
         assert state.attributes.get('message') == 'Hello World 2'
         assert state.attributes.get('title') == '2 beers'
 
+        notification = notifications.get(entity_ids[0])
+        assert notification['status'] == pn.STATUS_UNREAD
+        assert notification['message'] == 'Hello World 2'
+        assert notification['title'] == '2 beers'
+        notifications.clear()
+
     def test_create_notification_id(self):
         """Ensure overwrites existing notification with same id."""
+        notifications = self.hass.data[pn.DOMAIN]['notifications']
         assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+        assert len(notifications) == 0
 
         pn.create(self.hass, 'test', notification_id='Beer 2')
         self.hass.block_till_done()
 
         assert len(self.hass.states.entity_ids()) == 1
-        state = self.hass.states.get('persistent_notification.beer_2')
+        assert len(notifications) == 1
+
+        entity_id = 'persistent_notification.beer_2'
+        state = self.hass.states.get(entity_id)
         assert state.attributes.get('message') == 'test'
 
+        notification = notifications.get(entity_id)
+        assert notification['message'] == 'test'
+        assert notification['title'] is None
+
         pn.create(self.hass, 'test 2', notification_id='Beer 2')
         self.hass.block_till_done()
 
         # We should have overwritten old one
         assert len(self.hass.states.entity_ids()) == 1
-        state = self.hass.states.get('persistent_notification.beer_2')
+        state = self.hass.states.get(entity_id)
         assert state.attributes.get('message') == 'test 2'
 
+        notification = notifications.get(entity_id)
+        assert notification['message'] == 'test 2'
+        notifications.clear()
+
     def test_create_template_error(self):
         """Ensure we output templates if contain error."""
+        notifications = self.hass.data[pn.DOMAIN]['notifications']
         assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+        assert len(notifications) == 0
 
         pn.create(self.hass, '{{ message + 1 }}', '{{ title + 1 }}')
         self.hass.block_till_done()
 
         entity_ids = self.hass.states.entity_ids(pn.DOMAIN)
         assert len(entity_ids) == 1
+        assert len(notifications) == 1
 
         state = self.hass.states.get(entity_ids[0])
         assert state.attributes.get('message') == '{{ message + 1 }}'
         assert state.attributes.get('title') == '{{ title + 1 }}'
 
+        notification = notifications.get(entity_ids[0])
+        assert notification['message'] == '{{ message + 1 }}'
+        assert notification['title'] == '{{ title + 1 }}'
+        notifications.clear()
+
     def test_dismiss_notification(self):
         """Ensure removal of specific notification."""
+        notifications = self.hass.data[pn.DOMAIN]['notifications']
         assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+        assert len(notifications) == 0
 
         pn.create(self.hass, 'test', notification_id='Beer 2')
         self.hass.block_till_done()
 
         assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 1
+        assert len(notifications) == 1
         pn.dismiss(self.hass, notification_id='Beer 2')
         self.hass.block_till_done()
 
         assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0
+        assert len(notifications) == 0
+        notifications.clear()
+
+    def test_mark_read(self):
+        """Ensure notification is marked as Read."""
+        notifications = self.hass.data[pn.DOMAIN]['notifications']
+        assert len(notifications) == 0
+
+        pn.create(self.hass, 'test', notification_id='Beer 2')
+        self.hass.block_till_done()
+
+        entity_id = 'persistent_notification.beer_2'
+        assert len(notifications) == 1
+        notification = notifications.get(entity_id)
+        assert notification['status'] == pn.STATUS_UNREAD
+
+        self.hass.services.call(pn.DOMAIN, pn.SERVICE_MARK_READ, {
+            'notification_id': 'Beer 2'
+        })
+        self.hass.block_till_done()
+
+        assert len(notifications) == 1
+        notification = notifications.get(entity_id)
+        assert notification['status'] == pn.STATUS_READ
+        notifications.clear()
+
+
+async def test_ws_get_notifications(hass, hass_ws_client):
+    """Test websocket endpoint for retrieving persistent notifications."""
+    await async_setup_component(hass, pn.DOMAIN, {})
+
+    client = await hass_ws_client(hass)
+
+    await client.send_json({
+        'id': 5,
+        'type': 'persistent_notification/get'
+    })
+    msg = await client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == websocket_api.TYPE_RESULT
+    assert msg['success']
+    notifications = msg['result']
+    assert len(notifications) == 0
+
+    # Create
+    hass.components.persistent_notification.async_create(
+        'test', notification_id='Beer 2')
+    await client.send_json({
+        'id': 6,
+        'type': 'persistent_notification/get'
+    })
+    msg = await client.receive_json()
+    assert msg['id'] == 6
+    assert msg['type'] == websocket_api.TYPE_RESULT
+    assert msg['success']
+    notifications = msg['result']
+    assert len(notifications) == 1
+    notification = notifications[0]
+    assert notification['notification_id'] == 'Beer 2'
+    assert notification['message'] == 'test'
+    assert notification['title'] is None
+    assert notification['status'] == pn.STATUS_UNREAD
+
+    # Mark Read
+    await hass.services.async_call(pn.DOMAIN, pn.SERVICE_MARK_READ, {
+        'notification_id': 'Beer 2'
+    })
+    await client.send_json({
+        'id': 7,
+        'type': 'persistent_notification/get'
+    })
+    msg = await client.receive_json()
+    notifications = msg['result']
+    assert len(notifications) == 1
+    assert notifications[0]['status'] == pn.STATUS_READ
+
+    # Dismiss
+    hass.components.persistent_notification.async_dismiss('Beer 2')
+    await client.send_json({
+        'id': 8,
+        'type': 'persistent_notification/get'
+    })
+    msg = await client.receive_json()
+    notifications = msg['result']
+    assert len(notifications) == 0