Add Pure Energie integration (#66846)
This commit is contained in:
parent
5359050afc
commit
6c2d6fde66
21 changed files with 729 additions and 0 deletions
|
@ -145,6 +145,7 @@ homeassistant.components.persistent_notification.*
|
|||
homeassistant.components.pi_hole.*
|
||||
homeassistant.components.proximity.*
|
||||
homeassistant.components.pvoutput.*
|
||||
homeassistant.components.pure_energie.*
|
||||
homeassistant.components.rainmachine.*
|
||||
homeassistant.components.rdw.*
|
||||
homeassistant.components.recollect_waste.*
|
||||
|
|
|
@ -738,6 +738,8 @@ tests/components/prosegur/* @dgomes
|
|||
homeassistant/components/proxmoxve/* @jhollowe @Corbeno
|
||||
homeassistant/components/ps4/* @ktnrg45
|
||||
tests/components/ps4/* @ktnrg45
|
||||
homeassistant/components/pure_energie/* @klaasnicolaas
|
||||
tests/components/pure_energie/* @klaasnicolaas
|
||||
homeassistant/components/push/* @dgomes
|
||||
tests/components/push/* @dgomes
|
||||
homeassistant/components/pvoutput/* @fabaff @frenck
|
||||
|
|
76
homeassistant/components/pure_energie/__init__.py
Normal file
76
homeassistant/components/pure_energie/__init__.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
"""The Pure Energie integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
from gridnet import Device, GridNet, SmartBridge
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
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
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Pure Energie from a config entry."""
|
||||
|
||||
coordinator = PureEnergieDataUpdateCoordinator(hass)
|
||||
try:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
except ConfigEntryNotReady:
|
||||
await coordinator.gridnet.close()
|
||||
raise
|
||||
|
||||
hass.data.setdefault(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 Pure Energie config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
return unload_ok
|
||||
|
||||
|
||||
class PureEnergieData(NamedTuple):
|
||||
"""Class for defining data in dict."""
|
||||
|
||||
device: Device
|
||||
smartbridge: SmartBridge
|
||||
|
||||
|
||||
class PureEnergieDataUpdateCoordinator(DataUpdateCoordinator[PureEnergieData]):
|
||||
"""Class to manage fetching Pure Energie data from single eindpoint."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Initialize global Pure Energie data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
self.gridnet = GridNet(
|
||||
self.config_entry.data[CONF_HOST], session=async_get_clientsession(hass)
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> PureEnergieData:
|
||||
"""Fetch data from SmartBridge."""
|
||||
return PureEnergieData(
|
||||
device=await self.gridnet.device(),
|
||||
smartbridge=await self.gridnet.smartbridge(),
|
||||
)
|
107
homeassistant/components/pure_energie/config_flow.py
Normal file
107
homeassistant/components/pure_energie/config_flow.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
"""Config flow for Pure Energie integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from gridnet import Device, GridNet, GridNetConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
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 PureEnergieFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for Pure Energie integration."""
|
||||
|
||||
VERSION = 1
|
||||
discovered_host: str
|
||||
discovered_device: Device
|
||||
|
||||
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:
|
||||
try:
|
||||
device = await self._async_get_device(user_input[CONF_HOST])
|
||||
except GridNetConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(device.n2g_id)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: user_input[CONF_HOST]}
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title="Pure Energie Meter",
|
||||
data={
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
}
|
||||
),
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> FlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
self.discovered_host = discovery_info.host
|
||||
try:
|
||||
self.discovered_device = await self._async_get_device(discovery_info.host)
|
||||
except GridNetConnectionError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
await self.async_set_unique_id(self.discovered_device.n2g_id)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
|
||||
|
||||
self.context.update(
|
||||
{
|
||||
"title_placeholders": {
|
||||
CONF_NAME: "Pure Energie Meter",
|
||||
CONF_HOST: self.discovered_host,
|
||||
"model": self.discovered_device.model,
|
||||
},
|
||||
}
|
||||
)
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initiated by zeroconf."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title="Pure Energie Meter",
|
||||
data={
|
||||
CONF_HOST: self.discovered_host,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
description_placeholders={
|
||||
CONF_NAME: "Pure Energie Meter",
|
||||
"model": self.discovered_device.model,
|
||||
},
|
||||
)
|
||||
|
||||
async def _async_get_device(self, host: str) -> Device:
|
||||
"""Get device information from Pure Energie device."""
|
||||
session = async_get_clientsession(self.hass)
|
||||
gridnet = GridNet(host, session=session)
|
||||
return await gridnet.device()
|
10
homeassistant/components/pure_energie/const.py
Normal file
10
homeassistant/components/pure_energie/const.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
"""Constants for the Pure Energie integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "pure_energie"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
16
homeassistant/components/pure_energie/manifest.json
Normal file
16
homeassistant/components/pure_energie/manifest.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"domain": "pure_energie",
|
||||
"name": "Pure Energie",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/pure_energie",
|
||||
"requirements": ["gridnet==4.0.0"],
|
||||
"codeowners": ["@klaasnicolaas"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "local_polling",
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
"name": "smartbridge*"
|
||||
}
|
||||
]
|
||||
}
|
113
homeassistant/components/pure_energie/sensor.py
Normal file
113
homeassistant/components/pure_energie/sensor.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
"""Support for Pure Energie sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, ENERGY_KILO_WATT_HOUR, POWER_WATT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import PureEnergieData, PureEnergieDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@dataclass
|
||||
class PureEnergieSensorEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[PureEnergieData], int | float]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PureEnergieSensorEntityDescription(
|
||||
SensorEntityDescription, PureEnergieSensorEntityDescriptionMixin
|
||||
):
|
||||
"""Describes a Pure Energie sensor entity."""
|
||||
|
||||
|
||||
SENSORS: tuple[PureEnergieSensorEntityDescription, ...] = (
|
||||
PureEnergieSensorEntityDescription(
|
||||
key="power_flow",
|
||||
name="Power Flow",
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.smartbridge.power_flow,
|
||||
),
|
||||
PureEnergieSensorEntityDescription(
|
||||
key="energy_consumption_total",
|
||||
name="Energy Consumption",
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda data: data.smartbridge.energy_consumption_total,
|
||||
),
|
||||
PureEnergieSensorEntityDescription(
|
||||
key="energy_production_total",
|
||||
name="Energy Production",
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda data: data.smartbridge.energy_production_total,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Pure Energie Sensors based on a config entry."""
|
||||
async_add_entities(
|
||||
PureEnergieSensorEntity(
|
||||
coordinator=hass.data[DOMAIN][entry.entry_id],
|
||||
description=description,
|
||||
entry=entry,
|
||||
)
|
||||
for description in SENSORS
|
||||
)
|
||||
|
||||
|
||||
class PureEnergieSensorEntity(CoordinatorEntity[PureEnergieData], SensorEntity):
|
||||
"""Defines an Pure Energie sensor."""
|
||||
|
||||
coordinator: PureEnergieDataUpdateCoordinator
|
||||
entity_description: PureEnergieSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: PureEnergieDataUpdateCoordinator,
|
||||
description: PureEnergieSensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize Pure Energie sensor."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
self.entity_id = f"{SENSOR_DOMAIN}.pem_{description.key}"
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.data.device.n2g_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.data.device.n2g_id)},
|
||||
configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}",
|
||||
sw_version=coordinator.data.device.firmware,
|
||||
manufacturer=coordinator.data.device.manufacturer,
|
||||
model=coordinator.data.device.model,
|
||||
name=entry.title,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | float:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
23
homeassistant/components/pure_energie/strings.json
Normal file
23
homeassistant/components/pure_energie/strings.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "{model} ({host})",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add Pure Energie Meter (`{model}`) to Home Assistant?",
|
||||
"title": "Discovered Pure Energie Meter device"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
}
|
||||
}
|
23
homeassistant/components/pure_energie/translations/en.json
Normal file
23
homeassistant/components/pure_energie/translations/en.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"cannot_connect": "Failed to connect"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect"
|
||||
},
|
||||
"flow_title": "{name} ({host})",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add Pure Energie Meter (`{model}`) to Home Assistant?",
|
||||
"title": "Discovered Pure Energie Meter device"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -257,6 +257,7 @@ FLOWS = [
|
|||
"progettihwsw",
|
||||
"prosegur",
|
||||
"ps4",
|
||||
"pure_energie",
|
||||
"pvoutput",
|
||||
"pvpc_hourly_pricing",
|
||||
"rachio",
|
||||
|
|
|
@ -175,6 +175,10 @@ ZEROCONF = {
|
|||
"manufacturer": "nettigo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"domain": "pure_energie",
|
||||
"name": "smartbridge*"
|
||||
},
|
||||
{
|
||||
"domain": "rachio",
|
||||
"name": "rachio*"
|
||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -1404,6 +1404,17 @@ no_implicit_optional = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.pure_energie.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.rainmachine.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -781,6 +781,9 @@ greeneye_monitor==3.0.1
|
|||
# homeassistant.components.greenwave
|
||||
greenwavereality==0.5.1
|
||||
|
||||
# homeassistant.components.pure_energie
|
||||
gridnet==4.0.0
|
||||
|
||||
# homeassistant.components.growatt_server
|
||||
growattServer==1.1.0
|
||||
|
||||
|
|
|
@ -509,6 +509,9 @@ greeclimate==1.0.2
|
|||
# homeassistant.components.greeneye_monitor
|
||||
greeneye_monitor==3.0.1
|
||||
|
||||
# homeassistant.components.pure_energie
|
||||
gridnet==4.0.0
|
||||
|
||||
# homeassistant.components.growatt_server
|
||||
growattServer==1.1.0
|
||||
|
||||
|
|
1
tests/components/pure_energie/__init__.py
Normal file
1
tests/components/pure_energie/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the Pure Energie integration."""
|
83
tests/components/pure_energie/conftest.py
Normal file
83
tests/components/pure_energie/conftest.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
"""Fixtures for Pure Energie integration tests."""
|
||||
from collections.abc import Generator
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from gridnet import Device as GridNetDevice, SmartBridge
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.pure_energie.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="home",
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "192.168.1.123"},
|
||||
unique_id="unique_thingy",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[None, None, None]:
|
||||
"""Mock setting up a config entry."""
|
||||
with patch(
|
||||
"homeassistant.components.pure_energie.async_setup_entry", return_value=True
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pure_energie_config_flow(
|
||||
request: pytest.FixtureRequest,
|
||||
) -> Generator[None, MagicMock, None]:
|
||||
"""Return a mocked Pure Energie client."""
|
||||
with patch(
|
||||
"homeassistant.components.pure_energie.config_flow.GridNet", autospec=True
|
||||
) as pure_energie_mock:
|
||||
pure_energie = pure_energie_mock.return_value
|
||||
pure_energie.device.return_value = GridNetDevice.from_dict(
|
||||
json.loads(load_fixture("device.json", DOMAIN))
|
||||
)
|
||||
yield pure_energie
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pure_energie():
|
||||
"""Return a mocked Pure Energie client."""
|
||||
with patch(
|
||||
"homeassistant.components.pure_energie.GridNet", autospec=True
|
||||
) as pure_energie_mock:
|
||||
pure_energie = pure_energie_mock.return_value
|
||||
pure_energie.smartbridge = AsyncMock(
|
||||
return_value=SmartBridge.from_dict(
|
||||
json.loads(load_fixture("pure_energie/smartbridge.json"))
|
||||
)
|
||||
)
|
||||
pure_energie.device = AsyncMock(
|
||||
return_value=GridNetDevice.from_dict(
|
||||
json.loads(load_fixture("pure_energie/device.json"))
|
||||
)
|
||||
)
|
||||
yield pure_energie_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_pure_energie: MagicMock,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Pure Energie 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
|
1
tests/components/pure_energie/fixtures/device.json
Normal file
1
tests/components/pure_energie/fixtures/device.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"id":"aabbccddeeff","mf":"NET2GRID","model":"SBWF3102","fw":"1.6.16","hw":1,"batch":"SBP-HMX-210318"}
|
1
tests/components/pure_energie/fixtures/smartbridge.json
Normal file
1
tests/components/pure_energie/fixtures/smartbridge.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"status":"ok","elec":{"power":{"now":{"value":338,"unit":"W","time":1634749148},"min":{"value":-7345,"unit":"W","time":1631360893},"max":{"value":13725,"unit":"W","time":1633749513}},"import":{"now":{"value":17762055,"unit":"Wh","time":1634749148}},"export":{"now":{"value":21214589,"unit":"Wh","time":1634749148}}},"gas":{}}
|
123
tests/components/pure_energie/test_config_flow.py
Normal file
123
tests/components/pure_energie/test_config_flow.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
"""Test the Pure Energie config flow."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from gridnet import GridNetConnectionError
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.pure_energie.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import (
|
||||
RESULT_TYPE_ABORT,
|
||||
RESULT_TYPE_CREATE_ENTRY,
|
||||
RESULT_TYPE_FORM,
|
||||
)
|
||||
|
||||
|
||||
async def test_full_user_flow_implementation(
|
||||
hass: HomeAssistant,
|
||||
mock_pure_energie_config_flow: MagicMock,
|
||||
mock_setup_entry: None,
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result.get("step_id") == SOURCE_USER
|
||||
assert result.get("type") == RESULT_TYPE_FORM
|
||||
assert "flow_id" in result
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "192.168.1.123"}
|
||||
)
|
||||
|
||||
assert result.get("title") == "Pure Energie Meter"
|
||||
assert result.get("type") == RESULT_TYPE_CREATE_ENTRY
|
||||
assert "data" in result
|
||||
assert result["data"][CONF_HOST] == "192.168.1.123"
|
||||
assert "result" in result
|
||||
assert result["result"].unique_id == "aabbccddeeff"
|
||||
|
||||
|
||||
async def test_full_zeroconf_flow_implementationn(
|
||||
hass: HomeAssistant,
|
||||
mock_pure_energie_config_flow: MagicMock,
|
||||
mock_setup_entry: None,
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
host="192.168.1.123",
|
||||
addresses=["192.168.1.123"],
|
||||
hostname="example.local.",
|
||||
name="mock_name",
|
||||
port=None,
|
||||
properties={CONF_MAC: "aabbccddeeff"},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
|
||||
assert result.get("description_placeholders") == {
|
||||
"model": "SBWF3102",
|
||||
CONF_NAME: "Pure Energie Meter",
|
||||
}
|
||||
assert result.get("step_id") == "zeroconf_confirm"
|
||||
assert result.get("type") == RESULT_TYPE_FORM
|
||||
assert "flow_id" in result
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
assert result2.get("title") == "Pure Energie Meter"
|
||||
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
assert "data" in result2
|
||||
assert result2["data"][CONF_HOST] == "192.168.1.123"
|
||||
assert "result" in result2
|
||||
assert result2["result"].unique_id == "aabbccddeeff"
|
||||
|
||||
|
||||
async def test_connection_error(
|
||||
hass: HomeAssistant, mock_pure_energie_config_flow: MagicMock
|
||||
) -> None:
|
||||
"""Test we show user form on Pure Energie connection error."""
|
||||
mock_pure_energie_config_flow.device.side_effect = GridNetConnectionError
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: "example.com"},
|
||||
)
|
||||
|
||||
assert result.get("type") == RESULT_TYPE_FORM
|
||||
assert result.get("step_id") == "user"
|
||||
assert result.get("errors") == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_zeroconf_connection_error(
|
||||
hass: HomeAssistant, mock_pure_energie_config_flow: MagicMock
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on Pure Energie connection error."""
|
||||
mock_pure_energie_config_flow.device.side_effect = GridNetConnectionError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
host="192.168.1.123",
|
||||
addresses=["192.168.1.123"],
|
||||
hostname="example.local.",
|
||||
name="mock_name",
|
||||
port=None,
|
||||
properties={CONF_MAC: "aabbccddeeff"},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
|
||||
assert result.get("type") == RESULT_TYPE_ABORT
|
||||
assert result.get("reason") == "cannot_connect"
|
52
tests/components/pure_energie/test_init.py
Normal file
52
tests/components/pure_energie/test_init.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
"""Tests for the Pure Energie integration."""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from gridnet import GridNetConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.pure_energie.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_pure_energie", ["pure_energie/device.json"], indirect=True
|
||||
)
|
||||
async def test_load_unload_config_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_pure_energie: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the Pure Energie 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
|
||||
assert mock_config_entry.unique_id == "unique_thingy"
|
||||
assert len(mock_pure_energie.mock_calls) == 3
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.data.get(DOMAIN)
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.components.pure_energie.GridNet.request",
|
||||
side_effect=GridNetConnectionError,
|
||||
)
|
||||
async def test_config_entry_not_ready(
|
||||
mock_request: MagicMock,
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Pure Energie 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_config_entry.state is ConfigEntryState.SETUP_RETRY
|
75
tests/components/pure_energie/test_sensor.py
Normal file
75
tests/components/pure_energie/test_sensor.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
"""Tests for the sensors provided by the Pure Energie integration."""
|
||||
|
||||
from homeassistant.components.pure_energie.const import DOMAIN
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_STATE_CLASS,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_ICON,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
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_sensors(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Pure Energie - SmartBridge sensors."""
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
state = hass.states.get("sensor.pem_energy_consumption_total")
|
||||
entry = entity_registry.async_get("sensor.pem_energy_consumption_total")
|
||||
assert entry
|
||||
assert state
|
||||
assert entry.unique_id == "aabbccddeeff_energy_consumption_total"
|
||||
assert state.state == "17762.1"
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption"
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY
|
||||
assert ATTR_ICON not in state.attributes
|
||||
|
||||
state = hass.states.get("sensor.pem_energy_production_total")
|
||||
entry = entity_registry.async_get("sensor.pem_energy_production_total")
|
||||
assert entry
|
||||
assert state
|
||||
assert entry.unique_id == "aabbccddeeff_energy_production_total"
|
||||
assert state.state == "21214.6"
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Production"
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY
|
||||
assert ATTR_ICON not in state.attributes
|
||||
|
||||
state = hass.states.get("sensor.pem_power_flow")
|
||||
entry = entity_registry.async_get("sensor.pem_power_flow")
|
||||
assert entry
|
||||
assert state
|
||||
assert entry.unique_id == "aabbccddeeff_power_flow"
|
||||
assert state.state == "338"
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Flow"
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.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, "aabbccddeeff")}
|
||||
assert device_entry.name == "home"
|
||||
assert device_entry.manufacturer == "NET2GRID"
|
||||
assert device_entry.entry_type is dr.DeviceEntryType.SERVICE
|
||||
assert device_entry.model == "SBWF3102"
|
||||
assert device_entry.sw_version == "1.6.16"
|
Loading…
Add table
Add a link
Reference in a new issue