diff --git a/homeassistant/components/frontend/www_static/websocket_test.html b/homeassistant/components/frontend/www_static/websocket_test.html
new file mode 100644
index 0000000000000000000000000000000000000000..d4c0974899c6f6c69adc908b144fb52957487afe
--- /dev/null
+++ b/homeassistant/components/frontend/www_static/websocket_test.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>WebSocket debug</title>
+    <style>
+      .controls {
+        display: flex;
+        flex-direction: row;
+      }
+
+      .controls textarea {
+        height: 160px;
+        min-width: 400px;
+        margin-right: 24px;
+      }
+    </style>
+  </head>
+  <body>
+    <div class='controls'>
+      <textarea id="messageinput">
+        {
+          "id": 1, "type": "subscribe_events", "event_type": "state_changed"
+        }
+      </textarea>
+      <pre>
+Examples:
+{
+  "id": 2, "type": "subscribe_events", "event_type": "state_changed"
+}
+
+{
+  "id": 3, "type": "call_service", "domain": "light", "service": "turn_off"
+}
+
+{
+  "id": 4, "type": "unsubscribe_events", "subscription": 2
+}
+
+{
+  "id": 5, "type": "get_states"
+}
+
+{
+  "id": 6, "type": "get_config"
+}
+
+{
+  "id": 7, "type": "get_services"
+}
+
+{
+  "id": 8, "type": "get_panels"
+}
+      </pre>
+    </div>
+    <div>
+      <button type="button" onclick="openSocket();" >Open</button>
+      <button type="button" onclick="send();" >Send</button>
+      <button type="button" onclick="closeSocket();" >Close</button>
+    </div>
+    <!-- Server responses get written here -->
+    <pre id="messages"></pre>
+
+    <!-- Script to utilise the WebSocket -->
+    <script type="text/javascript">
+      var webSocket;
+      var messages = document.getElementById("messages");
+
+      function openSocket(){
+        var isOpen = false;
+        // Ensures only one connection is open at a time
+        if(webSocket !== undefined && webSocket.readyState !== WebSocket.CLOSED){
+          writeResponse("WebSocket is already opened.");
+          return;
+        }
+        // Create a new instance of the websocket
+        webSocket = new WebSocket("ws://localhost:8123/api/websocket");
+
+        /**
+         * Binds functions to the listeners for the websocket.
+         */
+        webSocket.onopen = function(event){
+          if (!isOpen) {
+            isOpen = true;
+            writeResponse('Connection opened');
+          }
+          // For reasons I can't determine, onopen gets called twice
+          // and the first time event.data is undefined.
+          // Leave a comment if you know the answer.
+          if(event.data === undefined)
+            return;
+
+          writeResponse(event.data);
+        };
+
+        webSocket.onmessage = function(event){
+          writeResponse(event.data);
+        };
+
+        webSocket.onclose = function(event){
+          writeResponse("Connection closed");
+        };
+      }
+
+      /**
+       * Sends the value of the text input to the server
+       */
+      function send(){
+        var text = document.getElementById("messageinput").value;
+        webSocket.send(text);
+      }
+
+      function closeSocket(){
+        webSocket.close();
+      }
+
+      function writeResponse(text){
+        messages.innerHTML += "\n" + text;
+      }
+
+      openSocket();
+    </script>
+  </body>
+</html>
diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py
index 14b442e5dde43557974044c8d851cb2605b1d5fb..6ff653eef358c66da3da4790957098583f6f9bb5 100644
--- a/homeassistant/components/http/auth.py
+++ b/homeassistant/components/http/auth.py
@@ -28,18 +28,17 @@ def auth_middleware(app, handler):
     @asyncio.coroutine
     def auth_middleware_handler(request):
         """Auth middleware to check authentication."""
-        hass = app['hass']
-
         # Auth code verbose on purpose
         authenticated = False
 
-        if hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
-                               hass.http.api_password):
+        if (HTTP_HEADER_HA_AUTH in request.headers and
+                validate_password(request,
+                                  request.headers[HTTP_HEADER_HA_AUTH])):
             # A valid auth header has been set
             authenticated = True
 
-        elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''),
-                                 hass.http.api_password):
+        elif (DATA_API_PASSWORD in request.GET and
+              validate_password(request, request.GET[DATA_API_PASSWORD])):
             authenticated = True
 
         elif is_trusted_ip(request):
@@ -59,3 +58,9 @@ def is_trusted_ip(request):
     return ip_addr and any(
         ip_addr in trusted_network for trusted_network
         in request.app[KEY_TRUSTED_NETWORKS])
+
+
+def validate_password(request, api_password):
+    """Test if password is valid."""
+    return hmac.compare_digest(api_password,
+                               request.app['hass'].http.api_password)
diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..391c27e88941f6fecab4f0bbc539b515887871eb
--- /dev/null
+++ b/homeassistant/components/websocket_api.py
@@ -0,0 +1,401 @@
+"""Websocket based API for Home Assistant."""
+import asyncio
+from functools import partial
+import json
+import logging
+
+from aiohttp import web
+import voluptuous as vol
+from voluptuous.humanize import humanize_error
+
+from homeassistant.const import (
+    MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP,
+    __version__)
+from homeassistant.components import api, frontend
+from homeassistant.core import callback
+from homeassistant.remote import JSONEncoder
+from homeassistant.helpers import config_validation as cv
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.components.http.auth import validate_password
+from homeassistant.components.http.const import KEY_AUTHENTICATED
+
+DOMAIN = 'websocket_api'
+
+URL = "/api/websocket"
+DEPENDENCIES = 'http',
+
+ERR_ID_REUSE = 1
+ERR_INVALID_FORMAT = 2
+ERR_NOT_FOUND = 3
+
+TYPE_AUTH = 'auth'
+TYPE_AUTH_OK = 'auth_ok'
+TYPE_AUTH_REQUIRED = 'auth_required'
+TYPE_AUTH_INVALID = 'auth_invalid'
+TYPE_EVENT = 'event'
+TYPE_SUBSCRIBE_EVENTS = 'subscribe_events'
+TYPE_UNSUBSCRIBE_EVENTS = 'unsubscribe_events'
+TYPE_CALL_SERVICE = 'call_service'
+TYPE_GET_STATES = 'get_states'
+TYPE_GET_SERVICES = 'get_services'
+TYPE_GET_CONFIG = 'get_config'
+TYPE_GET_PANELS = 'get_panels'
+TYPE_RESULT = 'result'
+
+_LOGGER = logging.getLogger(__name__)
+
+JSON_DUMP = partial(json.dumps, cls=JSONEncoder)
+
+AUTH_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('type'): TYPE_AUTH,
+    vol.Required('api_password'): str,
+})
+
+SUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_SUBSCRIBE_EVENTS,
+    vol.Optional('event_type', default=MATCH_ALL): str,
+})
+
+UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_UNSUBSCRIBE_EVENTS,
+    vol.Required('subscription'): cv.positive_int,
+})
+
+CALL_SERVICE_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_CALL_SERVICE,
+    vol.Required('domain'): str,
+    vol.Required('service'): str,
+    vol.Optional('service_data', default=None): dict
+})
+
+GET_STATES_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_GET_STATES,
+})
+
+GET_SERVICES_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_GET_SERVICES,
+})
+
+GET_CONFIG_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_GET_CONFIG,
+})
+
+GET_PANELS_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): TYPE_GET_PANELS,
+})
+
+BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({
+    vol.Required('id'): cv.positive_int,
+    vol.Required('type'): vol.Any(TYPE_CALL_SERVICE,
+                                  TYPE_SUBSCRIBE_EVENTS,
+                                  TYPE_UNSUBSCRIBE_EVENTS,
+                                  TYPE_GET_STATES,
+                                  TYPE_GET_SERVICES,
+                                  TYPE_GET_CONFIG,
+                                  TYPE_GET_PANELS)
+}, extra=vol.ALLOW_EXTRA)
+
+
+def auth_ok_message():
+    """Return an auth_ok message."""
+    return {
+        'type': TYPE_AUTH_OK,
+        'ha_version': __version__,
+    }
+
+
+def auth_required_message():
+    """Return an auth_required message."""
+    return {
+        'type': TYPE_AUTH_REQUIRED,
+        'ha_version': __version__,
+    }
+
+
+def auth_invalid_message(message):
+    """Return an auth_invalid message."""
+    return {
+        'type': TYPE_AUTH_INVALID,
+        'message': message,
+    }
+
+
+def event_message(iden, event):
+    """Return an event message."""
+    return {
+        'id': iden,
+        'type': TYPE_EVENT,
+        'event': event.as_dict(),
+    }
+
+
+def error_message(iden, code, message):
+    """Return an error result message."""
+    return {
+        'id': iden,
+        'type': TYPE_RESULT,
+        'success': False,
+        'error': {
+            'code': code,
+            'message': message,
+        },
+    }
+
+
+def result_message(iden, result=None):
+    """Return a success result message."""
+    return {
+        'id': iden,
+        'type': TYPE_RESULT,
+        'success': True,
+        'result': result,
+    }
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+    """Initialize the websocket API."""
+    hass.http.register_view(WebsocketAPIView)
+    return True
+
+
+class WebsocketAPIView(HomeAssistantView):
+    """View to serve a websockets endpoint."""
+
+    name = "websocketapi"
+    url = URL
+    requires_auth = False
+
+    @asyncio.coroutine
+    def get(self, request):
+        """Handle an incoming websocket connection."""
+        # pylint: disable=no-self-use
+        return ActiveConnection(request.app['hass'], request).handle()
+
+
+class ActiveConnection:
+    """Handle an active websocket client connection."""
+
+    def __init__(self, hass, request):
+        """Initialize an active connection."""
+        self.hass = hass
+        self.request = request
+        self.wsock = None
+        self.socket_task = None
+        self.event_listeners = {}
+
+    def debug(self, message1, message2=''):
+        """Print a debug message."""
+        _LOGGER.debug('WS %s: %s %s', id(self.wsock), message1, message2)
+
+    def log_error(self, message1, message2=''):
+        """Print an error message."""
+        _LOGGER.error('WS %s: %s %s', id(self.wsock), message1, message2)
+
+    def send_message(self, message):
+        """Helper method to send messages."""
+        self.debug('Sending', message)
+        self.wsock.send_json(message, dumps=JSON_DUMP)
+
+    @callback
+    def _cancel_connection(self, event):
+        """Cancel this connection."""
+        self.socket_task.cancel()
+
+    @asyncio.coroutine
+    def _call_service_helper(self, msg):
+        """Helper to call a service and fire complete message."""
+        yield from self.hass.services.async_call(msg['domain'], msg['service'],
+                                                 msg['service_data'], True)
+        try:
+            self.send_message(result_message(msg['id']))
+        except RuntimeError:
+            # Socket has been closed.
+            pass
+
+    @callback
+    def _forward_event(self, iden, event):
+        """Helper to forward events to websocket."""
+        if event.event_type == EVENT_TIME_CHANGED:
+            return
+
+        try:
+            self.send_message(event_message(iden, event))
+        except RuntimeError:
+            # Socket has been closed.
+            pass
+
+    @asyncio.coroutine
+    def handle(self):
+        """Handle the websocket connection."""
+        wsock = self.wsock = web.WebSocketResponse()
+        yield from wsock.prepare(self.request)
+
+        # Set up to cancel this connection when Home Assistant shuts down
+        self.socket_task = asyncio.Task.current_task(loop=self.hass.loop)
+        self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP,
+                                   self._cancel_connection)
+
+        self.debug('Connected')
+
+        msg = None
+        authenticated = False
+
+        try:
+            if self.request[KEY_AUTHENTICATED]:
+                authenticated = True
+
+            else:
+                self.send_message(auth_required_message())
+                msg = yield from wsock.receive_json()
+                msg = AUTH_MESSAGE_SCHEMA(msg)
+
+                if validate_password(self.request, msg['api_password']):
+                    authenticated = True
+
+                else:
+                    self.debug('Invalid password')
+                    self.send_message(auth_invalid_message('Invalid password'))
+                    return wsock
+
+            if not authenticated:
+                return wsock
+
+            self.send_message(auth_ok_message())
+
+            msg = yield from wsock.receive_json()
+
+            last_id = 0
+
+            while msg:
+                self.debug('Received', msg)
+                msg = BASE_COMMAND_MESSAGE_SCHEMA(msg)
+                cur_id = msg['id']
+
+                if cur_id <= last_id:
+                    self.send_message(error_message(
+                        cur_id, ERR_ID_REUSE,
+                        'Identifier values have to increase.'))
+
+                else:
+                    handler_name = 'handle_{}'.format(msg['type'])
+                    getattr(self, handler_name)(msg)
+
+                last_id = cur_id
+                msg = yield from wsock.receive_json()
+
+        except vol.Invalid as err:
+            error_msg = 'Message incorrectly formatted: '
+            if msg:
+                error_msg += humanize_error(msg, err)
+            else:
+                error_msg += str(err)
+
+            self.log_error(error_msg)
+
+            if not authenticated:
+                self.send_message(auth_invalid_message(error_msg))
+
+            else:
+                if isinstance(msg, dict):
+                    iden = msg.get('id')
+                else:
+                    iden = None
+
+                self.send_message(error_message(iden, ERR_INVALID_FORMAT,
+                                                error_msg))
+
+        except TypeError as err:
+            if wsock.closed:
+                self.debug('Connection closed by client')
+            else:
+                self.log_error('Unexpected TypeError', msg)
+
+        except ValueError as err:
+            msg = 'Received invalid JSON'
+            value = getattr(err, 'doc', None)  # Py3.5+ only
+            if value:
+                msg += ': {}'.format(value)
+            self.log_error(msg)
+
+        except asyncio.CancelledError:
+            self.debug('Connection cancelled by server')
+
+        except Exception:  # pylint: disable=broad-except
+            error = 'Unexpected error inside websocket API. '
+            if msg is not None:
+                error += str(msg)
+            _LOGGER.exception(error)
+
+        finally:
+            for unsub in self.event_listeners.values():
+                unsub()
+
+            yield from wsock.close()
+            self.debug('Closed connection')
+
+        return wsock
+
+    def handle_subscribe_events(self, msg):
+        """Handle subscribe events command."""
+        msg = SUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg)
+
+        self.event_listeners[msg['id']] = self.hass.bus.async_listen(
+            msg['event_type'], partial(self._forward_event, msg['id']))
+
+        self.send_message(result_message(msg['id']))
+
+    def handle_unsubscribe_events(self, msg):
+        """Handle unsubscribe events command."""
+        msg = UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg)
+
+        subscription = msg['subscription']
+
+        if subscription not in self.event_listeners:
+            self.send_message(error_message(
+                msg['id'], ERR_NOT_FOUND,
+                'Subscription not found.'))
+        else:
+            self.event_listeners.pop(subscription)()
+            self.send_message(result_message(msg['id']))
+
+    def handle_call_service(self, msg):
+        """Handle call service command."""
+        msg = CALL_SERVICE_MESSAGE_SCHEMA(msg)
+
+        self.hass.async_add_job(self._call_service_helper(msg))
+
+    def handle_get_states(self, msg):
+        """Handle get states command."""
+        msg = GET_STATES_MESSAGE_SCHEMA(msg)
+
+        self.send_message(result_message(msg['id'],
+                                         self.hass.states.async_all()))
+
+    def handle_get_services(self, msg):
+        """Handle get services command."""
+        msg = GET_SERVICES_MESSAGE_SCHEMA(msg)
+
+        self.send_message(result_message(msg['id'],
+                                         api.async_services_json(self.hass)))
+
+    def handle_get_config(self, msg):
+        """Handle get config command."""
+        msg = GET_CONFIG_MESSAGE_SCHEMA(msg)
+
+        self.send_message(result_message(msg['id'],
+                                         self.hass.config.as_dict()))
+
+    def handle_get_panels(self, msg):
+        """Handle get panels command."""
+        msg = GET_PANELS_MESSAGE_SCHEMA(msg)
+
+        self.send_message(result_message(
+            msg['id'], self.hass.data[frontend.DATA_PANELS]))
diff --git a/requirements_test.txt b/requirements_test.txt
index 838e4c96875e8938d02bff4ad174c7d340b002c1..2dbc98326dba9b8c77df85e7d5119401b8da8c94 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -12,5 +12,6 @@ pytest-asyncio>=0.5.0
 pytest-cov>=2.3.1
 pytest-timeout>=1.2.0
 pytest-catchlog>=1.2.2
+pytest-sugar>=0.7.1
 requests_mock>=1.0
 mock-open>=1.3.1
diff --git a/tests/common.py b/tests/common.py
index fc779e120f8fba50a4beb52fbf1fda6502c3c553..25a674dd995f3880ab323c5c3999e7ae9a343d0c 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -3,8 +3,7 @@ import asyncio
 import os
 import sys
 from datetime import timedelta
-from unittest import mock
-from unittest.mock import patch
+from unittest.mock import patch, MagicMock
 from io import StringIO
 import logging
 import threading
@@ -26,7 +25,7 @@ from homeassistant.const import (
 from homeassistant.components import sun, mqtt
 from homeassistant.components.http.auth import auth_middleware
 from homeassistant.components.http.const import (
-    KEY_USE_X_FORWARDED_FOR, KEY_BANS_ENABLED)
+    KEY_USE_X_FORWARDED_FOR, KEY_BANS_ENABLED, KEY_TRUSTED_NETWORKS)
 
 _TEST_INSTANCE_PORT = SERVER_PORT
 _LOGGER = logging.getLogger(__name__)
@@ -207,7 +206,7 @@ def mock_state_change_event(hass, new_state, old_state=None):
 
 def mock_http_component(hass):
     """Mock the HTTP component."""
-    hass.http = mock.MagicMock()
+    hass.http = MagicMock()
     hass.config.components.append('http')
     hass.http.views = {}
 
@@ -222,19 +221,20 @@ def mock_http_component(hass):
     hass.http.register_view = mock_register_view
 
 
-def mock_http_component_app(hass):
+def mock_http_component_app(hass, api_password=None):
     """Create an aiohttp.web.Application instance for testing."""
-    hass.http.api_password = None
+    hass.http = MagicMock(api_password=api_password)
     app = web.Application(middlewares=[auth_middleware], loop=hass.loop)
     app['hass'] = hass
     app[KEY_USE_X_FORWARDED_FOR] = False
     app[KEY_BANS_ENABLED] = False
+    app[KEY_TRUSTED_NETWORKS] = []
     return app
 
 
 def mock_mqtt_component(hass):
     """Mock the MQTT component."""
-    with mock.patch('homeassistant.components.mqtt.MQTT') as mock_mqtt:
+    with patch('homeassistant.components.mqtt.MQTT') as mock_mqtt:
         setup_component(hass, mqtt.DOMAIN, {
             mqtt.DOMAIN: {
                 mqtt.CONF_BROKER: 'mock-broker',
diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b70f0cde100e3cabb61cce74f0229e861246393
--- /dev/null
+++ b/tests/components/test_websocket_api.py
@@ -0,0 +1,285 @@
+import asyncio
+from unittest.mock import patch
+
+from aiohttp import WSMsgType
+from async_timeout import timeout
+import pytest
+
+from homeassistant.core import callback
+from homeassistant.components import websocket_api as wapi, api, frontend
+
+from tests.common import mock_http_component_app
+
+API_PASSWORD = 'test1234'
+
+
+@pytest.fixture
+def websocket_client(loop, hass, test_client):
+    """Websocket client fixture connected to websocket server."""
+    websocket_app = mock_http_component_app(hass)
+    wapi.WebsocketAPIView().register(websocket_app.router)
+
+    client = loop.run_until_complete(test_client(websocket_app))
+    ws = loop.run_until_complete(client.ws_connect(wapi.URL))
+
+    auth_ok = loop.run_until_complete(ws.receive_json())
+    assert auth_ok['type'] == wapi.TYPE_AUTH_OK
+
+    yield ws
+
+    if not ws.closed:
+        loop.run_until_complete(ws.close())
+
+
+@pytest.fixture
+def no_auth_websocket_client(hass, loop, test_client):
+    """Websocket connection that requires authentication."""
+    websocket_app = mock_http_component_app(hass, API_PASSWORD)
+    wapi.WebsocketAPIView().register(websocket_app.router)
+
+    client = loop.run_until_complete(test_client(websocket_app))
+    ws = loop.run_until_complete(client.ws_connect(wapi.URL))
+
+    auth_ok = loop.run_until_complete(ws.receive_json())
+    assert auth_ok['type'] == wapi.TYPE_AUTH_REQUIRED
+
+    yield ws
+
+    if not ws.closed:
+        loop.run_until_complete(ws.close())
+
+
+@asyncio.coroutine
+def test_auth_via_msg(no_auth_websocket_client):
+    """Test authenticating."""
+    no_auth_websocket_client.send_json({
+        'type': wapi.TYPE_AUTH,
+        'api_password': API_PASSWORD
+    })
+
+    msg = yield from no_auth_websocket_client.receive_json()
+
+    assert msg['type'] == wapi.TYPE_AUTH_OK
+
+
+@asyncio.coroutine
+def test_auth_via_msg_incorrect_pass(no_auth_websocket_client):
+    """Test authenticating."""
+    no_auth_websocket_client.send_json({
+        'type': wapi.TYPE_AUTH,
+        'api_password': API_PASSWORD + 'wrong'
+    })
+
+    msg = yield from no_auth_websocket_client.receive_json()
+
+    assert msg['type'] == wapi.TYPE_AUTH_INVALID
+    assert msg['message'] == 'Invalid password'
+
+
+@asyncio.coroutine
+def test_pre_auth_only_auth_allowed(no_auth_websocket_client):
+    """Verify that before authentication, only auth messages are allowed."""
+    no_auth_websocket_client.send_json({
+        'type': wapi.TYPE_CALL_SERVICE,
+        'domain': 'domain_test',
+        'service': 'test_service',
+        'service_data': {
+            'hello': 'world'
+        }
+    })
+
+    msg = yield from no_auth_websocket_client.receive_json()
+
+    assert msg['type'] == wapi.TYPE_AUTH_INVALID
+    assert msg['message'].startswith('Message incorrectly formatted')
+
+
+@asyncio.coroutine
+def test_invalid_message_format(websocket_client):
+    """Test sending invalid JSON."""
+    websocket_client.send_json({'type': 5})
+
+    msg = yield from websocket_client.receive_json()
+
+    assert msg['type'] == wapi.TYPE_RESULT
+    error = msg['error']
+    assert error['code'] == wapi.ERR_INVALID_FORMAT
+    assert error['message'].startswith('Message incorrectly formatted')
+
+
+@asyncio.coroutine
+def test_invalid_json(websocket_client):
+    """Test sending invalid JSON."""
+    websocket_client.send_str('this is not JSON')
+
+    msg = yield from websocket_client.receive()
+
+    assert msg.type == WSMsgType.close
+
+
+@asyncio.coroutine
+def test_quiting_hass(hass, websocket_client):
+    """Test sending invalid JSON."""
+    with patch.object(hass.loop, 'stop'):
+        yield from hass.async_stop()
+
+    msg = yield from websocket_client.receive()
+
+    assert msg.type == WSMsgType.CLOSE
+
+
+@asyncio.coroutine
+def test_call_service(hass, websocket_client):
+    """Test call service command."""
+    calls = []
+
+    @callback
+    def service_call(call):
+        calls.append(call)
+
+    hass.services.async_register('domain_test', 'test_service', service_call)
+
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_CALL_SERVICE,
+        'domain': 'domain_test',
+        'service': 'test_service',
+        'service_data': {
+            'hello': 'world'
+        }
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+
+    assert len(calls) == 1
+    call = calls[0]
+
+    assert call.domain == 'domain_test'
+    assert call.service == 'test_service'
+    assert call.data == {'hello': 'world'}
+
+
+@asyncio.coroutine
+def test_subscribe_unsubscribe_events(hass, websocket_client):
+    """Test subscribe/unsubscribe events command."""
+    init_count = sum(hass.bus.async_listeners().values())
+
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_SUBSCRIBE_EVENTS,
+        'event_type': 'test_event'
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+
+    # Verify we have a new listener
+    assert sum(hass.bus.async_listeners().values()) == init_count + 1
+
+    hass.bus.async_fire('ignore_event')
+    hass.bus.async_fire('test_event', {'hello': 'world'})
+    hass.bus.async_fire('ignore_event')
+
+    with timeout(3, loop=hass.loop):
+        msg = yield from websocket_client.receive_json()
+
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_EVENT
+    event = msg['event']
+
+    assert event['event_type'] == 'test_event'
+    assert event['data'] == {'hello': 'world'}
+    assert event['origin'] == 'LOCAL'
+
+    websocket_client.send_json({
+        'id': 6,
+        'type': wapi.TYPE_UNSUBSCRIBE_EVENTS,
+        'subscription': 5
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 6
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+
+    # Check our listener got unsubscribed
+    assert sum(hass.bus.async_listeners().values()) == init_count
+
+
+@asyncio.coroutine
+def test_get_states(hass, websocket_client):
+    """ Test get_states command."""
+    hass.states.async_set('greeting.hello', 'world')
+    hass.states.async_set('greeting.bye', 'universe')
+
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_GET_STATES,
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+
+    states = []
+    for state in hass.states.async_all():
+        state = state.as_dict()
+        state['last_changed'] = state['last_changed'].isoformat()
+        state['last_updated'] = state['last_updated'].isoformat()
+        states.append(state)
+
+    assert msg['result'] == states
+
+
+@asyncio.coroutine
+def test_get_services(hass, websocket_client):
+    """ Test get_services command."""
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_GET_SERVICES,
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+    assert msg['result'] == api.async_services_json(hass)
+
+
+@asyncio.coroutine
+def test_get_config(hass, websocket_client):
+    """ Test get_config command."""
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_GET_CONFIG,
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+    assert msg['result'] == hass.config.as_dict()
+
+
+@asyncio.coroutine
+def test_get_panels(hass, websocket_client):
+    """ Test get_panels command."""
+    frontend.register_built_in_panel(hass, 'map', 'Map',
+                                     'mdi:account-location')
+
+    websocket_client.send_json({
+        'id': 5,
+        'type': wapi.TYPE_GET_PANELS,
+    })
+
+    msg = yield from websocket_client.receive_json()
+    assert msg['id'] == 5
+    assert msg['type'] == wapi.TYPE_RESULT
+    assert msg['success']
+    assert msg['result'] == hass.data[frontend.DATA_PANELS]
diff --git a/tox.ini b/tox.ini
index 609e17087b0fb6744329596424bdb2821bed20f1..1cf402468b587584a4d334d8b3fddcbdcd61e319 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,7 +11,7 @@ setenv =
     LANG=en_US.UTF-8
     PYTHONPATH = {toxinidir}:{toxinidir}/homeassistant
 commands =
-     py.test -v --timeout=30 --duration=10 --cov --cov-report= {posargs}
+     py.test --timeout=30 --duration=10 --cov --cov-report= {posargs}
 deps =
      -r{toxinidir}/requirements_all.txt
      -r{toxinidir}/requirements_test.txt