Add support for HomeWizard Plug-In Battery and v2 API (#136733)

pull/136744/head
Joost Lekkerkerker 2025-01-28 19:09:49 +01:00 committed by GitHub
parent a8c382566c
commit cb407bdfc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1916 additions and 239 deletions

View File

@ -1,12 +1,18 @@
"""The Homewizard integration."""
from homewizard_energy import HomeWizardEnergy, HomeWizardEnergyV1, HomeWizardEnergyV2
from homewizard_energy import (
HomeWizardEnergy,
HomeWizardEnergyV1,
HomeWizardEnergyV2,
has_v2_api,
)
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import DOMAIN, PLATFORMS
from .coordinator import HWEnergyDeviceUpdateCoordinator
@ -31,6 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -
clientsession=async_get_clientsession(hass),
)
await async_check_v2_support_and_create_issue(hass, entry)
coordinator = HWEnergyDeviceUpdateCoordinator(hass, api)
try:
await coordinator.async_config_entry_first_refresh()
@ -63,3 +71,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -
async def async_unload_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_check_v2_support_and_create_issue(
hass: HomeAssistant, entry: HomeWizardConfigEntry
) -> None:
"""Check if the device supports v2 and create an issue if not."""
if not await has_v2_api(entry.data[CONF_IP_ADDRESS], async_get_clientsession(hass)):
return
async_create_issue(
hass,
DOMAIN,
f"migrate_to_v2_api_{entry.entry_id}",
is_fixable=True,
is_persistent=False,
learn_more_url="https://home-assistant.io/integrations/homewizard/#which-button-do-i-need-to-press-to-configure-the-device",
translation_key="migrate_to_v2_api",
translation_placeholders={
"title": entry.title,
},
severity=IssueSeverity.WARNING,
data={"entry_id": entry.entry_id},
)

View File

@ -5,28 +5,31 @@ from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from homewizard_energy import HomeWizardEnergyV1
from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError
from homewizard_energy import (
HomeWizardEnergy,
HomeWizardEnergyV1,
HomeWizardEnergyV2,
has_v2_api,
)
from homewizard_energy.errors import (
DisabledError,
RequestError,
UnauthorizedError,
UnsupportedError,
)
from homewizard_energy.models import Device
import voluptuous as vol
from homeassistant.components import onboarding
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_IP_ADDRESS, CONF_PATH
from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import TextSelector
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import (
CONF_API_ENABLED,
CONF_PRODUCT_NAME,
CONF_PRODUCT_TYPE,
CONF_SERIAL,
DOMAIN,
LOGGER,
)
from .const import CONF_PRODUCT_NAME, CONF_PRODUCT_TYPE, CONF_SERIAL, DOMAIN, LOGGER
class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
@ -46,10 +49,14 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] | None = None
if user_input is not None:
try:
device_info = await self._async_try_connect(user_input[CONF_IP_ADDRESS])
device_info = await async_try_connect(user_input[CONF_IP_ADDRESS])
except RecoverableError as ex:
LOGGER.error(ex)
errors = {"base": ex.error_code}
except UnauthorizedError:
# Device responded, so IP is correct. But we have to authorize
self.ip_address = user_input[CONF_IP_ADDRESS]
return await self.async_step_authorize()
else:
await self.async_set_unique_id(
f"{device_info.product_type}_{device_info.serial}"
@ -73,22 +80,54 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_authorize(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step where we attempt to get a token."""
assert self.ip_address
# Tell device we want a token, user must now press the button within 30 seconds
# The first attempt will always fail, but this opens the window to press the button
token = await async_request_token(self.ip_address)
errors: dict[str, str] | None = None
if token is None:
if user_input is not None:
errors = {"base": "authorization_failed"}
return self.async_show_form(step_id="authorize", errors=errors)
# Now we got a token, we can ask for some more info
async with HomeWizardEnergyV2(self.ip_address, token=token) as api:
device_info = await api.device()
data = {
CONF_IP_ADDRESS: self.ip_address,
CONF_TOKEN: token,
}
await self.async_set_unique_id(
f"{device_info.product_type}_{device_info.serial}"
)
self._abort_if_unique_id_configured(updates=data)
return self.async_create_entry(
title=f"{device_info.product_name}",
data=data,
)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
if (
CONF_API_ENABLED not in discovery_info.properties
or CONF_PATH not in discovery_info.properties
or CONF_PRODUCT_NAME not in discovery_info.properties
CONF_PRODUCT_NAME not in discovery_info.properties
or CONF_PRODUCT_TYPE not in discovery_info.properties
or CONF_SERIAL not in discovery_info.properties
):
return self.async_abort(reason="invalid_discovery_parameters")
if (discovery_info.properties[CONF_PATH]) != "/api/v1":
return self.async_abort(reason="unsupported_api_version")
self.ip_address = discovery_info.host
self.product_type = discovery_info.properties[CONF_PRODUCT_TYPE]
self.product_name = discovery_info.properties[CONF_PRODUCT_NAME]
@ -109,10 +148,12 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
This flow is triggered only by DHCP discovery of known devices.
"""
try:
device = await self._async_try_connect(discovery_info.ip)
device = await async_try_connect(discovery_info.ip)
except RecoverableError as ex:
LOGGER.error(ex)
return self.async_abort(reason="unknown")
except UnauthorizedError:
return self.async_abort(reason="unsupported_api_version")
await self.async_set_unique_id(
f"{device.product_type}_{discovery_info.macaddress}"
@ -139,10 +180,12 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] | None = None
if user_input is not None or not onboarding.async_is_onboarded(self.hass):
try:
await self._async_try_connect(self.ip_address)
await async_try_connect(self.ip_address)
except RecoverableError as ex:
LOGGER.error(ex)
errors = {"base": ex.error_code}
except UnauthorizedError:
return await self.async_step_authorize()
else:
return self.async_create_entry(
title=self.product_name,
@ -172,25 +215,57 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-auth if API was disabled."""
return await self.async_step_reauth_confirm()
self.ip_address = entry_data[CONF_IP_ADDRESS]
async def async_step_reauth_confirm(
# If token exists, we assume we use the v2 API and that the token has been invalidated
if entry_data.get(CONF_TOKEN):
return await self.async_step_reauth_confirm_update_token()
# Else we assume we use the v1 API and that the API has been disabled
return await self.async_step_reauth_enable_api()
async def async_step_reauth_enable_api(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
"""Confirm reauth dialog, where user is asked to re-enable the HomeWizard API."""
errors: dict[str, str] | None = None
if user_input is not None:
reauth_entry = self._get_reauth_entry()
try:
await self._async_try_connect(reauth_entry.data[CONF_IP_ADDRESS])
await async_try_connect(reauth_entry.data[CONF_IP_ADDRESS])
except RecoverableError as ex:
LOGGER.error(ex)
errors = {"base": ex.error_code}
else:
await self.hass.config_entries.async_reload(reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_abort(reason="reauth_enable_api_successful")
return self.async_show_form(step_id="reauth_confirm", errors=errors)
return self.async_show_form(step_id="reauth_enable_api", errors=errors)
async def async_step_reauth_confirm_update_token(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
assert self.ip_address
errors: dict[str, str] | None = None
token = await async_request_token(self.ip_address)
if user_input is not None:
if token is None:
errors = {"base": "authorization_failed"}
else:
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
data_updates={
CONF_TOKEN: token,
},
)
return self.async_show_form(
step_id="reauth_confirm_update_token", errors=errors
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
@ -199,7 +274,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input:
try:
device_info = await self._async_try_connect(user_input[CONF_IP_ADDRESS])
device_info = await async_try_connect(user_input[CONF_IP_ADDRESS])
except RecoverableError as ex:
LOGGER.error(ex)
@ -230,37 +305,65 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
@staticmethod
async def _async_try_connect(ip_address: str) -> Device:
"""Try to connect.
Make connection with device to test the connection
and to get info for unique_id.
"""
async def async_try_connect(ip_address: str) -> Device:
"""Try to connect.
Make connection with device to test the connection
and to get info for unique_id.
"""
energy_api: HomeWizardEnergy
# Determine if device is v1 or v2 capable
if await has_v2_api(ip_address):
energy_api = HomeWizardEnergyV2(ip_address)
else:
energy_api = HomeWizardEnergyV1(ip_address)
try:
return await energy_api.device()
except DisabledError as ex:
raise RecoverableError(
"API disabled, API must be enabled in the app", "api_not_enabled"
) from ex
try:
return await energy_api.device()
except UnsupportedError as ex:
LOGGER.error("API version unsuppored")
raise AbortFlow("unsupported_api_version") from ex
except DisabledError as ex:
raise RecoverableError(
"API disabled, API must be enabled in the app", "api_not_enabled"
) from ex
except RequestError as ex:
raise RecoverableError(
"Device unreachable or unexpected response", "network_error"
) from ex
except UnsupportedError as ex:
LOGGER.error("API version unsuppored")
raise AbortFlow("unsupported_api_version") from ex
except Exception as ex:
LOGGER.exception("Unexpected exception")
raise AbortFlow("unknown_error") from ex
except RequestError as ex:
raise RecoverableError(
"Device unreachable or unexpected response", "network_error"
) from ex
finally:
await energy_api.close()
except UnauthorizedError as ex:
raise UnauthorizedError("Unauthorized") from ex
except Exception as ex:
LOGGER.exception("Unexpected exception")
raise AbortFlow("unknown_error") from ex
finally:
await energy_api.close()
async def async_request_token(ip_address: str) -> str | None:
"""Try to request a token from the device.
This method is used to request a token from the device,
it will return None if the token request failed.
"""
api = HomeWizardEnergyV2(ip_address)
try:
return await api.get_token("home-assistant")
except DisabledError:
return None
finally:
await api.close()
class RecoverableError(HomeAssistantError):

View File

@ -13,8 +13,6 @@ PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
LOGGER = logging.getLogger(__package__)
# Platform config.
CONF_API_ENABLED = "api_enabled"
CONF_DATA = "data"
CONF_PRODUCT_NAME = "product_name"
CONF_PRODUCT_TYPE = "product_type"
CONF_SERIAL = "serial"

View File

@ -3,11 +3,12 @@
from __future__ import annotations
from homewizard_energy import HomeWizardEnergy
from homewizard_energy.errors import DisabledError, RequestError
from homewizard_energy.errors import DisabledError, RequestError, UnauthorizedError
from homewizard_energy.models import CombinedModels as DeviceResponseEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, UPDATE_INTERVAL
@ -51,6 +52,9 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry]
ex, translation_domain=DOMAIN, translation_key="api_disabled"
) from ex
except UnauthorizedError as ex:
raise ConfigEntryAuthFailed from ex
self.api_disabled = False
self.data = data

View File

@ -15,6 +15,9 @@
"any_power_fail_count": {
"default": "mdi:transmission-tower-off"
},
"cycles": {
"default": "mdi:battery-sync-outline"
},
"dsmr_version": {
"default": "mdi:counter"
},

View File

@ -12,6 +12,6 @@
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==v8.1.1"],
"zeroconf": ["_hwenergy._tcp.local."]
"requirements": ["python-homewizard-energy==v8.2.0"],
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
}

View File

@ -47,7 +47,10 @@ rules:
devices: done
diagnostics: done
discovery-update-info: done
discovery: done
discovery:
status: done
comment: |
DHCP IP address updates are not supported for the v2 API.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
@ -66,10 +69,7 @@ rules:
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues:
status: exempt
comment: |
This integration does not raise any repairable issues.
repair-issues: done
stale-devices:
status: exempt
comment: |

View File

@ -0,0 +1,79 @@
"""Repairs for HomeWizard integration."""
from __future__ import annotations
from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from .config_flow import async_request_token
class MigrateToV2ApiRepairFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
def __init__(self, entry: ConfigEntry) -> None:
"""Create flow."""
self.entry = entry
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the confirm step of a fix flow."""
if user_input is not None:
return await self.async_step_authorize()
return self.async_show_form(
step_id="confirm", description_placeholders={"title": self.entry.title}
)
async def async_step_authorize(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the authorize step of a fix flow."""
ip_address = self.entry.data[CONF_IP_ADDRESS]
# Tell device we want a token, user must now press the button within 30 seconds
# The first attempt will always fail, but this opens the window to press the button
token = await async_request_token(ip_address)
errors: dict[str, str] | None = None
if token is None:
if user_input is not None:
errors = {"base": "authorization_failed"}
return self.async_show_form(step_id="authorize", errors=errors)
data = {**self.entry.data, CONF_TOKEN: token}
self.hass.config_entries.async_update_entry(self.entry, data=data)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_create_entry(data={})
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create flow."""
assert data is not None
assert isinstance(data["entry_id"], str)
if issue_id.startswith("migrate_to_v2_api_") and (
entry := hass.config_entries.async_get_entry(data["entry_id"])
):
return MigrateToV2ApiRepairFlow(entry)
raise ValueError(f"unknown repair {issue_id}") # pragma: no cover

View File

@ -6,7 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Final
from homewizard_energy.models import ExternalDevice, Measurement
from homewizard_energy.models import CombinedModels, ExternalDevice
from homeassistant.components.sensor import (
DEVICE_CLASS_UNITS,
@ -46,9 +46,9 @@ PARALLEL_UPDATES = 1
class HomeWizardSensorEntityDescription(SensorEntityDescription):
"""Class describing HomeWizard sensor entities."""
enabled_fn: Callable[[Measurement], bool] = lambda x: True
has_fn: Callable[[Measurement], bool]
value_fn: Callable[[Measurement], StateType]
enabled_fn: Callable[[CombinedModels], bool] = lambda x: True
has_fn: Callable[[CombinedModels], bool]
value_fn: Callable[[CombinedModels], StateType]
@dataclass(frozen=True, kw_only=True)
@ -69,35 +69,43 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
key="smr_version",
translation_key="dsmr_version",
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.protocol_version is not None,
value_fn=lambda data: data.protocol_version,
has_fn=lambda data: data.measurement.protocol_version is not None,
value_fn=lambda data: data.measurement.protocol_version,
),
HomeWizardSensorEntityDescription(
key="meter_model",
translation_key="meter_model",
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.meter_model is not None,
value_fn=lambda data: data.meter_model,
has_fn=lambda data: data.measurement.meter_model is not None,
value_fn=lambda data: data.measurement.meter_model,
),
HomeWizardSensorEntityDescription(
key="unique_meter_id",
translation_key="unique_meter_id",
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.unique_id is not None,
value_fn=lambda data: data.unique_id,
has_fn=lambda data: data.measurement.unique_id is not None,
value_fn=lambda data: data.measurement.unique_id,
),
HomeWizardSensorEntityDescription(
key="wifi_ssid",
translation_key="wifi_ssid",
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.wifi_ssid is not None,
value_fn=lambda data: data.wifi_ssid,
has_fn=(
lambda data: data.system is not None and data.system.wifi_ssid is not None
),
value_fn=(
lambda data: data.system.wifi_ssid if data.system is not None else None
),
),
HomeWizardSensorEntityDescription(
key="active_tariff",
translation_key="active_tariff",
has_fn=lambda data: data.tariff is not None,
value_fn=lambda data: None if data.tariff is None else str(data.tariff),
has_fn=lambda data: data.measurement.tariff is not None,
value_fn=(
lambda data: None
if data.measurement.tariff is None
else str(data.measurement.tariff)
),
device_class=SensorDeviceClass.ENUM,
options=["1", "2", "3", "4"],
),
@ -108,8 +116,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=lambda data: data.wifi_strength is not None,
value_fn=lambda data: data.wifi_strength,
has_fn=lambda data: data.measurement.wifi_strength is not None,
value_fn=lambda data: data.measurement.wifi_strength,
),
HomeWizardSensorEntityDescription(
key="total_power_import_kwh",
@ -117,8 +125,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.energy_import_kwh is not None,
value_fn=lambda data: data.energy_import_kwh,
has_fn=lambda data: data.measurement.energy_import_kwh is not None,
value_fn=lambda data: data.measurement.energy_import_kwh,
),
HomeWizardSensorEntityDescription(
key="total_power_import_t1_kwh",
@ -129,10 +137,10 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: (
# SKT/SDM230/630 provides both total and tariff 1: duplicate.
data.energy_import_t1_kwh is not None
and data.energy_export_t2_kwh is not None
data.measurement.energy_import_t1_kwh is not None
and data.measurement.energy_export_t2_kwh is not None
),
value_fn=lambda data: data.energy_import_t1_kwh,
value_fn=lambda data: data.measurement.energy_import_t1_kwh,
),
HomeWizardSensorEntityDescription(
key="total_power_import_t2_kwh",
@ -141,8 +149,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.energy_import_t2_kwh is not None,
value_fn=lambda data: data.energy_import_t2_kwh,
has_fn=lambda data: data.measurement.energy_import_t2_kwh is not None,
value_fn=lambda data: data.measurement.energy_import_t2_kwh,
),
HomeWizardSensorEntityDescription(
key="total_power_import_t3_kwh",
@ -151,8 +159,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.energy_import_t3_kwh is not None,
value_fn=lambda data: data.energy_import_t3_kwh,
has_fn=lambda data: data.measurement.energy_import_t3_kwh is not None,
value_fn=lambda data: data.measurement.energy_import_t3_kwh,
),
HomeWizardSensorEntityDescription(
key="total_power_import_t4_kwh",
@ -161,8 +169,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.energy_import_t4_kwh is not None,
value_fn=lambda data: data.energy_import_t4_kwh,
has_fn=lambda data: data.measurement.energy_import_t4_kwh is not None,
value_fn=lambda data: data.measurement.energy_import_t4_kwh,
),
HomeWizardSensorEntityDescription(
key="total_power_export_kwh",
@ -170,9 +178,9 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.energy_export_kwh is not None,
enabled_fn=lambda data: data.energy_export_kwh != 0,
value_fn=lambda data: data.energy_export_kwh,
has_fn=lambda data: data.measurement.energy_export_kwh is not None,
enabled_fn=lambda data: data.measurement.energy_export_kwh != 0,
value_fn=lambda data: data.measurement.energy_export_kwh,
),
HomeWizardSensorEntityDescription(
key="total_power_export_t1_kwh",
@ -183,11 +191,11 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: (
# SKT/SDM230/630 provides both total and tariff 1: duplicate.
data.energy_export_t1_kwh is not None
and data.energy_export_t2_kwh is not None
data.measurement.energy_export_t1_kwh is not None
and data.measurement.energy_export_t2_kwh is not None
),
enabled_fn=lambda data: data.energy_export_t1_kwh != 0,
value_fn=lambda data: data.energy_export_t1_kwh,
enabled_fn=lambda data: data.measurement.energy_export_t1_kwh != 0,
value_fn=lambda data: data.measurement.energy_export_t1_kwh,
),
HomeWizardSensorEntityDescription(
key="total_power_export_t2_kwh",
@ -196,9 +204,9 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.energy_export_t2_kwh is not None,
enabled_fn=lambda data: data.energy_export_t2_kwh != 0,
value_fn=lambda data: data.energy_export_t2_kwh,
has_fn=lambda data: data.measurement.energy_export_t2_kwh is not None,
enabled_fn=lambda data: data.measurement.energy_export_t2_kwh != 0,
value_fn=lambda data: data.measurement.energy_export_t2_kwh,
),
HomeWizardSensorEntityDescription(
key="total_power_export_t3_kwh",
@ -207,9 +215,9 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.energy_export_t3_kwh is not None,
enabled_fn=lambda data: data.energy_export_t3_kwh != 0,
value_fn=lambda data: data.energy_export_t3_kwh,
has_fn=lambda data: data.measurement.energy_export_t3_kwh is not None,
enabled_fn=lambda data: data.measurement.energy_export_t3_kwh != 0,
value_fn=lambda data: data.measurement.energy_export_t3_kwh,
),
HomeWizardSensorEntityDescription(
key="total_power_export_t4_kwh",
@ -218,9 +226,9 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.energy_export_t4_kwh is not None,
enabled_fn=lambda data: data.energy_export_t4_kwh != 0,
value_fn=lambda data: data.energy_export_t4_kwh,
has_fn=lambda data: data.measurement.energy_export_t4_kwh is not None,
enabled_fn=lambda data: data.measurement.energy_export_t4_kwh != 0,
value_fn=lambda data: data.measurement.energy_export_t4_kwh,
),
HomeWizardSensorEntityDescription(
key="active_power_w",
@ -228,8 +236,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda data: data.power_w is not None,
value_fn=lambda data: data.power_w,
has_fn=lambda data: data.measurement.power_w is not None,
value_fn=lambda data: data.measurement.power_w,
),
HomeWizardSensorEntityDescription(
key="active_power_l1_w",
@ -239,8 +247,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda data: data.power_l1_w is not None,
value_fn=lambda data: data.power_l1_w,
has_fn=lambda data: data.measurement.power_l1_w is not None,
value_fn=lambda data: data.measurement.power_l1_w,
),
HomeWizardSensorEntityDescription(
key="active_power_l2_w",
@ -250,8 +258,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda data: data.power_l2_w is not None,
value_fn=lambda data: data.power_l2_w,
has_fn=lambda data: data.measurement.power_l2_w is not None,
value_fn=lambda data: data.measurement.power_l2_w,
),
HomeWizardSensorEntityDescription(
key="active_power_l3_w",
@ -261,8 +269,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda data: data.power_l3_w is not None,
value_fn=lambda data: data.power_l3_w,
has_fn=lambda data: data.measurement.power_l3_w is not None,
value_fn=lambda data: data.measurement.power_l3_w,
),
HomeWizardSensorEntityDescription(
key="active_voltage_v",
@ -270,8 +278,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.voltage_v is not None,
value_fn=lambda data: data.voltage_v,
has_fn=lambda data: data.measurement.voltage_v is not None,
value_fn=lambda data: data.measurement.voltage_v,
),
HomeWizardSensorEntityDescription(
key="active_voltage_l1_v",
@ -281,8 +289,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.voltage_l1_v is not None,
value_fn=lambda data: data.voltage_l1_v,
has_fn=lambda data: data.measurement.voltage_l1_v is not None,
value_fn=lambda data: data.measurement.voltage_l1_v,
),
HomeWizardSensorEntityDescription(
key="active_voltage_l2_v",
@ -292,8 +300,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.voltage_l2_v is not None,
value_fn=lambda data: data.voltage_l2_v,
has_fn=lambda data: data.measurement.voltage_l2_v is not None,
value_fn=lambda data: data.measurement.voltage_l2_v,
),
HomeWizardSensorEntityDescription(
key="active_voltage_l3_v",
@ -303,8 +311,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.voltage_l3_v is not None,
value_fn=lambda data: data.voltage_l3_v,
has_fn=lambda data: data.measurement.voltage_l3_v is not None,
value_fn=lambda data: data.measurement.voltage_l3_v,
),
HomeWizardSensorEntityDescription(
key="active_current_a",
@ -312,8 +320,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.current_a is not None,
value_fn=lambda data: data.current_a,
has_fn=lambda data: data.measurement.current_a is not None,
value_fn=lambda data: data.measurement.current_a,
),
HomeWizardSensorEntityDescription(
key="active_current_l1_a",
@ -323,8 +331,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.current_l1_a is not None,
value_fn=lambda data: data.current_l1_a,
has_fn=lambda data: data.measurement.current_l1_a is not None,
value_fn=lambda data: data.measurement.current_l1_a,
),
HomeWizardSensorEntityDescription(
key="active_current_l2_a",
@ -334,8 +342,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.current_l2_a is not None,
value_fn=lambda data: data.current_l2_a,
has_fn=lambda data: data.measurement.current_l2_a is not None,
value_fn=lambda data: data.measurement.current_l2_a,
),
HomeWizardSensorEntityDescription(
key="active_current_l3_a",
@ -345,8 +353,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.current_l3_a is not None,
value_fn=lambda data: data.current_l3_a,
has_fn=lambda data: data.measurement.current_l3_a is not None,
value_fn=lambda data: data.measurement.current_l3_a,
),
HomeWizardSensorEntityDescription(
key="active_frequency_hz",
@ -354,8 +362,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.frequency_hz is not None,
value_fn=lambda data: data.frequency_hz,
has_fn=lambda data: data.measurement.frequency_hz is not None,
value_fn=lambda data: data.measurement.frequency_hz,
),
HomeWizardSensorEntityDescription(
key="active_apparent_power_va",
@ -363,8 +371,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.APPARENT_POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.apparent_power_va is not None,
value_fn=lambda data: data.apparent_power_va,
has_fn=lambda data: data.measurement.apparent_power_va is not None,
value_fn=lambda data: data.measurement.apparent_power_va,
),
HomeWizardSensorEntityDescription(
key="active_apparent_power_l1_va",
@ -374,8 +382,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.APPARENT_POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.apparent_power_l1_va is not None,
value_fn=lambda data: data.apparent_power_l1_va,
has_fn=lambda data: data.measurement.apparent_power_l1_va is not None,
value_fn=lambda data: data.measurement.apparent_power_l1_va,
),
HomeWizardSensorEntityDescription(
key="active_apparent_power_l2_va",
@ -385,8 +393,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.APPARENT_POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.apparent_power_l2_va is not None,
value_fn=lambda data: data.apparent_power_l2_va,
has_fn=lambda data: data.measurement.apparent_power_l2_va is not None,
value_fn=lambda data: data.measurement.apparent_power_l2_va,
),
HomeWizardSensorEntityDescription(
key="active_apparent_power_l3_va",
@ -396,8 +404,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.APPARENT_POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.apparent_power_l3_va is not None,
value_fn=lambda data: data.apparent_power_l3_va,
has_fn=lambda data: data.measurement.apparent_power_l3_va is not None,
value_fn=lambda data: data.measurement.apparent_power_l3_va,
),
HomeWizardSensorEntityDescription(
key="active_reactive_power_var",
@ -405,8 +413,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.REACTIVE_POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.reactive_power_var is not None,
value_fn=lambda data: data.reactive_power_var,
has_fn=lambda data: data.measurement.reactive_power_var is not None,
value_fn=lambda data: data.measurement.reactive_power_var,
),
HomeWizardSensorEntityDescription(
key="active_reactive_power_l1_var",
@ -416,8 +424,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.REACTIVE_POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.reactive_power_l1_var is not None,
value_fn=lambda data: data.reactive_power_l1_var,
has_fn=lambda data: data.measurement.reactive_power_l1_var is not None,
value_fn=lambda data: data.measurement.reactive_power_l1_var,
),
HomeWizardSensorEntityDescription(
key="active_reactive_power_l2_var",
@ -427,8 +435,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.REACTIVE_POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.reactive_power_l2_var is not None,
value_fn=lambda data: data.reactive_power_l2_var,
has_fn=lambda data: data.measurement.reactive_power_l2_var is not None,
value_fn=lambda data: data.measurement.reactive_power_l2_var,
),
HomeWizardSensorEntityDescription(
key="active_reactive_power_l3_var",
@ -438,8 +446,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.REACTIVE_POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.reactive_power_l3_var is not None,
value_fn=lambda data: data.reactive_power_l3_var,
has_fn=lambda data: data.measurement.reactive_power_l3_var is not None,
value_fn=lambda data: data.measurement.reactive_power_l3_var,
),
HomeWizardSensorEntityDescription(
key="active_power_factor",
@ -447,8 +455,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.power_factor is not None,
value_fn=lambda data: to_percentage(data.power_factor),
has_fn=lambda data: data.measurement.power_factor is not None,
value_fn=lambda data: to_percentage(data.measurement.power_factor),
),
HomeWizardSensorEntityDescription(
key="active_power_factor_l1",
@ -458,8 +466,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.power_factor_l1 is not None,
value_fn=lambda data: to_percentage(data.power_factor_l1),
has_fn=lambda data: data.measurement.power_factor_l1 is not None,
value_fn=lambda data: to_percentage(data.measurement.power_factor_l1),
),
HomeWizardSensorEntityDescription(
key="active_power_factor_l2",
@ -469,8 +477,8 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.power_factor_l2 is not None,
value_fn=lambda data: to_percentage(data.power_factor_l2),
has_fn=lambda data: data.measurement.power_factor_l2 is not None,
value_fn=lambda data: to_percentage(data.measurement.power_factor_l2),
),
HomeWizardSensorEntityDescription(
key="active_power_factor_l3",
@ -480,94 +488,94 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
has_fn=lambda data: data.power_factor_l3 is not None,
value_fn=lambda data: to_percentage(data.power_factor_l3),
has_fn=lambda data: data.measurement.power_factor_l3 is not None,
value_fn=lambda data: to_percentage(data.measurement.power_factor_l3),
),
HomeWizardSensorEntityDescription(
key="voltage_sag_l1_count",
translation_key="voltage_sag_phase_count",
translation_placeholders={"phase": "1"},
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.voltage_sag_l1_count is not None,
value_fn=lambda data: data.voltage_sag_l1_count,
has_fn=lambda data: data.measurement.voltage_sag_l1_count is not None,
value_fn=lambda data: data.measurement.voltage_sag_l1_count,
),
HomeWizardSensorEntityDescription(
key="voltage_sag_l2_count",
translation_key="voltage_sag_phase_count",
translation_placeholders={"phase": "2"},
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.voltage_sag_l2_count is not None,
value_fn=lambda data: data.voltage_sag_l2_count,
has_fn=lambda data: data.measurement.voltage_sag_l2_count is not None,
value_fn=lambda data: data.measurement.voltage_sag_l2_count,
),
HomeWizardSensorEntityDescription(
key="voltage_sag_l3_count",
translation_key="voltage_sag_phase_count",
translation_placeholders={"phase": "3"},
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.voltage_sag_l3_count is not None,
value_fn=lambda data: data.voltage_sag_l3_count,
has_fn=lambda data: data.measurement.voltage_sag_l3_count is not None,
value_fn=lambda data: data.measurement.voltage_sag_l3_count,
),
HomeWizardSensorEntityDescription(
key="voltage_swell_l1_count",
translation_key="voltage_swell_phase_count",
translation_placeholders={"phase": "1"},
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.voltage_swell_l1_count is not None,
value_fn=lambda data: data.voltage_swell_l1_count,
has_fn=lambda data: data.measurement.voltage_swell_l1_count is not None,
value_fn=lambda data: data.measurement.voltage_swell_l1_count,
),
HomeWizardSensorEntityDescription(
key="voltage_swell_l2_count",
translation_key="voltage_swell_phase_count",
translation_placeholders={"phase": "2"},
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.voltage_swell_l2_count is not None,
value_fn=lambda data: data.voltage_swell_l2_count,
has_fn=lambda data: data.measurement.voltage_swell_l2_count is not None,
value_fn=lambda data: data.measurement.voltage_swell_l2_count,
),
HomeWizardSensorEntityDescription(
key="voltage_swell_l3_count",
translation_key="voltage_swell_phase_count",
translation_placeholders={"phase": "3"},
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.voltage_swell_l3_count is not None,
value_fn=lambda data: data.voltage_swell_l3_count,
has_fn=lambda data: data.measurement.voltage_swell_l3_count is not None,
value_fn=lambda data: data.measurement.voltage_swell_l3_count,
),
HomeWizardSensorEntityDescription(
key="any_power_fail_count",
translation_key="any_power_fail_count",
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.any_power_fail_count is not None,
value_fn=lambda data: data.any_power_fail_count,
has_fn=lambda data: data.measurement.any_power_fail_count is not None,
value_fn=lambda data: data.measurement.any_power_fail_count,
),
HomeWizardSensorEntityDescription(
key="long_power_fail_count",
translation_key="long_power_fail_count",
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.long_power_fail_count is not None,
value_fn=lambda data: data.long_power_fail_count,
has_fn=lambda data: data.measurement.long_power_fail_count is not None,
value_fn=lambda data: data.measurement.long_power_fail_count,
),
HomeWizardSensorEntityDescription(
key="active_power_average_w",
translation_key="active_power_average_w",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
has_fn=lambda data: data.average_power_15m_w is not None,
value_fn=lambda data: data.average_power_15m_w,
has_fn=lambda data: data.measurement.average_power_15m_w is not None,
value_fn=lambda data: data.measurement.average_power_15m_w,
),
HomeWizardSensorEntityDescription(
key="monthly_power_peak_w",
translation_key="monthly_power_peak_w",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
has_fn=lambda data: data.monthly_power_peak_w is not None,
value_fn=lambda data: data.monthly_power_peak_w,
has_fn=lambda data: data.measurement.monthly_power_peak_w is not None,
value_fn=lambda data: data.measurement.monthly_power_peak_w,
),
HomeWizardSensorEntityDescription(
key="active_liter_lpm",
translation_key="active_liter_lpm",
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
has_fn=lambda data: data.active_liter_lpm is not None,
value_fn=lambda data: data.active_liter_lpm,
has_fn=lambda data: data.measurement.active_liter_lpm is not None,
value_fn=lambda data: data.measurement.active_liter_lpm,
),
HomeWizardSensorEntityDescription(
key="total_liter_m3",
@ -575,8 +583,26 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.WATER,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.total_liter_m3 is not None,
value_fn=lambda data: data.total_liter_m3,
has_fn=lambda data: data.measurement.total_liter_m3 is not None,
value_fn=lambda data: data.measurement.total_liter_m3,
),
HomeWizardSensorEntityDescription(
key="state_of_charge_pct",
translation_key="state_of_charge_pct",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda data: data.measurement.state_of_charge_pct is not None,
value_fn=lambda data: data.measurement.state_of_charge_pct,
),
HomeWizardSensorEntityDescription(
key="cycles",
translation_key="cycles",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
has_fn=lambda data: data.measurement.cycles is not None,
value_fn=lambda data: data.measurement.cycles,
),
)
@ -622,16 +648,15 @@ async def async_setup_entry(
) -> None:
"""Initialize sensors."""
measurement = entry.runtime_data.data.measurement
# Initialize default sensors
entities: list = [
HomeWizardSensorEntity(entry.runtime_data, description)
for description in SENSORS
if description.has_fn(measurement)
if description.has_fn(entry.runtime_data.data)
]
# Initialize external devices
measurement = entry.runtime_data.data.measurement
if measurement.external_devices is not None:
for unique_id, device in measurement.external_devices.items():
if device.type is not None and (
@ -661,13 +686,13 @@ class HomeWizardSensorEntity(HomeWizardEntity, SensorEntity):
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
if not description.enabled_fn(self.coordinator.data.measurement):
if not description.enabled_fn(self.coordinator.data):
self._attr_entity_registry_enabled_default = False
@property
def native_value(self) -> StateType:
"""Return the sensor value."""
return self.entity_description.value_fn(self.coordinator.data.measurement)
return self.entity_description.value_fn(self.coordinator.data)
@property
def available(self) -> bool:

View File

@ -15,9 +15,17 @@
"title": "Confirm",
"description": "Do you want to set up {product_type} ({serial}) at {ip_address}?"
},
"reauth_confirm": {
"reauth_enable_api": {
"description": "The local API is disabled. Go to the HomeWizard Energy app and enable the API in the device settings."
},
"reauth_confirm_update_token": {
"title": "Re-authenticate",
"description": "[%key:component::homewizard::config::step::authorize::description%]"
},
"authorize": {
"title": "Authorize",
"description": "Press the button on the HomeWizard Energy device, then select the button below."
},
"reconfigure": {
"description": "Update configuration for {title}.",
"data": {
@ -30,7 +38,8 @@
},
"error": {
"api_not_enabled": "The local API is disabled. Go to the HomeWizard Energy app and enable the API in the device settings.",
"network_error": "Device unreachable, make sure that you have entered the correct IP address and that the device is available in your network"
"network_error": "Device unreachable, make sure that you have entered the correct IP address and that the device is available in your network",
"authorization_failed": "Failed to authorize, make sure to press the button of the device within 30 seconds"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
@ -38,7 +47,8 @@
"device_not_supported": "This device is not supported",
"unknown_error": "[%key:common::config_flow::error::unknown%]",
"unsupported_api_version": "Detected unsupported API version",
"reauth_successful": "Enabling API was successful",
"reauth_enable_api_successful": "Enabling API was successful",
"reauth_successful": "Authorization successful",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"wrong_device": "The configured device is not the same found on this IP address."
}
@ -121,6 +131,12 @@
},
"total_liter_m3": {
"name": "Total water usage"
},
"cycles": {
"name": "Battery cycles"
},
"state_of_charge_pct": {
"name": "State of charge"
}
},
"switch": {
@ -139,5 +155,26 @@
"communication_error": {
"message": "An error occurred while communicating with HomeWizard device"
}
},
"issues": {
"migrate_to_v2_api": {
"title": "Update authentication method",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::homewizard::issues::migrate_to_v2_api::title%]",
"description": "Your {title} now supports a more secure and feature-rich communication method. To take advantage of this, you need to reconfigure the integration.\n\nSelect **Submit** to start the reconfiguration."
},
"authorize": {
"title": "[%key:component::homewizard::config::step::authorize::title%]",
"description": "[%key:component::homewizard::config::step::authorize::description%]"
}
},
"error": {
"authorization_failed": "[%key:component::homewizard::config::error::authorization_failed%]",
"unknown_error": "[%key:common::config_flow::error::unknown%]"
}
}
}
}
}

View File

@ -59,7 +59,7 @@ SWITCHES = [
key="cloud_connection",
translation_key="cloud_connection",
entity_category=EntityCategory.CONFIG,
create_fn=lambda _: True,
create_fn=lambda x: x.device.supports_cloud_enable(),
available_fn=lambda x: x.system is not None,
is_on_fn=lambda x: x.system.cloud_enabled if x.system else None,
set_fn=lambda api, active: api.system(cloud_enabled=active),

View File

@ -522,6 +522,11 @@ ZEROCONF = {
"domain": "homekit",
},
],
"_homewizard._tcp.local.": [
{
"domain": "homewizard",
},
],
"_hscp._tcp.local.": [
{
"domain": "apple_tv",

2
requirements_all.txt generated
View File

@ -2388,7 +2388,7 @@ python-gitlab==1.6.0
python-homeassistant-analytics==0.8.1
# homeassistant.components.homewizard
python-homewizard-energy==v8.1.1
python-homewizard-energy==v8.2.0
# homeassistant.components.hp_ilo
python-hpilo==4.4.3

View File

@ -1933,7 +1933,7 @@ python-fullykiosk==0.0.14
python-homeassistant-analytics==0.8.1
# homeassistant.components.homewizard
python-homewizard-energy==v8.1.1
python-homewizard-energy==v8.2.0
# homeassistant.components.izone
python-izone==1.2.9

View File

@ -3,11 +3,18 @@
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
from homewizard_energy.models import CombinedModels, Device, Measurement, State, System
from homewizard_energy.models import (
CombinedModels,
Device,
Measurement,
State,
System,
Token,
)
import pytest
from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, get_fixture_path, load_json_object_fixture
@ -65,6 +72,59 @@ def mock_homewizardenergy(
yield client
@pytest.fixture
def mock_homewizardenergy_v2(
device_fixture: str,
) -> MagicMock:
"""Return a mock bridge."""
with (
patch(
"homeassistant.components.homewizard.HomeWizardEnergyV2",
autospec=True,
) as homewizard,
patch(
"homeassistant.components.homewizard.config_flow.HomeWizardEnergyV2",
new=homewizard,
),
):
client = homewizard.return_value
client.combined.return_value = CombinedModels(
device=Device.from_dict(
load_json_object_fixture(f"v2/{device_fixture}/device.json", DOMAIN)
),
measurement=Measurement.from_dict(
load_json_object_fixture(
f"v2/{device_fixture}/measurement.json", DOMAIN
)
),
state=(
State.from_dict(
load_json_object_fixture(f"v2/{device_fixture}/state.json", DOMAIN)
)
if get_fixture_path(f"v2/{device_fixture}/state.json", DOMAIN).exists()
else None
),
system=(
System.from_dict(
load_json_object_fixture(f"v2/{device_fixture}/system.json", DOMAIN)
)
if get_fixture_path(f"v2/{device_fixture}/system.json", DOMAIN).exists()
else None
),
)
# device() call is used during configuration flow
client.device.return_value = client.combined.return_value.device
# Authorization flow is used during configuration flow
client.get_token.return_value = Token.from_dict(
load_json_object_fixture("v2/generic/token.json", DOMAIN)
).token
yield client
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Mock setting up a config entry."""
@ -90,6 +150,20 @@ def mock_config_entry() -> MockConfigEntry:
)
@pytest.fixture
def mock_config_entry_v2() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="Device",
domain=DOMAIN,
data={
CONF_IP_ADDRESS: "127.0.0.1",
CONF_TOKEN: "00112233445566778899ABCDEFABCDEF",
},
unique_id="HWE-P1_5c2fafabcdef",
)
@pytest.fixture
async def init_integration(
hass: HomeAssistant,

View File

@ -0,0 +1,12 @@
{
"wifi_ssid": "simulating v1 support",
"wifi_strength": 100,
"total_power_import_kwh": 123.456,
"total_power_export_kwh": 123.456,
"active_power_w": 123,
"active_voltage_v": 230,
"active_current_a": 1.5,
"active_frequency_hz": 50,
"state_of_charge_pct": 50,
"cycles": 123
}

View File

@ -0,0 +1,7 @@
{
"product_type": "HWE-BAT",
"product_name": "Plug-In Battery",
"serial": "5c2fafabcdef",
"firmware_version": "1.00",
"api_version": "v1"
}

View File

@ -0,0 +1,7 @@
{
"wifi_ssid": "My Wi-Fi",
"wifi_rssi_db": -77,
"cloud_enabled": false,
"uptime_s": 356,
"status_led_brightness_pct": 100
}

View File

@ -0,0 +1,7 @@
{
"product_type": "HWE-P1",
"product_name": "P1 meter",
"serial": "5c2fafabcdef",
"firmware_version": "4.19",
"api_version": "2.0.0"
}

View File

@ -0,0 +1,48 @@
{
"protocol_version": 50,
"meter_model": "ISKRA 2M550T-101",
"unique_id": "4E6576657220476F6E6E61204C657420596F7520446F776E",
"timestamp": "2024-06-28T14:12:34",
"tariff": 2,
"energy_import_kwh": 13779.338,
"energy_import_t1_kwh": 10830.511,
"energy_import_t2_kwh": 2948.827,
"energy_export_kwh": 1234.567,
"energy_export_t1_kwh": 234.567,
"energy_export_t2_kwh": 1000,
"power_w": -543,
"power_l1_w": -676,
"power_l2_w": 133,
"power_l3_w": 0,
"current_a": 6,
"current_l1_a": -4,
"current_l2_a": 2,
"current_l3_a": 0,
"voltage_sag_l1_count": 1,
"voltage_sag_l2_count": 1,
"voltage_sag_l3_count": 0,
"voltage_swell_l1_count": 0,
"voltage_swell_l2_count": 0,
"voltage_swell_l3_count": 0,
"any_power_fail_count": 4,
"long_power_fail_count": 5,
"average_power_15m_w": 123.0,
"monthly_power_peak_w": 1111.0,
"monthly_power_peak_timestamp": "2024-06-04T10:11:22",
"external": [
{
"unique_id": "4E6576657220676F6E6E612072756E2061726F756E64",
"type": "gas_meter",
"timestamp": "2024-06-28T14:00:00",
"value": 2569.646,
"unit": "m3"
},
{
"unique_id": "616E642064657365727420796F75",
"type": "water_meter",
"timestamp": "2024-06-28T14:05:00",
"value": 123.456,
"unit": "m3"
}
]
}

View File

@ -0,0 +1,8 @@
{
"wifi_ssid": "My Wi-Fi",
"wifi_rssi_db": -77,
"cloud_enabled": false,
"uptime_s": 356,
"status_led_brightness_pct": 100,
"api_v1_enabled": true
}

View File

@ -0,0 +1,4 @@
{
"token": "00112233445566778899aabbccddeeff",
"name": "local/new_user"
}

View File

@ -1,9 +1,99 @@
# serializer version: 1
# name: test_diagnostics[HWE-BAT]
dict({
'data': dict({
'device': dict({
'api_version': '1.0.0',
'firmware_version': '1.00',
'id': '**REDACTED**',
'model_name': 'Plug-In Battery',
'product_name': 'Plug-In Battery',
'product_type': 'HWE-BAT',
'serial': '**REDACTED**',
}),
'measurement': dict({
'active_liter_lpm': None,
'any_power_fail_count': None,
'apparent_power_l1_va': None,
'apparent_power_l2_va': None,
'apparent_power_l3_va': None,
'apparent_power_va': None,
'average_power_15m_w': None,
'current_a': 1.5,
'current_l1_a': None,
'current_l2_a': None,
'current_l3_a': None,
'cycles': 123,
'energy_export_kwh': 123.456,
'energy_export_t1_kwh': None,
'energy_export_t2_kwh': None,
'energy_export_t3_kwh': None,
'energy_export_t4_kwh': None,
'energy_import_kwh': 123.456,
'energy_import_t1_kwh': None,
'energy_import_t2_kwh': None,
'energy_import_t3_kwh': None,
'energy_import_t4_kwh': None,
'external_devices': None,
'frequency_hz': 50.0,
'long_power_fail_count': None,
'meter_model': None,
'monthly_power_peak_timestamp': None,
'monthly_power_peak_w': None,
'power_factor': None,
'power_factor_l1': None,
'power_factor_l2': None,
'power_factor_l3': None,
'power_l1_w': None,
'power_l2_w': None,
'power_l3_w': None,
'power_w': 123.0,
'protocol_version': None,
'reactive_power_l1_var': None,
'reactive_power_l2_var': None,
'reactive_power_l3_var': None,
'reactive_power_var': None,
'state_of_charge_pct': 50.0,
'tariff': None,
'timestamp': None,
'total_liter_m3': None,
'unique_id': None,
'voltage_l1_v': None,
'voltage_l2_v': None,
'voltage_l3_v': None,
'voltage_sag_l1_count': None,
'voltage_sag_l2_count': None,
'voltage_sag_l3_count': None,
'voltage_swell_l1_count': None,
'voltage_swell_l2_count': None,
'voltage_swell_l3_count': None,
'voltage_v': 230.0,
'wifi_ssid': '**REDACTED**',
'wifi_strength': 100,
}),
'state': None,
'system': dict({
'api_v1_enabled': None,
'cloud_enabled': False,
'status_led_brightness_pct': 100,
'uptime_s': 356,
'wifi_rssi_db': -77,
'wifi_ssid': '**REDACTED**',
}),
}),
'entry': dict({
'ip_address': '**REDACTED**',
'product_name': 'P1 Meter',
'product_type': 'HWE-P1',
'serial': '**REDACTED**',
}),
})
# ---
# name: test_diagnostics[HWE-KWH1]
dict({
'data': dict({
'device': dict({
'api_version': 'v1',
'api_version': '1.0.0',
'firmware_version': '3.06',
'id': '**REDACTED**',
'model_name': 'Wi-Fi kWh Meter 1-phase',
@ -93,7 +183,7 @@
dict({
'data': dict({
'device': dict({
'api_version': 'v1',
'api_version': '1.0.0',
'firmware_version': '3.06',
'id': '**REDACTED**',
'model_name': 'Wi-Fi kWh Meter 3-phase',
@ -183,7 +273,7 @@
dict({
'data': dict({
'device': dict({
'api_version': 'v1',
'api_version': '1.0.0',
'firmware_version': '4.19',
'id': '**REDACTED**',
'model_name': 'Wi-Fi P1 Meter',
@ -309,7 +399,7 @@
dict({
'data': dict({
'device': dict({
'api_version': 'v1',
'api_version': '1.0.0',
'firmware_version': '3.03',
'id': '**REDACTED**',
'model_name': 'Wi-Fi Energy Socket',
@ -403,7 +493,7 @@
dict({
'data': dict({
'device': dict({
'api_version': 'v1',
'api_version': '1.0.0',
'firmware_version': '4.07',
'id': '**REDACTED**',
'model_name': 'Wi-Fi Energy Socket',
@ -497,7 +587,7 @@
dict({
'data': dict({
'device': dict({
'api_version': 'v1',
'api_version': '1.0.0',
'firmware_version': '2.03',
'id': '**REDACTED**',
'model_name': 'Wi-Fi Watermeter',
@ -587,7 +677,7 @@
dict({
'data': dict({
'device': dict({
'api_version': 'v1',
'api_version': '1.0.0',
'firmware_version': '3.06',
'id': '**REDACTED**',
'model_name': 'Wi-Fi kWh Meter 1-phase',
@ -677,7 +767,7 @@
dict({
'data': dict({
'device': dict({
'api_version': 'v1',
'api_version': '1.0.0',
'firmware_version': '3.06',
'id': '**REDACTED**',
'model_name': 'Wi-Fi kWh Meter 3-phase',

View File

@ -1,4 +1,704 @@
# serializer version: 1
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_battery_cycles:device-registry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'5c:2f:af:ab:cd:ef',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'homewizard',
'5c2fafabcdef',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'Plug-In Battery',
'model_id': 'HWE-BAT',
'name': 'Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.00',
'via_device_id': None,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_battery_cycles:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.device_battery_cycles',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Battery cycles',
'platform': 'homewizard',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'cycles',
'unique_id': 'HWE-P1_5c2fafabcdef_cycles',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_battery_cycles:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Device Battery cycles',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'context': <ANY>,
'entity_id': 'sensor.device_battery_cycles',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '123',
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_current:device-registry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'5c:2f:af:ab:cd:ef',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'homewizard',
'5c2fafabcdef',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'Plug-In Battery',
'model_id': 'HWE-BAT',
'name': 'Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.00',
'via_device_id': None,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_current:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.device_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None,
'original_name': 'Current',
'platform': 'homewizard',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'HWE-P1_5c2fafabcdef_active_current_a',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_current:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': 'Device Current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
'entity_id': 'sensor.device_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.5',
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_energy_export:device-registry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'5c:2f:af:ab:cd:ef',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'homewizard',
'5c2fafabcdef',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'Plug-In Battery',
'model_id': 'HWE-BAT',
'name': 'Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.00',
'via_device_id': None,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_energy_export:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.device_energy_export',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy export',
'platform': 'homewizard',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_energy_export_kwh',
'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_energy_export:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Device Energy export',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.device_energy_export',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '123.456',
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_energy_import:device-registry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'5c:2f:af:ab:cd:ef',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'homewizard',
'5c2fafabcdef',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'Plug-In Battery',
'model_id': 'HWE-BAT',
'name': 'Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.00',
'via_device_id': None,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_energy_import:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.device_energy_import',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy import',
'platform': 'homewizard',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_energy_import_kwh',
'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_energy_import:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Device Energy import',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.device_energy_import',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '123.456',
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_frequency:device-registry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'5c:2f:af:ab:cd:ef',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'homewizard',
'5c2fafabcdef',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'Plug-In Battery',
'model_id': 'HWE-BAT',
'name': 'Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.00',
'via_device_id': None,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_frequency:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.device_frequency',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.FREQUENCY: 'frequency'>,
'original_icon': None,
'original_name': 'Frequency',
'platform': 'homewizard',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz',
'unit_of_measurement': <UnitOfFrequency.HERTZ: 'Hz'>,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_frequency:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'frequency',
'friendly_name': 'Device Frequency',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfFrequency.HERTZ: 'Hz'>,
}),
'context': <ANY>,
'entity_id': 'sensor.device_frequency',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '50.0',
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_power:device-registry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'5c:2f:af:ab:cd:ef',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'homewizard',
'5c2fafabcdef',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'Plug-In Battery',
'model_id': 'HWE-BAT',
'name': 'Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.00',
'via_device_id': None,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_power:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.device_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'platform': 'homewizard',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w',
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_power:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Device Power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.device_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '123.0',
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_state_of_charge:device-registry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'5c:2f:af:ab:cd:ef',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'homewizard',
'5c2fafabcdef',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'Plug-In Battery',
'model_id': 'HWE-BAT',
'name': 'Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.00',
'via_device_id': None,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_state_of_charge:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.device_state_of_charge',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'State of charge',
'platform': 'homewizard',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'state_of_charge_pct',
'unique_id': 'HWE-P1_5c2fafabcdef_state_of_charge_pct',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_state_of_charge:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Device State of charge',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.device_state_of_charge',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '50.0',
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_voltage:device-registry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'5c:2f:af:ab:cd:ef',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'homewizard',
'5c2fafabcdef',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'HomeWizard',
'model': 'Plug-In Battery',
'model_id': 'HWE-BAT',
'name': 'Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.00',
'via_device_id': None,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_voltage:entity-registry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.device_voltage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
'original_icon': None,
'original_name': 'Voltage',
'platform': 'homewizard',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_v',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
})
# ---
# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_voltage:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'Device Voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.device_voltage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '230.0',
})
# ---
# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_apparent_power:device-registry]
DeviceRegistryEntrySnapshot({
'area_id': None,

View File

@ -1,15 +1,20 @@
"""Test the homewizard config flow."""
from ipaddress import ip_address
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock, MagicMock, patch
from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError
from homewizard_energy.errors import (
DisabledError,
RequestError,
UnauthorizedError,
UnsupportedError,
)
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant import config_entries
from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
@ -225,10 +230,10 @@ async def test_discovery_missing_data_in_service_info(hass: HomeAssistant) -> No
type="",
name="",
properties={
# "api_enabled": "1", --> removed
"api_enabled": "1",
"path": "/api/v1",
"product_name": "P1 meter",
"product_type": "HWE-P1",
# "product_type": "HWE-P1", --> removed
"serial": "5c2fafabcdef",
},
),
@ -238,32 +243,6 @@ async def test_discovery_missing_data_in_service_info(hass: HomeAssistant) -> No
assert result["reason"] == "invalid_discovery_parameters"
async def test_discovery_invalid_api(hass: HomeAssistant) -> None:
"""Test discovery detecting invalid_api."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=ip_address("127.0.0.1"),
ip_addresses=[ip_address("127.0.0.1")],
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": "5c2fafabcdef",
},
),
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "unsupported_api_version"
async def test_dhcp_discovery_updates_entry(
hass: HomeAssistant,
mock_homewizardenergy: MagicMock,
@ -338,6 +317,32 @@ async def test_dhcp_discovery_ignores_unknown(
assert result.get("reason") == "unknown"
async def test_dhcp_discovery_aborts_for_v2_api(
hass: HomeAssistant,
mock_homewizardenergy: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test DHCP discovery aborts when v2 API is detected.
DHCP discovery requires authorization which is not yet implemented
"""
mock_homewizardenergy.device.side_effect = UnauthorizedError
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=DhcpServiceInfo(
ip="1.0.0.127",
hostname="HW-p1meter-aabbcc",
macaddress="5c2fafabcdef",
),
)
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "unsupported_api_version"
async def test_discovery_flow_updates_new_ip(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
@ -455,12 +460,12 @@ async def test_reauth_flow(
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["step_id"] == "reauth_enable_api"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert result["reason"] == "reauth_enable_api_successful"
async def test_reauth_error(
@ -475,7 +480,7 @@ async def test_reauth_error(
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["step_id"] == "reauth_enable_api"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
@ -609,3 +614,222 @@ async def test_reconfigure_cannot_connect(
# changed entry
assert mock_config_entry.data[CONF_IP_ADDRESS] == "1.0.0.127"
### TESTS FOR V2 IMPLEMENTATION ###
@pytest.mark.usefixtures("mock_setup_entry")
async def test_manual_flow_works_with_v2_api_support(
hass: HomeAssistant,
mock_homewizardenergy_v2: MagicMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test config flow accepts user configuration and triggers authorization when detected v2 support."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
# Simulate v2 support but not authorized
mock_homewizardenergy_v2.device.side_effect = UnauthorizedError
mock_homewizardenergy_v2.get_token.side_effect = DisabledError
with patch(
"homeassistant.components.homewizard.config_flow.has_v2_api", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "authorize"
# Simulate user authorizing
mock_homewizardenergy_v2.device.side_effect = None
mock_homewizardenergy_v2.get_token.side_effect = None
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_setup_entry")
async def test_manual_flow_detects_failed_user_authorization(
hass: HomeAssistant,
mock_homewizardenergy_v2: MagicMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test config flow accepts user configuration and detects failed button press by user."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
# Simulate v2 support but not authorized
mock_homewizardenergy_v2.device.side_effect = UnauthorizedError
mock_homewizardenergy_v2.get_token.side_effect = DisabledError
with patch(
"homeassistant.components.homewizard.config_flow.has_v2_api", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "authorize"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "authorize"
assert result["errors"] == {"base": "authorization_failed"}
# Restore normal functionality
mock_homewizardenergy_v2.device.side_effect = None
mock_homewizardenergy_v2.get_token.side_effect = None
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_setup_entry")
async def test_reauth_flow_updates_token(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_config_entry_v2: MockConfigEntry,
mock_homewizardenergy_v2: MagicMock,
) -> None:
"""Test reauth flow token is updated."""
mock_config_entry_v2.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_v2.entry_id)
await hass.async_block_till_done()
result = await mock_config_entry_v2.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm_update_token"
# Simulate user pressing the button and getting a new token
mock_homewizardenergy_v2.get_token.return_value = "cool_new_token"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
# Verify that the token was updated
await hass.async_block_till_done()
assert (
hass.config_entries.async_entries(DOMAIN)[0].data.get(CONF_TOKEN)
== "cool_new_token"
)
assert len(mock_setup_entry.mock_calls) == 2
@pytest.mark.usefixtures("mock_setup_entry")
async def test_reauth_flow_handles_user_not_pressing_button(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_config_entry_v2: MockConfigEntry,
mock_homewizardenergy_v2: MagicMock,
) -> None:
"""Test reauth flow token is updated."""
mock_config_entry_v2.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_v2.entry_id)
await hass.async_block_till_done()
result = await mock_config_entry_v2.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm_update_token"
assert result["errors"] is None
# Simulate button not being pressed
mock_homewizardenergy_v2.get_token.side_effect = DisabledError
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "authorization_failed"}
# Simulate user pressing the button and getting a new token
mock_homewizardenergy_v2.get_token.side_effect = None
mock_homewizardenergy_v2.get_token.return_value = "cool_new_token"
# Successful reauth
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
# Verify that the token was updated
await hass.async_block_till_done()
assert (
hass.config_entries.async_entries(DOMAIN)[0].data.get(CONF_TOKEN)
== "cool_new_token"
)
assert len(mock_setup_entry.mock_calls) == 2
@pytest.mark.usefixtures("mock_setup_entry")
async def test_discovery_with_v2_api_ask_authorization(
hass: HomeAssistant,
# mock_setup_entry: AsyncMock,
mock_homewizardenergy_v2: MagicMock,
) -> None:
"""Test discovery detecting missing discovery info."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=ip_address("127.0.0.1"),
ip_addresses=[ip_address("127.0.0.1")],
port=443,
hostname="p1meter-abcdef.local.",
type="",
name="",
properties={
"api_version": "2.0.0",
"id": "appliance/p1dongle/5c2fafabcdef",
"product_name": "P1 meter",
"product_type": "HWE-P1",
"serial": "5c2fafabcdef",
},
),
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "discovery_confirm"
mock_homewizardenergy_v2.device.side_effect = UnauthorizedError
mock_homewizardenergy_v2.get_token.side_effect = DisabledError
with patch(
"homeassistant.components.homewizard.config_flow.has_v2_api", return_value=True
):
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "authorize"
mock_homewizardenergy_v2.get_token.side_effect = None
mock_homewizardenergy_v2.get_token.return_value = "cool_token"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"][CONF_TOKEN] == "cool_token"

View File

@ -21,6 +21,7 @@ from tests.typing import ClientSessionGenerator
"SDM630",
"HWE-KWH1",
"HWE-KWH3",
"HWE-BAT",
],
)
async def test_diagnostics(

View File

@ -4,7 +4,7 @@ from datetime import timedelta
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
from homewizard_energy.errors import DisabledError
from homewizard_energy.errors import DisabledError, UnauthorizedError
import pytest
from homeassistant.components.homewizard.const import DOMAIN
@ -14,12 +14,12 @@ from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_load_unload(
async def test_load_unload_v1(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_homewizardenergy: MagicMock,
) -> None:
"""Test loading and unloading of integration."""
"""Test loading and unloading of integration with v1 config."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
@ -33,6 +33,25 @@ async def test_load_unload(
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_load_unload_v2(
hass: HomeAssistant,
mock_config_entry_v2: MockConfigEntry,
mock_homewizardenergy_v2: MagicMock,
) -> None:
"""Test loading and unloading of integration with v2 config."""
mock_config_entry_v2.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_v2.entry_id)
await hass.async_block_till_done()
assert mock_config_entry_v2.state is ConfigEntryState.LOADED
assert len(mock_homewizardenergy_v2.combined.mock_calls) == 1
await hass.config_entries.async_unload(mock_config_entry_v2.entry_id)
await hass.async_block_till_done()
assert mock_config_entry_v2.state is ConfigEntryState.NOT_LOADED
async def test_load_failed_host_unavailable(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
@ -64,7 +83,7 @@ async def test_load_detect_api_disabled(
assert len(flows) == 1
flow = flows[0]
assert flow.get("step_id") == "reauth_confirm"
assert flow.get("step_id") == "reauth_enable_api"
assert flow.get("handler") == DOMAIN
assert "context" in flow
@ -72,6 +91,31 @@ async def test_load_detect_api_disabled(
assert flow["context"].get("entry_id") == mock_config_entry.entry_id
async def test_load_detect_invalid_token(
hass: HomeAssistant,
mock_config_entry_v2: MockConfigEntry,
mock_homewizardenergy_v2: MagicMock,
) -> None:
"""Test setup detects invalid token."""
mock_homewizardenergy_v2.combined.side_effect = UnauthorizedError()
mock_config_entry_v2.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_v2.entry_id)
await hass.async_block_till_done()
assert mock_config_entry_v2.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
flow = flows[0]
assert flow.get("step_id") == "reauth_confirm_update_token"
assert flow.get("handler") == DOMAIN
assert "context" in flow
assert flow["context"].get("source") == SOURCE_REAUTH
assert flow["context"].get("entry_id") == mock_config_entry_v2.entry_id
@pytest.mark.usefixtures("mock_homewizardenergy")
async def test_load_removes_reauth_flow(
hass: HomeAssistant,
@ -128,5 +172,5 @@ async def test_disablederror_reloads_integration(
assert len(flows) == 1
flow = flows[0]
assert flow.get("step_id") == "reauth_confirm"
assert flow.get("step_id") == "reauth_enable_api"
assert flow.get("handler") == DOMAIN

View File

@ -0,0 +1,82 @@
"""Test the homewizard config flow."""
from unittest.mock import MagicMock, patch
from homewizard_energy.errors import DisabledError
from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.components.repairs import (
async_process_repairs_platforms,
process_repair_fix_flow,
start_repair_fix_flow,
)
from tests.typing import ClientSessionGenerator
async def test_repair_acquires_token(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_homewizardenergy: MagicMock,
mock_homewizardenergy_v2: MagicMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair flow is able to obtain and use token."""
assert await async_setup_component(hass, "repairs", {})
await async_process_repairs_platforms(hass)
client = await hass_client()
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.homewizard.has_v2_api", return_value=True):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Get active repair flow
issue_id = f"migrate_to_v2_api_{mock_config_entry.entry_id}"
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
assert issue is not None
assert issue.data.get("entry_id") == mock_config_entry.entry_id
mock_homewizardenergy_v2.get_token.side_effect = DisabledError
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
flow_id = result["flow_id"]
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "confirm"
result = await process_repair_fix_flow(client, flow_id)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "authorize"
# Simulate user not pressing the button
result = await process_repair_fix_flow(client, flow_id, json={})
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "authorize"
assert result["errors"] == {"base": "authorization_failed"}
# Simulate user pressing the button and getting a new token
mock_homewizardenergy_v2.get_token.side_effect = None
mock_homewizardenergy_v2.get_token.return_value = "cool_token"
result = await process_repair_fix_flow(client, flow_id, json={})
assert result["type"] == FlowResultType.CREATE_ENTRY
assert mock_config_entry.data[CONF_TOKEN] == "cool_token"
assert mock_config_entry.state is ConfigEntryState.LOADED
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None

View File

@ -291,6 +291,19 @@ pytestmark = [
"sensor.water_meter_water",
],
),
(
"HWE-BAT",
[
"sensor.device_battery_cycles",
"sensor.device_current",
"sensor.device_energy_export",
"sensor.device_energy_import",
"sensor.device_frequency",
"sensor.device_power",
"sensor.device_state_of_charge",
"sensor.device_voltage",
],
),
],
)
async def test_sensors(
@ -431,6 +444,14 @@ async def test_sensors(
"sensor.device_wi_fi_strength",
],
),
(
"HWE-BAT",
[
"sensor.device_current",
"sensor.device_frequency",
"sensor.device_voltage",
],
),
],
)
async def test_disabled_by_default_sensors(
@ -492,6 +513,7 @@ async def test_external_sensors_unreachable(
"sensor.device_apparent_power_phase_3",
"sensor.device_apparent_power",
"sensor.device_average_demand",
"sensor.device_battery_cycles",
"sensor.device_current_phase_1",
"sensor.device_current_phase_2",
"sensor.device_current_phase_3",
@ -521,6 +543,7 @@ async def test_external_sensors_unreachable(
"sensor.device_reactive_power",
"sensor.device_smart_meter_identifier",
"sensor.device_smart_meter_model",
"sensor.device_state_of_charge",
"sensor.device_tariff",
"sensor.device_total_water_usage",
"sensor.device_voltage_phase_1",
@ -543,6 +566,7 @@ async def test_external_sensors_unreachable(
"sensor.device_apparent_power_phase_2",
"sensor.device_apparent_power_phase_3",
"sensor.device_average_demand",
"sensor.device_battery_cycles",
"sensor.device_current_phase_1",
"sensor.device_current_phase_2",
"sensor.device_current_phase_3",
@ -568,6 +592,7 @@ async def test_external_sensors_unreachable(
"sensor.device_reactive_power_phase_3",
"sensor.device_smart_meter_identifier",
"sensor.device_smart_meter_model",
"sensor.device_state_of_charge",
"sensor.device_tariff",
"sensor.device_total_water_usage",
"sensor.device_voltage_phase_1",
@ -590,6 +615,7 @@ async def test_external_sensors_unreachable(
"sensor.device_apparent_power_phase_3",
"sensor.device_apparent_power",
"sensor.device_average_demand",
"sensor.device_battery_cycles",
"sensor.device_current_phase_1",
"sensor.device_current_phase_2",
"sensor.device_current_phase_3",
@ -623,6 +649,7 @@ async def test_external_sensors_unreachable(
"sensor.device_reactive_power",
"sensor.device_smart_meter_identifier",
"sensor.device_smart_meter_model",
"sensor.device_state_of_charge",
"sensor.device_tariff",
"sensor.device_voltage_phase_1",
"sensor.device_voltage_phase_2",
@ -644,6 +671,7 @@ async def test_external_sensors_unreachable(
"sensor.device_apparent_power_phase_3",
"sensor.device_average_demand",
"sensor.device_average_demand",
"sensor.device_battery_cycles",
"sensor.device_current_phase_1",
"sensor.device_current_phase_2",
"sensor.device_current_phase_3",
@ -670,6 +698,7 @@ async def test_external_sensors_unreachable(
"sensor.device_reactive_power_phase_3",
"sensor.device_smart_meter_identifier",
"sensor.device_smart_meter_model",
"sensor.device_state_of_charge",
"sensor.device_tariff",
"sensor.device_total_water_usage",
"sensor.device_voltage_phase_1",
@ -688,6 +717,7 @@ async def test_external_sensors_unreachable(
"SDM630",
[
"sensor.device_average_demand",
"sensor.device_battery_cycles",
"sensor.device_current_phase_1",
"sensor.device_current_phase_2",
"sensor.device_current_phase_3",
@ -706,6 +736,7 @@ async def test_external_sensors_unreachable(
"sensor.device_power_failures_detected",
"sensor.device_smart_meter_identifier",
"sensor.device_smart_meter_model",
"sensor.device_state_of_charge",
"sensor.device_tariff",
"sensor.device_total_water_usage",
"sensor.device_voltage_phase_1",
@ -729,6 +760,7 @@ async def test_external_sensors_unreachable(
"sensor.device_apparent_power_phase_3",
"sensor.device_average_demand",
"sensor.device_average_demand",
"sensor.device_battery_cycles",
"sensor.device_current_phase_1",
"sensor.device_current_phase_2",
"sensor.device_current_phase_3",
@ -755,6 +787,7 @@ async def test_external_sensors_unreachable(
"sensor.device_reactive_power_phase_3",
"sensor.device_smart_meter_identifier",
"sensor.device_smart_meter_model",
"sensor.device_state_of_charge",
"sensor.device_tariff",
"sensor.device_total_water_usage",
"sensor.device_voltage_phase_1",
@ -773,6 +806,7 @@ async def test_external_sensors_unreachable(
"HWE-KWH3",
[
"sensor.device_average_demand",
"sensor.device_battery_cycles",
"sensor.device_current_phase_1",
"sensor.device_current_phase_2",
"sensor.device_current_phase_3",
@ -791,6 +825,7 @@ async def test_external_sensors_unreachable(
"sensor.device_power_failures_detected",
"sensor.device_smart_meter_identifier",
"sensor.device_smart_meter_model",
"sensor.device_state_of_charge",
"sensor.device_tariff",
"sensor.device_total_water_usage",
"sensor.device_voltage_phase_1",
@ -806,6 +841,54 @@ async def test_external_sensors_unreachable(
"sensor.device_water_usage",
],
),
(
"HWE-BAT",
[
"sensor.device_apparent_power_phase_1",
"sensor.device_apparent_power_phase_2",
"sensor.device_apparent_power_phase_3",
"sensor.device_apparent_power",
"sensor.device_average_demand",
"sensor.device_current_phase_1",
"sensor.device_current_phase_2",
"sensor.device_current_phase_3",
"sensor.device_dsmr_version",
"sensor.device_energy_export_tariff_1",
"sensor.device_energy_export_tariff_2",
"sensor.device_energy_export_tariff_4",
"sensor.device_energy_import_tariff_1",
"sensor.device_energy_import_tariff_2",
"sensor.device_energy_import_tariff_3",
"sensor.device_energy_import_tariff_4",
"sensor.device_long_power_failures_detected",
"sensor.device_peak_demand_current_month",
"sensor.device_power_factor_phase_1",
"sensor.device_power_factor_phase_2",
"sensor.device_power_factor_phase_3",
"sensor.device_power_factor",
"sensor.device_power_failures_detected",
"sensor.device_power_phase_1",
"sensor.device_power_phase_3",
"sensor.device_reactive_power_phase_1",
"sensor.device_reactive_power_phase_2",
"sensor.device_reactive_power_phase_3",
"sensor.device_reactive_power",
"sensor.device_smart_meter_identifier",
"sensor.device_smart_meter_model",
"sensor.device_tariff",
"sensor.device_total_water_usage",
"sensor.device_voltage_phase_1",
"sensor.device_voltage_phase_2",
"sensor.device_voltage_phase_3",
"sensor.device_voltage_sags_detected_phase_1",
"sensor.device_voltage_sags_detected_phase_2",
"sensor.device_voltage_sags_detected_phase_3",
"sensor.device_voltage_swells_detected_phase_1",
"sensor.device_voltage_swells_detected_phase_2",
"sensor.device_voltage_swells_detected_phase_3",
"sensor.device_water_usage",
],
),
],
)
async def test_entities_not_created_for_device(