Add Pure Energie integration (#66846)

This commit is contained in:
Klaas Schoute 2022-02-19 17:53:25 +01:00 committed by GitHub
parent 5359050afc
commit 6c2d6fde66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 729 additions and 0 deletions

View file

@ -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.*

View file

@ -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

View 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(),
)

View 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()

View 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)

View 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*"
}
]
}

View 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)

View 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%]"
}
}
}

View 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"
}
}
}
}

View file

@ -257,6 +257,7 @@ FLOWS = [
"progettihwsw",
"prosegur",
"ps4",
"pure_energie",
"pvoutput",
"pvpc_hourly_pricing",
"rachio",

View file

@ -175,6 +175,10 @@ ZEROCONF = {
"manufacturer": "nettigo"
}
},
{
"domain": "pure_energie",
"name": "smartbridge*"
},
{
"domain": "rachio",
"name": "rachio*"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
"""Tests for the Pure Energie integration."""

View 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

View file

@ -0,0 +1 @@
{"id":"aabbccddeeff","mf":"NET2GRID","model":"SBWF3102","fw":"1.6.16","hw":1,"batch":"SBP-HMX-210318"}

View 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":{}}

View 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"

View 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

View 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"