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.pi_hole.*
|
||||||
homeassistant.components.proximity.*
|
homeassistant.components.proximity.*
|
||||||
homeassistant.components.pvoutput.*
|
homeassistant.components.pvoutput.*
|
||||||
|
homeassistant.components.pure_energie.*
|
||||||
homeassistant.components.rainmachine.*
|
homeassistant.components.rainmachine.*
|
||||||
homeassistant.components.rdw.*
|
homeassistant.components.rdw.*
|
||||||
homeassistant.components.recollect_waste.*
|
homeassistant.components.recollect_waste.*
|
||||||
|
|
|
@ -738,6 +738,8 @@ tests/components/prosegur/* @dgomes
|
||||||
homeassistant/components/proxmoxve/* @jhollowe @Corbeno
|
homeassistant/components/proxmoxve/* @jhollowe @Corbeno
|
||||||
homeassistant/components/ps4/* @ktnrg45
|
homeassistant/components/ps4/* @ktnrg45
|
||||||
tests/components/ps4/* @ktnrg45
|
tests/components/ps4/* @ktnrg45
|
||||||
|
homeassistant/components/pure_energie/* @klaasnicolaas
|
||||||
|
tests/components/pure_energie/* @klaasnicolaas
|
||||||
homeassistant/components/push/* @dgomes
|
homeassistant/components/push/* @dgomes
|
||||||
tests/components/push/* @dgomes
|
tests/components/push/* @dgomes
|
||||||
homeassistant/components/pvoutput/* @fabaff @frenck
|
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",
|
"progettihwsw",
|
||||||
"prosegur",
|
"prosegur",
|
||||||
"ps4",
|
"ps4",
|
||||||
|
"pure_energie",
|
||||||
"pvoutput",
|
"pvoutput",
|
||||||
"pvpc_hourly_pricing",
|
"pvpc_hourly_pricing",
|
||||||
"rachio",
|
"rachio",
|
||||||
|
|
|
@ -175,6 +175,10 @@ ZEROCONF = {
|
||||||
"manufacturer": "nettigo"
|
"manufacturer": "nettigo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"domain": "pure_energie",
|
||||||
|
"name": "smartbridge*"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"domain": "rachio",
|
"domain": "rachio",
|
||||||
"name": "rachio*"
|
"name": "rachio*"
|
||||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -1404,6 +1404,17 @@ no_implicit_optional = true
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = 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.*]
|
[mypy-homeassistant.components.rainmachine.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
|
@ -781,6 +781,9 @@ greeneye_monitor==3.0.1
|
||||||
# homeassistant.components.greenwave
|
# homeassistant.components.greenwave
|
||||||
greenwavereality==0.5.1
|
greenwavereality==0.5.1
|
||||||
|
|
||||||
|
# homeassistant.components.pure_energie
|
||||||
|
gridnet==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.growatt_server
|
# homeassistant.components.growatt_server
|
||||||
growattServer==1.1.0
|
growattServer==1.1.0
|
||||||
|
|
||||||
|
|
|
@ -509,6 +509,9 @@ greeclimate==1.0.2
|
||||||
# homeassistant.components.greeneye_monitor
|
# homeassistant.components.greeneye_monitor
|
||||||
greeneye_monitor==3.0.1
|
greeneye_monitor==3.0.1
|
||||||
|
|
||||||
|
# homeassistant.components.pure_energie
|
||||||
|
gridnet==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.growatt_server
|
# homeassistant.components.growatt_server
|
||||||
growattServer==1.1.0
|
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