Add HomeWizard Energy integration (#55812)

Co-authored-by: Matthias Alphart <farmio@alphart.net>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
pull/63870/head
Duco Sebel 2022-01-11 01:23:31 +01:00 committed by GitHub
parent d3cd813c5e
commit 8f6e24aa1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1974 additions and 0 deletions

View File

@ -65,6 +65,7 @@ homeassistant.components.group.*
homeassistant.components.guardian.*
homeassistant.components.history.*
homeassistant.components.homeassistant.triggers.event
homeassistant.components.homewizard.*
homeassistant.components.http.*
homeassistant.components.huawei_lte.*
homeassistant.components.hyperion.*

View File

@ -392,6 +392,8 @@ homeassistant/components/homekit_controller/* @Jc2k @bdraco
tests/components/homekit_controller/* @Jc2k @bdraco
homeassistant/components/homematic/* @pvizeli @danielperna84
tests/components/homematic/* @pvizeli @danielperna84
homeassistant/components/homewizard/* @DCSBL
tests/components/homewizard/* @DCSBL
homeassistant/components/honeywell/* @rdfurman
tests/components/honeywell/* @rdfurman
homeassistant/components/http/* @home-assistant/core

View File

@ -0,0 +1,69 @@
"""The Homewizard integration."""
import asyncio
import logging
from aiohwenergy import DisabledError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import UpdateFailed
from .const import COORDINATOR, DOMAIN, PLATFORMS
from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Homewizard from a config entry."""
_LOGGER.debug("__init__ async_setup_entry")
# Create coordinator
coordinator = Coordinator(hass, entry.data[CONF_IP_ADDRESS])
try:
await coordinator.initialize_api()
except DisabledError:
_LOGGER.error("API is disabled, enable API in HomeWizard Energy app")
return False
except UpdateFailed as ex:
raise ConfigEntryNotReady from ex
await coordinator.async_config_entry_first_refresh()
# Finalize
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR: coordinator,
}
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
_LOGGER.debug("__init__ async_unload_entry")
unload_ok = all(
await asyncio.gather(
*(
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
)
)
)
if unload_ok:
config_data = hass.data[DOMAIN].pop(entry.entry_id)
await config_data[COORDINATOR].api.close()
return unload_ok

View File

@ -0,0 +1,198 @@
"""Config flow for Homewizard."""
from __future__ import annotations
import logging
from typing import Any
import aiohwenergy
from aiohwenergy.hwenergy import SUPPORTED_DEVICES
import async_timeout
from voluptuous import Required, Schema
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from .const import CONF_PRODUCT_NAME, CONF_PRODUCT_TYPE, CONF_SERIAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for P1 meter."""
VERSION = 1
config: dict[str, str | int] = {}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initiated by the user."""
_LOGGER.debug("config_flow async_step_user")
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=Schema(
{
Required(CONF_IP_ADDRESS): str,
}
),
errors=None,
)
device_info = await self._async_try_connect_and_fetch(
user_input[CONF_IP_ADDRESS]
)
# Sets unique ID and aborts if it is already exists
await self._async_set_and_check_unique_id(
{
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
CONF_PRODUCT_TYPE: device_info[CONF_PRODUCT_TYPE],
CONF_SERIAL: device_info[CONF_SERIAL],
}
)
# Add entry
return self.async_create_entry(
title=f"{device_info[CONF_PRODUCT_NAME]} ({device_info[CONF_SERIAL]})",
data={
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
},
)
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Handle zeroconf discovery."""
_LOGGER.debug("config_flow async_step_zeroconf")
# Validate doscovery entry
if (
"api_enabled" not in discovery_info.properties
or "path" not in discovery_info.properties
or "product_name" not in discovery_info.properties
or "product_type" not in discovery_info.properties
or "serial" not in discovery_info.properties
):
return self.async_abort(reason="invalid_discovery_parameters")
if (discovery_info.properties["path"]) != "/api/v1":
return self.async_abort(reason="unsupported_api_version")
if (discovery_info.properties["api_enabled"]) != "1":
return self.async_abort(reason="api_not_enabled")
# Sets unique ID and aborts if it is already exists
await self._async_set_and_check_unique_id(
{
CONF_IP_ADDRESS: discovery_info.host,
CONF_PRODUCT_TYPE: discovery_info.properties["product_type"],
CONF_SERIAL: discovery_info.properties["serial"],
}
)
# Check connection and fetch
device_info: dict[str, Any] = await self._async_try_connect_and_fetch(
discovery_info.host
)
# Pass parameters
self.config = {
CONF_IP_ADDRESS: discovery_info.host,
CONF_PRODUCT_TYPE: device_info[CONF_PRODUCT_TYPE],
CONF_PRODUCT_NAME: device_info[CONF_PRODUCT_NAME],
CONF_SERIAL: device_info[CONF_SERIAL],
}
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm discovery."""
if user_input is not None:
return self.async_create_entry(
title=f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})",
data={
CONF_IP_ADDRESS: self.config[CONF_IP_ADDRESS],
},
)
self._set_confirm_only()
return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={
CONF_PRODUCT_TYPE: self.config[CONF_PRODUCT_TYPE],
CONF_SERIAL: self.config[CONF_SERIAL],
CONF_IP_ADDRESS: self.config[CONF_IP_ADDRESS],
},
)
@staticmethod
async def _async_try_connect_and_fetch(ip_address: str) -> dict[str, Any]:
"""Try to connect."""
_LOGGER.debug("config_flow _async_try_connect_and_fetch")
# Make connection with device
# This is to test the connection and to get info for unique_id
energy_api = aiohwenergy.HomeWizardEnergy(ip_address)
initialized = False
try:
with async_timeout.timeout(10):
await energy_api.initialize()
if energy_api.device is not None:
initialized = True
except aiohwenergy.DisabledError as ex:
_LOGGER.error("API disabled, API must be enabled in the app")
raise AbortFlow("api_not_enabled") from ex
except Exception as ex: # pylint: disable=broad-except
_LOGGER.error(
"Error connecting with Energy Device at %s",
ip_address,
)
raise AbortFlow("unknown_error") from ex
finally:
await energy_api.close()
if not initialized:
_LOGGER.error("Initialization failed")
raise AbortFlow("unknown_error")
# Validate metadata
if energy_api.device.api_version != "v1":
raise AbortFlow("unsupported_api_version")
if energy_api.device.product_type not in SUPPORTED_DEVICES:
_LOGGER.error(
"Device (%s) not supported by integration",
energy_api.device.product_type,
)
raise AbortFlow("device_not_supported")
return {
CONF_PRODUCT_NAME: energy_api.device.product_name,
CONF_PRODUCT_TYPE: energy_api.device.product_type,
CONF_SERIAL: energy_api.device.serial,
}
async def _async_set_and_check_unique_id(self, entry_info: dict[str, Any]) -> None:
"""Validate if entry exists."""
_LOGGER.debug("config_flow _async_set_and_check_unique_id")
await self.async_set_unique_id(
f"{entry_info[CONF_PRODUCT_TYPE]}_{entry_info[CONF_SERIAL]}"
)
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: entry_info[CONF_IP_ADDRESS]}
)

View File

@ -0,0 +1,30 @@
"""Constants for the Homewizard integration."""
from __future__ import annotations
from datetime import timedelta
from typing import TypedDict
# Set up.
from aiohwenergy.device import Device
from homeassistant.helpers.typing import StateType
DOMAIN = "homewizard"
COORDINATOR = "coordinator"
PLATFORMS = ["sensor"]
# Platform config.
CONF_SERIAL = "serial"
CONF_PRODUCT_NAME = "product_name"
CONF_PRODUCT_TYPE = "product_type"
CONF_DEVICE = "device"
CONF_DATA = "data"
UPDATE_INTERVAL = timedelta(seconds=5)
class DeviceResponseEntry(TypedDict):
"""Dict describing a single response entry."""
device: Device
data: dict[str, StateType]

View File

@ -0,0 +1,88 @@
"""Update coordinator for HomeWizard."""
from __future__ import annotations
import asyncio
import logging
import aiohwenergy
import async_timeout
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_INTERVAL, DeviceResponseEntry
_LOGGER = logging.getLogger(__name__)
class HWEnergyDeviceUpdateCoordinator(
DataUpdateCoordinator[aiohwenergy.HomeWizardEnergy]
):
"""Gather data for the energy device."""
api: aiohwenergy
def __init__(
self,
hass: HomeAssistant,
host: str,
) -> None:
"""Initialize Update Coordinator."""
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL)
self.api = aiohwenergy.HomeWizardEnergy(host)
async def _async_update_data(self) -> DeviceResponseEntry:
"""Fetch all device and sensor data from api."""
async with async_timeout.timeout(10):
if self.api.device is None:
await self.initialize_api()
# Update all properties
try:
if not await self.api.update():
raise UpdateFailed("Failed to communicate with device")
except aiohwenergy.DisabledError as ex:
raise UpdateFailed(
"API disabled, API must be enabled in the app"
) from ex
except Exception as ex: # pylint: disable=broad-except
raise UpdateFailed(
f"Error connecting with Energy Device at {self.api.host}"
) from ex
data: DeviceResponseEntry = {
"device": self.api.device,
"data": {},
}
for datapoint in self.api.data.available_datapoints:
data["data"][datapoint] = getattr(self.api.data, datapoint)
return data
async def initialize_api(self) -> aiohwenergy:
"""Initialize API and validate connection."""
try:
await self.api.initialize()
except (asyncio.TimeoutError, aiohwenergy.RequestError) as ex:
raise UpdateFailed(
f"Error connecting to the Energy device at {self.api.host}"
) from ex
except aiohwenergy.DisabledError as ex:
raise ex
except aiohwenergy.AiohwenergyException as ex:
raise UpdateFailed("Unknown Energy API error occurred") from ex
except Exception as ex: # pylint: disable=broad-except
raise UpdateFailed(
f"Unknown error connecting with Energy Device at {self.api.host}"
) from ex

View File

@ -0,0 +1,13 @@
{
"domain": "homewizard",
"name": "HomeWizard Energy",
"documentation": "https://www.home-assistant.io/integrations/homewizard",
"codeowners": ["@DCSBL"],
"dependencies": [],
"requirements": [
"aiohwenergy==0.6.0"
],
"zeroconf": ["_hwenergy._tcp.local."],
"config_flow": true,
"iot_class": "local_polling"
}

View File

@ -0,0 +1,201 @@
"""Creates Homewizard sensor entities."""
from __future__ import annotations
import logging
from typing import Final
from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
ENTITY_CATEGORY_DIAGNOSTIC,
PERCENTAGE,
POWER_WATT,
VOLUME_CUBIC_METERS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import COORDINATOR, DOMAIN, DeviceResponseEntry
from .coordinator import HWEnergyDeviceUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
SENSORS: Final[tuple[SensorEntityDescription, ...]] = (
SensorEntityDescription(
key="smr_version",
name="DSMR Version",
icon="mdi:counter",
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
SensorEntityDescription(
key="meter_model",
name="Smart Meter Model",
icon="mdi:gauge",
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
SensorEntityDescription(
key="wifi_ssid",
name="Wifi SSID",
icon="mdi:wifi",
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
SensorEntityDescription(
key="wifi_strength",
name="Wifi Strength",
icon="mdi:wifi",
native_unit_of_measurement=PERCENTAGE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="total_power_import_t1_kwh",
name="Total Power Import T1",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
SensorEntityDescription(
key="total_power_import_t2_kwh",
name="Total Power Import T2",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
SensorEntityDescription(
key="total_power_export_t1_kwh",
name="Total Power Export T1",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
SensorEntityDescription(
key="total_power_export_t2_kwh",
name="Total Power Export T2",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
SensorEntityDescription(
key="active_power_w",
name="Active Power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
SensorEntityDescription(
key="active_power_l1_w",
name="Active Power L1",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
SensorEntityDescription(
key="active_power_l2_w",
name="Active Power L2",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
SensorEntityDescription(
key="active_power_l3_w",
name="Active Power L3",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
SensorEntityDescription(
key="total_gas_m3",
name="Total Gas",
native_unit_of_measurement=VOLUME_CUBIC_METERS,
device_class=DEVICE_CLASS_GAS,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Initialize sensors."""
coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
COORDINATOR
]
entities = []
if coordinator.api.data is not None:
for description in SENSORS:
if (
description.key in coordinator.api.data.available_datapoints
and getattr(coordinator.api.data, description.key) is not None
):
entities.append(HWEnergySensor(coordinator, entry, description))
async_add_entities(entities)
class HWEnergySensor(CoordinatorEntity[DeviceResponseEntry], SensorEntity):
"""Representation of a HomeWizard Sensor."""
def __init__(
self,
coordinator: HWEnergyDeviceUpdateCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
"""Initialize Sensor Domain."""
super().__init__(coordinator)
self.entity_description = description
self.entry = entry
# Config attributes.
self._attr_name = f"{entry.title} {description.name}"
self.data_type = description.key
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
# Special case for export, not everyone has solarpanels
# The change that 'export' is non-zero when you have solar panels is nil
if self.data_type in [
"total_power_export_t1_kwh",
"total_power_export_t2_kwh",
]:
if self.data["data"][self.data_type] == 0:
self._attr_entity_registry_enabled_default = False
@property
def device_info(self) -> DeviceInfo:
"""Return device information."""
return {
"name": self.entry.title,
"manufacturer": "HomeWizard",
"sw_version": self.data["device"].firmware_version,
"model": self.data["device"].product_type,
"identifiers": {(DOMAIN, self.data["device"].serial)},
}
@property
def data(self) -> DeviceResponseEntry:
"""Return data object from DataUpdateCoordinator."""
return self.coordinator.data
@property
def native_value(self) -> StateType:
"""Return state of meter."""
return self.data["data"][self.data_type]
@property
def available(self) -> bool:
"""Return availability of meter."""
return self.data_type in self.data["data"]

View File

@ -0,0 +1,24 @@
{
"config": {
"step": {
"user": {
"title": "Configure device",
"description": "Enter the IP address of your HomeWizard Energy device to integrate with Home Assistant.",
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]"
}
},
"discovery_confirm": {
"title": "Confirm",
"description": "Do you want to setup {product_type} ({serial}) at {ip_address}?"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_discovery_parameters": "unsupported_api_version",
"api_not_enabled": "The API is not enabled. Enable API in the HomeWizard Energy App under settings",
"device_not_supported": "This device is not supported",
"unknown_error": "[%key:common::config_flow::error::unknown%]"
}
}
}

View File

@ -136,6 +136,7 @@ FLOWS = [
"homekit",
"homekit_controller",
"homematicip_cloud",
"homewizard",
"honeywell",
"huawei_lte",
"hue",

View File

@ -179,6 +179,11 @@ ZEROCONF = {
"domain": "hue"
}
],
"_hwenergy._tcp.local.": [
{
"domain": "homewizard"
}
],
"_ipp._tcp.local.": [
{
"domain": "ipp"

View File

@ -718,6 +718,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.homewizard.*]
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.http.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -193,6 +193,9 @@ aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.11
# homeassistant.components.homewizard
aiohwenergy==0.6.0
# homeassistant.components.imap
aioimaplib==0.9.0

View File

@ -140,6 +140,9 @@ aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.11
# homeassistant.components.homewizard
aiohwenergy==0.6.0
# homeassistant.components.apache_kafka
aiokafka==0.6.0

View File

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

View File

@ -0,0 +1,30 @@
"""Fixtures for HomeWizard integration tests."""
import pytest
from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.const import CONF_IP_ADDRESS
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry_data():
"""Return the default mocked config entry data."""
return {
"product_name": "Product Name",
"product_type": "product_type",
"serial": "aabbccddeeff",
"name": "Product Name",
CONF_IP_ADDRESS: "1.2.3.4",
}
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="Product Name (aabbccddeeff)",
domain=DOMAIN,
data={},
unique_id="aabbccddeeff",
)

View File

@ -0,0 +1,27 @@
"""Helper files for unit tests."""
from unittest.mock import AsyncMock
def get_mock_device(
serial="aabbccddeeff",
host="1.2.3.4",
product_name="P1 meter",
product_type="HWE-P1",
):
"""Return a mock bridge."""
mock_device = AsyncMock()
mock_device.host = host
mock_device.device.product_name = product_name
mock_device.device.product_type = product_type
mock_device.device.serial = serial
mock_device.device.api_version = "v1"
mock_device.device.firmware_version = "1.00"
mock_device.state = None
mock_device.initialize = AsyncMock()
mock_device.close = AsyncMock()
return mock_device

View File

@ -0,0 +1,324 @@
"""Test the homewizard config flow."""
import logging
from unittest.mock import patch
from aiohwenergy import DisabledError
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY
from .generator import get_mock_device
_LOGGER = logging.getLogger(__name__)
async def test_manual_flow_works(hass, aioclient_mock):
"""Test config flow accepts user configuration."""
device = get_mock_device()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"}
)
assert result["type"] == "create_entry"
assert result["title"] == f"{device.device.product_name} (aabbccddeeff)"
assert result["data"][CONF_IP_ADDRESS] == "2.2.2.2"
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
entry = entries[0]
assert entry.unique_id == f"{device.device.product_type}_{device.device.serial}"
assert len(device.initialize.mock_calls) == 2
assert len(device.close.mock_calls) == 1
async def test_discovery_flow_works(hass, aioclient_mock):
"""Test discovery setup flow works."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
port=80,
hostname="p1meter-ddeeff.local.",
type="",
name="",
properties={
"api_enabled": "1",
"path": "/api/v1",
"product_name": "P1 meter",
"product_type": "HWE-P1",
"serial": "aabbccddeeff",
},
)
with patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()), patch(
"homeassistant.components.homewizard.async_setup_entry",
return_value=True,
):
flow = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=service_info,
)
with patch(
"homeassistant.components.homewizard.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
flow["flow_id"], user_input={}
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "P1 meter (aabbccddeeff)"
assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183"
assert result["result"]
assert result["result"].unique_id == "HWE-P1_aabbccddeeff"
async def test_discovery_disabled_api(hass, aioclient_mock):
"""Test discovery detecting disabled api."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
port=80,
hostname="p1meter-ddeeff.local.",
type="",
name="",
properties={
"api_enabled": "0",
"path": "/api/v1",
"product_name": "P1 meter",
"product_type": "HWE-P1",
"serial": "aabbccddeeff",
},
)
with patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()), patch(
"homeassistant.components.homewizard.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=service_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "api_not_enabled"
async def test_discovery_missing_data_in_service_info(hass, aioclient_mock):
"""Test discovery detecting missing discovery info."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
port=80,
hostname="p1meter-ddeeff.local.",
type="",
name="",
properties={
# "api_enabled": "1", --> removed
"path": "/api/v1",
"product_name": "P1 meter",
"product_type": "HWE-P1",
"serial": "aabbccddeeff",
},
)
with patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()), patch(
"homeassistant.components.homewizard.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=service_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "invalid_discovery_parameters"
async def test_discovery_invalid_api(hass, aioclient_mock):
"""Test discovery detecting invalid_api."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
port=80,
hostname="p1meter-ddeeff.local.",
type="",
name="",
properties={
"api_enabled": "1",
"path": "/api/not_v1",
"product_name": "P1 meter",
"product_type": "HWE-P1",
"serial": "aabbccddeeff",
},
)
with patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()), patch(
"homeassistant.components.homewizard.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=service_info,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unsupported_api_version"
async def test_check_disabled_api(hass, aioclient_mock):
"""Test check detecting disabled api."""
def MockInitialize():
raise DisabledError
device = get_mock_device()
device.initialize.side_effect = MockInitialize
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "api_not_enabled"
async def test_check_error_handling_api(hass, aioclient_mock):
"""Test check detecting error with api."""
def MockInitialize():
raise Exception()
device = get_mock_device()
device.initialize.side_effect = MockInitialize
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown_error"
async def test_check_detects_unexpected_api_response(hass, aioclient_mock):
"""Test check detecting device endpoint failed fetching data."""
device = get_mock_device()
device.device = None
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown_error"
async def test_check_detects_invalid_api(hass, aioclient_mock):
"""Test check detecting device endpoint failed fetching data."""
device = get_mock_device()
device.device.api_version = "not_v1"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unsupported_api_version"
async def test_check_detects_unsuported_device(hass, aioclient_mock):
"""Test check detecting device endpoint failed fetching data."""
device = get_mock_device(product_type="not_an_energy_device")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "device_not_supported"

View File

@ -0,0 +1,131 @@
"""Test the update coordinator for HomeWizard."""
from datetime import timedelta
import json
from unittest.mock import AsyncMock, patch
from aiohwenergy import errors
from pytest import raises
from homeassistant.components.homewizard.const import CONF_DATA, CONF_DEVICE
from homeassistant.components.homewizard.coordinator import (
HWEnergyDeviceUpdateCoordinator as Coordinator,
)
from homeassistant.helpers.update_coordinator import UpdateFailed
from .generator import get_mock_device
async def test_coordinator_sets_update_interval(aioclient_mock, hass):
"""Test coordinator calculates correct update interval."""
# P1 meter
meter = get_mock_device(product_type="p1_meter")
coordinator = Coordinator(hass, meter)
assert coordinator.update_interval == timedelta(seconds=5)
def mock_request_response(
status: int, data: str, content_type: str = "application/json"
):
"""Return the default mocked config entry data."""
mock_response = AsyncMock()
mock_response.status = status
mock_response.content_type = content_type
async def return_json():
return json.loads(data)
async def return_text(format: str):
return data
mock_response.json = return_json
mock_response.text = return_text
return mock_response
async def test_coordinator_fetches_data(aioclient_mock, hass):
"""Test coordinator fetches data."""
# P1 meter and (very advanced kWh meter)
meter = get_mock_device(product_type="p1_meter")
meter.data.smr_version = 50
meter.data.available_datapoints = [
"active_power_l1_w",
"active_power_l2_w",
"active_power_l3_w",
"active_power_w",
"meter_model",
"smr_version",
"total_power_export_t1_kwh",
"total_power_export_t2_kwh",
"total_power_import_t1_kwh",
"total_power_import_t2_kwh",
"total_gas_m3",
"wifi_ssid",
"wifi_strength",
]
coordinator = Coordinator(hass, "1.2.3.4")
coordinator.api = meter
data = await coordinator._async_update_data()
print(data[CONF_DEVICE])
print(meter.device.product_type)
assert data[CONF_DEVICE] == meter.device
assert coordinator.api.host == "1.2.3.4"
assert coordinator.api == meter
assert (
len(coordinator.api.initialize.mock_calls) == 0
) # Already initialized by 'coordinator.api = meter'
assert len(coordinator.api.update.mock_calls) == 2 # Init and update
assert len(coordinator.api.close.mock_calls) == 0
for datapoint in meter.data.available_datapoints:
assert datapoint in data[CONF_DATA]
async def test_coordinator_failed_to_update(aioclient_mock, hass):
"""Test coordinator handles failed update correctly."""
# Update failed by internal error
meter = get_mock_device(product_type="p1_meter")
async def _failed_update() -> bool:
return False
meter.update = _failed_update
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=meter,
):
coordinator = Coordinator(hass, "1.2.3.4")
with raises(UpdateFailed):
await coordinator._async_update_data()
async def test_coordinator_detected_disabled_api(aioclient_mock, hass):
"""Test coordinator handles disabled api correctly."""
# Update failed by internal error
meter = get_mock_device(product_type="p1_meter")
async def _failed_update() -> bool:
raise errors.DisabledError()
meter.update = _failed_update
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=meter,
):
coordinator = Coordinator(hass, "1.2.3.4")
with raises(UpdateFailed):
await coordinator._async_update_data()

View File

@ -0,0 +1,173 @@
"""Tests for the homewizard component."""
from asyncio import TimeoutError
from unittest.mock import patch
from aiohwenergy import AiohwenergyException, DisabledError
from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_IP_ADDRESS
from .generator import get_mock_device
from tests.common import MockConfigEntry
async def test_load_unload(aioclient_mock, hass):
"""Test loading and unloading of integration."""
device = get_mock_device()
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_IP_ADDRESS: "1.1.1.1"},
unique_id=DOMAIN,
)
entry.add_to_hass(hass)
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
async def test_load_failed_host_unavailable(aioclient_mock, hass):
"""Test setup handles unreachable host."""
def MockInitialize():
raise TimeoutError()
device = get_mock_device()
device.initialize.side_effect = MockInitialize
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_IP_ADDRESS: "1.1.1.1"},
unique_id=DOMAIN,
)
entry.add_to_hass(hass)
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_load_detect_api_disabled(aioclient_mock, hass):
"""Test setup detects disabled API."""
def MockInitialize():
raise DisabledError()
device = get_mock_device()
device.initialize.side_effect = MockInitialize
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_IP_ADDRESS: "1.1.1.1"},
unique_id=DOMAIN,
)
entry.add_to_hass(hass)
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR
async def test_load_handles_aiohwenergy_exception(aioclient_mock, hass):
"""Test setup handles exception from API."""
def MockInitialize():
raise AiohwenergyException()
device = get_mock_device()
device.initialize.side_effect = MockInitialize
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_IP_ADDRESS: "1.1.1.1"},
unique_id=DOMAIN,
)
entry.add_to_hass(hass)
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY or ConfigEntryState.SETUP_ERROR
async def test_load_handles_generic_exception(aioclient_mock, hass):
"""Test setup handles global exception."""
def MockInitialize():
raise Exception()
device = get_mock_device()
device.initialize.side_effect = MockInitialize
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_IP_ADDRESS: "1.1.1.1"},
unique_id=DOMAIN,
)
entry.add_to_hass(hass)
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY or ConfigEntryState.SETUP_ERROR
async def test_load_handles_initialization_error(aioclient_mock, hass):
"""Test handles non-exception error."""
device = get_mock_device()
device.device = None
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_IP_ADDRESS: "1.1.1.1"},
unique_id=DOMAIN,
)
entry.add_to_hass(hass)
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=device,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY or ConfigEntryState.SETUP_ERROR

View File

@ -0,0 +1,639 @@
"""Test the update coordinator for HomeWizard."""
from unittest.mock import patch
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,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
POWER_WATT,
VOLUME_CUBIC_METERS,
)
from homeassistant.helpers import entity_registry as er
from .generator import get_mock_device
async def test_sensor_entity_smr_version(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads smr version."""
api = get_mock_device()
api.data.available_datapoints = [
"smr_version",
]
api.data.smr_version = 50
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_dsmr_version")
entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_dsmr_version")
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_smr_version"
assert not entry.disabled
assert state.state == "50"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) DSMR Version"
)
assert ATTR_STATE_CLASS not in state.attributes
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
assert ATTR_DEVICE_CLASS not in state.attributes
assert state.attributes.get(ATTR_ICON) == "mdi:counter"
async def test_sensor_entity_meter_model(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads meter model."""
api = get_mock_device()
api.data.available_datapoints = [
"meter_model",
]
api.data.meter_model = "Model X"
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_smart_meter_model")
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_smart_meter_model"
)
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_meter_model"
assert not entry.disabled
assert state.state == "Model X"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Smart Meter Model"
)
assert ATTR_STATE_CLASS not in state.attributes
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
assert ATTR_DEVICE_CLASS not in state.attributes
assert state.attributes.get(ATTR_ICON) == "mdi:gauge"
async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config_entry):
"""Test entity loads wifi ssid."""
api = get_mock_device()
api.data.available_datapoints = [
"wifi_ssid",
]
api.data.wifi_ssid = "My Wifi"
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_wifi_ssid")
entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wifi_ssid")
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_wifi_ssid"
assert not entry.disabled
assert state.state == "My Wifi"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Wifi SSID"
)
assert ATTR_STATE_CLASS not in state.attributes
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
assert ATTR_DEVICE_CLASS not in state.attributes
assert state.attributes.get(ATTR_ICON) == "mdi:wifi"
async def test_sensor_entity_wifi_strength(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads wifi strength."""
api = get_mock_device()
api.data.available_datapoints = [
"wifi_strength",
]
api.data.wifi_strength = 42
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wifi_strength")
assert entry
assert entry.unique_id == "aabbccddeeff_wifi_strength"
assert entry.disabled
async def test_sensor_entity_total_power_import_t1_kwh(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads total power import t1."""
api = get_mock_device()
api.data.available_datapoints = [
"total_power_import_t1_kwh",
]
api.data.total_power_import_t1_kwh = 1234.123
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_total_power_import_t1")
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_total_power_import_t1"
)
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_total_power_import_t1_kwh"
assert not entry.disabled
assert state.state == "1234.123"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Total Power Import T1"
)
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
async def test_sensor_entity_total_power_import_t2_kwh(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads total power import t2."""
api = get_mock_device()
api.data.available_datapoints = [
"total_power_import_t2_kwh",
]
api.data.total_power_import_t2_kwh = 1234.123
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_total_power_import_t2")
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_total_power_import_t2"
)
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_total_power_import_t2_kwh"
assert not entry.disabled
assert state.state == "1234.123"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Total Power Import T2"
)
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
async def test_sensor_entity_total_power_export_t1_kwh(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads total power export t1."""
api = get_mock_device()
api.data.available_datapoints = [
"total_power_export_t1_kwh",
]
api.data.total_power_export_t1_kwh = 1234.123
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_total_power_export_t1")
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_total_power_export_t1"
)
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_total_power_export_t1_kwh"
assert not entry.disabled
assert state.state == "1234.123"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Total Power Export T1"
)
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
async def test_sensor_entity_total_power_export_t2_kwh(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads total power export t2."""
api = get_mock_device()
api.data.available_datapoints = [
"total_power_export_t2_kwh",
]
api.data.total_power_export_t2_kwh = 1234.123
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_total_power_export_t2")
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_total_power_export_t2"
)
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_total_power_export_t2_kwh"
assert not entry.disabled
assert state.state == "1234.123"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Total Power Export T2"
)
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
async def test_sensor_entity_active_power(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads active power."""
api = get_mock_device()
api.data.available_datapoints = [
"active_power_w",
]
api.data.active_power_w = 123.123
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_active_power")
entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_active_power")
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_active_power_w"
assert not entry.disabled
assert state.state == "123.123"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Active Power"
)
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
async def test_sensor_entity_active_power_l1(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads active power l1."""
api = get_mock_device()
api.data.available_datapoints = [
"active_power_l1_w",
]
api.data.active_power_l1_w = 123.123
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_active_power_l1")
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_active_power_l1"
)
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_active_power_l1_w"
assert not entry.disabled
assert state.state == "123.123"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Active Power 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
async def test_sensor_entity_active_power_l2(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads active power l2."""
api = get_mock_device()
api.data.available_datapoints = [
"active_power_l2_w",
]
api.data.active_power_l2_w = 456.456
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_active_power_l2")
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_active_power_l2"
)
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_active_power_l2_w"
assert not entry.disabled
assert state.state == "456.456"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Active Power L2"
)
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
async def test_sensor_entity_active_power_l3(
hass, mock_config_entry_data, mock_config_entry
):
"""Test entity loads active power l3."""
api = get_mock_device()
api.data.available_datapoints = [
"active_power_l3_w",
]
api.data.active_power_l3_w = 789.789
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_active_power_l3")
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_active_power_l3"
)
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_active_power_l3_w"
assert not entry.disabled
assert state.state == "789.789"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Active Power L3"
)
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
async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config_entry):
"""Test entity loads total gas."""
api = get_mock_device()
api.data.available_datapoints = [
"total_gas_m3",
]
api.data.total_gas_m3 = 50
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
state = hass.states.get("sensor.product_name_aabbccddeeff_total_gas")
entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_total_gas")
assert entry
assert state
assert entry.unique_id == "aabbccddeeff_total_gas_m3"
assert not entry.disabled
assert state.state == "50"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Product Name (aabbccddeeff) Total Gas"
)
assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS
assert ATTR_ICON not in state.attributes
async def test_sensor_entity_disabled_when_null(
hass, mock_config_entry_data, mock_config_entry
):
"""Test sensor disables data with null by default."""
api = get_mock_device()
api.data.available_datapoints = [
"active_power_l2_w",
"active_power_l3_w",
"total_gas_m3",
]
api.data.active_power_l2_w = None
api.data.active_power_l3_w = None
api.data.total_gas_m3 = None
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_active_power_l2"
)
assert entry is None
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_active_power_l3"
)
assert entry is None
entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_total_gas")
assert entry is None
async def test_sensor_entity_export_disabled_when_unused(
hass, mock_config_entry_data, mock_config_entry
):
"""Test sensor disables export if value is 0."""
api = get_mock_device()
api.data.available_datapoints = [
"total_power_export_t1_kwh",
"total_power_export_t2_kwh",
]
api.data.total_power_export_t1_kwh = 0
api.data.total_power_export_t2_kwh = 0
with patch(
"aiohwenergy.HomeWizardEnergy",
return_value=api,
):
entry = mock_config_entry
entry.data = mock_config_entry_data
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_total_power_export_t1"
)
assert entry
assert entry.disabled
entry = entity_registry.async_get(
"sensor.product_name_aabbccddeeff_total_power_export_t2"
)
assert entry
assert entry.disabled