Replace apcaccess dependency with aioapcaccess in apcupsd (#104571)

* Replace apcaccess dependency with async version aioapcaccess

* Upgrade the dependency to the latest version (v0.4.2)

* Handle asyncio.IncompleteReadError
pull/105203/head
Yuxin Wang 2023-12-08 06:40:09 -05:00 committed by GitHub
parent 949ca6bafc
commit 88ddc25129
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 38 additions and 62 deletions

View File

@ -7,7 +7,7 @@ from datetime import timedelta
import logging
from typing import Final
from apcaccess import status
import aioapcaccess
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@ -90,13 +90,8 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
Note that the result dict uses upper case for each resource, where our
integration uses lower cases as keys internally.
"""
async with asyncio.timeout(10):
try:
raw = await self.hass.async_add_executor_job(
status.get, self._host, self._port
)
result: OrderedDict[str, str] = status.parse(raw)
return result
except OSError as error:
return await aioapcaccess.request_status(self._host, self._port)
except (OSError, asyncio.IncompleteReadError) as error:
raise UpdateFailed(error) from error

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["apcaccess"],
"quality_scale": "silver",
"requirements": ["apcaccess==0.0.13"]
"requirements": ["aioapcaccess==0.4.2"]
}

View File

@ -196,6 +196,9 @@ aioairzone==0.6.9
# homeassistant.components.ambient_station
aioambient==2023.04.0
# homeassistant.components.apcupsd
aioapcaccess==0.4.2
# homeassistant.components.aseko_pool_live
aioaseko==0.0.2
@ -433,9 +436,6 @@ anova-wifi==0.10.0
# homeassistant.components.anthemav
anthemav==1.4.1
# homeassistant.components.apcupsd
apcaccess==0.0.13
# homeassistant.components.weatherkit
apple_weatherkit==1.1.2

View File

@ -175,6 +175,9 @@ aioairzone==0.6.9
# homeassistant.components.ambient_station
aioambient==2023.04.0
# homeassistant.components.apcupsd
aioapcaccess==0.4.2
# homeassistant.components.aseko_pool_live
aioaseko==0.0.2
@ -397,9 +400,6 @@ anova-wifi==0.10.0
# homeassistant.components.anthemav
anthemav==1.4.1
# homeassistant.components.apcupsd
apcaccess==0.0.13
# homeassistant.components.weatherkit
apple_weatherkit==1.1.2

View File

@ -95,10 +95,7 @@ async def async_init_integration(
entry.add_to_hass(hass)
with (
patch("apcaccess.status.parse", return_value=status),
patch("apcaccess.status.get", return_value=b""),
):
with patch("aioapcaccess.request_status", return_value=status):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

View File

@ -24,7 +24,7 @@ def _patch_setup():
async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None:
"""Test config flow setup with connection error."""
with patch("apcaccess.status.get") as mock_get:
with patch("aioapcaccess.request_status") as mock_get:
mock_get.side_effect = OSError()
result = await hass.config_entries.flow.async_init(
@ -38,10 +38,7 @@ async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None:
async def test_config_flow_no_status(hass: HomeAssistant) -> None:
"""Test config flow setup with successful connection but no status is reported."""
with (
patch("apcaccess.status.parse", return_value={}), # Returns no status.
patch("apcaccess.status.get", return_value=b""),
):
with patch("aioapcaccess.request_status", return_value={}): # Returns no status.
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@ -64,11 +61,10 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None:
mock_entry.add_to_hass(hass)
with (
patch("apcaccess.status.parse") as mock_parse,
patch("apcaccess.status.get", return_value=b""),
patch("aioapcaccess.request_status") as mock_request_status,
_patch_setup(),
):
mock_parse.return_value = MOCK_STATUS
mock_request_status.return_value = MOCK_STATUS
# Now, create the integration again using the same config data, we should reject
# the creation due same host / port.
@ -98,7 +94,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None:
# Now we change the serial number and add it again. This should be successful.
another_device_status = copy(MOCK_STATUS)
another_device_status["SERIALNO"] = MOCK_STATUS["SERIALNO"] + "ZZZ"
mock_parse.return_value = another_device_status
mock_request_status.return_value = another_device_status
result = await hass.config_entries.flow.async_init(
DOMAIN,
@ -112,8 +108,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None:
async def test_flow_works(hass: HomeAssistant) -> None:
"""Test successful creation of config entries via user configuration."""
with (
patch("apcaccess.status.parse", return_value=MOCK_STATUS),
patch("apcaccess.status.get", return_value=b""),
patch("aioapcaccess.request_status", return_value=MOCK_STATUS),
_patch_setup() as mock_setup,
):
result = await hass.config_entries.flow.async_init(
@ -152,12 +147,11 @@ async def test_flow_minimal_status(
integration will vary.
"""
with (
patch("apcaccess.status.parse") as mock_parse,
patch("apcaccess.status.get", return_value=b""),
patch("aioapcaccess.request_status") as mock_request_status,
_patch_setup() as mock_setup,
):
status = MOCK_MINIMAL_STATUS | extra_status
mock_parse.return_value = status
mock_request_status.return_value = status
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA

View File

@ -1,4 +1,5 @@
"""Test init of APCUPSd integration."""
import asyncio
from collections import OrderedDict
from unittest.mock import patch
@ -97,7 +98,11 @@ async def test_multiple_integrations(hass: HomeAssistant) -> None:
assert state1.state != state2.state
async def test_connection_error(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
"error",
(OSError(), asyncio.IncompleteReadError(partial=b"", expected=0)),
)
async def test_connection_error(hass: HomeAssistant, error: Exception) -> None:
"""Test connection error during integration setup."""
entry = MockConfigEntry(
version=1,
@ -109,10 +114,7 @@ async def test_connection_error(hass: HomeAssistant) -> None:
entry.add_to_hass(hass)
with (
patch("apcaccess.status.parse", side_effect=OSError()),
patch("apcaccess.status.get"),
):
with patch("aioapcaccess.request_status", side_effect=error):
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.SETUP_RETRY
@ -156,12 +158,9 @@ async def test_availability(hass: HomeAssistant) -> None:
assert state.state != STATE_UNAVAILABLE
assert pytest.approx(float(state.state)) == 14.0
with (
patch("apcaccess.status.parse") as mock_parse,
patch("apcaccess.status.get", return_value=b""),
):
with patch("aioapcaccess.request_status") as mock_request_status:
# Mock a network error and then trigger an auto-polling event.
mock_parse.side_effect = OSError()
mock_request_status.side_effect = OSError()
future = utcnow() + UPDATE_INTERVAL
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
@ -172,8 +171,8 @@ async def test_availability(hass: HomeAssistant) -> None:
assert state.state == STATE_UNAVAILABLE
# Reset the API to return a new status and update.
mock_parse.side_effect = None
mock_parse.return_value = MOCK_STATUS | {"LOADPCT": "15.0 Percent"}
mock_request_status.side_effect = None
mock_request_status.return_value = MOCK_STATUS | {"LOADPCT": "15.0 Percent"}
future = future + UPDATE_INTERVAL
async_fire_time_changed(hass, future)
await hass.async_block_till_done()

View File

@ -127,10 +127,7 @@ async def test_state_update(hass: HomeAssistant) -> None:
assert state.state == "14.0"
new_status = MOCK_STATUS | {"LOADPCT": "15.0 Percent"}
with (
patch("apcaccess.status.parse", return_value=new_status),
patch("apcaccess.status.get", return_value=b""),
):
with patch("aioapcaccess.request_status", return_value=new_status):
future = utcnow() + timedelta(minutes=2)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
@ -154,11 +151,8 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None:
# Setup HASS for calling the update_entity service.
await async_setup_component(hass, "homeassistant", {})
with (
patch("apcaccess.status.parse") as mock_parse,
patch("apcaccess.status.get", return_value=b"") as mock_get,
):
mock_parse.return_value = MOCK_STATUS | {
with patch("aioapcaccess.request_status") as mock_request_status:
mock_request_status.return_value = MOCK_STATUS | {
"LOADPCT": "15.0 Percent",
"BCHARGE": "99.0 Percent",
}
@ -174,8 +168,7 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None:
)
# Even if we requested updates for two entities, our integration should smartly
# group the API calls to just one.
assert mock_parse.call_count == 1
assert mock_get.call_count == 1
assert mock_request_status.call_count == 1
# The new state should be effective.
state = hass.states.get("sensor.ups_load")
@ -194,10 +187,9 @@ async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None:
# Setup HASS for calling the update_entity service.
await async_setup_component(hass, "homeassistant", {})
with (
patch("apcaccess.status.parse", return_value=MOCK_STATUS) as mock_parse,
patch("apcaccess.status.get", return_value=b"") as mock_get,
):
with patch(
"aioapcaccess.request_status", return_value=MOCK_STATUS
) as mock_request_status:
# Fast-forward time to just pass the initial debouncer cooldown.
future = utcnow() + timedelta(seconds=REQUEST_REFRESH_COOLDOWN)
async_fire_time_changed(hass, future)
@ -207,5 +199,4 @@ async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: ["sensor.ups_load", "sensor.ups_input_voltage"]},
blocking=True,
)
assert mock_parse.call_count == 1
assert mock_get.call_count == 1
assert mock_request_status.call_count == 1