diff --git a/.coveragerc b/.coveragerc
index 150867054b296861fc666b60eea3a0f8e167b77c..3f537e39f0dd79fc1e70d887f2b15474609e596e 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -624,6 +624,8 @@ omit =
     homeassistant/components/plum_lightpad/light.py
     homeassistant/components/pocketcasts/sensor.py
     homeassistant/components/point/*
+    homeassistant/components/poolsense/__init__.py
+    homeassistant/components/poolsense/sensor.py
     homeassistant/components/prezzibenzina/sensor.py
     homeassistant/components/proliphix/climate.py
     homeassistant/components/prometheus/*
diff --git a/CODEOWNERS b/CODEOWNERS
index f91f857ac1aefc81437ace71965408b8bcaf010f..a2f9668b33032047a0a7ef7a649b2582cb2717e5 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -318,6 +318,7 @@ homeassistant/components/plex/* @jjlawren
 homeassistant/components/plugwise/* @CoMPaTech @bouwew
 homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa
 homeassistant/components/point/* @fredrike
+homeassistant/components/poolsense/* @haemishkyd
 homeassistant/components/powerwall/* @bdraco @jrester
 homeassistant/components/prometheus/* @knyar
 homeassistant/components/proxmoxve/* @k4ds3 @jhollowe
diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..472c09ffef9089249fcb6b23fe195193230a8be5
--- /dev/null
+++ b/homeassistant/components/poolsense/__init__.py
@@ -0,0 +1,103 @@
+"""The PoolSense integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+import async_timeout
+from poolsense import PoolSense
+from poolsense.exceptions import PoolSenseError
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import aiohttp_client, update_coordinator
+from homeassistant.helpers.update_coordinator import UpdateFailed
+
+from .const import DOMAIN
+
+PLATFORMS = ["sensor"]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+    """Set up the PoolSense component."""
+    # Make sure coordinator is initialized.
+    hass.data.setdefault(DOMAIN, {})
+    return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+    """Set up PoolSense from a config entry."""
+    poolsense = PoolSense()
+    auth_valid = await poolsense.test_poolsense_credentials(
+        aiohttp_client.async_get_clientsession(hass),
+        entry.data[CONF_EMAIL],
+        entry.data[CONF_PASSWORD],
+    )
+
+    if not auth_valid:
+        _LOGGER.error("Invalid authentication")
+        return False
+
+    coordinator = await get_coordinator(hass, entry)
+
+    await hass.data[DOMAIN][entry.entry_id].async_refresh()
+
+    if not coordinator.last_update_success:
+        raise ConfigEntryNotReady
+
+    hass.data[DOMAIN][entry.entry_id] = coordinator
+
+    for component in PLATFORMS:
+        hass.async_create_task(
+            hass.config_entries.async_forward_entry_setup(entry, component)
+        )
+
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+    """Unload a config entry."""
+    unload_ok = all(
+        await asyncio.gather(
+            *[
+                hass.config_entries.async_forward_entry_unload(entry, component)
+                for component in PLATFORMS
+            ]
+        )
+    )
+
+    if unload_ok:
+        hass.data[DOMAIN].pop(entry.entry_id)
+
+    return unload_ok
+
+
+async def get_coordinator(hass, entry):
+    """Get the data update coordinator."""
+
+    async def async_get_data():
+        _LOGGER.info("Run query to server")
+        poolsense = PoolSense()
+        return_data = {}
+        with async_timeout.timeout(10):
+            try:
+                return_data = await poolsense.get_poolsense_data(
+                    aiohttp_client.async_get_clientsession(hass),
+                    entry.data[CONF_EMAIL],
+                    entry.data[CONF_PASSWORD],
+                )
+            except (PoolSenseError) as error:
+                raise UpdateFailed(error)
+
+        return return_data
+
+    return update_coordinator.DataUpdateCoordinator(
+        hass,
+        logging.getLogger(__name__),
+        name=DOMAIN,
+        update_method=async_get_data,
+        update_interval=timedelta(hours=1),
+    )
diff --git a/homeassistant/components/poolsense/config_flow.py b/homeassistant/components/poolsense/config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..61087ce89b87500b65c6e0ae34ad4d6c7164ed7c
--- /dev/null
+++ b/homeassistant/components/poolsense/config_flow.py
@@ -0,0 +1,71 @@
+"""Config flow for PoolSense integration."""
+import logging
+
+from poolsense import PoolSense
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+from homeassistant.helpers import aiohttp_client
+
+from .const import DOMAIN  # pylint:disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class PoolSenseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+    """Handle a config flow for PoolSense."""
+
+    VERSION = 1
+    CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+    _options = None
+
+    def __init__(self):
+        """Initialize PoolSense config flow."""
+        self._email = None
+        self._password = None
+        self._errors = {}
+
+    async def async_step_user(self, user_input=None):
+        """Handle the initial step."""
+        self._errors = {}
+
+        if user_input is not None:
+            await self.async_set_unique_id(user_input[CONF_EMAIL])
+            self._abort_if_unique_id_configured()
+
+            self._email = user_input[CONF_EMAIL]
+            self._password = user_input[CONF_PASSWORD]
+            _LOGGER.debug("Configuring user: %s - Password hidden.", self._email)
+
+            poolsense = PoolSense()
+            api_key_valid = await poolsense.test_poolsense_credentials(
+                aiohttp_client.async_get_clientsession(self.hass),
+                self._email,
+                self._password,
+            )
+
+            if not api_key_valid:
+                self._errors["base"] = "auth"
+
+            if not self._errors:
+                return self.async_create_entry(
+                    title=self._email,
+                    data={CONF_EMAIL: self._email, CONF_PASSWORD: self._password},
+                )
+
+        return await self._show_setup_form(user_input, self._errors)
+
+    async def _show_setup_form(self, user_input=None, errors=None):
+        """Show the setup form to the user."""
+        if user_input is None:
+            user_input = {}
+
+        return self.async_show_form(
+            step_id="user",
+            data_schema=vol.Schema(
+                {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
+            ),
+            errors=errors or {},
+        )
diff --git a/homeassistant/components/poolsense/const.py b/homeassistant/components/poolsense/const.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef4fc46dd5e2156a75de0e64e17aefa55c90d8ea
--- /dev/null
+++ b/homeassistant/components/poolsense/const.py
@@ -0,0 +1,4 @@
+"""Constants for the PoolSense integration."""
+
+DOMAIN = "poolsense"
+ATTRIBUTION = "PoolSense Data"
diff --git a/homeassistant/components/poolsense/manifest.json b/homeassistant/components/poolsense/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..b50ed2771706c95bb3cdaf3cdcb63c0a352a0903
--- /dev/null
+++ b/homeassistant/components/poolsense/manifest.json
@@ -0,0 +1,12 @@
+{
+  "domain": "poolsense",
+  "name": "PoolSense",
+  "config_flow": true,
+  "documentation": "https://www.home-assistant.io/integrations/poolsense",
+  "requirements": [
+    "poolsense==0.0.5"
+  ],
+  "codeowners": [
+    "@haemishkyd"
+  ]
+}
diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c816cba0c5d2f6de009477192f2d02e666dbcdc
--- /dev/null
+++ b/homeassistant/components/poolsense/sensor.py
@@ -0,0 +1,172 @@
+"""Sensor platform for the PoolSense sensor."""
+import logging
+
+from homeassistant.const import (
+    ATTR_ATTRIBUTION,
+    CONF_EMAIL,
+    DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_TIMESTAMP,
+    STATE_OK,
+    STATE_PROBLEM,
+    TEMP_CELSIUS,
+    UNIT_PERCENTAGE,
+)
+from homeassistant.helpers.entity import Entity
+
+from .const import ATTRIBUTION, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+SENSORS = {
+    "Chlorine": {
+        "unit": "mV",
+        "icon": "mdi:pool",
+        "name": "Chlorine",
+        "device_class": None,
+    },
+    "pH": {"unit": None, "icon": "mdi:pool", "name": "pH", "device_class": None},
+    "Battery": {
+        "unit": UNIT_PERCENTAGE,
+        "icon": "mdi:battery",
+        "name": "Battery",
+        "device_class": DEVICE_CLASS_BATTERY,
+    },
+    "Water Temp": {
+        "unit": TEMP_CELSIUS,
+        "icon": "mdi:coolant-temperature",
+        "name": "Temperature",
+        "device_class": DEVICE_CLASS_TEMPERATURE,
+    },
+    "Last Seen": {
+        "unit": None,
+        "icon": "mdi:clock",
+        "name": "Last Seen",
+        "device_class": DEVICE_CLASS_TIMESTAMP,
+    },
+    "Chlorine High": {
+        "unit": "mV",
+        "icon": "mdi:pool",
+        "name": "Chlorine High",
+        "device_class": None,
+    },
+    "Chlorine Low": {
+        "unit": "mV",
+        "icon": "mdi:pool",
+        "name": "Chlorine Low",
+        "device_class": None,
+    },
+    "pH High": {
+        "unit": None,
+        "icon": "mdi:pool",
+        "name": "pH High",
+        "device_class": None,
+    },
+    "pH Low": {
+        "unit": None,
+        "icon": "mdi:pool",
+        "name": "pH Low",
+        "device_class": None,
+    },
+    "pH Status": {
+        "unit": None,
+        "icon": "mdi:pool",
+        "name": "pH Status",
+        "device_class": None,
+    },
+    "Chlorine Status": {
+        "unit": None,
+        "icon": "mdi:pool",
+        "name": "Chlorine Status",
+        "device_class": None,
+    },
+}
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+    """Defer sensor setup to the shared sensor module."""
+    coordinator = hass.data[DOMAIN][config_entry.entry_id]
+
+    async_add_entities(
+        PoolSenseSensor(coordinator, config_entry.data[CONF_EMAIL], info_type)
+        for info_type in SENSORS
+    )
+
+
+class PoolSenseSensor(Entity):
+    """Sensor representing poolsense data."""
+
+    unique_id = None
+
+    def __init__(self, coordinator, email, info_type):
+        """Initialize poolsense sensor."""
+        self._email = email
+        self.unique_id = f"{email}-{info_type}"
+        self.coordinator = coordinator
+        self.info_type = info_type
+
+    @property
+    def available(self):
+        """Return if sensor is available."""
+        return self.coordinator.last_update_success
+
+    @property
+    def name(self):
+        """Return the name of the particular component."""
+        return "PoolSense {}".format(SENSORS[self.info_type]["name"])
+
+    @property
+    def should_poll(self):
+        """Return False, updates are controlled via coordinator."""
+        return False
+
+    @property
+    def state(self):
+        """State of the sensor."""
+        if self.info_type == "pH Status":
+            if self.coordinator.data[self.info_type] == "red":
+                return STATE_PROBLEM
+            return STATE_OK
+        if self.info_type == "Chlorine Status":
+            if self.coordinator.data[self.info_type] == "red":
+                return STATE_PROBLEM
+            return STATE_OK
+        return self.coordinator.data[self.info_type]
+
+    @property
+    def device_class(self):
+        """Return the device class."""
+        return SENSORS[self.info_type]["device_class"]
+
+    @property
+    def icon(self):
+        """Return the icon."""
+        if self.info_type == "pH Status":
+            if self.coordinator.data[self.info_type] == "red":
+                return "mdi:thumb-down"
+            return "mdi:thumb-up"
+        if self.info_type == "Chlorine Status":
+            if self.coordinator.data[self.info_type] == "red":
+                return "mdi:thumb-down"
+            return "mdi:thumb-up"
+        return SENSORS[self.info_type]["icon"]
+
+    @property
+    def unit_of_measurement(self):
+        """Return unit of measurement."""
+        return SENSORS[self.info_type]["unit"]
+
+    @property
+    def device_state_attributes(self):
+        """Return device attributes."""
+        return {ATTR_ATTRIBUTION: ATTRIBUTION}
+
+    async def async_update(self):
+        """Update status of sensor."""
+        await self.coordinator.async_request_refresh()
+
+    async def async_added_to_hass(self):
+        """When entity is added to hass."""
+        self.async_on_remove(
+            self.coordinator.async_add_listener(self.async_write_ha_state)
+        )
diff --git a/homeassistant/components/poolsense/strings.json b/homeassistant/components/poolsense/strings.json
new file mode 100644
index 0000000000000000000000000000000000000000..21a1ab393a4d41a34aed7c64e55e5dcdcd5b8864
--- /dev/null
+++ b/homeassistant/components/poolsense/strings.json
@@ -0,0 +1,22 @@
+{
+  "config": {
+    "step": {
+      "user": {
+        "title": "PoolSense",
+        "description": "[%key:common::config_flow::description%]",
+        "data": {
+          "email": "[%key:common::config_flow::data::email%]",
+          "password": "[%key:common::config_flow::data::password%]"
+        }
+      }
+    },
+    "error": {
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+      "unknown": "[%key:common::config_flow::error::unknown%]"
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+    }
+  }
+}
diff --git a/homeassistant/components/poolsense/translations/en.json b/homeassistant/components/poolsense/translations/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..92588cbfd3aeed6104324f78c525db99676dc1aa
--- /dev/null
+++ b/homeassistant/components/poolsense/translations/en.json
@@ -0,0 +1,22 @@
+{
+    "config": {
+        "step": {
+            "user": {
+                "title": "PoolSense",
+                "description": "Set up PoolSense integration. Register on the dedicated app to get your username and password. Serial is optional.",
+                "data": {
+                    "email": "Email",
+                    "password": "Password"
+                }
+            }
+        },
+        "error": {
+            "cannot_connect": "Can't connect to PoolSense.",
+            "invalid_auth": "Invalid authorisation details.",
+            "unknown": "Unknown Error."
+        },
+        "abort": {
+            "already_configured": "Device already configured."
+        }
+    }
+}
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 977be4bae875c1a4b84be5aaa74a61d49d74254b..fa95014ee7ac2cfb87f1dff9721705c5588ddcd6 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -124,6 +124,7 @@ FLOWS = [
     "plugwise",
     "plum_lightpad",
     "point",
+    "poolsense",
     "powerwall",
     "ps4",
     "pvpc_hourly_pricing",
diff --git a/requirements_all.txt b/requirements_all.txt
index 55e9c617f0b56eea59da4c20e0e63a11e4488e17..97ceb797d8e7c2666fd19eafa84d55249cb9867e 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1106,6 +1106,9 @@ pmsensor==0.4
 # homeassistant.components.pocketcasts
 pocketcasts==0.1
 
+# homeassistant.components.poolsense
+poolsense==0.0.5
+
 # homeassistant.components.reddit
 praw==6.5.1
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index afa1e12433d19019a0bfc9bb8f8fc8195b39f2ac..9605228036409c30a82af38867009e03edb494d4 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -496,6 +496,9 @@ plumlightpad==0.0.11
 # homeassistant.components.serial_pm
 pmsensor==0.4
 
+# homeassistant.components.poolsense
+poolsense==0.0.5
+
 # homeassistant.components.reddit
 praw==6.5.1
 
diff --git a/tests/components/poolsense/__init__.py b/tests/components/poolsense/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ace3a6997fb85cd0e7b605db4dbfd1b3c9858d6d
--- /dev/null
+++ b/tests/components/poolsense/__init__.py
@@ -0,0 +1 @@
+"""Tests for the PoolSense integration."""
diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a7b824f22690422ef3369b3d6b9e833e40bbece
--- /dev/null
+++ b/tests/components/poolsense/test_config_flow.py
@@ -0,0 +1,55 @@
+"""Test the PoolSense config flow."""
+from homeassistant import data_entry_flow
+from homeassistant.components.poolsense.const import DOMAIN
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
+
+from tests.async_mock import patch
+
+
+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": SOURCE_USER}
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+    assert result["step_id"] == SOURCE_USER
+
+
+async def test_invalid_credentials(hass):
+    """Test we handle invalid credentials."""
+    with patch(
+        "poolsense.PoolSense.test_poolsense_credentials", return_value=False,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": SOURCE_USER},
+            data={CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"},
+        )
+
+    assert result["type"] == "form"
+    assert result["errors"] == {"base": "auth"}
+
+
+async def test_valid_credentials(hass):
+    """Test we handle invalid credentials."""
+    with patch(
+        "poolsense.PoolSense.test_poolsense_credentials", return_value=True
+    ), patch(
+        "homeassistant.components.poolsense.async_setup", return_value=True
+    ) as mock_setup, patch(
+        "homeassistant.components.poolsense.async_setup_entry", return_value=True
+    ) as mock_setup_entry:
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": SOURCE_USER},
+            data={CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"},
+        )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+    assert result["title"] == "test-email"
+
+    await hass.async_block_till_done()
+    assert len(mock_setup.mock_calls) == 1
+    assert len(mock_setup_entry.mock_calls) == 1