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>pull/54911/head
parent
32e297f4a0
commit
68fbc0792a
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
)
|
|
@ -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",
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -198,6 +198,7 @@ FLOWS = [
|
|||
"ovo_energy",
|
||||
"owntracks",
|
||||
"ozw",
|
||||
"p1_monitor",
|
||||
"panasonic_viera",
|
||||
"philips_js",
|
||||
"pi_hole",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the P1 Monitor integration."""
|
|
@ -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
|
|
@ -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"}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue