Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for HomeWizard v2 API configuration flow #135414

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c3b1bd5
Move HomeWizard API initialization to async_setup_entry
DCSBL Jan 10, 2025
187780e
Add support for HomeWizard v2 via user config flow
DCSBL Jan 10, 2025
fd945ce
Update quality_scale
DCSBL Jan 11, 2025
8e6eafa
Get config_flow coverage back to 100%
DCSBL Jan 11, 2025
6537346
Remove unused fixtures
DCSBL Jan 11, 2025
549b3fc
Use token according to specs in fixtures
DCSBL Jan 11, 2025
61c0941
Merge branch 'dev' into homewizard-user-v2-support
DCSBL Jan 15, 2025
f283442
Move HomeWizard config flow parameters inside class
DCSBL Jan 16, 2025
93a0f9f
Don't lie about type and fix typing checks
DCSBL Jan 16, 2025
ea39f83
Merge branch 'dev' into move-homewizard-config-options-to-class
DCSBL Jan 16, 2025
ddcde31
Merge branch 'move-homewizard-config-options-to-class' into homewizar…
DCSBL Jan 16, 2025
9b71260
Adjust typing for async_step_authorize::token
DCSBL Jan 16, 2025
923c8d4
Add description for authorize step
DCSBL Jan 16, 2025
3b9592c
Restore unrelated changes
DCSBL Jan 16, 2025
8868fc1
Merge branch 'dev' of github.com:home-assistant/core into homewizard-…
DCSBL Jan 16, 2025
e6eee5d
Update tests/components/homewizard/test_config_flow.py
DCSBL Jan 19, 2025
7c33e4c
Update quality_scale
DCSBL Jan 19, 2025
9ccb2a3
Remove unused snapshots
DCSBL Jan 19, 2025
4339426
Remove unused fixtures and rename token.json
DCSBL Jan 19, 2025
81a6353
Update homeassistant/components/homewizard/config_flow.py
DCSBL Jan 19, 2025
4576715
Close unclosed clientsessions
DCSBL Jan 19, 2025
4f90cfa
Merge branch 'dev' into homewizard-user-v2-support
DCSBL Jan 21, 2025
d058520
Also finish 'failed' test with CREATE_ENTRY
DCSBL Jan 21, 2025
c2e55e8
Also finish 'failed' test with CREATE_ENTRY
DCSBL Jan 21, 2025
869f267
Merge branch 'dev' into homewizard-user-v2-support
DCSBL Jan 21, 2025
58cbb5e
Fix issue with invalid 'dhcp' import
DCSBL Jan 21, 2025
06b6903
Merge branch 'dev' into homewizard-user-v2-support
DCSBL Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 72 additions & 4 deletions homeassistant/components/homewizard/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,27 @@
from __future__ import annotations

from collections.abc import Mapping
import contextlib
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_PATH, CONF_TOKEN
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import TextSelector
Expand Down Expand Up @@ -50,6 +61,10 @@ async def async_step_user(
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}"
Expand All @@ -73,6 +88,46 @@ async def async_step_user(
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

api = HomeWizardEnergyV2(self.ip_address)
token: str | None = None
errors: dict[str, str] | None = None

# Tell device we want a token, user must now press the button within 30 seconds
# The first attempt will always fail and raise DisabledError but this opens the window to press the button
with contextlib.suppress(DisabledError):
token = await api.get_token("home-assistant")

if token is None:
errors = {"base": "authorization_failed"}

if user_input is None or token is None:
await api.close()
return self.async_show_form(step_id="authorize", errors=errors)

# Now we got a token, we can ask for some more info
device_info = await api.device()
await api.close()

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:
Expand Down Expand Up @@ -113,6 +168,8 @@ async def async_step_dhcp(
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}"
Expand Down Expand Up @@ -237,7 +294,15 @@ async def _async_try_connect(ip_address: str) -> Device:
Make connection with device to test the connection
and to get info for unique_id.
"""
energy_api = HomeWizardEnergyV1(ip_address)

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()

Expand All @@ -255,6 +320,9 @@ async def _async_try_connect(ip_address: str) -> Device:
"Device unreachable or unexpected response", "network_error"
) from ex

except UnauthorizedError as ex:
raise UnauthorizedError("Unauthorized") from ex

except Exception as ex:
LOGGER.exception("Unexpected exception")
raise AbortFlow("unknown_error") from ex
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homewizard/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"documentation": "https://www.home-assistant.io/integrations/homewizard",
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"quality_scale": "bronze",
DCSBL marked this conversation as resolved.
Show resolved Hide resolved
"requirements": ["python-homewizard-energy==v8.1.1"],
"zeroconf": ["_hwenergy._tcp.local."]
}
16 changes: 11 additions & 5 deletions homeassistant/components/homewizard/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,20 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
reauthentication-flow:
status: todo
comment: |
Reauthorization for v2 API token revocation needs to be implemented.
test-coverage: done

# Gold
devices: done
diagnostics: done
discovery-update-info: done
discovery: done
discovery:
status: todo
comment: |
Zeroconf discovery is not implemented for v2 API.
DHCP IP address updates are not implemented for v2 API.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
Expand All @@ -67,9 +73,9 @@ rules:
icon-translations: done
reconfiguration-flow: done
repair-issues:
status: exempt
status: todo
comment: |
This integration does not raise any repairable issues.
Rejected token should trigger reconfiguration/repair issue
stale-devices:
status: exempt
comment: |
Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/homewizard/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"reauth_confirm": {
"description": "The local API is disabled. Go to the HomeWizard Energy app and enable the API in the device settings."
},
"authorize": {
"title": "Authorize",
"description": "Press the button on the HomeWizard Energy device, then click the button below."
frenck marked this conversation as resolved.
Show resolved Hide resolved
},
"reconfigure": {
"description": "Update configuration for {title}.",
"data": {
Expand All @@ -30,7 +34,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%]",
Expand Down
78 changes: 76 additions & 2 deletions tests/components/homewizard/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,6 +72,59 @@ def mock_homewizardenergy(
yield client


@pytest.fixture
def mock_homewizardenergy_v2(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels dirty, if you have ideas/examples how to improve this let me know!

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."""
Expand All @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions tests/components/homewizard/fixtures/v2/HWE-P1/device.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"product_type": "HWE-P1",
"product_name": "P1 meter",
"serial": "5c2fafabcdef",
"firmware_version": "4.19",
"api_version": "2.0.0"
}
48 changes: 48 additions & 0 deletions tests/components/homewizard/fixtures/v2/HWE-P1/measurement.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
8 changes: 8 additions & 0 deletions tests/components/homewizard/fixtures/v2/HWE-P1/system.json
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions tests/components/homewizard/fixtures/v2/generic/token.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"token": "00112233445566778899aabbccddeeff",
"name": "local/new_user"
}
Loading