Compare commits

...

9 Commits

Author SHA1 Message Date
Jan Bouwhuis fd76693d6e Merge branch 'dev' into homewizard-usage 2026-05-07 13:28:42 +02:00
Jan Bouwhuis 234aadd2e1 Merge branch 'dev' into homewizard-usage 2026-03-30 17:03:54 +02:00
Jan Bouwhuis bd095ebf0a Merge branch 'dev' into homewizard-usage 2026-02-16 18:08:07 +01:00
jbouwh 1edfd2da23 Do not purge deleted devices 2026-02-16 17:00:29 +00:00
Jan Bouwhuis 42308f8b68 Merge branch 'dev' into homewizard-usage 2026-02-13 19:07:51 +01:00
jbouwh 21bf96e1ad Add test cases for energy monitors without production energy 2026-02-12 17:15:53 +00:00
jbouwh 365bd95963 Test disabled sensors with usage option set 2026-02-11 08:20:40 +00:00
jbouwh d889217944 Test setting up engergy plug via v1 API 2026-02-09 16:27:47 +00:00
jbouwh 6b8915dcba Allow to configure usage to determine default sensors during homewizard power monitoring setup 2026-02-09 13:35:31 +00:00
14 changed files with 800 additions and 86 deletions
@@ -25,15 +25,36 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import AbortFlow from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import instance_id from homeassistant.helpers import instance_id
from homeassistant.helpers.selector import TextSelector from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
)
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import CONF_PRODUCT_NAME, CONF_PRODUCT_TYPE, CONF_SERIAL, DOMAIN, LOGGER from .const import (
CONF_PRODUCT_NAME,
CONF_PRODUCT_TYPE,
CONF_SERIAL,
CONF_USAGE,
DOMAIN,
ENERGY_MONITORING_DEVICES,
LOGGER,
)
USAGE_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=["consumption", "generation"],
translation_key="usage",
mode=SelectSelectorMode.LIST,
)
)
class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for P1 meter.""" """Handle a config flow for HomeWizard devices."""
VERSION = 1 VERSION = 1
@@ -41,6 +62,8 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
product_name: str | None = None product_name: str | None = None
product_type: str | None = None product_type: str | None = None
serial: str | None = None serial: str | None = None
token: str | None = None
usage: str | None = None
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@@ -62,6 +85,12 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
f"{device_info.product_type}_{device_info.serial}" f"{device_info.product_type}_{device_info.serial}"
) )
self._abort_if_unique_id_configured(updates=user_input) self._abort_if_unique_id_configured(updates=user_input)
if device_info.product_type in ENERGY_MONITORING_DEVICES:
self.ip_address = user_input[CONF_IP_ADDRESS]
self.product_name = device_info.product_name
self.product_type = device_info.product_type
self.serial = device_info.serial
return await self.async_step_usage()
return self.async_create_entry( return self.async_create_entry(
title=f"{device_info.product_name}", title=f"{device_info.product_name}",
data=user_input, data=user_input,
@@ -80,6 +109,45 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_usage(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step where we ask how the energy monitor is used."""
assert self.ip_address
assert self.product_name
assert self.product_type
assert self.serial
data: dict[str, Any] = {CONF_IP_ADDRESS: self.ip_address}
if self.token:
data[CONF_TOKEN] = self.token
if user_input is not None:
return self.async_create_entry(
title=f"{self.product_name}",
data=data | user_input,
)
return self.async_show_form(
step_id="usage",
data_schema=vol.Schema(
{
vol.Required(
CONF_USAGE,
default=user_input.get(CONF_USAGE)
if user_input is not None
else "consumption",
): USAGE_SELECTOR,
}
),
description_placeholders={
CONF_PRODUCT_NAME: self.product_name,
CONF_PRODUCT_TYPE: self.product_type,
CONF_SERIAL: self.serial,
CONF_IP_ADDRESS: self.ip_address,
},
)
async def async_step_authorize( async def async_step_authorize(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
@@ -99,8 +167,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
# Now we got a token, we can ask for some more info # 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 HomeWizardEnergyV2(self.ip_address, token=token).device()
device_info = await api.device()
data = { data = {
CONF_IP_ADDRESS: self.ip_address, CONF_IP_ADDRESS: self.ip_address,
@@ -111,6 +178,14 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
f"{device_info.product_type}_{device_info.serial}" f"{device_info.product_type}_{device_info.serial}"
) )
self._abort_if_unique_id_configured(updates=data) self._abort_if_unique_id_configured(updates=data)
self.product_name = device_info.product_name
self.product_type = device_info.product_type
self.serial = device_info.serial
if device_info.product_type in ENERGY_MONITORING_DEVICES:
self.token = token
return await self.async_step_usage()
return self.async_create_entry( return self.async_create_entry(
title=f"{device_info.product_name}", title=f"{device_info.product_name}",
data=data, data=data,
@@ -137,6 +212,8 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured( self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: discovery_info.host} updates={CONF_IP_ADDRESS: discovery_info.host}
) )
if self.product_type in ENERGY_MONITORING_DEVICES:
return await self.async_step_usage()
return await self.async_step_discovery_confirm() return await self.async_step_discovery_confirm()
@@ -3,6 +3,8 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
from homewizard_energy.const import Model
from homeassistant.const import Platform from homeassistant.const import Platform
DOMAIN = "homewizard" DOMAIN = "homewizard"
@@ -20,5 +22,14 @@ LOGGER = logging.getLogger(__package__)
CONF_PRODUCT_NAME = "product_name" CONF_PRODUCT_NAME = "product_name"
CONF_PRODUCT_TYPE = "product_type" CONF_PRODUCT_TYPE = "product_type"
CONF_SERIAL = "serial" CONF_SERIAL = "serial"
CONF_USAGE = "usage"
UPDATE_INTERVAL = timedelta(seconds=5) UPDATE_INTERVAL = timedelta(seconds=5)
ENERGY_MONITORING_DEVICES = (
Model.ENERGY_SOCKET,
Model.ENERGY_METER_1_PHASE,
Model.ENERGY_METER_3_PHASE,
Model.ENERGY_METER_EASTRON_SDM230,
Model.ENERGY_METER_EASTRON_SDM630,
)
+30 -21
View File
@@ -37,7 +37,7 @@ from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance from homeassistant.util.variance import ignore_variance
from .const import DOMAIN from .const import CONF_USAGE, DOMAIN, ENERGY_MONITORING_DEVICES
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
from .entity import HomeWizardEntity from .entity import HomeWizardEntity
@@ -265,15 +265,6 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
enabled_fn=lambda data: data.measurement.energy_export_t4_kwh != 0, enabled_fn=lambda data: data.measurement.energy_export_t4_kwh != 0,
value_fn=lambda data: data.measurement.energy_export_t4_kwh or None, value_fn=lambda data: data.measurement.energy_export_t4_kwh or None,
), ),
HomeWizardSensorEntityDescription(
key="active_power_w",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda data: data.measurement.power_w is not None,
value_fn=lambda data: data.measurement.power_w,
),
HomeWizardSensorEntityDescription( HomeWizardSensorEntityDescription(
key="active_power_l1_w", key="active_power_l1_w",
translation_key="active_power_phase_w", translation_key="active_power_phase_w",
@@ -699,22 +690,30 @@ async def async_setup_entry(
entry: HomeWizardConfigEntry, entry: HomeWizardConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Initialize sensors.""" """Cleanup deleted entrity registry item."""
# Initialize default sensors
entities: list = [ entities: list = [
HomeWizardSensorEntity(entry.runtime_data, description) HomeWizardSensorEntity(entry.runtime_data, description)
for description in SENSORS for description in SENSORS
if description.has_fn(entry.runtime_data.data) if description.has_fn(entry.runtime_data.data)
] ]
active_power_sensor_description = HomeWizardSensorEntityDescription(
key="active_power_w",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
entity_registry_enabled_default=(
entry.runtime_data.data.device.product_type != Model.BATTERY
and entry.data.get(CONF_USAGE, "consumption") == "consumption"
),
has_fn=lambda x: True,
value_fn=lambda data: data.measurement.power_w,
)
# Add optional production power sensor for supported energy monitoring devices # Add optional production power sensor for supported energy monitoring devices
# or plug-in battery # or plug-in battery
if entry.runtime_data.data.device.product_type in ( if entry.runtime_data.data.device.product_type in (
Model.ENERGY_SOCKET, *ENERGY_MONITORING_DEVICES,
Model.ENERGY_METER_1_PHASE,
Model.ENERGY_METER_3_PHASE,
Model.ENERGY_METER_EASTRON_SDM230,
Model.ENERGY_METER_EASTRON_SDM630,
Model.BATTERY, Model.BATTERY,
): ):
active_prodution_power_sensor_description = HomeWizardSensorEntityDescription( active_prodution_power_sensor_description = HomeWizardSensorEntityDescription(
@@ -734,17 +733,27 @@ async def async_setup_entry(
is not None is not None
and total_export > 0 and total_export > 0
) )
or entry.data.get(CONF_USAGE, "consumption") == "generation"
), ),
has_fn=lambda x: True, has_fn=lambda x: True,
value_fn=lambda data: ( value_fn=lambda data: (
power_w * -1 if (power_w := data.measurement.power_w) else power_w power_w * -1 if (power_w := data.measurement.power_w) else power_w
), ),
) )
entities.append( entities.extend(
HomeWizardSensorEntity( (
entry.runtime_data, active_prodution_power_sensor_description HomeWizardSensorEntity(
entry.runtime_data, active_power_sensor_description
),
HomeWizardSensorEntity(
entry.runtime_data, active_prodution_power_sensor_description
),
) )
) )
elif (data := entry.runtime_data.data) and data.measurement.power_w is not None:
entities.append(
HomeWizardSensorEntity(entry.runtime_data, active_power_sensor_description)
)
# Initialize external devices # Initialize external devices
measurement = entry.runtime_data.data.measurement measurement = entry.runtime_data.data.measurement
@@ -41,6 +41,16 @@
}, },
"description": "Update configuration for {title}." "description": "Update configuration for {title}."
}, },
"usage": {
"data": {
"usage": "Usage"
},
"data_description": {
"usage": "This will enable either a power consumption or power production sensor the first time this device is set up."
},
"description": "What are you going to monitor with your {product_name} ({product_type} {serial} at {ip_address})?",
"title": "Usage"
},
"user": { "user": {
"data": { "data": {
"ip_address": "[%key:common::config_flow::data::ip%]" "ip_address": "[%key:common::config_flow::data::ip%]"
@@ -199,5 +209,13 @@
}, },
"title": "Update the authentication method for {title}" "title": "Update the authentication method for {title}"
} }
},
"selector": {
"usage": {
"options": {
"consumption": "Monitoring consumed energy",
"generation": "Monitoring generated energy"
}
}
} }
} }
@@ -0,0 +1,16 @@
{
"wifi_ssid": "My Wi-Fi",
"wifi_strength": 92,
"total_power_import_t1_kwh": 0.003,
"total_power_export_t1_kwh": 0.0,
"active_power_w": 0.0,
"active_power_l1_w": 0.0,
"active_voltage_v": 228.472,
"active_current_a": 0.273,
"active_apparent_current_a": 0.0,
"active_reactive_current_a": 0.0,
"active_apparent_power_va": 9.0,
"active_reactive_power_var": -9.0,
"active_power_factor": 0.611,
"active_frequency_hz": 50
}
@@ -0,0 +1,7 @@
{
"product_type": "HWE-KWH1",
"product_name": "kWh meter",
"serial": "5c2fafabcdef",
"firmware_version": "5.0103",
"api_version": "v2"
}
@@ -0,0 +1,3 @@
{
"cloud_enabled": true
}
@@ -0,0 +1,16 @@
{
"wifi_ssid": "My Wi-Fi",
"wifi_strength": 100,
"total_power_import_kwh": 0.003,
"total_power_import_t1_kwh": 0.003,
"total_power_export_kwh": 0.0,
"total_power_export_t1_kwh": 0.0,
"active_power_w": 0.0,
"active_power_l1_w": 0.0,
"active_voltage_v": 231.539,
"active_current_a": 0.0,
"active_reactive_power_var": 0.0,
"active_apparent_power_va": 0.0,
"active_power_factor": 0.0,
"active_frequency_hz": 50.005
}
@@ -0,0 +1,7 @@
{
"product_type": "HWE-SKT",
"product_name": "Energy Socket",
"serial": "5c2fafabcdef",
"firmware_version": "4.07",
"api_version": "v1"
}
@@ -0,0 +1,5 @@
{
"power_on": true,
"switch_lock": false,
"brightness": 255
}
@@ -0,0 +1,3 @@
{
"cloud_enabled": true
}
@@ -92,56 +92,7 @@
'version': 1, 'version': 1,
}) })
# --- # ---
# name: test_discovery_flow_works # name: test_manual_flow_works[HWE-P1]
FlowResultSnapshot({
'context': dict({
'confirm_only': True,
'dismiss_protected': True,
'source': 'zeroconf',
'title_placeholders': dict({
'name': 'Energy Socket (5c2fafabcdef)',
}),
'unique_id': 'HWE-SKT_5c2fafabcdef',
}),
'data': dict({
'ip_address': '127.0.0.1',
}),
'description': None,
'description_placeholders': None,
'flow_id': <ANY>,
'handler': 'homewizard',
'minor_version': 1,
'options': dict({
}),
'result': ConfigEntrySnapshot({
'data': dict({
'ip_address': '127.0.0.1',
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'homewizard',
'entry_id': <ANY>,
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'zeroconf',
'subentries': list([
]),
'title': 'Energy Socket',
'unique_id': 'HWE-SKT_5c2fafabcdef',
'version': 1,
}),
'subentries': tuple(
),
'title': 'Energy Socket',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1,
})
# ---
# name: test_manual_flow_works
FlowResultSnapshot({ FlowResultSnapshot({
'context': dict({ 'context': dict({
'source': 'user', 'source': 'user',
@@ -185,3 +136,238 @@
'version': 1, 'version': 1,
}) })
# --- # ---
# name: test_manual_flow_works_device_energy_monitoring[consumption-HWE-SKT-21]
FlowResultSnapshot({
'context': dict({
'source': 'user',
'unique_id': 'HWE-SKT_5c2fafabcdef',
}),
'data': dict({
'ip_address': '2.2.2.2',
'usage': 'consumption',
}),
'description': None,
'description_placeholders': None,
'flow_id': <ANY>,
'handler': 'homewizard',
'minor_version': 1,
'options': dict({
}),
'result': ConfigEntrySnapshot({
'data': dict({
'ip_address': '2.2.2.2',
'usage': 'consumption',
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'homewizard',
'entry_id': <ANY>,
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'user',
'subentries': list([
]),
'title': 'Energy Socket',
'unique_id': 'HWE-SKT_5c2fafabcdef',
'version': 1,
}),
'subentries': tuple(
),
'title': 'Energy Socket',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1,
})
# ---
# name: test_manual_flow_works_device_energy_monitoring[generation-HWE-SKT-21]
FlowResultSnapshot({
'context': dict({
'source': 'user',
'unique_id': 'HWE-SKT_5c2fafabcdef',
}),
'data': dict({
'ip_address': '2.2.2.2',
'usage': 'generation',
}),
'description': None,
'description_placeholders': None,
'flow_id': <ANY>,
'handler': 'homewizard',
'minor_version': 1,
'options': dict({
}),
'result': ConfigEntrySnapshot({
'data': dict({
'ip_address': '2.2.2.2',
'usage': 'generation',
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'homewizard',
'entry_id': <ANY>,
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'user',
'subentries': list([
]),
'title': 'Energy Socket',
'unique_id': 'HWE-SKT_5c2fafabcdef',
'version': 1,
}),
'subentries': tuple(
),
'title': 'Energy Socket',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1,
})
# ---
# name: test_power_monitoring_discovery_flow_works[consumption]
FlowResultSnapshot({
'context': dict({
'dismiss_protected': True,
'source': 'zeroconf',
'unique_id': 'HWE-SKT_5c2fafabcdef',
}),
'data': dict({
'ip_address': '127.0.0.1',
'usage': 'consumption',
}),
'description': None,
'description_placeholders': None,
'flow_id': <ANY>,
'handler': 'homewizard',
'minor_version': 1,
'options': dict({
}),
'result': ConfigEntrySnapshot({
'data': dict({
'ip_address': '127.0.0.1',
'usage': 'consumption',
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'homewizard',
'entry_id': <ANY>,
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'zeroconf',
'subentries': list([
]),
'title': 'Energy Socket',
'unique_id': 'HWE-SKT_5c2fafabcdef',
'version': 1,
}),
'subentries': tuple(
),
'title': 'Energy Socket',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1,
})
# ---
# name: test_power_monitoring_discovery_flow_works[generation]
FlowResultSnapshot({
'context': dict({
'dismiss_protected': True,
'source': 'zeroconf',
'unique_id': 'HWE-SKT_5c2fafabcdef',
}),
'data': dict({
'ip_address': '127.0.0.1',
'usage': 'generation',
}),
'description': None,
'description_placeholders': None,
'flow_id': <ANY>,
'handler': 'homewizard',
'minor_version': 1,
'options': dict({
}),
'result': ConfigEntrySnapshot({
'data': dict({
'ip_address': '127.0.0.1',
'usage': 'generation',
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'homewizard',
'entry_id': <ANY>,
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'zeroconf',
'subentries': list([
]),
'title': 'Energy Socket',
'unique_id': 'HWE-SKT_5c2fafabcdef',
'version': 1,
}),
'subentries': tuple(
),
'title': 'Energy Socket',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1,
})
# ---
# name: test_water_monitoring_discovery_flow_works
FlowResultSnapshot({
'context': dict({
'confirm_only': True,
'dismiss_protected': True,
'source': 'zeroconf',
'title_placeholders': dict({
'name': 'Watermeter',
}),
'unique_id': 'HWE-WTR_3c39efabcdef',
}),
'data': dict({
'ip_address': '127.0.0.1',
}),
'description': None,
'description_placeholders': None,
'flow_id': <ANY>,
'handler': 'homewizard',
'minor_version': 1,
'options': dict({
}),
'result': ConfigEntrySnapshot({
'data': dict({
'ip_address': '127.0.0.1',
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'homewizard',
'entry_id': <ANY>,
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'zeroconf',
'subentries': list([
]),
'title': 'Watermeter',
'unique_id': 'HWE-WTR_3c39efabcdef',
'version': 1,
}),
'subentries': tuple(
),
'title': 'Watermeter',
'type': <FlowResultType.CREATE_ENTRY: 'create_entry'>,
'version': 1,
})
# ---
+158 -9
View File
@@ -24,6 +24,7 @@ from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_setup_entry") @pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize(("device_fixture"), ["HWE-P1"])
async def test_manual_flow_works( async def test_manual_flow_works(
hass: HomeAssistant, hass: HomeAssistant,
mock_homewizardenergy: MagicMock, mock_homewizardenergy: MagicMock,
@@ -51,12 +52,50 @@ async def test_manual_flow_works(
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_homewizardenergy", "mock_setup_entry") @pytest.mark.usefixtures("mock_setup_entry")
async def test_discovery_flow_works( @pytest.mark.parametrize(("device_fixture"), ["HWE-SKT-21"])
@pytest.mark.parametrize(("usage"), ["consumption", "generation"])
async def test_manual_flow_works_device_energy_monitoring(
hass: HomeAssistant, hass: HomeAssistant,
mock_homewizardenergy: MagicMock,
mock_setup_entry: AsyncMock,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
usage: str,
) -> None: ) -> None:
"""Test discovery setup flow works.""" """Test config flow accepts user configuration for energy plug."""
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"
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"] == "usage"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"usage": usage}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result == snapshot
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_homewizardenergy.close.mock_calls) == 1
assert len(mock_homewizardenergy.device.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_homewizardenergy", "mock_setup_entry")
@pytest.mark.parametrize("usage", ["consumption", "generation"])
async def test_power_monitoring_discovery_flow_works(
hass: HomeAssistant, snapshot: SnapshotAssertion, usage: str
) -> None:
"""Test discovery energy monitoring setup flow works."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF}, context={"source": config_entries.SOURCE_ZEROCONF},
@@ -77,6 +116,42 @@ async def test_discovery_flow_works(
), ),
) )
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "usage"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"usage": usage}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result == snapshot
@pytest.mark.usefixtures("mock_homewizardenergy", "mock_setup_entry")
async def test_water_monitoring_discovery_flow_works(
hass: HomeAssistant, snapshot: SnapshotAssertion
) -> None:
"""Test discovery energy monitoring setup flow works."""
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="watermeter-ddeeff.local.",
type="",
name="",
properties={
"api_enabled": "1",
"path": "/api/v1",
"product_name": "Watermeter",
"product_type": "HWE-WTR",
"serial": "3c39efabcdef",
},
),
)
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "discovery_confirm" assert result["step_id"] == "discovery_confirm"
@@ -620,7 +695,7 @@ async def test_reconfigure_cannot_connect(
@pytest.mark.usefixtures("mock_setup_entry") @pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize(("device_fixture"), ["HWE-P1", "HWE-KWH1"]) @pytest.mark.parametrize(("device_fixture"), ["HWE-P1"])
async def test_manual_flow_works_with_v2_api_support( async def test_manual_flow_works_with_v2_api_support(
hass: HomeAssistant, hass: HomeAssistant,
mock_homewizardenergy_v2: MagicMock, mock_homewizardenergy_v2: MagicMock,
@@ -652,7 +727,70 @@ async def test_manual_flow_works_with_v2_api_support(
mock_homewizardenergy_v2.device.side_effect = None mock_homewizardenergy_v2.device.side_effect = None
mock_homewizardenergy_v2.get_token.side_effect = None mock_homewizardenergy_v2.get_token.side_effect = None
result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) 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.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")
@pytest.mark.parametrize(("device_fixture"), ["HWE-KWH1"])
async def test_manual_flow_energy_monitoring_works_with_v2_api_support(
hass: HomeAssistant,
mock_homewizardenergy_v2: MagicMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test config flow accepts user configuration for energy monitoring.
This should trigger authorization when v2 support is detected.
It should ask for usage if a energy monitoring device is configured.
"""
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
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"] == "usage"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"usage": "generation"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1
@@ -700,7 +838,16 @@ async def test_manual_flow_detects_failed_user_authorization(
result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY # Energy monitoring devices with an with configurable usage have an extra flow step
assert (
result["type"] is FlowResultType.CREATE_ENTRY and result["data"][CONF_TOKEN]
) or (result["type"] is FlowResultType.FORM and result["step_id"] == "usage")
if result["type"] is FlowResultType.FORM and result["step_id"] == "usage":
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"usage": "generation"}
)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@@ -830,10 +977,12 @@ async def test_discovery_with_v2_api_ask_authorization(
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "authorize" assert result["step_id"] == "authorize"
mock_homewizardenergy_v2.device.side_effect = None
mock_homewizardenergy_v2.get_token.side_effect = None 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"], {}) result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY # Energy monitoring devices with an with configurable usage have an extra flow step
assert result["data"][CONF_TOKEN] == "cool_token" assert (
result["type"] is FlowResultType.CREATE_ENTRY and result["data"][CONF_TOKEN]
) or (result["type"] is FlowResultType.FORM and result["step_id"] == "usage")
+208 -1
View File
@@ -1,5 +1,6 @@
"""Tests for the homewizard component.""" """Tests for the homewizard component."""
from collections.abc import Iterable
from datetime import timedelta from datetime import timedelta
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import weakref import weakref
@@ -11,8 +12,14 @@ import pytest
from homeassistant.components.homewizard import get_main_device from homeassistant.components.homewizard import get_main_device
from homeassistant.components.homewizard.const import DOMAIN from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.entity_platform import EntityRegistry
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@@ -254,3 +261,203 @@ async def test_disablederror_reloads_integration(
flow = flows[0] flow = flows[0]
assert flow.get("step_id") == "reauth_enable_api" assert flow.get("step_id") == "reauth_enable_api"
assert flow.get("handler") == DOMAIN assert flow.get("handler") == DOMAIN
@pytest.mark.usefixtures("mock_homewizardenergy")
@pytest.mark.parametrize(
("device_fixture", "mock_config_entry", "enabled", "disabled"),
[
(
"HWE-SKT-21-initial",
MockConfigEntry(
title="Device",
domain=DOMAIN,
data={
CONF_IP_ADDRESS: "127.0.0.1",
"usage": "consumption",
},
unique_id="HWE-SKT_5c2fafabcdef",
),
("sensor.device_power",),
("sensor.device_production_power",),
),
(
"HWE-SKT-21-initial",
MockConfigEntry(
title="Device",
domain=DOMAIN,
data={
CONF_IP_ADDRESS: "127.0.0.1",
"usage": "generation",
},
unique_id="HWE-SKT_5c2fafabcdef",
),
# we explicitly indicated that the device was monitoring
# generated energy, so we ignore power sensor to avoid confusion
("sensor.device_production_power",),
("sensor.device_power",),
),
(
"HWE-SKT-21",
MockConfigEntry(
title="Device",
domain=DOMAIN,
data={
CONF_IP_ADDRESS: "127.0.0.1",
"usage": "consumption",
},
unique_id="HWE-SKT_5c2fafabcdef",
),
# device has a non zero export, so both sensors are enabled
(
"sensor.device_power",
"sensor.device_production_power",
),
(),
),
(
"HWE-SKT-21",
MockConfigEntry(
title="Device",
domain=DOMAIN,
data={
CONF_IP_ADDRESS: "127.0.0.1",
"usage": "generation",
},
unique_id="HWE-SKT_5c2fafabcdef",
),
# we explicitly indicated that the device was monitoring
# generated energy, so we ignore power sensor to avoid confusion
("sensor.device_production_power",),
("sensor.device_power",),
),
],
ids=[
"consumption_intital",
"generation_initial",
"consumption_used",
"generation_used",
],
)
async def test_setup_device_energy_monitoring_v1(
hass: HomeAssistant,
entity_registry: EntityRegistry,
mock_config_entry: MockConfigEntry,
enabled: Iterable[str],
disabled: Iterable[str],
) -> None:
"""Test correct entities are enabled by default."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
for enabled_item in enabled:
assert (entry := entity_registry.async_get(enabled_item))
assert not entry.disabled
for disabled_item in disabled:
assert (entry := entity_registry.async_get(disabled_item))
assert entry.disabled
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
@pytest.mark.usefixtures("mock_homewizardenergy")
@pytest.mark.parametrize(
("device_fixture", "mock_config_entry", "enabled", "disabled"),
[
(
"HWE-KWH1-initial",
MockConfigEntry(
title="Device",
domain=DOMAIN,
data={
CONF_IP_ADDRESS: "127.0.0.1",
"usage": "consumption",
},
unique_id="HWE-KWH1_5c2fafabcdef",
),
("sensor.device_power",),
("sensor.device_production_power",),
),
(
"HWE-KWH1-initial",
MockConfigEntry(
title="Device",
domain=DOMAIN,
data={
CONF_IP_ADDRESS: "127.0.0.1",
"usage": "generation",
},
unique_id="HWE-KWH1_5c2fafabcdef",
),
# we explicitly indicated that the device was monitoring
# generated energy, so we ignore power sensor to avoid confusion
("sensor.device_production_power",),
("sensor.device_power",),
),
(
"HWE-KWH1",
MockConfigEntry(
title="Device",
domain=DOMAIN,
data={
CONF_IP_ADDRESS: "127.0.0.1",
"usage": "consumption",
},
unique_id="HWE-KWH1_5c2fafabcdef",
),
# device has a non zero export, so both sensors are enabled
(
"sensor.device_power",
"sensor.device_production_power",
),
(),
),
(
"HWE-KWH1",
MockConfigEntry(
title="Device",
domain=DOMAIN,
data={
CONF_IP_ADDRESS: "127.0.0.1",
"usage": "generation",
},
unique_id="HWE-KWH1_5c2fafabcdef",
),
# we explicitly indicated that the device was monitoring
# generated energy, so we ignore power sensor to avoid confusion
("sensor.device_production_power",),
("sensor.device_power",),
),
],
ids=[
"consumption_intital",
"generation_initial",
"consumption_used",
"generation_used",
],
)
async def test_setup_device_energy_monitoring_v2(
hass: HomeAssistant,
entity_registry: EntityRegistry,
mock_config_entry: MockConfigEntry,
enabled: Iterable[str],
disabled: Iterable[str],
) -> None:
"""Test correct entities are enabled by default."""
mock_config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.homewizard.config_flow.has_v2_api", return_value=True
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
for enabled_item in enabled:
assert (entry := entity_registry.async_get(enabled_item))
assert not entry.disabled
for disabled_item in disabled:
assert (entry := entity_registry.async_get(disabled_item))
assert entry.disabled
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION