diff --git a/.coveragerc b/.coveragerc
index e256be60466d36fd69205f43c25ef36f0d917a3f..bf6c81bf51471e8f60db6ffadc811b836b8872fd 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -262,6 +262,9 @@ omit =
     homeassistant/components/fibaro/*
     homeassistant/components/filesize/sensor.py
     homeassistant/components/fints/sensor.py
+    homeassistant/components/fireservicerota/__init__.py
+    homeassistant/components/fireservicerota/const.py
+    homeassistant/components/fireservicerota/sensor.py
     homeassistant/components/firmata/__init__.py
     homeassistant/components/firmata/binary_sensor.py
     homeassistant/components/firmata/board.py
diff --git a/CODEOWNERS b/CODEOWNERS
index 1c63bd37c456b11d0fa77cdb58f417cb515df4a5..23eacec8f22461900ec0f5ceb580eca467ac68c4 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -146,6 +146,7 @@ homeassistant/components/ezviz/* @baqs
 homeassistant/components/fastdotcom/* @rohankapoorcom
 homeassistant/components/file/* @fabaff
 homeassistant/components/filter/* @dgomes
+homeassistant/components/fireservicerota/* @cyberjunky
 homeassistant/components/firmata/* @DaAwesomeP
 homeassistant/components/fixer/* @fabaff
 homeassistant/components/flick_electric/* @ZephireNZ
diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a0fc1d68d230309f153f66aee1e0c04cd01a3856
--- /dev/null
+++ b/homeassistant/components/fireservicerota/__init__.py
@@ -0,0 +1,246 @@
+"""The FireServiceRota integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+from pyfireservicerota import (
+    ExpiredTokenError,
+    FireServiceRota,
+    FireServiceRotaIncidents,
+    InvalidAuthError,
+    InvalidTokenError,
+)
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
+from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import DOMAIN, WSS_BWRURL
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORTED_PLATFORMS = {SENSOR_DOMAIN}
+
+
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
+    """Set up the FireServiceRota component."""
+
+    return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up FireServiceRota from a config entry."""
+
+    hass.data.setdefault(DOMAIN, {})
+    coordinator = FireServiceRotaCoordinator(hass, entry)
+    await coordinator.setup()
+    await coordinator.async_availability_update()
+
+    if coordinator.token_refresh_failure:
+        return False
+
+    hass.data[DOMAIN][entry.entry_id] = coordinator
+
+    for platform in SUPPORTED_PLATFORMS:
+        hass.async_create_task(
+            hass.config_entries.async_forward_entry_setup(entry, platform)
+        )
+
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Unload FireServiceRota config entry."""
+
+    hass.data[DOMAIN][entry.entry_id].websocket.stop_listener()
+
+    unload_ok = all(
+        await asyncio.gather(
+            *[
+                hass.config_entries.async_forward_entry_unload(entry, platform)
+                for platform in SUPPORTED_PLATFORMS
+            ]
+        )
+    )
+
+    if unload_ok:
+        del hass.data[DOMAIN][entry.entry_id]
+
+    return unload_ok
+
+
+class FireServiceRotaOauth:
+    """Handle authentication tokens."""
+
+    def __init__(self, hass, entry, fsr):
+        """Initialize the oauth object."""
+        self._hass = hass
+        self._entry = entry
+
+        self._url = entry.data[CONF_URL]
+        self._username = entry.data[CONF_USERNAME]
+        self._fsr = fsr
+
+    async def async_refresh_tokens(self) -> bool:
+        """Refresh tokens and update config entry."""
+        _LOGGER.debug("Refreshing authentication tokens after expiration")
+
+        try:
+            token_info = await self._hass.async_add_executor_job(
+                self._fsr.refresh_tokens
+            )
+
+        except (InvalidAuthError, InvalidTokenError):
+            _LOGGER.error("Error refreshing tokens, triggered reauth workflow")
+            self._hass.add_job(
+                self._hass.config_entries.flow.async_init(
+                    DOMAIN,
+                    context={"source": SOURCE_REAUTH},
+                    data={
+                        **self._entry.data,
+                    },
+                )
+            )
+
+            return False
+
+        _LOGGER.debug("Saving new tokens in config entry")
+        self._hass.config_entries.async_update_entry(
+            self._entry,
+            data={
+                "auth_implementation": DOMAIN,
+                CONF_URL: self._url,
+                CONF_USERNAME: self._username,
+                CONF_TOKEN: token_info,
+            },
+        )
+
+        return True
+
+
+class FireServiceRotaWebSocket:
+    """Define a FireServiceRota websocket manager object."""
+
+    def __init__(self, hass, entry):
+        """Initialize the websocket object."""
+        self._hass = hass
+        self._entry = entry
+
+        self._fsr_incidents = FireServiceRotaIncidents(on_incident=self._on_incident)
+        self._incident_data = None
+
+    def _construct_url(self) -> str:
+        """Return URL with latest access token."""
+        return WSS_BWRURL.format(
+            self._entry.data[CONF_URL], self._entry.data[CONF_TOKEN]["access_token"]
+        )
+
+    def incident_data(self) -> object:
+        """Return incident data."""
+        return self._incident_data
+
+    def _on_incident(self, data) -> None:
+        """Received new incident, update data."""
+        _LOGGER.debug("Received new incident via websocket: %s", data)
+        self._incident_data = data
+        dispatcher_send(self._hass, f"{DOMAIN}_{self._entry.entry_id}_update")
+
+    def start_listener(self) -> None:
+        """Start the websocket listener."""
+        _LOGGER.debug("Starting incidents listener")
+        self._fsr_incidents.start(self._construct_url())
+
+    def stop_listener(self) -> None:
+        """Stop the websocket listener."""
+        _LOGGER.debug("Stopping incidents listener")
+        self._fsr_incidents.stop()
+
+
+class FireServiceRotaCoordinator(DataUpdateCoordinator):
+    """Getting the latest data from fireservicerota."""
+
+    def __init__(self, hass, entry):
+        """Initialize the data object."""
+        self._hass = hass
+        self._entry = entry
+
+        super().__init__(
+            hass,
+            _LOGGER,
+            name=DOMAIN,
+            update_method=self.async_availability_update,
+            update_interval=MIN_TIME_BETWEEN_UPDATES,
+        )
+
+        self._url = entry.data[CONF_URL]
+        self._tokens = entry.data[CONF_TOKEN]
+
+        self.token_refresh_failure = False
+        self.incident_id = None
+
+        self.fsr = FireServiceRota(base_url=self._url, token_info=self._tokens)
+
+        self.oauth = FireServiceRotaOauth(
+            self._hass,
+            self._entry,
+            self.fsr,
+        )
+
+        self.websocket = FireServiceRotaWebSocket(self._hass, self._entry)
+
+    async def setup(self) -> None:
+        """Set up the coordinator."""
+        await self._hass.async_add_executor_job(self.websocket.start_listener)
+
+    async def update_call(self, func, *args):
+        """Perform update call and return data."""
+        if self.token_refresh_failure:
+            return
+
+        try:
+            return await self._hass.async_add_executor_job(func, *args)
+        except (ExpiredTokenError, InvalidTokenError):
+            self.websocket.stop_listener()
+            self.token_refresh_failure = True
+            self.update_interval = None
+
+            if await self.oauth.async_refresh_tokens():
+                self.update_interval = MIN_TIME_BETWEEN_UPDATES
+                self.token_refresh_failure = False
+                self.websocket.start_listener()
+
+                return await self._hass.async_add_executor_job(func, *args)
+
+    async def async_availability_update(self) -> None:
+        """Get the latest availability data."""
+        _LOGGER.debug("Updating availability data")
+
+        return await self.update_call(
+            self.fsr.get_availability, str(self._hass.config.time_zone)
+        )
+
+    async def async_response_update(self) -> object:
+        """Get the latest incident response data."""
+        data = self.websocket.incident_data()
+        if data is None or "id" not in data:
+            return
+
+        self.incident_id = data("id")
+        _LOGGER.debug("Updating incident response data for id: %s", self.incident_id)
+
+        return await self.update_call(self.fsr.get_incident_response, self.incident_id)
+
+    async def async_set_response(self, value) -> None:
+        """Set incident response status."""
+        _LOGGER.debug(
+            "Setting incident response for incident '%s' to status '%s'",
+            self.incident_id,
+            value,
+        )
+
+        await self.update_call(self.fsr.set_incident_response, self.incident_id, value)
diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..e5c49fda6ad06a3e3c4e5bc403d270f497c0f069
--- /dev/null
+++ b/homeassistant/components/fireservicerota/config_flow.py
@@ -0,0 +1,129 @@
+"""Config flow for FireServiceRota."""
+from pyfireservicerota import FireServiceRota, InvalidAuthError
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME
+
+from .const import DOMAIN, URL_LIST  # pylint: disable=unused-import
+
+DATA_SCHEMA = vol.Schema(
+    {
+        vol.Required(CONF_URL, default="www.brandweerrooster.nl"): vol.In(URL_LIST),
+        vol.Required(CONF_USERNAME): str,
+        vol.Required(CONF_PASSWORD): str,
+    }
+)
+
+
+class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+    """Handle a FireServiceRota config flow."""
+
+    VERSION = 1
+    CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+    def __init__(self):
+        """Initialize config flow."""
+        self.api = None
+        self._base_url = None
+        self._username = None
+        self._password = None
+        self._existing_entry = None
+        self._description_placeholders = None
+
+    async def async_step_user(self, user_input=None):
+        """Handle a flow initiated by the user."""
+        errors = {}
+
+        if user_input is None:
+            return self._show_setup_form(user_input, errors)
+
+        return await self._validate_and_create_entry(user_input, "user")
+
+    async def _validate_and_create_entry(self, user_input, step_id):
+        """Check if config is valid and create entry if so."""
+        self._password = user_input[CONF_PASSWORD]
+
+        extra_inputs = user_input
+
+        if self._existing_entry:
+            extra_inputs = self._existing_entry
+
+        self._username = extra_inputs[CONF_USERNAME]
+        self._base_url = extra_inputs[CONF_URL]
+
+        if self.unique_id is None:
+            await self.async_set_unique_id(self._username)
+            self._abort_if_unique_id_configured()
+
+        try:
+            self.api = FireServiceRota(
+                base_url=self._base_url,
+                username=self._username,
+                password=self._password,
+            )
+            token_info = await self.hass.async_add_executor_job(self.api.request_tokens)
+
+        except InvalidAuthError:
+            self.api = None
+            return self.async_show_form(
+                step_id=step_id,
+                data_schema=DATA_SCHEMA,
+                errors={"base": "invalid_auth"},
+            )
+
+        data = {
+            "auth_implementation": DOMAIN,
+            CONF_URL: self._base_url,
+            CONF_USERNAME: self._username,
+            CONF_TOKEN: token_info,
+        }
+
+        if step_id == "user":
+            return self.async_create_entry(title=self._username, data=data)
+
+        for entry in self.hass.config_entries.async_entries(DOMAIN):
+            if entry.unique_id == self.unique_id:
+                self.hass.config_entries.async_update_entry(entry, data=data)
+                await self.hass.config_entries.async_reload(entry.entry_id)
+                return self.async_abort(reason="reauth_successful")
+
+    def _show_setup_form(self, user_input=None, errors=None, step_id="user"):
+        """Show the setup form to the user."""
+
+        if user_input is None:
+            user_input = {}
+
+        if step_id == "user":
+            schema = {
+                vol.Required(CONF_URL, default="www.brandweerrooster.nl"): vol.In(
+                    URL_LIST
+                ),
+                vol.Required(CONF_USERNAME): str,
+                vol.Required(CONF_PASSWORD): str,
+            }
+        else:
+            schema = {vol.Required(CONF_PASSWORD): str}
+
+        return self.async_show_form(
+            step_id=step_id,
+            data_schema=vol.Schema(schema),
+            errors=errors or {},
+            description_placeholders=self._description_placeholders,
+        )
+
+    async def async_step_reauth(self, user_input=None):
+        """Get new tokens for a config entry that can't authenticate."""
+
+        if not self._existing_entry:
+            await self.async_set_unique_id(user_input[CONF_USERNAME])
+            self._existing_entry = user_input.copy()
+            self._description_placeholders = {"username": user_input[CONF_USERNAME]}
+            user_input = None
+
+        if user_input is None:
+            return self._show_setup_form(step_id=config_entries.SOURCE_REAUTH)
+
+        return await self._validate_and_create_entry(
+            user_input, config_entries.SOURCE_REAUTH
+        )
diff --git a/homeassistant/components/fireservicerota/const.py b/homeassistant/components/fireservicerota/const.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ca0b7d7e64231bb62bfde18e6e67911f617c71f
--- /dev/null
+++ b/homeassistant/components/fireservicerota/const.py
@@ -0,0 +1,9 @@
+"""Constants for the FireServiceRota integration."""
+
+DOMAIN = "fireservicerota"
+
+URL_LIST = {
+    "www.brandweerrooster.nl": "BrandweerRooster",
+    "www.fireservicerota.co.uk": "FireServiceRota",
+}
+WSS_BWRURL = "wss://{0}/cable?access_token={1}"
diff --git a/homeassistant/components/fireservicerota/manifest.json b/homeassistant/components/fireservicerota/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..6485d155f50e433987e139deb10f6256898c7a0b
--- /dev/null
+++ b/homeassistant/components/fireservicerota/manifest.json
@@ -0,0 +1,8 @@
+{
+  "domain": "fireservicerota",
+  "name": "FireServiceRota",
+  "config_flow": true,
+  "documentation": "https://www.home-assistant.io/integrations/fireservicerota",
+  "requirements": ["pyfireservicerota==0.0.40"],
+  "codeowners": ["@cyberjunky"]
+}
diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..4360a834288c6868bcd51a56eabafa21ae8f0d00
--- /dev/null
+++ b/homeassistant/components/fireservicerota/sensor.py
@@ -0,0 +1,128 @@
+"""Sensor platform for FireServiceRota integration."""
+import logging
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import DOMAIN as FIRESERVICEROTA_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+    hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+    """Set up FireServiceRota sensor based on a config entry."""
+    coordinator = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id]
+
+    async_add_entities([IncidentsSensor(coordinator)])
+
+
+class IncidentsSensor(RestoreEntity):
+    """Representation of FireServiceRota incidents sensor."""
+
+    def __init__(self, coordinator):
+        """Initialize."""
+        self._coordinator = coordinator
+        self._entry_id = self._coordinator._entry.entry_id
+        self._unique_id = f"{self._coordinator._entry.unique_id}_Incidents"
+        self._state = None
+        self._state_attributes = {}
+
+    @property
+    def name(self) -> str:
+        """Return the name of the sensor."""
+        return "Incidents"
+
+    @property
+    def icon(self) -> str:
+        """Return the icon to use in the frontend."""
+        if (
+            "prio" in self._state_attributes
+            and self._state_attributes["prio"][0] == "a"
+        ):
+            return "mdi:ambulance"
+
+        return "mdi:fire-truck"
+
+    @property
+    def state(self) -> str:
+        """Return the state of the sensor."""
+        return self._state
+
+    @property
+    def unique_id(self) -> str:
+        """Return the unique ID of the sensor."""
+        return self._unique_id
+
+    @property
+    def should_poll(self) -> bool:
+        """No polling needed."""
+        return False
+
+    @property
+    def device_state_attributes(self) -> object:
+        """Return available attributes for sensor."""
+        attr = {}
+        data = self._state_attributes
+
+        if not data:
+            return attr
+
+        for value in (
+            "trigger",
+            "created_at",
+            "message_to_speech_url",
+            "prio",
+            "type",
+            "responder_mode",
+            "can_respond_until",
+        ):
+            if data.get(value):
+                attr[value] = data[value]
+
+            if "address" not in data:
+                continue
+
+            for address_value in (
+                "latitude",
+                "longitude",
+                "address_type",
+                "formatted_address",
+            ):
+                if address_value in data["address"]:
+                    attr[address_value] = data["address"][address_value]
+
+        return attr
+
+    async def async_added_to_hass(self) -> None:
+        """Run when about to be added to hass."""
+        await super().async_added_to_hass()
+
+        state = await self.async_get_last_state()
+        if state:
+            self._state = state.state
+            self._state_attributes = state.attributes
+            _LOGGER.debug("Restored entity 'Incidents' state to: %s", self._state)
+
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass,
+                f"{FIRESERVICEROTA_DOMAIN}_{self._entry_id}_update",
+                self.coordinator_update,
+            )
+        )
+
+    @callback
+    def coordinator_update(self) -> None:
+        """Handle updated data from the coordinator."""
+        data = self._coordinator.websocket.incident_data()
+        if not data or "body" not in data:
+            return
+
+        self._state = data["body"]
+        self._state_attributes = data
+        self.async_write_ha_state()
diff --git a/homeassistant/components/fireservicerota/strings.json b/homeassistant/components/fireservicerota/strings.json
new file mode 100644
index 0000000000000000000000000000000000000000..c44673d6c2c3610e171ef753f1203b6a02f0a5cf
--- /dev/null
+++ b/homeassistant/components/fireservicerota/strings.json
@@ -0,0 +1,29 @@
+{
+  "config": {
+    "step": {
+      "user": {
+        "data": {
+          "password": "[%key:common::config_flow::data::password%]",
+          "username": "[%key:common::config_flow::data::username%]",
+          "url": "Website"
+        }
+      },
+      "reauth": {
+        "description": "Authentication tokens baceame invalid, login to recreate them.",
+        "data": {
+          "password": "[%key:common::config_flow::data::password%]"
+        }
+      }
+    },
+    "error": {
+      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+      "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+    },
+    "create_entry": {
+      "default": "[%key:common::config_flow::create_entry::authenticated%]"
+    }
+  }
+}
diff --git a/homeassistant/components/fireservicerota/translations/en.json b/homeassistant/components/fireservicerota/translations/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..fabf02b30022a81acf664fe1fb35044f63daf1d4
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/en.json
@@ -0,0 +1,30 @@
+{
+    "config": {
+        "step": {
+            "user": {
+                "title": "FireServiceRota",
+                "data": {
+                    "password": "Password",
+                    "username": "Username",
+                    "url": "Website"
+                }
+            },
+            "reauth": {
+                "description": "Authentication tokens became invalid, login to recreate them.",
+                "data": {
+                    "password": "Password"
+                }
+            }
+        },
+        "error": {
+            "invalid_auth": "Invalid authentication."
+        },
+        "abort": {
+            "already_configured": "Account is already configured",
+            "reauth_successful": "Re-authentication was successful"
+        },
+        "create_entry": {
+            "default": "Successfully authenticated"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 02fdac9ed3e64607b2d311af125863b9d1ec6b34..a0d9cc2dd7962857a37dcd3c3382de116c90e2f9 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -58,6 +58,7 @@ FLOWS = [
     "enocean",
     "epson",
     "esphome",
+    "fireservicerota",
     "flick_electric",
     "flo",
     "flume",
diff --git a/requirements_all.txt b/requirements_all.txt
index 024e15f509a00864c37f4b721018eaafbdecc95f..30904d7520e1e1cba2100ead7525e6357ad719c6 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1381,6 +1381,9 @@ pyeverlights==0.1.0
 # homeassistant.components.fido
 pyfido==2.1.1
 
+# homeassistant.components.fireservicerota
+pyfireservicerota==0.0.40
+
 # homeassistant.components.flexit
 pyflexit==0.3
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 94b279722e0f4c73fcf2e71d4f3fecb28bb4ff82..862157e45cef928bf73581cb8776da82f4903b1b 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -684,6 +684,9 @@ pyeverlights==0.1.0
 # homeassistant.components.fido
 pyfido==2.1.1
 
+# homeassistant.components.fireservicerota
+pyfireservicerota==0.0.40
+
 # homeassistant.components.flume
 pyflume==0.5.5
 
diff --git a/tests/components/fireservicerota/__init__.py b/tests/components/fireservicerota/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..37e5364d782c10376476548a7080e65ad45d9658
--- /dev/null
+++ b/tests/components/fireservicerota/__init__.py
@@ -0,0 +1 @@
+"""Tests for the FireServiceRota integration."""
diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..b826e6b303bf882b9917b6346bc44153996bf124
--- /dev/null
+++ b/tests/components/fireservicerota/test_config_flow.py
@@ -0,0 +1,114 @@
+"""Test the FireServiceRota config flow."""
+from pyfireservicerota import InvalidAuthError
+
+from homeassistant import data_entry_flow
+from homeassistant.components.fireservicerota.const import (  # pylint: disable=unused-import
+    DOMAIN,
+)
+from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+MOCK_CONF = {
+    CONF_USERNAME: "my@email.address",
+    CONF_PASSWORD: "mypassw0rd",
+    CONF_URL: "www.brandweerrooster.nl",
+}
+
+
+MOCK_DATA = {
+    "auth_implementation": DOMAIN,
+    CONF_URL: MOCK_CONF[CONF_URL],
+    CONF_USERNAME: MOCK_CONF[CONF_USERNAME],
+    "token": {
+        "access_token": "test-access-token",
+        "token_type": "Bearer",
+        "expires_in": 1234,
+        "refresh_token": "test-refresh-token",
+        "created_at": 4321,
+    },
+}
+
+MOCK_TOKEN_INFO = {
+    "access_token": "test-access-token",
+    "token_type": "Bearer",
+    "expires_in": 1234,
+    "refresh_token": "test-refresh-token",
+    "created_at": 4321,
+}
+
+
+async def test_show_form(hass):
+    """Test that the form is served with no input."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "user"}
+    )
+    assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+    assert result["step_id"] == "user"
+
+
+async def test_abort_if_already_setup(hass):
+    """Test abort if already setup."""
+    entry = MockConfigEntry(
+        domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_USERNAME]
+    )
+    entry.add_to_hass(hass)
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "user"}, data=MOCK_CONF
+    )
+    assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+    assert result["reason"] == "already_configured"
+
+
+async def test_invalid_credentials(hass):
+    """Test that invalid credentials throws an error."""
+
+    with patch(
+        "homeassistant.components.fireservicerota.FireServiceRota.request_tokens",
+        side_effect=InvalidAuthError,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN, context={"source": "user"}, data=MOCK_CONF
+        )
+        assert result["errors"] == {"base": "invalid_auth"}
+
+
+async def test_step_user(hass):
+    """Test the start of the config flow."""
+
+    with patch(
+        "homeassistant.components.fireservicerota.config_flow.FireServiceRota"
+    ) as MockFireServiceRota, patch(
+        "homeassistant.components.fireservicerota.async_setup", return_value=True
+    ) as mock_setup, patch(
+        "homeassistant.components.fireservicerota.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+
+        mock_fireservicerota = MockFireServiceRota.return_value
+        mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO
+
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN, context={"source": "user"}, data=MOCK_CONF
+        )
+
+        await hass.async_block_till_done()
+
+        assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+        assert result["title"] == MOCK_CONF[CONF_USERNAME]
+        assert result["data"] == {
+            "auth_implementation": "fireservicerota",
+            CONF_URL: "www.brandweerrooster.nl",
+            CONF_USERNAME: "my@email.address",
+            "token": {
+                "access_token": "test-access-token",
+                "token_type": "Bearer",
+                "expires_in": 1234,
+                "refresh_token": "test-refresh-token",
+                "created_at": 4321,
+            },
+        }
+
+        assert len(mock_setup.mock_calls) == 1
+        assert len(mock_setup_entry.mock_calls) == 1