Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
54 changes: 0 additions & 54 deletions homeassistant/components/nanogrid_air/api.py

This file was deleted.

12 changes: 7 additions & 5 deletions homeassistant/components/nanogrid_air/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

from __future__ import annotations

from ctek import NanogridAir
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_URL

from .api import fetch_mac, get_ip
from .const import DOMAIN

API_DEFAULT = "http://ctek-ng-air.local/meter/"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No longer needed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not here, but below. Conflig flow do not have a reason to use the API url any more?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed

Expand Down Expand Up @@ -37,8 +37,9 @@ async def async_step_user(self, user_input=None) -> ConfigFlowResult:
# Attempt to automatically detect the device
if user_input is None:
try:
if await get_ip():
mac_address = await fetch_mac()
if await NanogridAir().get_ip():
status = await NanogridAir().fetch_status()
mac_address = status.device_info.mac
if mac_address:
unique_id = mac_address
await self.async_set_unique_id(unique_id)
Expand All @@ -61,8 +62,9 @@ async def async_step_user(self, user_input=None) -> ConfigFlowResult:

# Handle user input
try:
if await get_ip(user_input[CONF_URL]):
mac_address = await fetch_mac()
if await NanogridAir(device_ip=user_input[CONF_URL]).get_ip():
status = await NanogridAir().fetch_status()
mac_address = status.device_info.mac
if mac_address:
unique_id = mac_address
await self.async_set_unique_id(unique_id)
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/nanogrid_air/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nanogrid_air",
"integration_type": "device",
"iot_class": "local_polling"
"iot_class": "local_polling",
"requirements": ["ctek==0.2.0"]
}
20 changes: 10 additions & 10 deletions homeassistant/components/nanogrid_air/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from datetime import timedelta

from ctek import NanogridAir

from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
Expand All @@ -24,8 +26,6 @@
DataUpdateCoordinator,
)

from .api import fetch_meter_data

SENSOR = {
"current_0": SensorEntityDescription(
key="current_0",
Expand Down Expand Up @@ -106,7 +106,7 @@ async def async_setup_entry(
"""Set up sensor entities for the integration entry."""

async def update_data():
return await fetch_meter_data()
return await NanogridAir().fetch_meter_data()

coordinator = DataUpdateCoordinator(
hass,
Expand Down Expand Up @@ -149,23 +149,23 @@ def __init__(
def native_value(self) -> str | None:
"""Return the state of the sensor."""
data = self.coordinator.data
if data is None:
if data is None or isinstance(data, dict):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why filter out dicts?

return None

if self._sensor_id.startswith("current_"):
index = int(self._sensor_id.split("_")[-1])
return data.get("current", [None, None, None])[index]
return getattr(data, "current", [None])[index]
if self._sensor_id.startswith("voltage_"):
index = int(self._sensor_id.split("_")[-1])
return data.get("voltage", [None, None, None])[index]
return getattr(data, "voltage", [None])[index]
if self._sensor_id == "power_in":
return data.get("activePowerIn", None)
return getattr(data, "active_power_in", None)
if self._sensor_id == "power_out":
return data.get("activePowerOut", None)
return getattr(data, "active_power_out", None)
if self._sensor_id == "total_energy_import":
return data.get("totalEnergyActiveImport", None)
return getattr(data, "total_energy_active_import", None)
if self._sensor_id == "total_energy_export":
return data.get("totalEnergyActiveExport", None)
return getattr(data, "total_energy_active_export", None)
return None

@property
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,9 @@ crownstone-sse==2.0.4
# homeassistant.components.crownstone
crownstone-uart==2.1.0

# homeassistant.components.nanogrid_air
ctek==0.2.0

# homeassistant.components.datadog
datadog==0.15.0

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,9 @@ crownstone-sse==2.0.4
# homeassistant.components.crownstone
crownstone-uart==2.1.0

# homeassistant.components.nanogrid_air
ctek==0.2.0

# homeassistant.components.datadog
datadog==0.15.0

Expand Down
95 changes: 38 additions & 57 deletions tests/components/nanogrid_air/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@
from homeassistant.data_entry_flow import FlowResultType


class MockStatus: # noqa: D101
def __init__(self, mac=None) -> None: # noqa: D107
self.device_info = MockDeviceInfo(mac)


class MockDeviceInfo: # noqa: D101
def __init__(self, mac) -> None: # noqa: D107
self.mac = mac


@pytest.mark.parametrize(
(
"get_ip_return",
"fetch_mac_return",
"fetch_status_return",
"result_type",
"title",
"data",
Expand All @@ -23,17 +33,17 @@
[
(
True,
"00:11:22:33:44:55",
MockStatus(mac="00:11:22:33:44:55"),
FlowResultType.CREATE_ENTRY,
"Nanogrid Air",
{CONF_URL: "http://ctek-ng-air.local/meter/"},
None,
),
(False, None, FlowResultType.FORM, None, None, {"base": "cannot_connect"}),
(True, None, FlowResultType.FORM, None, None, {"base": "invalid_auth"}),
(True, MockStatus(), FlowResultType.FORM, None, None, {"base": "invalid_auth"}),
(
False,
"00:11:22:33:44:55",
MockStatus(mac="00:11:22:33:44:55"),
FlowResultType.FORM,
None,
None,
Expand All @@ -45,22 +55,16 @@ async def test_form_auto_detect(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
get_ip_return,
fetch_mac_return,
fetch_status_return,
result_type,
title,
data,
expected_errors,
) -> None:
"""Test auto-detect scenarios with different return values."""
with (
patch(
"homeassistant.components.nanogrid_air.config_flow.get_ip",
return_value=get_ip_return,
),
patch(
"homeassistant.components.nanogrid_air.config_flow.fetch_mac",
return_value=fetch_mac_return,
),
patch("ctek.NanogridAir.get_ip", return_value=get_ip_return),
patch("ctek.NanogridAir.fetch_status", return_value=fetch_status_return),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
Expand All @@ -82,13 +86,10 @@ async def test_form_manual_entry_success(
"""Test manual entry success."""
user_input = {CONF_URL: "http://user-provided-url.local/meter/"}
with (
patch("ctek.NanogridAir.get_ip", return_value=True),
patch(
"homeassistant.components.nanogrid_air.config_flow.get_ip",
return_value=True,
),
patch(
"homeassistant.components.nanogrid_air.config_flow.fetch_mac",
return_value="00:11:22:33:44:55",
"ctek.NanogridAir.fetch_status",
return_value=MockStatus(mac="00:11:22:33:44:55"),
),
):
result = await hass.config_entries.flow.async_init(
Expand All @@ -101,16 +102,15 @@ async def test_form_manual_entry_success(
assert result["data"][CONF_URL] == "http://user-provided-url.local/meter/"
assert len(mock_setup_entry.mock_calls) == 1

# Additional checks for user inputs
assert CONF_URL in result["data"]


async def test_form_not_responding(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:
"""Test device not responding."""
with patch(
"homeassistant.components.nanogrid_air.config_flow.get_ip", return_value=False
):
with patch("ctek.NanogridAir.get_ip", return_value=False):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
Expand All @@ -125,14 +125,8 @@ async def test_form_invalid_mac(
) -> None:
"""Test invalid MAC address handling."""
with (
patch(
"homeassistant.components.nanogrid_air.config_flow.get_ip",
return_value=True,
),
patch(
"homeassistant.components.nanogrid_air.config_flow.fetch_mac",
return_value=None,
),
patch("ctek.NanogridAir.get_ip", return_value=True),
patch("ctek.NanogridAir.fetch_status", return_value=MockStatus()),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
Expand All @@ -144,26 +138,20 @@ async def test_form_invalid_mac(


@pytest.mark.parametrize(
("get_ip_return", "fetch_mac_return", "expected_errors"),
("get_ip_return", "fetch_status_return", "expected_errors"),
[
(False, None, {"base": "cannot_connect"}),
(True, None, {"base": "invalid_auth"}),
(True, MockStatus(), {"base": "invalid_auth"}),
],
)
async def test_form_error_handling(
hass: HomeAssistant, get_ip_return, fetch_mac_return, expected_errors
hass: HomeAssistant, get_ip_return, fetch_status_return, expected_errors
) -> None:
"""Test handling various error scenarios."""
user_input = {CONF_URL: "http://user-provided-url.local/meter/"}
with (
patch(
"homeassistant.components.nanogrid_air.config_flow.get_ip",
return_value=get_ip_return,
),
patch(
"homeassistant.components.nanogrid_air.config_flow.fetch_mac",
return_value=fetch_mac_return,
),
patch("ctek.NanogridAir.get_ip", return_value=get_ip_return),
patch("ctek.NanogridAir.fetch_status", return_value=fetch_status_return),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input
Expand All @@ -178,10 +166,7 @@ async def test_form_network_failure(
hass: HomeAssistant, mock_setup_entry: AsyncMock
) -> None:
"""Test handling network failures."""
with patch(
"homeassistant.components.nanogrid_air.config_flow.get_ip",
side_effect=ConnectionError,
):
with patch("ctek.NanogridAir.get_ip", side_effect=ConnectionError):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
Expand All @@ -198,20 +183,18 @@ async def test_form_user_input_exception(
user_input = {CONF_URL: "http://user-provided-url.local/meter/"}

with (
patch("ctek.NanogridAir.get_ip", side_effect=Exception("Test exception")),
patch(
"homeassistant.components.nanogrid_air.config_flow.get_ip",
side_effect=Exception("Test exception"),
),
patch(
"homeassistant.components.nanogrid_air.config_flow.fetch_mac",
return_value="00:11:22:33:44:55",
"ctek.NanogridAir.fetch_status",
return_value=MockStatus(mac="00:11:22:33:44:55"),
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input
)
await hass.async_block_till_done()

# Check that the flow was aborted due to the exception
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "not_responding"

Expand All @@ -223,19 +206,17 @@ async def test_form_user_input_connection_error(
user_input = {CONF_URL: "http://user-provided-url.local/meter/"}

with (
patch("ctek.NanogridAir.get_ip", side_effect=ConnectionError),
patch(
"homeassistant.components.nanogrid_air.config_flow.get_ip",
side_effect=ConnectionError,
),
patch(
"homeassistant.components.nanogrid_air.config_flow.fetch_mac",
return_value="00:11:22:33:44:55",
"ctek.NanogridAir.fetch_status",
return_value=MockStatus(mac="00:11:22:33:44:55"),
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input
)
await hass.async_block_till_done()

# Check that the flow was aborted due to the connection error
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "not_responding"