diff --git a/.coveragerc b/.coveragerc
index cf7a5a2cd9c3a60cbea56295ce8c5284b2bb4c01..d2192ca2e46ebad471dbfb33aa9f592675ee47c2 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -166,6 +166,9 @@ omit =
     homeassistant/components/mailgun.py
     homeassistant/components/*/mailgun.py
 
+    homeassistant/components/matrix.py
+    homeassistant/components/*/matrix.py
+
     homeassistant/components/maxcube.py
     homeassistant/components/*/maxcube.py
 
@@ -519,7 +522,6 @@ omit =
     homeassistant/components/notify/lannouncer.py
     homeassistant/components/notify/llamalab_automate.py
     homeassistant/components/notify/mastodon.py
-    homeassistant/components/notify/matrix.py
     homeassistant/components/notify/message_bird.py
     homeassistant/components/notify/mycroft.py
     homeassistant/components/notify/nfandroidtv.py
diff --git a/CODEOWNERS b/CODEOWNERS
index a62ed67db664545d8f20002b8399a32ba40d51fd..33966d1badbd8ace556bcce45af7eec6f9becbb3 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -94,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline
 homeassistant/components/homekit/* @cdce8p
 homeassistant/components/knx.py @Julius2342
 homeassistant/components/*/knx.py @Julius2342
+homeassistant/components/matrix.py @tinloaf
+homeassistant/components/*/matrix.py @tinloaf
 homeassistant/components/qwikswitch.py @kellerza
 homeassistant/components/*/qwikswitch.py @kellerza
 homeassistant/components/*/rfxtrx.py @danielhiversen
diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix.py
new file mode 100644
index 0000000000000000000000000000000000000000..569b012b4846254d82fec86709b67efb58d7286e
--- /dev/null
+++ b/homeassistant/components/matrix.py
@@ -0,0 +1,351 @@
+"""
+The matrix bot component.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/matrix/
+"""
+import logging
+import os
+from functools import partial
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.notify import (ATTR_TARGET, ATTR_MESSAGE)
+from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
+                                 CONF_VERIFY_SSL, CONF_NAME,
+                                 EVENT_HOMEASSISTANT_STOP,
+                                 EVENT_HOMEASSISTANT_START)
+from homeassistant.util.json import load_json, save_json
+from homeassistant.exceptions import HomeAssistantError
+
+REQUIREMENTS = ['matrix-client==0.2.0']
+
+_LOGGER = logging.getLogger(__name__)
+
+SESSION_FILE = '.matrix.conf'
+
+CONF_HOMESERVER = 'homeserver'
+CONF_ROOMS = 'rooms'
+CONF_COMMANDS = 'commands'
+CONF_WORD = 'word'
+CONF_EXPRESSION = 'expression'
+
+EVENT_MATRIX_COMMAND = 'matrix_command'
+
+DOMAIN = 'matrix'
+
+COMMAND_SCHEMA = vol.All(
+    # Basic Schema
+    vol.Schema({
+        vol.Exclusive(CONF_WORD, 'trigger'): cv.string,
+        vol.Exclusive(CONF_EXPRESSION, 'trigger'): cv.is_regex,
+        vol.Required(CONF_NAME): cv.string,
+        vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list,
+                                                      [cv.string]),
+    }),
+    # Make sure it's either a word or an expression command
+    cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION)
+)
+
+CONFIG_SCHEMA = vol.Schema({
+    DOMAIN: vol.Schema({
+        vol.Required(CONF_HOMESERVER): cv.url,
+        vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
+        vol.Required(CONF_USERNAME): cv.matches_regex("@[^:]*:.*"),
+        vol.Required(CONF_PASSWORD): cv.string,
+        vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list,
+                                                      [cv.string]),
+        vol.Optional(CONF_COMMANDS, default=[]): [COMMAND_SCHEMA]
+    })
+}, extra=vol.ALLOW_EXTRA)
+
+SERVICE_SEND_MESSAGE = 'send_message'
+
+SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema({
+    vol.Required(ATTR_MESSAGE): cv.string,
+    vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]),
+})
+
+
+def setup(hass, config):
+    """Set up the Matrix bot component."""
+    from matrix_client.client import MatrixRequestError
+
+    config = config[DOMAIN]
+
+    try:
+        bot = MatrixBot(
+            hass,
+            os.path.join(hass.config.path(), SESSION_FILE),
+            config[CONF_HOMESERVER],
+            config[CONF_VERIFY_SSL],
+            config[CONF_USERNAME],
+            config[CONF_PASSWORD],
+            config[CONF_ROOMS],
+            config[CONF_COMMANDS])
+        hass.data[DOMAIN] = bot
+    except MatrixRequestError as exception:
+        _LOGGER.error("Matrix failed to log in: %s", str(exception))
+        return False
+
+    hass.services.register(
+        DOMAIN, SERVICE_SEND_MESSAGE, bot.handle_send_message,
+        schema=SERVICE_SCHEMA_SEND_MESSAGE)
+
+    return True
+
+
+class MatrixBot(object):
+    """The Matrix Bot."""
+
+    def __init__(self, hass, config_file, homeserver, verify_ssl,
+                 username, password, listening_rooms, commands):
+        """Set up the client."""
+        self.hass = hass
+
+        self._session_filepath = config_file
+        self._auth_tokens = self._get_auth_tokens()
+
+        self._homeserver = homeserver
+        self._verify_tls = verify_ssl
+        self._mx_id = username
+        self._password = password
+
+        self._listening_rooms = listening_rooms
+
+        # Logging in is deferred b/c it does I/O
+        self._setup_done = False
+
+        # We have to fetch the aliases for every room to make sure we don't
+        # join it twice by accident. However, fetching aliases is costly,
+        # so we only do it once per room.
+        self._aliases_fetched_for = set()
+
+        # word commands are stored dict-of-dict: First dict indexes by room ID
+        #  / alias, second dict indexes by the word
+        self._word_commands = {}
+
+        # regular expression commands are stored as a list of commands per
+        # room, i.e., a dict-of-list
+        self._expression_commands = {}
+
+        for command in commands:
+            if not command.get(CONF_ROOMS):
+                command[CONF_ROOMS] = listening_rooms
+
+            if command.get(CONF_WORD):
+                for room_id in command[CONF_ROOMS]:
+                    if room_id not in self._word_commands:
+                        self._word_commands[room_id] = {}
+                    self._word_commands[room_id][command[CONF_WORD]] = command
+            else:
+                for room_id in command[CONF_ROOMS]:
+                    if room_id not in self._expression_commands:
+                        self._expression_commands[room_id] = []
+                    self._expression_commands[room_id].append(command)
+
+        # Log in. This raises a MatrixRequestError if login is unsuccessful
+        self._client = self._login()
+
+        def handle_matrix_exception(exception):
+            """Handle exceptions raised inside the Matrix SDK."""
+            _LOGGER.error("Matrix exception:\n %s", str(exception))
+
+        self._client.start_listener_thread(
+            exception_handler=handle_matrix_exception)
+
+        def stop_client(_):
+            """Run once when Home Assistant stops."""
+            self._client.stop_listener_thread()
+
+        self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_client)
+
+        # Joining rooms potentially does a lot of I/O, so we defer it
+        def handle_startup(_):
+            """Run once when Home Assistant finished startup."""
+            self._join_rooms()
+
+        self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, handle_startup)
+
+    def _handle_room_message(self, room_id, room, event):
+        """Handle a message sent to a room."""
+        if event['content']['msgtype'] != 'm.text':
+            return
+
+        if event['sender'] == self._mx_id:
+            return
+
+        _LOGGER.debug("Handling message: %s", event['content']['body'])
+
+        if event['content']['body'][0] == "!":
+            # Could trigger a single-word command.
+            pieces = event['content']['body'].split(' ')
+            cmd = pieces[0][1:]
+
+            command = self._word_commands.get(room_id, {}).get(cmd)
+            if command:
+                event_data = {
+                    'command': command[CONF_NAME],
+                    'sender': event['sender'],
+                    'room': room_id,
+                    'args': pieces[1:]
+                }
+                self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data)
+
+        # After single-word commands, check all regex commands in the room
+        for command in self._expression_commands.get(room_id, []):
+            match = command[CONF_EXPRESSION].match(event['content']['body'])
+            if not match:
+                continue
+            event_data = {
+                'command': command[CONF_NAME],
+                'sender': event['sender'],
+                'room': room_id,
+                'args': match.groupdict()
+            }
+            self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data)
+
+    def _join_or_get_room(self, room_id_or_alias):
+        """Join a room or get it, if we are already in the room.
+
+        We can't just always call join_room(), since that seems to crash
+        the client if we're already in the room.
+        """
+        rooms = self._client.get_rooms()
+        if room_id_or_alias in rooms:
+            _LOGGER.debug("Already in room %s", room_id_or_alias)
+            return rooms[room_id_or_alias]
+
+        for room in rooms.values():
+            if room.room_id not in self._aliases_fetched_for:
+                room.update_aliases()
+                self._aliases_fetched_for.add(room.room_id)
+
+            if room_id_or_alias in room.aliases:
+                _LOGGER.debug("Already in room %s (known as %s)",
+                              room.room_id, room_id_or_alias)
+                return room
+
+        room = self._client.join_room(room_id_or_alias)
+        _LOGGER.info("Joined room %s (known as %s)", room.room_id,
+                     room_id_or_alias)
+        return room
+
+    def _join_rooms(self):
+        """Join the rooms that we listen for commands in."""
+        from matrix_client.client import MatrixRequestError
+
+        for room_id in self._listening_rooms:
+            try:
+                room = self._join_or_get_room(room_id)
+                room.add_listener(partial(self._handle_room_message, room_id),
+                                  "m.room.message")
+
+            except MatrixRequestError as ex:
+                _LOGGER.error("Could not join room %s: %s", room_id, ex)
+
+    def _get_auth_tokens(self):
+        """
+        Read sorted authentication tokens from disk.
+
+        Returns the auth_tokens dictionary.
+        """
+        try:
+            auth_tokens = load_json(self._session_filepath)
+
+            return auth_tokens
+        except HomeAssistantError as ex:
+            _LOGGER.warning(
+                "Loading authentication tokens from file '%s' failed: %s",
+                self._session_filepath, str(ex))
+            return {}
+
+    def _store_auth_token(self, token):
+        """Store authentication token to session and persistent storage."""
+        self._auth_tokens[self._mx_id] = token
+
+        save_json(self._session_filepath, self._auth_tokens)
+
+    def _login(self):
+        """Login to the matrix homeserver and return the client instance."""
+        from matrix_client.client import MatrixRequestError
+
+        # Attempt to generate a valid client using either of the two possible
+        # login methods:
+        client = None
+
+        # If we have an authentication token
+        if self._mx_id in self._auth_tokens:
+            try:
+                client = self._login_by_token()
+                _LOGGER.debug("Logged in using stored token.")
+
+            except MatrixRequestError as ex:
+                _LOGGER.warning(
+                    "Login by token failed, falling back to password. "
+                    "login_by_token raised: (%d) %s",
+                    ex.code, ex.content)
+
+        # If we still don't have a client try password.
+        if not client:
+            try:
+                client = self._login_by_password()
+                _LOGGER.debug("Logged in using password.")
+
+            except MatrixRequestError as ex:
+                _LOGGER.error(
+                    "Login failed, both token and username/password invalid "
+                    "login_by_password raised: (%d) %s",
+                    ex.code, ex.content)
+
+                # re-raise the error so _setup can catch it.
+                raise
+
+        return client
+
+    def _login_by_token(self):
+        """Login using authentication token and return the client."""
+        from matrix_client.client import MatrixClient
+
+        return MatrixClient(
+            base_url=self._homeserver,
+            token=self._auth_tokens[self._mx_id],
+            user_id=self._mx_id,
+            valid_cert_check=self._verify_tls)
+
+    def _login_by_password(self):
+        """Login using password authentication and return the client."""
+        from matrix_client.client import MatrixClient
+
+        _client = MatrixClient(
+            base_url=self._homeserver,
+            valid_cert_check=self._verify_tls)
+
+        _client.login_with_password(self._mx_id, self._password)
+
+        self._store_auth_token(_client.token)
+
+        return _client
+
+    def _send_message(self, message, target_rooms):
+        """Send the message to the matrix server."""
+        from matrix_client.client import MatrixRequestError
+
+        for target_room in target_rooms:
+            try:
+                room = self._join_or_get_room(target_room)
+                _LOGGER.debug(room.send_text(message))
+            except MatrixRequestError as ex:
+                _LOGGER.error(
+                    "Unable to deliver message to room '%s': (%d): %s",
+                    target_room, ex.code, ex.content)
+
+    def handle_send_message(self, service):
+        """Handle the send_message service."""
+        if not self._setup_done:
+            _LOGGER.warning("Could not send message: setup is not done!")
+            return
+
+        self._send_message(service.data[ATTR_MESSAGE],
+                           service.data[ATTR_TARGET])
diff --git a/homeassistant/components/notify/matrix.py b/homeassistant/components/notify/matrix.py
index 03bc53e204c72caa4af2c5f7f38420c0051f8b8f..fc29ad91dc9156a004916fdb9101697ae2fe952f 100644
--- a/homeassistant/components/notify/matrix.py
+++ b/homeassistant/components/notify/matrix.py
@@ -5,181 +5,46 @@ For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/notify.matrix/
 """
 import logging
-import os
-from urllib.parse import urlparse
 
 import voluptuous as vol
 
 import homeassistant.helpers.config_validation as cv
 from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA,
-                                             BaseNotificationService)
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL
-from homeassistant.util.json import load_json, save_json
-
-REQUIREMENTS = ['matrix-client==0.0.6']
+                                             BaseNotificationService,
+                                             ATTR_MESSAGE)
 
 _LOGGER = logging.getLogger(__name__)
 
-SESSION_FILE = 'matrix.conf'
-
-CONF_HOMESERVER = 'homeserver'
 CONF_DEFAULT_ROOM = 'default_room'
 
+DOMAIN = 'matrix'
+DEPENDENCIES = [DOMAIN]
+
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
-    vol.Required(CONF_HOMESERVER): cv.url,
-    vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
-    vol.Required(CONF_USERNAME): cv.string,
-    vol.Required(CONF_PASSWORD): cv.string,
     vol.Required(CONF_DEFAULT_ROOM): cv.string,
 })
 
 
 def get_service(hass, config, discovery_info=None):
     """Get the Matrix notification service."""
-    from matrix_client.client import MatrixRequestError
-
-    try:
-        return MatrixNotificationService(
-            os.path.join(hass.config.path(), SESSION_FILE),
-            config.get(CONF_HOMESERVER),
-            config.get(CONF_DEFAULT_ROOM),
-            config.get(CONF_VERIFY_SSL),
-            config.get(CONF_USERNAME),
-            config.get(CONF_PASSWORD))
-
-    except MatrixRequestError:
-        return None
+    return MatrixNotificationService(config.get(CONF_DEFAULT_ROOM))
 
 
 class MatrixNotificationService(BaseNotificationService):
     """Send Notifications to a Matrix Room."""
 
-    def __init__(self, config_file, homeserver, default_room, verify_ssl,
-                 username, password):
-        """Set up the client."""
-        self.session_filepath = config_file
-        self.auth_tokens = self.get_auth_tokens()
-
-        self.homeserver = homeserver
-        self.default_room = default_room
-        self.verify_tls = verify_ssl
-        self.username = username
-        self.password = password
-
-        self.mx_id = "{user}@{homeserver}".format(
-            user=username, homeserver=urlparse(homeserver).netloc)
-
-        # Login, this will raise a MatrixRequestError if login is unsuccessful
-        self.client = self.login()
-
-    def get_auth_tokens(self):
-        """
-        Read sorted authentication tokens from disk.
-
-        Returns the auth_tokens dictionary.
-        """
-        if not os.path.exists(self.session_filepath):
-            return {}
-
-        try:
-            data = load_json(self.session_filepath)
-
-            auth_tokens = {}
-            for mx_id, token in data.items():
-                auth_tokens[mx_id] = token
-
-            return auth_tokens
-
-        except (OSError, IOError, PermissionError) as ex:
-            _LOGGER.warning(
-                "Loading authentication tokens from file '%s' failed: %s",
-                self.session_filepath, str(ex))
-            return {}
-
-    def store_auth_token(self, token):
-        """Store authentication token to session and persistent storage."""
-        self.auth_tokens[self.mx_id] = token
-
-        save_json(self.session_filepath, self.auth_tokens)
-
-    def login(self):
-        """Login to the matrix homeserver and return the client instance."""
-        from matrix_client.client import MatrixRequestError
+    def __init__(self, default_room):
+        """Set up the notification service."""
+        self._default_room = default_room
 
-        # Attempt to generate a valid client using either of the two possible
-        # login methods:
-        client = None
-
-        # If we have an authentication token
-        if self.mx_id in self.auth_tokens:
-            try:
-                client = self.login_by_token()
-                _LOGGER.debug("Logged in using stored token.")
-
-            except MatrixRequestError as ex:
-                _LOGGER.warning(
-                    "Login by token failed, falling back to password. "
-                    "login_by_token raised: (%d) %s",
-                    ex.code, ex.content)
-
-        # If we still don't have a client try password.
-        if not client:
-            try:
-                client = self.login_by_password()
-                _LOGGER.debug("Logged in using password.")
-
-            except MatrixRequestError as ex:
-                _LOGGER.error(
-                    "Login failed, both token and username/password invalid "
-                    "login_by_password raised: (%d) %s",
-                    ex.code, ex.content)
-
-                # re-raise the error so the constructor can catch it.
-                raise
-
-        return client
-
-    def login_by_token(self):
-        """Login using authentication token and return the client."""
-        from matrix_client.client import MatrixClient
-
-        return MatrixClient(
-            base_url=self.homeserver,
-            token=self.auth_tokens[self.mx_id],
-            user_id=self.username,
-            valid_cert_check=self.verify_tls)
-
-    def login_by_password(self):
-        """Login using password authentication and return the client."""
-        from matrix_client.client import MatrixClient
-
-        _client = MatrixClient(
-            base_url=self.homeserver,
-            valid_cert_check=self.verify_tls)
-
-        _client.login_with_password(self.username, self.password)
-
-        self.store_auth_token(_client.token)
-
-        return _client
-
-    def send_message(self, message, **kwargs):
+    def send_message(self, message="", **kwargs):
         """Send the message to the matrix server."""
-        from matrix_client.client import MatrixRequestError
-
-        target_rooms = kwargs.get(ATTR_TARGET) or [self.default_room]
-
-        rooms = self.client.get_rooms()
-        for target_room in target_rooms:
-            try:
-                if target_room in rooms:
-                    room = rooms[target_room]
-                else:
-                    room = self.client.join_room(target_room)
+        target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room]
 
-                _LOGGER.debug(room.send_text(message))
+        service_data = {
+            ATTR_TARGET: target_rooms,
+            ATTR_MESSAGE: message
+        }
 
-            except MatrixRequestError as ex:
-                _LOGGER.error(
-                    "Unable to deliver message to room '%s': (%d): %s",
-                    target_room, ex.code, ex.content)
+        return self.hass.services.call(
+            DOMAIN, 'send_message', service_data=service_data)
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 8177999cc9490fcc9472368daf4b72130f55d77f..0bd490940a925c4ee86246ad1fb3bce2208ef836 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -96,6 +96,36 @@ def isdevice(value):
         raise vol.Invalid('No device at {} found'.format(value))
 
 
+def matches_regex(regex):
+    """Validate that the value is a string that matches a regex."""
+    regex = re.compile(regex)
+
+    def validator(value: Any) -> str:
+        """Validate that value matches the given regex."""
+        if not isinstance(value, str):
+            raise vol.Invalid('not a string value: {}'.format(value))
+
+        if not regex.match(value):
+            raise vol.Invalid('value {} does not match regular expression {}'
+                              .format(regex.pattern, value))
+
+        return value
+    return validator
+
+
+def is_regex(value):
+    """Validate that a string is a valid regular expression."""
+    try:
+        r = re.compile(value)
+        return r
+    except TypeError:
+        raise vol.Invalid("value {} is of the wrong type for a regular "
+                          "expression".format(value))
+    except re.error:
+        raise vol.Invalid("value {} is not a valid regular expression".format(
+            value))
+
+
 def isfile(value: Any) -> str:
     """Validate that the value is an existing file."""
     if value is None:
diff --git a/requirements_all.txt b/requirements_all.txt
index 88dd3d60904f345507c5cb66e1b9261db87419ea..74f9ff8e1959839b351abd04f75d6996a4ec84e9 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -511,8 +511,8 @@ luftdaten==0.1.3
 # homeassistant.components.sensor.lyft
 lyft_rides==0.2
 
-# homeassistant.components.notify.matrix
-matrix-client==0.0.6
+# homeassistant.components.matrix
+matrix-client==0.2.0
 
 # homeassistant.components.maxcube
 maxcube-api==0.1.0
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index aff0acf9e3a0187ce62e65782a391187969fd926..28efcb3e8685793c1502e18aa795b6af5af5ea29 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -565,3 +565,31 @@ def test_socket_timeout():  # pylint: disable=invalid-name
     assert _GLOBAL_DEFAULT_TIMEOUT == schema(None)
 
     assert schema(1) == 1.0
+
+
+def test_matches_regex():
+    """Test matches_regex validator."""
+    schema = vol.Schema(cv.matches_regex('.*uiae.*'))
+
+    with pytest.raises(vol.Invalid):
+        schema(1.0)
+
+    with pytest.raises(vol.Invalid):
+        schema("  nrtd   ")
+
+    test_str = "This is a test including uiae."
+    assert(schema(test_str) == test_str)
+
+
+def test_is_regex():
+    """Test the is_regex validator."""
+    schema = vol.Schema(cv.is_regex)
+
+    with pytest.raises(vol.Invalid):
+        schema("(")
+
+    with pytest.raises(vol.Invalid):
+        schema({"a dict": "is not a regex"})
+
+    valid_re = ".*"
+    schema(valid_re)