Skip to content

Commit

Permalink
Replace apcaccess dependency with aioapcaccess in apcupsd (#104571)
Browse files Browse the repository at this point in the history
* Replace apcaccess dependency with async version aioapcaccess

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

* Handle asyncio.IncompleteReadError
  • Loading branch information
yuxincs authored Dec 8, 2023
1 parent 949ca6b commit 88ddc25
Show file tree
Hide file tree
Showing 8 changed files with 38 additions and 62 deletions.
11 changes: 3 additions & 8 deletions homeassistant/components/apcupsd/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import logging
from typing import Final

from apcaccess import status
import aioapcaccess

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -90,13 +90,8 @@ async def _async_update_data(self) -> 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
2 changes: 1 addition & 1 deletion homeassistant/components/apcupsd/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["apcaccess"],
"quality_scale": "silver",
"requirements": ["apcaccess==0.0.13"]
"requirements": ["aioapcaccess==0.4.2"]
}
6 changes: 3 additions & 3 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
5 changes: 1 addition & 4 deletions tests/components/apcupsd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
22 changes: 8 additions & 14 deletions tests/components/apcupsd/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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}
)
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
23 changes: 11 additions & 12 deletions tests/components/apcupsd/test_init.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test init of APCUPSd integration."""
import asyncio
from collections import OrderedDict
from unittest.mock import patch

Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down
25 changes: 8 additions & 17 deletions tests/components/apcupsd/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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",
}
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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

0 comments on commit 88ddc25

Please sign in to comment.