Compare commits

..

11 Commits

Author SHA1 Message Date
Jan Bouwhuis fd76693d6e Merge branch 'dev' into homewizard-usage 2026-05-07 13:28:42 +02:00
Ronald van der Meer ff69557b17 Bump python-duco-client to 0.4.1 (#169991) 2026-05-07 13:26:22 +02:00
G Johansson 3b93ccc7ba Fix double reloading in unifi (#155147)
Co-authored-by: Copilot <copilot@github.com>
2026-05-07 13:09:30 +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
30 changed files with 905 additions and 385 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
"iot_class": "local_polling",
"loggers": ["duco"],
"quality_scale": "platinum",
"requirements": ["python-duco-client==0.4.0"],
"requirements": ["python-duco-client==0.4.1"],
"zeroconf": [
{
"name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*",
@@ -25,15 +25,36 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
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.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):
"""Handle a config flow for P1 meter."""
"""Handle a config flow for HomeWizard devices."""
VERSION = 1
@@ -41,6 +62,8 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
product_name: str | None = None
product_type: str | None = None
serial: str | None = None
token: str | None = None
usage: str | None = None
async def async_step_user(
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}"
)
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(
title=f"{device_info.product_name}",
data=user_input,
@@ -80,6 +109,45 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
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(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -99,8 +167,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
# 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()
device_info = await HomeWizardEnergyV2(self.ip_address, token=token).device()
data = {
CONF_IP_ADDRESS: self.ip_address,
@@ -111,6 +178,14 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
f"{device_info.product_type}_{device_info.serial}"
)
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(
title=f"{device_info.product_name}",
data=data,
@@ -137,6 +212,8 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(
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()
@@ -3,6 +3,8 @@
from datetime import timedelta
import logging
from homewizard_energy.const import Model
from homeassistant.const import Platform
DOMAIN = "homewizard"
@@ -20,5 +22,14 @@ LOGGER = logging.getLogger(__package__)
CONF_PRODUCT_NAME = "product_name"
CONF_PRODUCT_TYPE = "product_type"
CONF_SERIAL = "serial"
CONF_USAGE = "usage"
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.variance import ignore_variance
from .const import DOMAIN
from .const import CONF_USAGE, DOMAIN, ENERGY_MONITORING_DEVICES
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
from .entity import HomeWizardEntity
@@ -265,15 +265,6 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
enabled_fn=lambda data: data.measurement.energy_export_t4_kwh != 0,
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(
key="active_power_l1_w",
translation_key="active_power_phase_w",
@@ -699,22 +690,30 @@ async def async_setup_entry(
entry: HomeWizardConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize sensors."""
# Initialize default sensors
"""Cleanup deleted entrity registry item."""
entities: list = [
HomeWizardSensorEntity(entry.runtime_data, description)
for description in SENSORS
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
# or plug-in battery
if entry.runtime_data.data.device.product_type in (
Model.ENERGY_SOCKET,
Model.ENERGY_METER_1_PHASE,
Model.ENERGY_METER_3_PHASE,
Model.ENERGY_METER_EASTRON_SDM230,
Model.ENERGY_METER_EASTRON_SDM630,
*ENERGY_MONITORING_DEVICES,
Model.BATTERY,
):
active_prodution_power_sensor_description = HomeWizardSensorEntityDescription(
@@ -734,17 +733,27 @@ async def async_setup_entry(
is not None
and total_export > 0
)
or entry.data.get(CONF_USAGE, "consumption") == "generation"
),
has_fn=lambda x: True,
value_fn=lambda data: (
power_w * -1 if (power_w := data.measurement.power_w) else power_w
),
)
entities.append(
HomeWizardSensorEntity(
entry.runtime_data, active_prodution_power_sensor_description
entities.extend(
(
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
measurement = entry.runtime_data.data.measurement
@@ -41,6 +41,16 @@
},
"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": {
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]"
@@ -199,5 +209,13 @@
},
"title": "Update the authentication method for {title}"
}
},
"selector": {
"usage": {
"options": {
"consumption": "Monitoring consumed energy",
"generation": "Monitoring generated energy"
}
}
}
}
+1 -24
View File
@@ -2,7 +2,7 @@
import logging
from homeassistant.const import CONF_NAME, Platform
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
@@ -61,29 +61,6 @@ async def async_unload_entry(
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: MetWeatherConfigEntry
) -> bool:
"""Migrate old config entry."""
if config_entry.version > 1:
return False
if config_entry.version == 1 and config_entry.minor_version < 2:
data = dict(config_entry.data)
title = config_entry.title
if name := data.pop(CONF_NAME, None):
title = title or name
hass.config_entries.async_update_entry(
config_entry,
data=data,
title=title,
minor_version=2,
)
return True
async def cleanup_old_device(hass: HomeAssistant) -> None:
"""Cleanup device without proper device identifier."""
device_reg = dr.async_get(hass)
+11 -40
View File
@@ -1,6 +1,5 @@
"""Config flow to configure Met component."""
from collections.abc import Mapping
from typing import Any
import voluptuous as vol
@@ -15,6 +14,7 @@ from homeassistant.const import (
CONF_ELEVATION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
UnitOfLength,
)
from homeassistant.core import HomeAssistant, callback
@@ -29,37 +29,11 @@ from .const import (
CONF_TRACK_HOME,
DEFAULT_HOME_LATITUDE,
DEFAULT_HOME_LONGITUDE,
DEFAULT_NAME,
DOMAIN,
HOME_LOCATION_NAME,
)
def _location_data(user_input: dict[str, Any]) -> dict[str, Any]:
"""Return config entry data for a fixed location."""
return {
CONF_LATITUDE: user_input[CONF_LATITUDE],
CONF_LONGITUDE: user_input[CONF_LONGITUDE],
CONF_ELEVATION: user_input[CONF_ELEVATION],
}
def _fixed_location_title(data: Mapping[str, Any]) -> str:
"""Return the generated title for a fixed location."""
return f"{DEFAULT_NAME} ({data[CONF_LATITUDE]}, {data[CONF_LONGITUDE]})"
def _should_update_generated_title(config_entry: ConfigEntry) -> bool:
"""Return if the entry title still matches an integration-generated title."""
if not config_entry.title:
return True
if config_entry.data.get(CONF_TRACK_HOME, False):
return config_entry.title == HOME_LOCATION_NAME
return config_entry.title == _fixed_location_title(config_entry.data)
@callback
def configured_instances(hass: HomeAssistant) -> set[str]:
"""Return a set of configured met.no instances."""
@@ -82,6 +56,7 @@ def _get_data_schema(
if config_entry is None or config_entry.data.get(CONF_TRACK_HOME, False):
return vol.Schema(
{
vol.Required(CONF_NAME, default=HOME_LOCATION_NAME): str,
vol.Required(CONF_LATITUDE, default=hass.config.latitude): cv.latitude,
vol.Required(
CONF_LONGITUDE, default=hass.config.longitude
@@ -99,6 +74,7 @@ def _get_data_schema(
# Not tracking home, default values come from config entry
return vol.Schema(
{
vol.Required(CONF_NAME, default=config_entry.data.get(CONF_NAME)): str,
vol.Required(
CONF_LATITUDE, default=config_entry.data.get(CONF_LATITUDE)
): cv.latitude,
@@ -121,7 +97,6 @@ class MetConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Met component."""
VERSION = 1
MINOR_VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -130,15 +105,14 @@ class MetConfigFlowHandler(ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
data = _location_data(user_input)
if (
f"{data[CONF_LATITUDE]}-{data[CONF_LONGITUDE]}"
f"{user_input.get(CONF_LATITUDE)}-{user_input.get(CONF_LONGITUDE)}"
not in configured_instances(self.hass)
):
return self.async_create_entry(
title=_fixed_location_title(data), data=data
title=user_input[CONF_NAME], data=user_input
)
errors["base"] = "already_configured"
errors[CONF_NAME] = "already_configured"
return self.async_show_form(
step_id="user",
@@ -180,16 +154,13 @@ class MetOptionsFlowHandler(OptionsFlowWithReload):
"""Configure options for Met."""
if user_input is not None:
data = _location_data(user_input)
title = (
_fixed_location_title(data)
if _should_update_generated_title(self.config_entry)
else self.config_entry.title
)
# Update config entry with data from user input
self.hass.config_entries.async_update_entry(
self.config_entry, data=data, title=title
self.config_entry, data=user_input
)
return self.async_create_entry(
title=self.config_entry.title, data=user_input
)
return self.async_create_entry(title=title, data=data)
return self.async_show_form(
step_id="init",
-2
View File
@@ -38,8 +38,6 @@ from homeassistant.components.weather import (
DOMAIN = "met"
DEFAULT_NAME = "Met.no"
HOME_LOCATION_NAME = "Home"
CONF_TRACK_HOME = "track_home"
+4 -2
View File
@@ -11,7 +11,8 @@
"data": {
"elevation": "[%key:common::config_flow::data::elevation%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
"longitude": "[%key:common::config_flow::data::longitude%]",
"name": "[%key:common::config_flow::data::name%]"
},
"description": "Meteorologisk institutt",
"title": "[%key:common::config_flow::data::location%]"
@@ -29,7 +30,8 @@
"data": {
"elevation": "[%key:common::config_flow::data::elevation%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
"longitude": "[%key:common::config_flow::data::longitude%]",
"name": "[%key:common::config_flow::data::name%]"
},
"title": "[%key:common::config_flow::data::location%]"
}
+14 -10
View File
@@ -1,7 +1,7 @@
"""Support for Met.no weather service."""
from collections.abc import Mapping
from typing import Any
from typing import TYPE_CHECKING, Any
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
@@ -23,6 +23,7 @@ from homeassistant.components.weather import (
from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
@@ -40,7 +41,6 @@ from .const import (
ATTR_MAP,
CONDITIONS_MAP,
CONF_TRACK_HOME,
DEFAULT_NAME,
DOMAIN,
FORECAST_MAP,
)
@@ -48,6 +48,8 @@ from .coordinator import MetDataUpdateCoordinator, MetWeatherConfigEntry
PARALLEL_UPDATES = 0
DEFAULT_NAME = "Met.no"
async def async_setup_entry(
hass: HomeAssistant,
@@ -58,14 +60,16 @@ async def async_setup_entry(
coordinator = config_entry.runtime_data
entity_registry = er.async_get(hass)
device_name: str
name: str | None
is_metric = hass.config.units is METRIC_SYSTEM
if config_entry.data.get(CONF_TRACK_HOME, False):
device_name = hass.config.location_name
name = hass.config.location_name
else:
device_name = config_entry.title or DEFAULT_NAME
name = config_entry.data.get(CONF_NAME, DEFAULT_NAME)
if TYPE_CHECKING:
assert isinstance(name, str)
entities = [MetWeather(coordinator, config_entry, device_name, is_metric)]
entities = [MetWeather(coordinator, config_entry, name, is_metric)]
# Remove hourly entity from legacy config entries
if hourly_entity_id := entity_registry.async_get_entity_id(
@@ -105,7 +109,6 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
"Meteorological Institute."
)
_attr_has_entity_name = True
_attr_name = None
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
_attr_native_pressure_unit = UnitOfPressure.HPA
@@ -118,7 +121,7 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
self,
coordinator: MetDataUpdateCoordinator,
config_entry: MetWeatherConfigEntry,
device_name: str,
name: str,
is_metric: bool,
) -> None:
"""Initialise the platform with a data instance and site."""
@@ -127,14 +130,15 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
self._config = config_entry.data
self._is_metric = is_metric
self._attr_device_info = DeviceInfo(
name=device_name,
name="Forecast",
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, config_entry.entry_id)},
manufacturer=DEFAULT_NAME,
manufacturer="Met.no",
model="Forecast",
configuration_url="https://www.met.no/en",
)
self._attr_track_home = self._config.get(CONF_TRACK_HOME, False)
self._attr_name = name
@property
def condition(self) -> str | None:
@@ -166,7 +166,7 @@ class UnifiFlowHandler(ConfigFlow, domain=DOMAIN):
):
return self.async_abort(reason="already_configured")
return self.async_update_reload_and_abort(
return self.async_update_and_abort(
config_entry, data=self.config, reason=abort_reason
)
@@ -226,7 +226,7 @@ class UnifiFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(mac_address)
self._abort_if_unique_id_configured(updates=self.config)
self._abort_if_unique_id_configured(updates=self.config, reload_on_update=False)
self.context["title_placeholders"] = {
CONF_HOST: host,
+27
View File
@@ -5,6 +5,13 @@ from typing import TYPE_CHECKING
import aiounifi
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import (
@@ -129,6 +136,26 @@ class UnifiHub:
the entry might already have been reset and thus is not available.
"""
hub = config_entry.runtime_data
check_keys = {
CONF_HOST: "host",
CONF_PORT: "port",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
CONF_SITE_ID: "site",
CONF_VERIFY_SSL: "ssl_context",
}
for key, value in check_keys.items():
if key == CONF_VERIFY_SSL:
# ssl_context is either False or a SSLContext object, so we need to compare it differently
if config_entry.data[CONF_VERIFY_SSL] != bool(
getattr(hub.config, value)
):
hass.config_entries.async_schedule_reload(config_entry.entry_id)
return
if config_entry.data[key] != getattr(hub.config, value):
hass.config_entries.async_schedule_reload(config_entry.entry_id)
return
hub.config = UnifiConfig.from_config_entry(config_entry)
async_dispatcher_send(hass, hub.signal_options_update)
+1 -1
View File
@@ -2587,7 +2587,7 @@ python-digitalocean==1.13.2
python-dropbox-api==0.1.3
# homeassistant.components.duco
python-duco-client==0.4.0
python-duco-client==0.4.1
# homeassistant.components.ecobee
python-ecobee-api==0.3.2
+1 -1
View File
@@ -2213,7 +2213,7 @@ python-citybikes==0.3.3
python-dropbox-api==0.1.3
# homeassistant.components.duco
python-duco-client==0.4.0
python-duco-client==0.4.1
# homeassistant.components.ecobee
python-ecobee-api==0.3.2
@@ -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,
})
# ---
# name: test_discovery_flow_works
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
# name: test_manual_flow_works[HWE-P1]
FlowResultSnapshot({
'context': dict({
'source': 'user',
@@ -185,3 +136,238 @@
'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.parametrize(("device_fixture"), ["HWE-P1"])
async def test_manual_flow_works(
hass: HomeAssistant,
mock_homewizardenergy: MagicMock,
@@ -51,12 +52,50 @@ async def test_manual_flow_works(
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_homewizardenergy", "mock_setup_entry")
async def test_discovery_flow_works(
@pytest.mark.usefixtures("mock_setup_entry")
@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,
mock_homewizardenergy: MagicMock,
mock_setup_entry: AsyncMock,
snapshot: SnapshotAssertion,
usage: str,
) -> 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(
DOMAIN,
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["step_id"] == "discovery_confirm"
@@ -620,7 +695,7 @@ async def test_reconfigure_cannot_connect(
@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(
hass: HomeAssistant,
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.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 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"], {})
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(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["step_id"] == "authorize"
mock_homewizardenergy_v2.device.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"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"][CONF_TOKEN] == "cool_token"
# 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")
+208 -1
View File
@@ -1,5 +1,6 @@
"""Tests for the homewizard component."""
from collections.abc import Iterable
from datetime import timedelta
from unittest.mock import MagicMock, patch
import weakref
@@ -11,8 +12,14 @@ import pytest
from homeassistant.components.homewizard import get_main_device
from homeassistant.components.homewizard.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_IP_ADDRESS
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
@@ -254,3 +261,203 @@ async def test_disablederror_reloads_integration(
flow = flows[0]
assert flow.get("step_id") == "reauth_enable_api"
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
+5 -20
View File
@@ -2,23 +2,19 @@
from unittest.mock import patch
from homeassistant.components.met.const import (
CONF_TRACK_HOME,
DEFAULT_NAME,
DOMAIN,
HOME_LOCATION_NAME,
)
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.components.met.const import CONF_TRACK_HOME, DOMAIN
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def init_integration(
hass: HomeAssistant, track_home: bool = False, title: str | None = None
hass: HomeAssistant, track_home: bool = False
) -> MockConfigEntry:
"""Set up the Met integration in Home Assistant."""
entry_data = {
CONF_NAME: "test",
CONF_LATITUDE: 0,
CONF_LONGITUDE: 1.0,
CONF_ELEVATION: 1.0,
@@ -27,18 +23,7 @@ async def init_integration(
if track_home:
entry_data = {CONF_TRACK_HOME: True}
entry = MockConfigEntry(
domain=DOMAIN,
data=entry_data,
title=(
title
if title is not None
else HOME_LOCATION_NAME
if track_home
else f"{DEFAULT_NAME} (0, 1.0)"
),
minor_version=2,
)
entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
with patch(
"homeassistant.components.met.coordinator.metno.MetWeatherData.fetching_data",
return_value=True,
@@ -13,6 +13,7 @@
'elevation': 1.0,
'latitude': '**REDACTED**',
'longitude': '**REDACTED**',
'name': 'test',
}),
})
# ---
+11 -106
View File
@@ -7,13 +7,8 @@ from unittest.mock import ANY, patch
import pytest
from homeassistant import config_entries
from homeassistant.components.met.const import (
CONF_TRACK_HOME,
DEFAULT_NAME,
DOMAIN,
HOME_LOCATION_NAME,
)
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
@@ -61,6 +56,7 @@ async def test_flow_with_home_location(hass: HomeAssistant) -> None:
assert result["step_id"] == "user"
default_data = result["data_schema"]({})
assert default_data["name"] == HOME_LOCATION_NAME
assert default_data["latitude"] == 1
assert default_data["longitude"] == 2
assert default_data["elevation"] == 3
@@ -69,6 +65,7 @@ async def test_flow_with_home_location(hass: HomeAssistant) -> None:
async def test_create_entry(hass: HomeAssistant) -> None:
"""Test create entry from user input."""
test_data = {
"name": "home",
CONF_LONGITUDE: 0,
CONF_LATITUDE: 0,
CONF_ELEVATION: 0,
@@ -79,7 +76,7 @@ async def test_create_entry(hass: HomeAssistant) -> None:
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{DEFAULT_NAME} (0, 0)"
assert result["title"] == "home"
assert result["data"] == test_data
@@ -91,11 +88,12 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None:
"""
first_entry = MockConfigEntry(
domain="met",
data={CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_ELEVATION: 0},
data={"name": "home", CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_ELEVATION: 0},
)
first_entry.add_to_hass(hass)
test_data = {
"name": "home",
CONF_LONGITUDE: 0,
CONF_LATITUDE: 0,
CONF_ELEVATION: 0,
@@ -106,7 +104,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None:
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == "already_configured"
assert result["errors"]["name"] == "already_configured"
async def test_onboarding_step(hass: HomeAssistant) -> None:
@@ -124,7 +122,7 @@ async def test_onboarding_step(hass: HomeAssistant) -> None:
("latitude", "longitude"), [(52.3731339, 4.8903147), (0.0, 0.0)]
)
async def test_onboarding_step_abort_no_home(
hass: HomeAssistant, latitude: float, longitude: float
hass: HomeAssistant, latitude, longitude
) -> None:
"""Test entry not created when default step fails."""
await async_process_ha_core_config(
@@ -147,6 +145,7 @@ async def test_onboarding_step_abort_no_home(
async def test_options_flow(hass: HomeAssistant) -> None:
"""Test show options form."""
update_data = {
CONF_NAME: "test",
CONF_LATITUDE: 12,
CONF_LONGITUDE: 23,
CONF_ELEVATION: 456,
@@ -169,102 +168,8 @@ async def test_options_flow(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{DEFAULT_NAME} (12, 23)"
assert entry.title == f"{DEFAULT_NAME} (12, 23)"
assert result["title"] == "Mock Title"
assert result["data"] == update_data
weatherdatamock.assert_called_with(
{"lat": "12", "lon": "23", "msl": "456"}, ANY, api_url=ANY
)
@pytest.mark.disable_autouse_fixture
async def test_options_flow_updates_generated_fixed_location_title(
hass: HomeAssistant,
) -> None:
"""Test options flow updates a generated fixed-location title."""
update_data = {
CONF_LATITUDE: 12,
CONF_LONGITUDE: 23,
CONF_ELEVATION: 456,
}
entry = await init_integration(hass)
await hass.async_block_till_done()
with patch(
"homeassistant.components.met.coordinator.metno.MetWeatherData"
) as weatherdatamock:
result = await hass.config_entries.options.async_init(
entry.entry_id, data=update_data
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{DEFAULT_NAME} (12, 23)"
assert entry.title == f"{DEFAULT_NAME} (12, 23)"
assert entry.data == update_data
weatherdatamock.assert_called_with(
{"lat": "12", "lon": "23", "msl": "456"}, ANY, api_url=ANY
)
@pytest.mark.disable_autouse_fixture
async def test_options_flow_preserves_custom_title(hass: HomeAssistant) -> None:
"""Test options flow preserves a user-customized title."""
update_data = {
CONF_LATITUDE: 12,
CONF_LONGITUDE: 23,
CONF_ELEVATION: 456,
}
entry = await init_integration(hass, title="Custom title")
await hass.async_block_till_done()
with patch(
"homeassistant.components.met.coordinator.metno.MetWeatherData"
) as weatherdatamock:
result = await hass.config_entries.options.async_init(
entry.entry_id, data=update_data
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Custom title"
assert entry.title == "Custom title"
assert entry.data == update_data
weatherdatamock.assert_called_with(
{"lat": "12", "lon": "23", "msl": "456"}, ANY, api_url=ANY
)
@pytest.mark.disable_autouse_fixture
async def test_options_flow_tracking_home_entry_uses_fixed_location(
hass: HomeAssistant,
) -> None:
"""Test options flow switches a home-tracking entry to a fixed location."""
update_data = {
CONF_LATITUDE: 12,
CONF_LONGITUDE: 23,
CONF_ELEVATION: 456,
}
entry = await init_integration(hass, track_home=True)
await hass.async_block_till_done()
with patch(
"homeassistant.components.met.coordinator.metno.MetWeatherData"
) as weatherdatamock:
result = await hass.config_entries.options.async_init(
entry.entry_id, data=update_data
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"{DEFAULT_NAME} (12, 23)"
assert entry.title == f"{DEFAULT_NAME} (12, 23)"
assert result["data"] == update_data
assert entry.data == update_data
assert CONF_TRACK_HOME not in entry.data
weatherdatamock.assert_called_with(
{"lat": "12", "lon": "23", "msl": "456"}, ANY, api_url=ANY
)
+2 -50
View File
@@ -1,7 +1,5 @@
"""Test the Met integration init."""
from unittest.mock import MagicMock
import pytest
from homeassistant.components.met.const import (
@@ -9,16 +7,13 @@ from homeassistant.components.met.const import (
DEFAULT_HOME_LONGITUDE,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import device_registry as dr
from . import init_integration
from tests.common import MockConfigEntry
async def test_unload_entry(hass: HomeAssistant) -> None:
"""Test successful unload of entry."""
@@ -61,7 +56,7 @@ async def test_removing_incorrect_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
caplog: pytest.LogCaptureFixture,
mock_weather: MagicMock,
mock_weather,
) -> None:
"""Test we remove incorrect devices."""
entry = await init_integration(hass)
@@ -82,46 +77,3 @@ async def test_removing_incorrect_devices(
assert not device_registry.async_get_device(identifiers={(DOMAIN,)})
assert device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
assert "Removing improper device Forecast_legacy" in caplog.text
@pytest.mark.parametrize(
("title", "expected_title", "entity_id"),
[
("", "Somewhere", "weather.somewhere"),
("Custom title", "Custom title", "weather.custom_title"),
],
)
async def test_migrate_name_to_title(
hass: HomeAssistant,
mock_weather: MagicMock,
title: str,
expected_title: str,
entity_id: str,
) -> None:
"""Test legacy stored names migrate to the config entry title when needed."""
entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_USER,
data={
CONF_NAME: "Somewhere",
CONF_LATITUDE: 10,
CONF_LONGITUDE: 20,
CONF_ELEVATION: 0,
},
title=title,
minor_version=1,
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.version == 1
assert entry.minor_version == 2
assert entry.title == expected_title
assert entry.data == {
CONF_LATITUDE: 10,
CONF_LONGITUDE: 20,
CONF_ELEVATION: 0,
}
assert hass.states.async_entity_ids("weather") == [entity_id]
+10 -32
View File
@@ -1,10 +1,7 @@
"""Test Met weather entity."""
from unittest.mock import MagicMock
from homeassistant import config_entries
from homeassistant.components.met import DOMAIN
from homeassistant.components.met.const import DEFAULT_NAME
from homeassistant.components.weather import (
ATTR_CONDITION_CLOUDY,
ATTR_WEATHER_DEW_POINT,
@@ -16,7 +13,6 @@ from homeassistant.components.weather import (
ATTR_WEATHER_WIND_SPEED,
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -24,9 +20,7 @@ from . import init_integration
async def test_new_config_entry(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_weather: MagicMock,
hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_weather
) -> None:
"""Test the expected entities are created."""
await hass.config_entries.flow.async_init("met", context={"source": "onboarding"})
@@ -38,9 +32,7 @@ async def test_new_config_entry(
async def test_legacy_config_entry(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_weather: MagicMock,
hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_weather
) -> None:
"""Test the expected entities are created."""
entity_registry.async_get_or_create(
@@ -56,7 +48,7 @@ async def test_legacy_config_entry(
assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 1
async def test_weather(hass: HomeAssistant, mock_weather: MagicMock) -> None:
async def test_weather(hass: HomeAssistant, mock_weather) -> None:
"""Test states of the weather."""
await init_integration(hass)
@@ -75,19 +67,7 @@ async def test_weather(hass: HomeAssistant, mock_weather: MagicMock) -> None:
assert state.attributes[ATTR_WEATHER_UV_INDEX] == 1.1
async def test_fixed_location_uses_entry_title_as_main_entity_name(
hass: HomeAssistant, mock_weather: MagicMock
) -> None:
"""Test fixed-location weather entity uses the entry title as the entity name."""
await init_integration(hass)
state = hass.states.get("weather.met_no_0_1_0")
assert state
assert state.attributes[ATTR_FRIENDLY_NAME] == f"{DEFAULT_NAME} (0, 1.0)"
async def test_tracking_home(hass: HomeAssistant, mock_weather: MagicMock) -> None:
async def test_tracking_home(hass: HomeAssistant, mock_weather) -> None:
"""Test we track home."""
await hass.config_entries.flow.async_init("met", context={"source": "onboarding"})
await hass.async_block_till_done()
@@ -111,13 +91,13 @@ async def test_tracking_home(hass: HomeAssistant, mock_weather: MagicMock) -> No
assert len(hass.states.async_entity_ids("weather")) == 0
async def test_not_tracking_home(hass: HomeAssistant, mock_weather: MagicMock) -> None:
async def test_not_tracking_home(hass: HomeAssistant, mock_weather) -> None:
"""Test when we not track home."""
await hass.config_entries.flow.async_init(
"met",
context={"source": config_entries.SOURCE_USER},
data={"latitude": 10, "longitude": 20, "elevation": 0},
data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0},
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("weather")) == 1
@@ -136,9 +116,7 @@ async def test_not_tracking_home(hass: HomeAssistant, mock_weather: MagicMock) -
async def test_remove_hourly_entity(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_weather: MagicMock,
hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_weather
) -> None:
"""Test removing the hourly entity."""
@@ -157,8 +135,8 @@ async def test_remove_hourly_entity(
await hass.config_entries.flow.async_init(
"met",
context={"source": config_entries.SOURCE_USER},
data={"latitude": 10, "longitude": 20, "elevation": 0},
data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0},
)
await hass.async_block_till_done()
assert hass.states.async_entity_ids("weather") == ["weather.met_no_10_20"]
assert list(entity_registry.entities.keys()) == ["weather.met_no_10_20"]
assert hass.states.async_entity_ids("weather") == ["weather.forecast_somewhere"]
assert list(entity_registry.entities.keys()) == ["weather.forecast_somewhere"]
+14 -8
View File
@@ -475,14 +475,18 @@ async def test_simple_option_flow(
assert result["step_id"] == "simple_options"
assert result["last_step"]
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]],
},
)
with patch(
"homeassistant.config_entries.ConfigEntries.async_schedule_reload"
) as mock_reload:
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_TRACK_CLIENTS: False,
CONF_TRACK_DEVICES: False,
CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]],
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
@@ -490,6 +494,8 @@ async def test_simple_option_flow(
CONF_TRACK_DEVICES: False,
CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]],
}
# Check there is no reload on updating these options
assert mock_reload.call_count == 0
async def test_discover_unifi_positive(hass: HomeAssistant) -> None: