diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py
index 3b2972a702c410ac7235b15c2d57f46c52319e5b..fbfcce7d50d06d637c802805ee1fec4128656a62 100644
--- a/homeassistant/components/api.py
+++ b/homeassistant/components/api.py
@@ -9,6 +9,8 @@ import logging
 import re
 import threading
 
+from werkzeug.exceptions import NotFound, BadRequest
+
 import homeassistant.core as ha
 import homeassistant.remote as rem
 from homeassistant.bootstrap import ERROR_LOG_FILENAME
@@ -23,9 +25,10 @@ from homeassistant.const import (
 from homeassistant.exceptions import TemplateError
 from homeassistant.helpers.state import TrackStates
 from homeassistant.helpers import template
+from homeassistant.components.wsgi import HomeAssistantView
 
 DOMAIN = 'api'
-DEPENDENCIES = ['http']
+DEPENDENCIES = ['http', 'wsgi']
 
 STREAM_PING_PAYLOAD = "ping"
 STREAM_PING_INTERVAL = 50  # seconds
@@ -99,14 +102,38 @@ def setup(hass, config):
     hass.http.register_path('POST', URL_API_TEMPLATE,
                             _handle_post_api_template)
 
+    hass.wsgi.register_view(APIStatusView)
+    hass.wsgi.register_view(APIConfigView)
+    hass.wsgi.register_view(APIDiscoveryView)
+    hass.wsgi.register_view(APIEntityStateView)
+    hass.wsgi.register_view(APIStatesView)
+    hass.wsgi.register_view(APIEventListenersView)
+    hass.wsgi.register_view(APIServicesView)
+    hass.wsgi.register_view(APIDomainServicesView)
+
     return True
 
 
+class APIStatusView(HomeAssistantView):
+    url = URL_API
+    name = "api:status"
+
+    def get(self, request):
+        return {'message': 'API running.'}
+
+
 def _handle_get_api(handler, path_match, data):
     """Render the debug interface."""
     handler.write_json_message("API running.")
 
 
+class APIEventStream(HomeAssistantView):
+    url = ""
+    name = ""
+
+    # TODO Implement this...
+
+
 def _handle_get_api_stream(handler, path_match, data):
     """Provide a streaming interface for the event bus."""
     gracefully_closed = False
@@ -177,11 +204,28 @@ def _handle_get_api_stream(handler, path_match, data):
         hass.bus.remove_listener(MATCH_ALL, forward_events)
 
 
+class APIConfigView(HomeAssistantView):
+    url = URL_API_CONFIG
+    name = "api:config"
+
+    def get(self, request):
+        return self.hass.config.as_dict()
+
+
 def _handle_get_api_config(handler, path_match, data):
     """Return the Home Assistant configuration."""
     handler.write_json(handler.server.hass.config.as_dict())
 
 
+class APIDiscoveryView(HomeAssistantView):
+    url = URL_API_DISCOVERY_INFO
+    name = "api:discovery"
+
+    def get(self, request):
+        # TODO
+        return {}
+
+
 def _handle_get_api_discovery_info(handler, path_match, data):
     needs_auth = (handler.server.hass.config.api.api_password is not None)
     params = {
@@ -193,11 +237,69 @@ def _handle_get_api_discovery_info(handler, path_match, data):
     handler.write_json(params)
 
 
+class APIStatesView(HomeAssistantView):
+    url = URL_API_STATES
+    name = "api:states"
+
+    def get(self, request):
+        return self.hass.states.all()
+
+
 def _handle_get_api_states(handler, path_match, data):
     """Return a dict containing all entity ids and their state."""
     handler.write_json(handler.server.hass.states.all())
 
 
+class APIEntityStateView(HomeAssistantView):
+    url = "/api/states/<entity_id>"
+    name = "api:entity-state"
+
+    def get(self, request, entity_id):
+        state = self.hass.states.get(entity_id)
+        if state:
+            return state
+        else:
+            raise NotFound("State does not exist.")
+
+    def post(self, request, entity_id):
+        try:
+            new_state = request.values['state']
+        except KeyError:
+            raise BadRequest("state not specified")
+
+        attributes = request.values.get('attributes')
+
+        is_new_state = self.hass.states.get(entity_id) is None
+
+        # Write state
+        self.hass.states.set(entity_id, new_state, attributes)
+
+        # Read the state back for our response
+        msg = json.dumps(
+            self.hass.states.get(entity_id).as_dict(),
+            sort_keys=True,
+            cls=rem.JSONEncoder
+        ).encode('UTF-8')
+
+        resp = Response(msg, mimetype="application/json")
+
+        if is_new_state:
+            resp.status_code = HTTP_CREATED
+
+        resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
+
+        return resp
+
+    def delete(self, request, entity_id):
+        if self.hass.states.remove(entity_id):
+            return {"message:" "Entity removed"}
+        else:
+            return {
+                "message": "Entity not found",
+                "status_code": HTTP_NOT_FOUND,
+            }
+
+
 def _handle_get_api_states_entity(handler, path_match, data):
     """Return the state of a specific entity."""
     entity_id = path_match.group('entity_id')
@@ -257,11 +359,40 @@ def _handle_delete_state_entity(handler, path_match, data):
             "Entity removed", HTTP_OK)
 
 
+class APIEventListenersView(HomeAssistantView):
+    url = URL_API_EVENTS
+    name = "api:event-listeners"
+
+    def get(self, request):
+        return events_json(self.hass)
+
+
 def _handle_get_api_events(handler, path_match, data):
     """Handle getting overview of event listeners."""
     handler.write_json(events_json(handler.server.hass))
 
 
+class APIEventView(HomeAssistantView):
+    url = '/api/events/<event_type>'
+    name = "api:event"
+
+    def post(self, request, event_type):
+        event_data = request.values
+
+        # Special case handling for event STATE_CHANGED
+        # We will try to convert state dicts back to State objects
+        if event_type == ha.EVENT_STATE_CHANGED and event_data:
+            for key in ('old_state', 'new_state'):
+                state = ha.State.from_dict(event_data.get(key))
+
+                if state:
+                    event_data[key] = state
+
+        self.hass.bus.fire(event_type, request.values, ha.EventOrigin.remote)
+
+        return {"message": "Event {} fired.".format(event_type)}
+
+
 def _handle_api_post_events_event(handler, path_match, event_data):
     """Handle firing of an event.
 
@@ -292,11 +423,30 @@ def _handle_api_post_events_event(handler, path_match, event_data):
     handler.write_json_message("Event {} fired.".format(event_type))
 
 
+class APIServicesView(HomeAssistantView):
+    url = URL_API_SERVICES
+    name = "api:services"
+
+    def get(self, request):
+        return services_json(self.hass)
+
+
 def _handle_get_api_services(handler, path_match, data):
     """Handle getting overview of services."""
     handler.write_json(services_json(handler.server.hass))
 
 
+class APIDomainServicesView(HomeAssistantView):
+    url = "/api/services/<domain>/<service>"
+    name = "api:domain-services"
+
+    def post(self, request):
+        with TrackStates(self.hass) as changed_states:
+            self.hass.services.call(domain, service, request.values, True)
+
+        return changed_states
+
+
 # pylint: disable=invalid-name
 def _handle_post_api_services_domain_service(handler, path_match, data):
     """Handle calling a service.
@@ -312,6 +462,68 @@ def _handle_post_api_services_domain_service(handler, path_match, data):
     handler.write_json(changed_states)
 
 
+class APIEventForwardingView(HomeAssistantView):
+    url = URL_API_EVENT_FORWARD
+    name = "api:event-forward"
+
+    def post(self, request):
+        try:
+            host = request.values['host']
+            api_password = request.values['api_password']
+        except KeyError:
+            return {
+                "message": "No host or api_password received.",
+                "status_code": HTTP_BAD_REQUEST,
+            }
+
+        try:
+            port = int(data['port']) if 'port' in data else None
+        except ValueError:
+            return {
+                "message": "Invalid value received for port.",
+                "status_code": HTTP_UNPROCESSABLE_ENTITY,
+            }
+
+        api = rem.API(host, api_password, port)
+
+        if not api.validate_api():
+            return {
+                "message": "Unable to validate API.",
+                "status_code": HTTP_UNPROCESSABLE_ENTITY,
+            }
+
+        if self.hass.event_forwarder is None:
+            self.hass.event_forwarder = rem.EventForwarder(self.hass)
+
+        self.hass.event_forwarder.connect(api)
+
+        return {"message": "Event forwarding setup."}
+
+    def delete(self, request):
+        try:
+            host = request.values['host']
+        except KeyError:
+            return {
+                "message": "No host received.",
+                "status_code": HTTP_BAD_REQUEST,
+            }
+
+        try:
+            port = int(data['port']) if 'port' in data else None
+        except ValueError:
+            return {
+                "message": "Invalid value received for port",
+                "status_code": HTTP_UNPROCESSABLE_ENTITY,
+            }
+
+        if self.hass.event_forwarder is not None:
+            api = rem.API(host, None, port)
+
+            self.hass.event_forwarder.disconnect(api)
+
+        return {"message": "Event forwarding cancelled."}
+
+
 # pylint: disable=invalid-name
 def _handle_post_api_event_forward(handler, path_match, data):
     """Handle adding an event forwarding target."""
@@ -369,17 +581,43 @@ def _handle_delete_api_event_forward(handler, path_match, data):
     handler.write_json_message("Event forwarding cancelled.")
 
 
+class APIComponentsView(HomeAssistantView):
+    url = URL_API_COMPONENTS
+    name = "api:components"
+
+    def get(self, request):
+        return self.hass.config.components
+
+
 def _handle_get_api_components(handler, path_match, data):
     """Return all the loaded components."""
     handler.write_json(handler.server.hass.config.components)
 
 
+class APIErrorLogView(HomeAssistantView):
+    url = URL_API_ERROR_LOG
+    name = "api:error-log"
+
+    def get(self, request):
+        # TODO
+        return {}
+
+
 def _handle_get_api_error_log(handler, path_match, data):
     """Return the logged errors for this session."""
     handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
                        False)
 
 
+class APILogOutView(HomeAssistantView):
+    url = URL_API_LOG_OUT
+    name = "api:log-out"
+
+    def post(self, request):
+        # TODO
+        return {}
+
+
 def _handle_post_api_log_out(handler, path_match, data):
     """Log user out."""
     handler.send_response(HTTP_OK)
@@ -387,6 +625,15 @@ def _handle_post_api_log_out(handler, path_match, data):
     handler.end_headers()
 
 
+class APITemplateView(HomeAssistantView):
+    url = URL_API_TEMPLATE
+    name = "api:template"
+
+    def post(self, request):
+        # TODO
+        return {}
+
+
 def _handle_post_api_template(handler, path_match, data):
     """Log user out."""
     template_string = data.get('template', '')
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index c1025fd16578a193c4c37075d560f6746855514e..f591fdd14be2b9392403030bdd7801254640b7f1 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -3,16 +3,26 @@ import re
 import os
 import logging
 
+from jinja2 import FileSystemLoader, Environment
+from werkzeug.wrappers import Response
+
 from . import version, mdi_version
 import homeassistant.util as util
 from homeassistant.const import URL_ROOT, HTTP_OK
 from homeassistant.components import api
+from homeassistant.components.wsgi import HomeAssistantView
 
 DOMAIN = 'frontend'
 DEPENDENCIES = ['api']
 
 INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
 
+TEMPLATES = Environment(
+    loader=FileSystemLoader(
+        os.path.join(os.path.dirname(__file__), 'templates/')
+    )
+)
+
 _LOGGER = logging.getLogger(__name__)
 
 FRONTEND_URLS = [
@@ -49,9 +59,61 @@ def setup(hass, config):
         'GET', re.compile(r'/local/(?P<file>[a-zA-Z\._\-0-9/]+)'),
         _handle_get_local, False)
 
+    hass.wsgi.register_view(IndexView)
+    hass.wsgi.register_view(BootstrapView)
+
+    www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
+    if hass.wsgi.development:
+        sw_path = "home-assistant-polymer/build/service_worker.js"
+    else:
+        sw_path = "service_worker.js"
+
+    hass.wsgi.register_static_path(
+        "/service_worker.js",
+        os.path.join(www_static_path, sw_path)
+    )
+    hass.wsgi.register_static_path("/static", www_static_path)
+    hass.wsgi.register_static_path("/local", hass.config.path('www'))
+
     return True
 
 
+class BootstrapView(HomeAssistantView):
+    url = URL_API_BOOTSTRAP
+    name = "api:bootstrap"
+
+    def get(self, request):
+        """Return all data needed to bootstrap Home Assistant."""
+
+        return {
+            'config': self.hass.config.as_dict(),
+            'states': self.hass.states.all(),
+            'events': api.events_json(self.hass),
+            'services': api.services_json(self.hass),
+        }
+
+
+class IndexView(HomeAssistantView):
+    url = URL_ROOT
+    name = "frontend:index"
+    extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
+                  '/devEvent', '/devInfo', '/devTemplate', '/states/<entity>']
+
+    def get(self, request):
+        app_url = "frontend-{}.html".format(version.VERSION)
+
+        # auto login if no password was set, else check api_password param
+        auth = ('no_password_set' if request.api_password is None
+                else request.values.get('api_password', ''))
+
+        template = TEMPLATES.get_template('index.html')
+
+        resp = template.render(app_url=app_url, auth=auth,
+                               icons=mdi_version.VERSION)
+
+        return Response(resp, mimetype="text/html")
+
+
 def _handle_get_api_bootstrap(handler, path_match, data):
     """Return all data needed to bootstrap Home Assistant."""
     hass = handler.server.hass
diff --git a/homeassistant/components/frontend/templates/index.html b/homeassistant/components/frontend/templates/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..e21d00e86bc53b12629a9f45c32354e57b2fb33f
--- /dev/null
+++ b/homeassistant/components/frontend/templates/index.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Home Assistant</title>
+
+    <link rel='manifest' href='/static/manifest.json'>
+    <link rel='icon' href='/static/favicon.ico'>
+    <link rel='apple-touch-icon' sizes='180x180'
+          href='/static/favicon-apple-180x180.png'>
+    <meta name='apple-mobile-web-app-capable' content='yes'>
+    <meta name='mobile-web-app-capable' content='yes'>
+    <meta name='viewport' content='width=device-width, user-scalable=no'>
+    <meta name='theme-color' content='#03a9f4'>
+    <style>
+      #ha-init-skeleton {
+        display: -webkit-flex;
+        display: flex;
+        -webkit-flex-direction: column;
+        -webkit-justify-content: center;
+        -webkit-align-items: center;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        text-align: center;
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        margin-bottom: 123px;
+      }
+    </style>
+    <link rel='import' href='/static/{{ app_url }}' async>
+  </head>
+  <body fullbleed>
+    <div id='ha-init-skeleton'><img src='/static/favicon-192x192.png' height='192'></div>
+    <script>
+      var webComponentsSupported = ('registerElement' in document &&
+                                    'import' in document.createElement('link') &&
+                                    'content' in document.createElement('template'))
+      if (!webComponentsSupported) {
+        var script = document.createElement('script')
+        script.async = true
+        script.src = '/static/webcomponents-lite.min.js'
+        document.head.appendChild(script)
+      }
+    </script>
+    <home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
+  </body>
+</html>
diff --git a/homeassistant/components/wsgi.py b/homeassistant/components/wsgi.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2b1e319887d9d190c8c8bc984ca880df627b416
--- /dev/null
+++ b/homeassistant/components/wsgi.py
@@ -0,0 +1,201 @@
+"""
+This module provides WSGI application to serve the Home Assistant API.
+
+"""
+import json
+import logging
+import threading
+import re
+
+from eventlet import wsgi
+import eventlet
+
+import homeassistant.core as ha
+import homeassistant.remote as rem
+from homeassistant import util
+from homeassistant.const import (
+    SERVER_PORT, HTTP_OK, HTTP_NOT_FOUND, HTTP_BAD_REQUEST
+)
+
+from static import Cling
+
+from werkzeug.wsgi import DispatcherMiddleware
+from werkzeug.wrappers import Response, BaseRequest, AcceptMixin
+from werkzeug.routing import Map, Rule
+from werkzeug.exceptions import (
+    MethodNotAllowed, NotFound, BadRequest, Unauthorized
+)
+
+
+class Request(BaseRequest, AcceptMixin):
+    pass
+
+
+class StaticFileServer(object):
+    def __call__(self, environ, start_response):
+        app = DispatcherMiddleware(self.base_app, self.extra_apps)
+        # Strip out any cachebusting MD% fingerprints
+        fingerprinted = _FINGERPRINT.match(environ['PATH_INFO'])
+        if fingerprinted:
+            environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
+        return app(environ, start_response)
+
+DOMAIN = "wsgi"
+REQUIREMENTS = ("eventlet==0.18.4", "static3==0.6.1", "Werkzeug==0.11.5",)
+
+CONF_API_PASSWORD = "api_password"
+CONF_SERVER_HOST = "server_host"
+CONF_SERVER_PORT = "server_port"
+CONF_DEVELOPMENT = "development"
+CONF_SSL_CERTIFICATE = 'ssl_certificate'
+CONF_SSL_KEY = 'ssl_key'
+
+DATA_API_PASSWORD = 'api_password'
+
+_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup(hass, config):
+    """Set up the HTTP API and debug interface."""
+    conf = config.get(DOMAIN, {})
+
+    server = HomeAssistantWSGI(
+        hass,
+        development=str(conf.get(CONF_DEVELOPMENT, "")) == "1",
+        server_host=conf.get(CONF_SERVER_HOST, '0.0.0.0'),
+        server_port=conf.get(CONF_SERVER_PORT, SERVER_PORT),
+        api_password=util.convert(conf.get(CONF_API_PASSWORD), str),
+        ssl_certificate=conf.get(CONF_SSL_CERTIFICATE),
+        ssl_key=conf.get(CONF_SSL_KEY),
+    )
+
+    hass.bus.listen_once(
+        ha.EVENT_HOMEASSISTANT_START,
+        lambda event:
+        threading.Thread(target=server.start, daemon=True,
+                         name='WSGI-server').start())
+
+    hass.wsgi = server
+
+    return True
+
+
+class HomeAssistantWSGI(object):
+    def __init__(self, hass, development, api_password, ssl_certificate,
+                 ssl_key, server_host, server_port):
+        self.url_map = Map()
+        self.views = {}
+        self.hass = hass
+        self.extra_apps = {}
+        self.development = development
+        self.api_password = api_password
+        self.ssl_certificate = ssl_certificate
+        self.ssl_key = ssl_key
+
+    def register_view(self, view):
+        """ Register a view with the WSGI server.
+
+        The view argument must inherit from the HomeAssistantView class, and
+        it must have (globally unique) 'url' and 'name' attributes.
+        """
+        if view.name in self.views:
+            _LOGGER.warning("View '{}' is being overwritten".format(view.name))
+        self.views[view.name] = view(self.hass)
+        # TODO Warn if we're overriding an existing view
+        rule = Rule(view.url, endpoint=view.name)
+        self.url_map.add(rule)
+        for url in view.extra_urls:
+            rule = Rule(url, endpoint=view.name)
+            self.url_map.add(rule)
+
+    def register_static_path(self, url_root, path):
+        # TODO Warn if we're overwriting an existing path
+        self.extra_apps[url_root] = Cling(path)
+
+    def start(self):
+        sock = eventlet.listen(('', 8090))
+        if self.ssl_certificate:
+            eventlet.wrap_ssl(sock, certfile=self.ssl_certificate,
+                              keyfile=self.ssl_key, server_side=True)
+        wsgi.server(sock, self)
+
+    def dispatch_request(self, request):
+        adapter = self.url_map.bind_to_environ(request.environ)
+        try:
+            endpoint, values = adapter.match()
+            return self.views[endpoint].handle_request(request, **values)
+        except BadRequest as e:
+            return self.handle_error(request, str(e), HTTP_BAD_REQUEST)
+        except NotFound as e:
+            return self.handle_error(request, str(e), HTTP_NOT_FOUND)
+        except MethodNotAllowed as e:
+            return self.handle_error(request, str(e), 405)
+        except Unauthorized as e:
+            return self.handle_error(request, str(e), 401)
+        # TODO This long chain of except blocks is silly. _handle_error should
+        # just take the exception as an argument and parse the status code
+        # itself
+
+    def base_app(self, environ, start_response):
+        request = Request(environ)
+        request.api_password = self.api_password
+        request.development = self.development
+        response = self.dispatch_request(request)
+        return response(environ, start_response)
+
+    def __call__(self, environ, start_response):
+        app = DispatcherMiddleware(self.base_app, self.extra_apps)
+        # Strip out any cachebusting MD5 fingerprints
+        fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
+        if fingerprinted:
+            environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
+        return app(environ, start_response)
+
+    def _handle_error(self, request, message, status):
+        if request.accept_mimetypes.accept_json:
+            message = json.dumps({
+                "result": "error",
+                "message": message,
+            })
+            mimetype = "application/json"
+        else:
+            mimetype = "text/plain"
+        return Response(message, status=status, mimetype=mimetype)
+
+
+class HomeAssistantView(object):
+    extra_urls = []
+    requires_auth = True  # Views inheriting from this class can override this
+
+    def __init__(self, hass):
+        self.hass = hass
+
+    def handle_request(self, request, **values):
+        try:
+            handler = getattr(self, request.method.lower())
+        except AttributeError:
+            raise MethodNotAllowed
+        # TODO This would be a good place to check the auth if
+        # self.requires_auth is true, and raise Unauthorized on a failure
+        result = handler(request, **values)
+        if isinstance(result, Response):
+            # The method handler returned a ready-made Response, how nice of it
+            return result
+        elif (isinstance(result, dict) or
+              isinstance(result, list) or
+              isinstance(result, ha.State)):
+            # There are a few result types we know we always want to jsonify
+            if isinstance(result, dict) and 'status_code' in result:
+                status_code = result['status_code']
+                del result['status_code']
+            else:
+                status_code = HTTP_OK
+            msg = json.dumps(
+                result,
+                sort_keys=True,
+                cls=rem.JSONEncoder
+            ).encode('UTF-8')
+            return Response(msg, mimetype="application/json",
+                            status_code=status_code)
diff --git a/requirements_all.txt b/requirements_all.txt
index 1c441790028e5d92d6494a96048cf89866c3c846..7c89f68364df6367fe5ebafcc1411be3552cb673 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -23,6 +23,9 @@ SoCo==0.11.1
 # homeassistant.components.notify.twitter
 TwitterAPI==2.4.1
 
+# homeassistant.components.wsgi
+Werkzeug==0.11.5
+
 # homeassistant.components.apcupsd
 apcaccess==0.0.4
 
@@ -53,6 +56,9 @@ dweepy==0.2.0
 # homeassistant.components.sensor.eliqonline
 eliqonline==1.0.12
 
+# homeassistant.components.wsgi
+eventlet==0.18.4
+
 # homeassistant.components.thermostat.honeywell
 evohomeclient==0.2.5
 
@@ -331,6 +337,9 @@ somecomfort==0.2.1
 # homeassistant.components.sensor.speedtest
 speedtest-cli==0.3.4
 
+# homeassistant.components.wsgi
+static3==0.6.1
+
 # homeassistant.components.sensor.steam_online
 steamodd==4.21