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
parent
d3cd813c5e
commit
8f6e24aa1e
|
@ -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.*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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]}
|
||||
)
|
|
@ -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]
|
|
@ -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
|
|
@ -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"
|
||||
}
|
|
@ -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"]
|
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -136,6 +136,7 @@ FLOWS = [
|
|||
"homekit",
|
||||
"homekit_controller",
|
||||
"homematicip_cloud",
|
||||
"homewizard",
|
||||
"honeywell",
|
||||
"huawei_lte",
|
||||
"hue",
|
||||
|
|
|
@ -179,6 +179,11 @@ ZEROCONF = {
|
|||
"domain": "hue"
|
||||
}
|
||||
],
|
||||
"_hwenergy._tcp.local.": [
|
||||
{
|
||||
"domain": "homewizard"
|
||||
}
|
||||
],
|
||||
"_ipp._tcp.local.": [
|
||||
{
|
||||
"domain": "ipp"
|
||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the HomeWizard integration."""
|
|
@ -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",
|
||||
)
|
|
@ -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
|
|
@ -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"
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue