From 68fbc0792a1ec5d44ce4afccdeaf158d8c594a4e Mon Sep 17 00:00:00 2001
From: Klaas Schoute <klaas_schoute@hotmail.com>
Date: Fri, 20 Aug 2021 08:45:04 +0200
Subject: [PATCH] Add P1 Monitor integration (#54738)

* Init integration P1 Monitor

* Fix build error

* Add quality scale

* Remove last_reset and icon

* Change list to tuple

* Close client on connection exception

* Change min value to 5 (seconds)

* the used python package will close it

* Remove the options flow

* Add session and close client

* Smash to a single DataUpdateCoordinator

* Make a custom update coordinator class

* await the coordinator close

* Add second await the coordinator close

* Close when exit scope

* Removed unused code

* Fix test_sensor on entity_id change

* Fix test on test_sensor

* Transfer SENSOR dict to sensor platform

* device class for cost entity update entity_name

* Revert name in unique id and update sensor test

* Update code based on suggestions

* Fix typing

* Change code to fix mypy errors

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 CODEOWNERS                                    |   1 +
 .../components/p1_monitor/__init__.py         |  91 ++++++
 .../components/p1_monitor/config_flow.py      |  57 ++++
 homeassistant/components/p1_monitor/const.py  |  23 ++
 .../components/p1_monitor/manifest.json       |  10 +
 homeassistant/components/p1_monitor/sensor.py | 287 ++++++++++++++++++
 .../components/p1_monitor/strings.json        |  17 ++
 .../p1_monitor/translations/en.json           |  17 ++
 homeassistant/generated/config_flows.py       |   1 +
 requirements_all.txt                          |   3 +
 requirements_test_all.txt                     |   3 +
 tests/components/p1_monitor/__init__.py       |   1 +
 tests/components/p1_monitor/conftest.py       |  59 ++++
 .../components/p1_monitor/test_config_flow.py |  62 ++++
 tests/components/p1_monitor/test_init.py      |  44 +++
 tests/components/p1_monitor/test_sensor.py    | 201 ++++++++++++
 tests/fixtures/p1_monitor/phases.json         |  74 +++++
 tests/fixtures/p1_monitor/settings.json       |  27 ++
 tests/fixtures/p1_monitor/smartmeter.json     |  15 +
 19 files changed, 993 insertions(+)
 create mode 100644 homeassistant/components/p1_monitor/__init__.py
 create mode 100644 homeassistant/components/p1_monitor/config_flow.py
 create mode 100644 homeassistant/components/p1_monitor/const.py
 create mode 100644 homeassistant/components/p1_monitor/manifest.json
 create mode 100644 homeassistant/components/p1_monitor/sensor.py
 create mode 100644 homeassistant/components/p1_monitor/strings.json
 create mode 100644 homeassistant/components/p1_monitor/translations/en.json
 create mode 100644 tests/components/p1_monitor/__init__.py
 create mode 100644 tests/components/p1_monitor/conftest.py
 create mode 100644 tests/components/p1_monitor/test_config_flow.py
 create mode 100644 tests/components/p1_monitor/test_init.py
 create mode 100644 tests/components/p1_monitor/test_sensor.py
 create mode 100644 tests/fixtures/p1_monitor/phases.json
 create mode 100644 tests/fixtures/p1_monitor/settings.json
 create mode 100644 tests/fixtures/p1_monitor/smartmeter.json

diff --git a/CODEOWNERS b/CODEOWNERS
index 3606fade468..6c6a248cdc9 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -374,6 +374,7 @@ homeassistant/components/orangepi_gpio/* @pascallj
 homeassistant/components/oru/* @bvlaicu
 homeassistant/components/ovo_energy/* @timmo001
 homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare
+homeassistant/components/p1_monitor/* @klaasnicolaas
 homeassistant/components/panel_custom/* @home-assistant/frontend
 homeassistant/components/panel_iframe/* @home-assistant/frontend
 homeassistant/components/pcal9535a/* @Shulyaka
diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py
new file mode 100644
index 00000000000..5d649fa6d63
--- /dev/null
+++ b/homeassistant/components/p1_monitor/__init__.py
@@ -0,0 +1,91 @@
+"""The P1 Monitor integration."""
+from __future__ import annotations
+
+from typing import TypedDict
+
+from p1monitor import P1Monitor, Phases, Settings, SmartMeter
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import (
+    DOMAIN,
+    LOGGER,
+    SCAN_INTERVAL,
+    SERVICE_PHASES,
+    SERVICE_SETTINGS,
+    SERVICE_SMARTMETER,
+)
+
+PLATFORMS = (SENSOR_DOMAIN,)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up P1 Monitor from a config entry."""
+
+    coordinator = P1MonitorDataUpdateCoordinator(hass)
+    try:
+        await coordinator.async_config_entry_first_refresh()
+    except ConfigEntryNotReady:
+        await coordinator.p1monitor.close()
+        raise
+
+    hass.data.setdefault(DOMAIN, {})
+    hass.data[DOMAIN][entry.entry_id] = coordinator
+
+    hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Unload P1 Monitor config entry."""
+    unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+    if unload_ok:
+        coordinator = hass.data[DOMAIN].pop(entry.entry_id)
+        await coordinator.p1monitor.close()
+    return unload_ok
+
+
+class P1MonitorData(TypedDict):
+    """Class for defining data in dict."""
+
+    smartmeter: SmartMeter
+    phases: Phases
+    settings: Settings
+
+
+class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]):
+    """Class to manage fetching P1 Monitor data from single endpoint."""
+
+    config_entry: ConfigEntry
+
+    def __init__(
+        self,
+        hass: HomeAssistant,
+    ) -> None:
+        """Initialize global P1 Monitor data updater."""
+        super().__init__(
+            hass,
+            LOGGER,
+            name=DOMAIN,
+            update_interval=SCAN_INTERVAL,
+        )
+
+        self.p1monitor = P1Monitor(
+            self.config_entry.data[CONF_HOST], session=async_get_clientsession(hass)
+        )
+
+    async def _async_update_data(self) -> P1MonitorData:
+        """Fetch data from P1 Monitor."""
+        data: P1MonitorData = {
+            SERVICE_SMARTMETER: await self.p1monitor.smartmeter(),
+            SERVICE_PHASES: await self.p1monitor.phases(),
+            SERVICE_SETTINGS: await self.p1monitor.settings(),
+        }
+
+        return data
diff --git a/homeassistant/components/p1_monitor/config_flow.py b/homeassistant/components/p1_monitor/config_flow.py
new file mode 100644
index 00000000000..9e9d695f5e9
--- /dev/null
+++ b/homeassistant/components/p1_monitor/config_flow.py
@@ -0,0 +1,57 @@
+"""Config flow for P1 Monitor integration."""
+from __future__ import annotations
+
+from typing import Any
+
+from p1monitor import P1Monitor, P1MonitorError
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigFlow
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.data_entry_flow import FlowResult
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import DOMAIN
+
+
+class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN):
+    """Config flow for P1 Monitor."""
+
+    VERSION = 1
+
+    async def async_step_user(
+        self, user_input: dict[str, Any] | None = None
+    ) -> FlowResult:
+        """Handle a flow initialized by the user."""
+
+        errors = {}
+
+        if user_input is not None:
+            session = async_get_clientsession(self.hass)
+            try:
+                async with P1Monitor(
+                    host=user_input[CONF_HOST], session=session
+                ) as client:
+                    await client.smartmeter()
+            except P1MonitorError:
+                errors["base"] = "cannot_connect"
+            else:
+                return self.async_create_entry(
+                    title=user_input[CONF_NAME],
+                    data={
+                        CONF_HOST: user_input[CONF_HOST],
+                    },
+                )
+
+        return self.async_show_form(
+            step_id="user",
+            data_schema=vol.Schema(
+                {
+                    vol.Optional(
+                        CONF_NAME, default=self.hass.config.location_name
+                    ): str,
+                    vol.Required(CONF_HOST): str,
+                }
+            ),
+            errors=errors,
+        )
diff --git a/homeassistant/components/p1_monitor/const.py b/homeassistant/components/p1_monitor/const.py
new file mode 100644
index 00000000000..1af76d49176
--- /dev/null
+++ b/homeassistant/components/p1_monitor/const.py
@@ -0,0 +1,23 @@
+"""Constants for the P1 Monitor integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+from typing import Final
+
+DOMAIN: Final = "p1_monitor"
+LOGGER = logging.getLogger(__package__)
+SCAN_INTERVAL = timedelta(seconds=5)
+
+ATTR_ENTRY_TYPE: Final = "entry_type"
+ENTRY_TYPE_SERVICE: Final = "service"
+
+SERVICE_SMARTMETER: Final = "smartmeter"
+SERVICE_PHASES: Final = "phases"
+SERVICE_SETTINGS: Final = "settings"
+
+SERVICES: dict[str, str] = {
+    SERVICE_SMARTMETER: "SmartMeter",
+    SERVICE_PHASES: "Phases",
+    SERVICE_SETTINGS: "Settings",
+}
diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json
new file mode 100644
index 00000000000..9e61bca3089
--- /dev/null
+++ b/homeassistant/components/p1_monitor/manifest.json
@@ -0,0 +1,10 @@
+{
+  "domain": "p1_monitor",
+  "name": "P1 Monitor",
+  "config_flow": true,
+  "documentation": "https://www.home-assistant.io/integrations/p1_monitor",
+  "requirements": ["p1monitor == 0.2.0"],
+  "codeowners": ["@klaasnicolaas"],
+  "quality_scale": "platinum",
+  "iot_class": "local_polling"
+}
\ No newline at end of file
diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py
new file mode 100644
index 00000000000..36a991c7333
--- /dev/null
+++ b/homeassistant/components/p1_monitor/sensor.py
@@ -0,0 +1,287 @@
+"""Support for P1 Monitor sensors."""
+from __future__ import annotations
+
+from typing import Literal
+
+from homeassistant.components.sensor import (
+    DOMAIN as SENSOR_DOMAIN,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
+    SensorEntity,
+    SensorEntityDescription,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+    ATTR_IDENTIFIERS,
+    ATTR_MANUFACTURER,
+    ATTR_NAME,
+    CURRENCY_EURO,
+    DEVICE_CLASS_CURRENT,
+    DEVICE_CLASS_ENERGY,
+    DEVICE_CLASS_GAS,
+    DEVICE_CLASS_MONETARY,
+    DEVICE_CLASS_POWER,
+    DEVICE_CLASS_VOLTAGE,
+    ELECTRIC_CURRENT_AMPERE,
+    ELECTRIC_POTENTIAL_VOLT,
+    ENERGY_KILO_WATT_HOUR,
+    POWER_WATT,
+    VOLUME_CUBIC_METERS,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from . import P1MonitorDataUpdateCoordinator
+from .const import (
+    ATTR_ENTRY_TYPE,
+    DOMAIN,
+    ENTRY_TYPE_SERVICE,
+    SERVICE_PHASES,
+    SERVICE_SETTINGS,
+    SERVICE_SMARTMETER,
+    SERVICES,
+)
+
+SENSORS: dict[
+    Literal["smartmeter", "phases", "settings"], tuple[SensorEntityDescription, ...]
+] = {
+    SERVICE_SMARTMETER: (
+        SensorEntityDescription(
+            key="gas_consumption",
+            name="Gas Consumption",
+            entity_registry_enabled_default=False,
+            native_unit_of_measurement=VOLUME_CUBIC_METERS,
+            device_class=DEVICE_CLASS_GAS,
+            state_class=STATE_CLASS_TOTAL_INCREASING,
+        ),
+        SensorEntityDescription(
+            key="power_consumption",
+            name="Power Consumption",
+            native_unit_of_measurement=POWER_WATT,
+            device_class=DEVICE_CLASS_POWER,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="energy_consumption_high",
+            name="Energy Consumption - High Tariff",
+            native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
+            device_class=DEVICE_CLASS_ENERGY,
+            state_class=STATE_CLASS_TOTAL_INCREASING,
+        ),
+        SensorEntityDescription(
+            key="energy_consumption_low",
+            name="Energy Consumption - Low Tariff",
+            native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
+            device_class=DEVICE_CLASS_ENERGY,
+            state_class=STATE_CLASS_TOTAL_INCREASING,
+        ),
+        SensorEntityDescription(
+            key="power_production",
+            name="Power Production",
+            native_unit_of_measurement=POWER_WATT,
+            device_class=DEVICE_CLASS_POWER,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="energy_production_high",
+            name="Energy Production - High Tariff",
+            native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
+            device_class=DEVICE_CLASS_ENERGY,
+            state_class=STATE_CLASS_TOTAL_INCREASING,
+        ),
+        SensorEntityDescription(
+            key="energy_production_low",
+            name="Energy Production - Low Tariff",
+            native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
+            device_class=DEVICE_CLASS_ENERGY,
+            state_class=STATE_CLASS_TOTAL_INCREASING,
+        ),
+        SensorEntityDescription(
+            key="energy_tariff_period",
+            name="Energy Tariff Period",
+            icon="mdi:calendar-clock",
+        ),
+    ),
+    SERVICE_PHASES: (
+        SensorEntityDescription(
+            key="voltage_phase_l1",
+            name="Voltage Phase L1",
+            native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
+            device_class=DEVICE_CLASS_VOLTAGE,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="voltage_phase_l2",
+            name="Voltage Phase L2",
+            native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
+            device_class=DEVICE_CLASS_VOLTAGE,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="voltage_phase_l3",
+            name="Voltage Phase L3",
+            native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
+            device_class=DEVICE_CLASS_VOLTAGE,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="current_phase_l1",
+            name="Current Phase L1",
+            native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
+            device_class=DEVICE_CLASS_CURRENT,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="current_phase_l2",
+            name="Current Phase L2",
+            native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
+            device_class=DEVICE_CLASS_CURRENT,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="current_phase_l3",
+            name="Current Phase L3",
+            native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
+            device_class=DEVICE_CLASS_CURRENT,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="power_consumed_phase_l1",
+            name="Power Consumed Phase L1",
+            native_unit_of_measurement=POWER_WATT,
+            device_class=DEVICE_CLASS_POWER,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="power_consumed_phase_l2",
+            name="Power Consumed Phase L2",
+            native_unit_of_measurement=POWER_WATT,
+            device_class=DEVICE_CLASS_POWER,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="power_consumed_phase_l3",
+            name="Power Consumed Phase L3",
+            native_unit_of_measurement=POWER_WATT,
+            device_class=DEVICE_CLASS_POWER,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="power_produced_phase_l1",
+            name="Power Produced Phase L1",
+            native_unit_of_measurement=POWER_WATT,
+            device_class=DEVICE_CLASS_POWER,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="power_produced_phase_l2",
+            name="Power Produced Phase L2",
+            native_unit_of_measurement=POWER_WATT,
+            device_class=DEVICE_CLASS_POWER,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+        SensorEntityDescription(
+            key="power_produced_phase_l3",
+            name="Power Produced Phase L3",
+            native_unit_of_measurement=POWER_WATT,
+            device_class=DEVICE_CLASS_POWER,
+            state_class=STATE_CLASS_MEASUREMENT,
+        ),
+    ),
+    SERVICE_SETTINGS: (
+        SensorEntityDescription(
+            key="gas_consumption_tariff",
+            name="Gas Consumption - Tariff",
+            entity_registry_enabled_default=False,
+            device_class=DEVICE_CLASS_MONETARY,
+            native_unit_of_measurement=CURRENCY_EURO,
+        ),
+        SensorEntityDescription(
+            key="energy_consumption_low_tariff",
+            name="Energy Consumption - Low Tariff",
+            device_class=DEVICE_CLASS_MONETARY,
+            native_unit_of_measurement=CURRENCY_EURO,
+        ),
+        SensorEntityDescription(
+            key="energy_consumption_high_tariff",
+            name="Energy Consumption - High Tariff",
+            device_class=DEVICE_CLASS_MONETARY,
+            native_unit_of_measurement=CURRENCY_EURO,
+        ),
+        SensorEntityDescription(
+            key="energy_production_low_tariff",
+            name="Energy Production - Low Tariff",
+            device_class=DEVICE_CLASS_MONETARY,
+            native_unit_of_measurement=CURRENCY_EURO,
+        ),
+        SensorEntityDescription(
+            key="energy_production_high_tariff",
+            name="Energy Production - High Tariff",
+            device_class=DEVICE_CLASS_MONETARY,
+            native_unit_of_measurement=CURRENCY_EURO,
+        ),
+    ),
+}
+
+
+async def async_setup_entry(
+    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+    """Set up P1 Monitor Sensors based on a config entry."""
+    async_add_entities(
+        P1MonitorSensorEntity(
+            coordinator=hass.data[DOMAIN][entry.entry_id],
+            description=description,
+            service_key=service_key,
+            name=entry.title,
+            service=SERVICES[service_key],
+        )
+        for service_key, service_sensors in SENSORS.items()
+        for description in service_sensors
+    )
+
+
+class P1MonitorSensorEntity(CoordinatorEntity, SensorEntity):
+    """Defines an P1 Monitor sensor."""
+
+    coordinator: P1MonitorDataUpdateCoordinator
+
+    def __init__(
+        self,
+        *,
+        coordinator: P1MonitorDataUpdateCoordinator,
+        description: SensorEntityDescription,
+        service_key: Literal["smartmeter", "phases", "settings"],
+        name: str,
+        service: str,
+    ) -> None:
+        """Initialize P1 Monitor sensor."""
+        super().__init__(coordinator=coordinator)
+        self._service_key = service_key
+
+        self.entity_id = f"{SENSOR_DOMAIN}.{name}_{description.key}"
+        self.entity_description = description
+        self._attr_unique_id = (
+            f"{coordinator.config_entry.entry_id}_{service_key}_{description.key}"
+        )
+
+        self._attr_device_info = {
+            ATTR_IDENTIFIERS: {
+                (DOMAIN, f"{coordinator.config_entry.entry_id}_{service_key}")
+            },
+            ATTR_NAME: service,
+            ATTR_MANUFACTURER: "P1 Monitor",
+            ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE,
+        }
+
+    @property
+    def native_value(self) -> StateType:
+        """Return the state of the sensor."""
+        value = getattr(
+            self.coordinator.data[self._service_key], self.entity_description.key
+        )
+        if isinstance(value, str):
+            return value.lower()
+        return value
diff --git a/homeassistant/components/p1_monitor/strings.json b/homeassistant/components/p1_monitor/strings.json
new file mode 100644
index 00000000000..c28a7129006
--- /dev/null
+++ b/homeassistant/components/p1_monitor/strings.json
@@ -0,0 +1,17 @@
+{
+  "config": {
+    "step": {
+      "user": {
+        "description": "Set up P1 Monitor to integrate with Home Assistant.",
+        "data": {
+          "host": "[%key:common::config_flow::data::host%]",
+          "name": "[%key:common::config_flow::data::name%]"
+        }
+      }
+    },
+    "error": {
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+      "unknown": "[%key:common::config_flow::error::unknown%]"
+    }
+  }
+}
\ No newline at end of file
diff --git a/homeassistant/components/p1_monitor/translations/en.json b/homeassistant/components/p1_monitor/translations/en.json
new file mode 100644
index 00000000000..34b64082b43
--- /dev/null
+++ b/homeassistant/components/p1_monitor/translations/en.json
@@ -0,0 +1,17 @@
+{
+    "config": {
+        "error": {
+            "cannot_connect": "Failed to connect",
+            "unknown": "Unexpected error"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "host": "Host",
+                    "name": "Name"
+                },
+                "description": "Set up P1 Monitor to integrate with Home Assistant."
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 1ebf71b369f..339bbb1ede3 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -198,6 +198,7 @@ FLOWS = [
     "ovo_energy",
     "owntracks",
     "ozw",
+    "p1_monitor",
     "panasonic_viera",
     "philips_js",
     "pi_hole",
diff --git a/requirements_all.txt b/requirements_all.txt
index 244b4e13188..667a1803924 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1133,6 +1133,9 @@ orvibo==1.1.1
 # homeassistant.components.ovo_energy
 ovoenergy==1.1.12
 
+# homeassistant.components.p1_monitor
+p1monitor == 0.2.0
+
 # homeassistant.components.mqtt
 # homeassistant.components.shiftr
 paho-mqtt==1.5.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 6557b0d6ce9..f4b934e5a6b 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -635,6 +635,9 @@ openerz-api==0.1.0
 # homeassistant.components.ovo_energy
 ovoenergy==1.1.12
 
+# homeassistant.components.p1_monitor
+p1monitor == 0.2.0
+
 # homeassistant.components.mqtt
 # homeassistant.components.shiftr
 paho-mqtt==1.5.1
diff --git a/tests/components/p1_monitor/__init__.py b/tests/components/p1_monitor/__init__.py
new file mode 100644
index 00000000000..53a063c5f5b
--- /dev/null
+++ b/tests/components/p1_monitor/__init__.py
@@ -0,0 +1 @@
+"""Tests for the P1 Monitor integration."""
diff --git a/tests/components/p1_monitor/conftest.py b/tests/components/p1_monitor/conftest.py
new file mode 100644
index 00000000000..dbdf572c6de
--- /dev/null
+++ b/tests/components/p1_monitor/conftest.py
@@ -0,0 +1,59 @@
+"""Fixtures for P1 Monitor integration tests."""
+import json
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from p1monitor import Phases, Settings, SmartMeter
+import pytest
+
+from homeassistant.components.p1_monitor.const import DOMAIN
+from homeassistant.const import CONF_HOST
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry, load_fixture
+
+
+@pytest.fixture
+def mock_config_entry() -> MockConfigEntry:
+    """Return the default mocked config entry."""
+    return MockConfigEntry(
+        title="monitor",
+        domain=DOMAIN,
+        data={CONF_HOST: "example"},
+        unique_id="unique_thingy",
+    )
+
+
+@pytest.fixture
+def mock_p1monitor():
+    """Return a mocked P1 Monitor client."""
+    with patch("homeassistant.components.p1_monitor.P1Monitor") as p1monitor_mock:
+        client = p1monitor_mock.return_value
+        client.smartmeter = AsyncMock(
+            return_value=SmartMeter.from_dict(
+                json.loads(load_fixture("p1_monitor/smartmeter.json"))
+            )
+        )
+        client.phases = AsyncMock(
+            return_value=Phases.from_dict(
+                json.loads(load_fixture("p1_monitor/phases.json"))
+            )
+        )
+        client.settings = AsyncMock(
+            return_value=Settings.from_dict(
+                json.loads(load_fixture("p1_monitor/settings.json"))
+            )
+        )
+        yield p1monitor_mock
+
+
+@pytest.fixture
+async def init_integration(
+    hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_p1monitor: MagicMock
+) -> MockConfigEntry:
+    """Set up the P1 Monitor integration for testing."""
+    mock_config_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_config_entry.entry_id)
+    await hass.async_block_till_done()
+
+    return mock_config_entry
diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py
new file mode 100644
index 00000000000..f6ce5fe5d9d
--- /dev/null
+++ b/tests/components/p1_monitor/test_config_flow.py
@@ -0,0 +1,62 @@
+"""Test the P1 Monitor config flow."""
+from unittest.mock import patch
+
+from p1monitor import P1MonitorError
+
+from homeassistant.components.p1_monitor.const import DOMAIN
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.const import CONF_HOST, CONF_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
+
+
+async def test_full_user_flow(hass: HomeAssistant) -> None:
+    """Test the full user configuration flow."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": SOURCE_USER}
+    )
+
+    assert result.get("type") == RESULT_TYPE_FORM
+    assert result.get("step_id") == SOURCE_USER
+    assert "flow_id" in result
+
+    with patch(
+        "homeassistant.components.p1_monitor.config_flow.P1Monitor.smartmeter"
+    ) as mock_p1monitor, patch(
+        "homeassistant.components.p1_monitor.async_setup_entry", return_value=True
+    ) as mock_setup_entry:
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            user_input={
+                CONF_NAME: "Name",
+                CONF_HOST: "example.com",
+            },
+        )
+
+    assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
+    assert result2.get("title") == "Name"
+    assert result2.get("data") == {
+        CONF_HOST: "example.com",
+    }
+
+    assert len(mock_setup_entry.mock_calls) == 1
+    assert len(mock_p1monitor.mock_calls) == 1
+
+
+async def test_api_error(hass: HomeAssistant) -> None:
+    """Test we handle cannot connect error."""
+    with patch(
+        "homeassistant.components.p1_monitor.P1Monitor.smartmeter",
+        side_effect=P1MonitorError,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": SOURCE_USER},
+            data={
+                CONF_NAME: "Name",
+                CONF_HOST: "example.com",
+            },
+        )
+
+    assert result.get("type") == RESULT_TYPE_FORM
+    assert result.get("errors") == {"base": "cannot_connect"}
diff --git a/tests/components/p1_monitor/test_init.py b/tests/components/p1_monitor/test_init.py
new file mode 100644
index 00000000000..bddaff137e6
--- /dev/null
+++ b/tests/components/p1_monitor/test_init.py
@@ -0,0 +1,44 @@
+"""Tests for the P1 Monitor integration."""
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from p1monitor import P1MonitorConnectionError
+
+from homeassistant.components.p1_monitor.const import DOMAIN
+from homeassistant.config_entries import ConfigEntryState
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry
+
+
+async def test_load_unload_config_entry(
+    hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_p1monitor: AsyncMock
+) -> None:
+    """Test the P1 Monitor configuration entry loading/unloading."""
+    mock_config_entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(mock_config_entry.entry_id)
+    await hass.async_block_till_done()
+
+    assert mock_config_entry.state is ConfigEntryState.LOADED
+
+    await hass.config_entries.async_unload(mock_config_entry.entry_id)
+    await hass.async_block_till_done()
+
+    assert not hass.data.get(DOMAIN)
+
+
+@patch(
+    "homeassistant.components.p1_monitor.P1Monitor.request",
+    side_effect=P1MonitorConnectionError,
+)
+async def test_config_entry_not_ready(
+    mock_request: MagicMock,
+    hass: HomeAssistant,
+    mock_config_entry: MockConfigEntry,
+) -> None:
+    """Test the P1 Monitor configuration entry not ready."""
+    mock_config_entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(mock_config_entry.entry_id)
+    await hass.async_block_till_done()
+
+    assert mock_request.call_count == 1
+    assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py
new file mode 100644
index 00000000000..baf73811636
--- /dev/null
+++ b/tests/components/p1_monitor/test_sensor.py
@@ -0,0 +1,201 @@
+"""Tests for the sensors provided by the P1 Monitor integration."""
+import pytest
+
+from homeassistant.components.p1_monitor.const import DOMAIN, ENTRY_TYPE_SERVICE
+from homeassistant.components.sensor import (
+    ATTR_STATE_CLASS,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
+)
+from homeassistant.const import (
+    ATTR_DEVICE_CLASS,
+    ATTR_FRIENDLY_NAME,
+    ATTR_ICON,
+    ATTR_UNIT_OF_MEASUREMENT,
+    CURRENCY_EURO,
+    DEVICE_CLASS_CURRENT,
+    DEVICE_CLASS_ENERGY,
+    DEVICE_CLASS_MONETARY,
+    DEVICE_CLASS_POWER,
+    DEVICE_CLASS_VOLTAGE,
+    ELECTRIC_CURRENT_AMPERE,
+    ELECTRIC_POTENTIAL_VOLT,
+    ENERGY_KILO_WATT_HOUR,
+    POWER_WATT,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+
+from tests.common import MockConfigEntry
+
+
+async def test_smartmeter(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+) -> None:
+    """Test the P1 Monitor - SmartMeter sensors."""
+    entry_id = init_integration.entry_id
+    entity_registry = er.async_get(hass)
+    device_registry = dr.async_get(hass)
+
+    state = hass.states.get("sensor.monitor_power_consumption")
+    entry = entity_registry.async_get("sensor.monitor_power_consumption")
+    assert entry
+    assert state
+    assert entry.unique_id == f"{entry_id}_smartmeter_power_consumption"
+    assert state.state == "877"
+    assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumption"
+    assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+    assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
+    assert ATTR_ICON not in state.attributes
+
+    state = hass.states.get("sensor.monitor_energy_consumption_high")
+    entry = entity_registry.async_get("sensor.monitor_energy_consumption_high")
+    assert entry
+    assert state
+    assert entry.unique_id == f"{entry_id}_smartmeter_energy_consumption_high"
+    assert state.state == "2770.133"
+    assert (
+        state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption - High Tariff"
+    )
+    assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING
+    assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY
+    assert ATTR_ICON not in state.attributes
+
+    state = hass.states.get("sensor.monitor_energy_tariff_period")
+    entry = entity_registry.async_get("sensor.monitor_energy_tariff_period")
+    assert entry
+    assert state
+    assert entry.unique_id == f"{entry_id}_smartmeter_energy_tariff_period"
+    assert state.state == "high"
+    assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Tariff Period"
+    assert state.attributes.get(ATTR_ICON) == "mdi:calendar-clock"
+    assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
+    assert ATTR_DEVICE_CLASS not in state.attributes
+
+    assert entry.device_id
+    device_entry = device_registry.async_get(entry.device_id)
+    assert device_entry
+    assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_smartmeter")}
+    assert device_entry.manufacturer == "P1 Monitor"
+    assert device_entry.name == "SmartMeter"
+    assert device_entry.entry_type == ENTRY_TYPE_SERVICE
+    assert not device_entry.model
+    assert not device_entry.sw_version
+
+
+async def test_phases(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+) -> None:
+    """Test the P1 Monitor - Phases sensors."""
+    entry_id = init_integration.entry_id
+    entity_registry = er.async_get(hass)
+    device_registry = dr.async_get(hass)
+
+    state = hass.states.get("sensor.monitor_voltage_phase_l1")
+    entry = entity_registry.async_get("sensor.monitor_voltage_phase_l1")
+    assert entry
+    assert state
+    assert entry.unique_id == f"{entry_id}_phases_voltage_phase_l1"
+    assert state.state == "233.6"
+    assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Voltage Phase L1"
+    assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+    assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_VOLTAGE
+    assert ATTR_ICON not in state.attributes
+
+    state = hass.states.get("sensor.monitor_current_phase_l1")
+    entry = entity_registry.async_get("sensor.monitor_current_phase_l1")
+    assert entry
+    assert state
+    assert entry.unique_id == f"{entry_id}_phases_current_phase_l1"
+    assert state.state == "1.6"
+    assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Current Phase L1"
+    assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+    assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT
+    assert ATTR_ICON not in state.attributes
+
+    state = hass.states.get("sensor.monitor_power_consumed_phase_l1")
+    entry = entity_registry.async_get("sensor.monitor_power_consumed_phase_l1")
+    assert entry
+    assert state
+    assert entry.unique_id == f"{entry_id}_phases_power_consumed_phase_l1"
+    assert state.state == "315"
+    assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumed Phase L1"
+    assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT
+    assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
+    assert ATTR_ICON not in state.attributes
+
+    assert entry.device_id
+    device_entry = device_registry.async_get(entry.device_id)
+    assert device_entry
+    assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_phases")}
+    assert device_entry.manufacturer == "P1 Monitor"
+    assert device_entry.name == "Phases"
+    assert device_entry.entry_type == ENTRY_TYPE_SERVICE
+    assert not device_entry.model
+    assert not device_entry.sw_version
+
+
+async def test_settings(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+) -> None:
+    """Test the P1 Monitor - Settings sensors."""
+    entry_id = init_integration.entry_id
+    entity_registry = er.async_get(hass)
+    device_registry = dr.async_get(hass)
+
+    state = hass.states.get("sensor.monitor_energy_consumption_low_tariff")
+    entry = entity_registry.async_get("sensor.monitor_energy_consumption_low_tariff")
+    assert entry
+    assert state
+    assert entry.unique_id == f"{entry_id}_settings_energy_consumption_low_tariff"
+    assert state.state == "0.20522"
+    assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption - Low Tariff"
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY
+    assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENCY_EURO
+
+    state = hass.states.get("sensor.monitor_energy_production_low_tariff")
+    entry = entity_registry.async_get("sensor.monitor_energy_production_low_tariff")
+    assert entry
+    assert state
+    assert entry.unique_id == f"{entry_id}_settings_energy_production_low_tariff"
+    assert state.state == "0.20522"
+    assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Production - Low Tariff"
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY
+    assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENCY_EURO
+
+    assert entry.device_id
+    device_entry = device_registry.async_get(entry.device_id)
+    assert device_entry
+    assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_settings")}
+    assert device_entry.manufacturer == "P1 Monitor"
+    assert device_entry.name == "Settings"
+    assert device_entry.entry_type == ENTRY_TYPE_SERVICE
+    assert not device_entry.model
+    assert not device_entry.sw_version
+
+
+@pytest.mark.parametrize(
+    "entity_id",
+    ("sensor.monitor_gas_consumption",),
+)
+async def test_smartmeter_disabled_by_default(
+    hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str
+) -> None:
+    """Test the P1 Monitor - SmartMeter sensors that are disabled by default."""
+    entity_registry = er.async_get(hass)
+
+    state = hass.states.get(entity_id)
+    assert state is None
+
+    entry = entity_registry.async_get(entity_id)
+    assert entry
+    assert entry.disabled
+    assert entry.disabled_by == er.DISABLED_INTEGRATION
diff --git a/tests/fixtures/p1_monitor/phases.json b/tests/fixtures/p1_monitor/phases.json
new file mode 100644
index 00000000000..b756f092c05
--- /dev/null
+++ b/tests/fixtures/p1_monitor/phases.json
@@ -0,0 +1,74 @@
+[
+    {
+        "LABEL": "Huidige KW verbruik L1 (21.7.0)",
+        "SECURITY": 0,
+        "STATUS": "0.315",
+        "STATUS_ID": 74
+    },
+    {
+        "LABEL": "Huidige KW verbruik L2 (41.7.0)",
+        "SECURITY": 0,
+        "STATUS": "0.0",
+        "STATUS_ID": 75
+    },
+    {
+        "LABEL": "Huidige KW verbruik L3 (61.7.0)",
+        "SECURITY": 0,
+        "STATUS": "0.624",
+        "STATUS_ID": 76
+    },
+    {
+        "LABEL": "Huidige KW levering L1 (22.7.0)",
+        "SECURITY": 0,
+        "STATUS": "0.0",
+        "STATUS_ID": 77
+    },
+    {
+        "LABEL": "Huidige KW levering L2 (42.7.0)",
+        "SECURITY": 0,
+        "STATUS": "0.0",
+        "STATUS_ID": 78
+    },
+    {
+        "LABEL": "Huidige KW levering L3 (62.7.0)",
+        "SECURITY": 0,
+        "STATUS": "0.0",
+        "STATUS_ID": 79
+    },
+    {
+        "LABEL": "Huidige Amperage L1 (31.7.0)",
+        "SECURITY": 0,
+        "STATUS": "1.6",
+        "STATUS_ID": 100
+    },
+    {
+        "LABEL": "Huidige Amperage L2 (51.7.0)",
+        "SECURITY": 0,
+        "STATUS": "4.44",
+        "STATUS_ID": 101
+    },
+    {
+        "LABEL": "Huidige Amperage L2 (71.7.0)",
+        "SECURITY": 0,
+        "STATUS": "3.51",
+        "STATUS_ID": 102
+    },
+    {
+        "LABEL": "Huidige Voltage L1 (32.7.0)",
+        "SECURITY": 0,
+        "STATUS": "233.6",
+        "STATUS_ID": 103
+    },
+    {
+        "LABEL": "Huidige Voltage L2 (52.7.0)",
+        "SECURITY": 0,
+        "STATUS": "0.0",
+        "STATUS_ID": 104
+    },
+    {
+        "LABEL": "Huidige Voltage L2 (72.7.0)",
+        "SECURITY": 0,
+        "STATUS": "233.0",
+        "STATUS_ID": 105
+    }
+]
\ No newline at end of file
diff --git a/tests/fixtures/p1_monitor/settings.json b/tests/fixtures/p1_monitor/settings.json
new file mode 100644
index 00000000000..eaa14765566
--- /dev/null
+++ b/tests/fixtures/p1_monitor/settings.json
@@ -0,0 +1,27 @@
+[
+    {
+        "CONFIGURATION_ID": 1,
+        "LABEL": "Verbruik tarief elektriciteit dal/nacht in euro.",
+        "PARAMETER": "0.20522"
+    },
+    {
+        "CONFIGURATION_ID": 2,
+        "LABEL": "Verbruik tarief elektriciteit piek/dag in euro.",
+        "PARAMETER": "0.20522"
+    },
+    {
+        "CONFIGURATION_ID": 3,
+        "LABEL": "Geleverd tarief elektriciteit dal/nacht in euro.",
+        "PARAMETER": "0.20522"
+    },
+    {
+        "CONFIGURATION_ID": 4,
+        "LABEL": "Geleverd tarief elektriciteit piek/dag in euro.",
+        "PARAMETER": "0.20522"
+    },
+    {
+        "CONFIGURATION_ID": 15,
+        "LABEL": "Verbruik tarief gas in euro.",
+        "PARAMETER": "0.64"
+    }
+]
\ No newline at end of file
diff --git a/tests/fixtures/p1_monitor/smartmeter.json b/tests/fixtures/p1_monitor/smartmeter.json
new file mode 100644
index 00000000000..d2ca0b38002
--- /dev/null
+++ b/tests/fixtures/p1_monitor/smartmeter.json
@@ -0,0 +1,15 @@
+[
+    {
+        "CONSUMPTION_GAS_M3": 2273.447,
+        "CONSUMPTION_KWH_HIGH": 2770.133,
+        "CONSUMPTION_KWH_LOW": 4988.071,
+        "CONSUMPTION_W": 877,
+        "PRODUCTION_KWH_HIGH": 3971.604,
+        "PRODUCTION_KWH_LOW": 1432.279,
+        "PRODUCTION_W": 0,
+        "RECORD_IS_PROCESSED": 0,
+        "TARIFCODE": "P",
+        "TIMESTAMP_UTC": 1629134632,
+        "TIMESTAMP_lOCAL": "2021-08-16 19:23:52"
+    }
+]
\ No newline at end of file
-- 
GitLab