New core integration for VegeHub (#129598)

* Initial commit for VegeHub integration

* Moved several pieces to library, continuing.

* All device contact moved to library

* Updated documentation link

* Fixed an error in strings.json

* Removed commented out code and unused file

* Removed unneeded info logging, and a few missed lines of commented code

* Added/removed comments for clarity

* Converted integration to use webhooks.

* Update __init__.py to remove unnecessary code.

Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com>

* Remove unnecessary code from config_flow.py

Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com>

* Simplify unique_id assertion.

* Switch to CONF_ constant for user input

* Added explanation for passing exception.

* Got rid of try-except, since I don't really handle the exceptions her anyway.

* Moved data transform to vegehub library

* Changed references to use HA constants.

* Fixed assigning and returning _attr properties.

* Moved temperature sensor transform to the library.

* Moved sensor names to strings.json

* Made webhook names unique to avoid collisions when multiple devices are added.

* Converted to using entry.runtime_data

* Removed options flow for first PR

* Removed switch support to limit PR to one platform

* Removed/updated outdated tests

* Update homeassistant/components/vegehub/__init__.py

Co-authored-by: Josef Zweck <josef@zweck.dev>

* Got rid of strings in favor of constants.

* Got rid of unnecessary check

* Imported constant directly.

* Added custom type for entry

* Expanded CONF_ constants into sensor.py

* Get rid of extra `str` and `get`

Co-authored-by: Josef Zweck <josef@zweck.dev>

* Added type to errors

* Added try/except to MAC address retrieval

* Moved functionality out of ConfigFlow that shouldn't have been there

* Removed IP:MAC tracking from ConfigFlow

* Added retries to VegeHub PyPI package, and implemented them in integration

* Removed different sensor types for now

* Fixed typo

* Changed abort to error

* Fixed error reporting in config flow

* Further simplify sensor.py to handle all sensors the same

* Added comment to clarify

* Got rid of unused constants

* Removed unused strings in strings.json

* Added quality_scale.yaml

* Fixed problems in sensor init

* Moved config url and sw version storage into vegehub package

* Get rid of extra declaration

Co-authored-by: Josef Zweck <josef@zweck.dev>

* Removed unnecessary task

* Fix type for entry

* Added a test before setup

* Fixed tests and got test coverage of config flow to 100%

* Fixed test descriptions

* Implemented a coordinator

* Removed unused property

* Fixed a few minor issues with the coordinator implementation

* Removed unused function

* Fixed some tests

* Trying to fix a problem with re-initialization when server reboots. Mostly working.

* Moved hub.setup from async_setup_entry to config flow to avoid running it on system reboot

* Delete tests/testing_config/.storage/http.auth

* Fixed errors in coordinator.py

* Added IP validation for manual input IP addresses

* Moved data into self._discovered to simplify

* Removed redundant typing

* Shortened sensor unique ID and added coordinator handler

* Added call to super()._handle_coordinator_update() so state gets handled correctly

* Fixed == and is

* Got rid of "slot" and moved functionality to lib

* Got rid of mocked aiohttp calls in favor of just mocking the vegehub library

* Rewrote config flow to make more sense.

* Changed order of data and data_description

* Changes to sensor.py

* Got rid of async_update_data in coordinator and moved async_set_updated_data into webhook callback

* Changed sensor updates so that they keep using last known values if update doesn't contain data for them

* Changed config flow to use homeassistant.helpers.service_info zeroconf instead of homeassistant.components zeroconf

* Added types to test parameters

* Changes and notes in config_flow.py

* Minor fix to get existing tests working before making changes to tests

* Removed unused data and simplified data passing

* Fixed tests, removed unused data, moved sensor tests to snapshots

* Mocked async_setup_entry and async_unload_entry

* Eliminated retry step so that retries just happen in the user flow or zeroconf_confirm

* Bumped the library version

* Bumped library version again

* Changed test-before-setup test

* Improved use of coordinator

* Almost done reworking tests. A few more changes still needed.

* Added via device to sensor.py and key reference to strings.json

* Webhook tests are almost, but not quite, working

* Fully functional again

* Change error to assert

* made identifiers and via_device the same

* made the via_device just be the mac

* Fixed strings.json and updated translations

* Fixed test_sensor.py

* Cleaned up tests and added autouse to several fixtures to simplify

* Switched from error to assert, and added exemption to quality scale.

* Cleaned up some tests and added update of IP if unique ID of discovered device is the same.

* Improved zeroconfig to update IP and hostname, and added a test to make sure those work.

* Fixed a comment.

* Improved ip/hostname update test.

* Changed Hub to VegeHub in strings.json for clarity.

* Switched to using a base entity to simplify and make adding platforms in the future easier.

* Moved the vegehub object into the coordinator to simplify.

* Removed actuators from sensors, and added unique name for battery sensor

* Changed coordinator to manage its own data, changed sensors to use descriptions and return their value as a property

* Updated data retrieval keys

* Minor updates to several files

* Fixed a few things for pytest

* Reverted to explicit check for None for pytest

* Fixed a comment and a variable name

* Fixed a comment

* Fix

* Bumped depenency version to eliminate pytest from dependencies.

---------

Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Geoff
2025-06-23 22:55:34 -06:00
committed by GitHub
parent 0cf7952964
commit eff35e93bd
21 changed files with 1342 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
"""Tests for the Vegetronix VegeHub integration."""
from homeassistant.components.vegehub.coordinator import VegeHubConfigEntry
from homeassistant.core import HomeAssistant
async def init_integration(
hass: HomeAssistant,
config_entry: VegeHubConfigEntry,
) -> None:
"""Load the VegeHub integration."""
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@@ -0,0 +1,82 @@
"""Fixtures and test data for VegeHub test methods."""
from collections.abc import Generator
from typing import Any
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_WEBHOOK_ID,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
TEST_IP = "192.168.0.100"
TEST_UNIQUE_ID = "aabbccddeeff"
TEST_SERVER = "http://example.com"
TEST_MAC = "A1:B2:C3:D4:E5:F6"
TEST_SIMPLE_MAC = "A1B2C3D4E5F6"
TEST_HOSTNAME = "VegeHub"
TEST_WEBHOOK_ID = "webhook_id"
HUB_DATA = {
"first_boot": False,
"page_updated": False,
"error_message": 0,
"num_channels": 2,
"num_actuators": 2,
"version": "3.4.5",
"agenda": 1,
"batt_v": 9.0,
"num_vsens": 0,
"is_ac": 0,
"has_sd": 0,
"on_ap": 0,
}
@pytest.fixture(autouse=True)
def mock_vegehub() -> Generator[Any, Any, Any]:
"""Mock the VegeHub library."""
with patch(
"homeassistant.components.vegehub.config_flow.VegeHub", autospec=True
) as mock_vegehub_class:
mock_instance = mock_vegehub_class.return_value
# Simulate successful API calls
mock_instance.retrieve_mac_address = AsyncMock(return_value=True)
mock_instance.setup = AsyncMock(return_value=True)
# Mock properties
mock_instance.ip_address = TEST_IP
mock_instance.mac_address = TEST_SIMPLE_MAC
mock_instance.unique_id = TEST_UNIQUE_ID
mock_instance.url = f"http://{TEST_IP}"
mock_instance.info = load_fixture("vegehub/info_hub.json")
mock_instance.num_sensors = 2
mock_instance.num_actuators = 2
mock_instance.sw_version = "3.4.5"
yield mock_instance
@pytest.fixture(name="mocked_config_entry")
async def fixture_mocked_config_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Create a mock VegeHub config entry."""
return MockConfigEntry(
domain="vegehub",
data={
CONF_MAC: TEST_SIMPLE_MAC,
CONF_IP_ADDRESS: TEST_IP,
CONF_HOST: TEST_HOSTNAME,
CONF_DEVICE: HUB_DATA,
CONF_WEBHOOK_ID: TEST_WEBHOOK_ID,
},
unique_id=TEST_SIMPLE_MAC,
title="VegeHub",
entry_id="12345",
)

View File

@@ -0,0 +1,24 @@
{
"hub": {
"first_boot": false,
"page_updated": false,
"error_message": 0,
"num_channels": 2,
"num_actuators": 2,
"version": "3.4.5",
"agenda": 1,
"batt_v": 9.0,
"num_vsens": 0,
"is_ac": 0,
"has_sd": 0,
"on_ap": 0
},
"wifi": {
"ssid": "YourWiFiName",
"strength": "-29",
"chan": "4",
"ip": "192.168.0.100",
"status": "3",
"mac_addr": "A1:B2:C3:D4:E5:F6"
}
}

View File

@@ -0,0 +1,160 @@
# serializer version: 1
# name: test_sensor_entities[sensor.vegehub_battery_voltage-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.vegehub_battery_voltage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
'original_icon': None,
'original_name': 'Battery voltage',
'platform': 'vegehub',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_volts',
'unique_id': 'A1B2C3D4E5F6_battery',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
})
# ---
# name: test_sensor_entities[sensor.vegehub_battery_voltage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'VegeHub Battery voltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.vegehub_battery_voltage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.330000043',
})
# ---
# name: test_sensor_entities[sensor.vegehub_input_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.vegehub_input_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
'original_icon': None,
'original_name': 'Input 1',
'platform': 'vegehub',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'analog_sensor',
'unique_id': 'A1B2C3D4E5F6_analog_0',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
})
# ---
# name: test_sensor_entities[sensor.vegehub_input_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'VegeHub Input 1',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.vegehub_input_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.5',
})
# ---
# name: test_sensor_entities[sensor.vegehub_input_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.vegehub_input_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
'original_icon': None,
'original_name': 'Input 2',
'platform': 'vegehub',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'analog_sensor',
'unique_id': 'A1B2C3D4E5F6_analog_1',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
})
# ---
# name: test_sensor_entities[sensor.vegehub_input_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'VegeHub Input 2',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.vegehub_input_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.45599997',
})
# ---

View File

@@ -0,0 +1,385 @@
"""Tests for VegeHub config flow."""
from collections.abc import Generator
from ipaddress import ip_address
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.vegehub.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_WEBHOOK_ID,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .conftest import TEST_HOSTNAME, TEST_IP, TEST_SIMPLE_MAC
from tests.common import MockConfigEntry
DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo(
ip_address=ip_address(TEST_IP),
ip_addresses=[ip_address(TEST_IP)],
port=80,
hostname=f"{TEST_HOSTNAME}.local.",
type="mock_type",
name="myVege",
properties={
zeroconf.ATTR_PROPERTIES_ID: TEST_HOSTNAME,
"version": "5.1.1",
},
)
@pytest.fixture(autouse=True)
def mock_setup_entry() -> Generator[Any, Any, Any]:
"""Prevent the actual integration from being set up."""
with (
patch("homeassistant.components.vegehub.async_setup_entry", return_value=True),
):
yield
# Tests for flows where the user manually inputs an IP address
async def test_user_flow_success(hass: HomeAssistant) -> None:
"""Test the user flow with successful configuration."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": 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: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_IP
assert result["data"][CONF_MAC] == TEST_SIMPLE_MAC
assert result["data"][CONF_IP_ADDRESS] == TEST_IP
assert result["data"][CONF_DEVICE] is not None
assert result["data"][CONF_WEBHOOK_ID] is not None
# Since this is user flow, there is no hostname, so hostname should be the IP address
assert result["data"][CONF_HOST] == TEST_IP
assert result["result"].unique_id == TEST_SIMPLE_MAC
# Confirm that the entry was created
entries = hass.config_entries.async_entries(domain=DOMAIN)
assert len(entries) == 1
async def test_user_flow_cannot_connect(
hass: HomeAssistant,
mock_vegehub: MagicMock,
) -> None:
"""Test the user flow with bad data."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
mock_vegehub.mac_address = ""
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == "cannot_connect"
mock_vegehub.mac_address = TEST_SIMPLE_MAC
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
@pytest.mark.parametrize(
("side_effect", "expected_error"),
[
(TimeoutError, "timeout_connect"),
(ConnectionError, "cannot_connect"),
],
)
async def test_user_flow_device_bad_connection_then_success(
hass: HomeAssistant,
mock_vegehub: MagicMock,
side_effect: Exception,
expected_error: str,
) -> None:
"""Test the user flow with a timeout."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
mock_vegehub.setup.side_effect = side_effect
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert "errors" in result
assert result["errors"] == {"base": expected_error}
mock_vegehub.setup.side_effect = None # Clear the error
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_IP
assert result["data"][CONF_IP_ADDRESS] == TEST_IP
assert result["data"][CONF_MAC] == TEST_SIMPLE_MAC
async def test_user_flow_no_ip_entered(hass: HomeAssistant) -> None:
"""Test the user flow with blank IP."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": 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: ""}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == "invalid_ip"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_user_flow_bad_ip_entered(hass: HomeAssistant) -> None:
"""Test the user flow with badly formed IP."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": 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: "192.168.0"}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == "invalid_ip"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_user_flow_duplicate_device(
hass: HomeAssistant, mocked_config_entry: MockConfigEntry
) -> None:
"""Test when user flow gets the same device twice."""
mocked_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.ABORT
# Tests for flows that start in zeroconf
async def test_zeroconf_flow_success(hass: HomeAssistant) -> None:
"""Test the zeroconf discovery flow with successful configuration."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
# Display the confirmation form
result = await hass.config_entries.flow.async_configure(result["flow_id"], None)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
# Proceed to creating the entry
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_HOSTNAME
assert result["data"][CONF_HOST] == TEST_HOSTNAME
assert result["data"][CONF_MAC] == TEST_SIMPLE_MAC
assert result["result"].unique_id == TEST_SIMPLE_MAC
async def test_zeroconf_flow_abort_device_asleep(
hass: HomeAssistant,
mock_vegehub: MagicMock,
) -> None:
"""Test when zeroconf tries to contact a device that is asleep."""
mock_vegehub.retrieve_mac_address.side_effect = TimeoutError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "timeout_connect"
async def test_zeroconf_flow_abort_same_id(
hass: HomeAssistant,
mocked_config_entry: MockConfigEntry,
) -> None:
"""Test when zeroconf gets the same device twice."""
mocked_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.ABORT
async def test_zeroconf_flow_abort_cannot_connect(
hass: HomeAssistant,
mock_vegehub: MagicMock,
) -> None:
"""Test when zeroconf gets bad data."""
mock_vegehub.mac_address = ""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_zeroconf_flow_abort_cannot_connect_404(
hass: HomeAssistant,
mock_vegehub: MagicMock,
) -> None:
"""Test when zeroconf gets bad responses."""
mock_vegehub.retrieve_mac_address.side_effect = ConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
@pytest.mark.parametrize(
("side_effect", "expected_error"),
[
(TimeoutError, "timeout_connect"),
(ConnectionError, "cannot_connect"),
],
)
async def test_zeroconf_flow_device_error_response(
hass: HomeAssistant,
mock_vegehub: MagicMock,
side_effect: Exception,
expected_error: str,
) -> None:
"""Test when zeroconf detects the device, but the communication fails at setup."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
# Part way through the process, we simulate getting bad responses
mock_vegehub.setup.side_effect = side_effect
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == expected_error
mock_vegehub.setup.side_effect = None
# Proceed to creating the entry
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_zeroconf_flow_update_ip_hostname(
hass: HomeAssistant,
mocked_config_entry: MockConfigEntry,
) -> None:
"""Test when zeroconf gets the same device with a new IP and hostname."""
mocked_config_entry.add_to_hass(hass)
# Use the same discovery info, but change the IP and hostname
new_ip = "192.168.0.99"
new_hostname = "new_hostname"
new_discovery_info = zeroconf.ZeroconfServiceInfo(
ip_address=ip_address(new_ip),
ip_addresses=[ip_address(new_ip)],
port=DISCOVERY_INFO.port,
hostname=f"{new_hostname}.local.",
type=DISCOVERY_INFO.type,
name=DISCOVERY_INFO.name,
properties=DISCOVERY_INFO.properties,
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=new_discovery_info,
)
assert result["type"] is FlowResultType.ABORT
# Check if the original config entry has been updated
entries = hass.config_entries.async_entries(domain=DOMAIN)
assert len(entries) == 1
assert mocked_config_entry.data[CONF_IP_ADDRESS] == new_ip
assert mocked_config_entry.data[CONF_HOST] == new_hostname

View File

@@ -0,0 +1,63 @@
"""Unit tests for the VegeHub integration's sensor.py."""
from unittest.mock import patch
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
from .conftest import TEST_SIMPLE_MAC, TEST_WEBHOOK_ID
from tests.common import MockConfigEntry, snapshot_platform
from tests.typing import ClientSessionGenerator
UPDATE_DATA = {
"api_key": "",
"mac": TEST_SIMPLE_MAC,
"error_code": 0,
"sensors": [
{"slot": 1, "samples": [{"v": 1.5, "t": "2025-01-15T16:51:23Z"}]},
{"slot": 2, "samples": [{"v": 1.45599997, "t": "2025-01-15T16:51:23Z"}]},
{"slot": 3, "samples": [{"v": 1.330000043, "t": "2025-01-15T16:51:23Z"}]},
{"slot": 4, "samples": [{"v": 0.075999998, "t": "2025-01-15T16:51:23Z"}]},
{"slot": 5, "samples": [{"v": 9.314800262, "t": "2025-01-15T16:51:23Z"}]},
],
"send_time": 1736959883,
"wifi_str": -27,
}
async def test_sensor_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
hass_client_no_auth: ClientSessionGenerator,
entity_registry: er.EntityRegistry,
mocked_config_entry: MockConfigEntry,
) -> None:
"""Test all entities."""
with patch("homeassistant.components.vegehub.PLATFORMS", [Platform.SENSOR]):
await init_integration(hass, mocked_config_entry)
assert TEST_WEBHOOK_ID in hass.data["webhook"], "Webhook was not registered"
# Verify the webhook handler
webhook_info = hass.data["webhook"][TEST_WEBHOOK_ID]
assert webhook_info["handler"], "Webhook handler is not set"
client = await hass_client_no_auth()
resp = await client.post(f"/api/webhook/{TEST_WEBHOOK_ID}", json=UPDATE_DATA)
# Send the same update again so that the coordinator modifies existing data
# instead of creating new data.
resp = await client.post(f"/api/webhook/{TEST_WEBHOOK_ID}", json=UPDATE_DATA)
# Wait for remaining tasks to complete.
await hass.async_block_till_done()
assert resp.status == 200, f"Unexpected status code: {resp.status}"
await snapshot_platform(
hass, entity_registry, snapshot, mocked_config_entry.entry_id
)