Prevent Zerproc leaving open unnecessary connections (#47401)

* Zerproc: Prevent leaving open unnecessary connections

* Fix config entry unloading
This commit is contained in:
Emily Mills 2021-03-05 13:14:03 -06:00 committed by GitHub
parent 3baeed3684
commit a547d0fea2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 20 additions and 60 deletions

View file

@ -20,6 +20,11 @@ async def async_setup(hass, config):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Zerproc from a config entry.""" """Set up Zerproc from a config entry."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
if "addresses" not in hass.data[DOMAIN]:
hass.data[DOMAIN]["addresses"] = set()
for platform in PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform) hass.config_entries.async_forward_entry_setup(entry, platform)
@ -30,6 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry.""" """Unload a config entry."""
hass.data.pop(DOMAIN, None)
return all( return all(
await asyncio.gather( await asyncio.gather(
*[ *[

View file

@ -1,5 +1,4 @@
"""Zerproc light platform.""" """Zerproc light platform."""
import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Callable, List, Optional from typing import Callable, List, Optional
@ -30,16 +29,6 @@ SUPPORT_ZERPROC = SUPPORT_BRIGHTNESS | SUPPORT_COLOR
DISCOVERY_INTERVAL = timedelta(seconds=60) DISCOVERY_INTERVAL = timedelta(seconds=60)
async def connect_light(light: pyzerproc.Light) -> Optional[pyzerproc.Light]:
"""Return the given light if it connects successfully."""
try:
await light.connect()
except pyzerproc.ZerprocException:
_LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True)
return None
return light
async def discover_entities(hass: HomeAssistant) -> List[Entity]: async def discover_entities(hass: HomeAssistant) -> List[Entity]:
"""Attempt to discover new lights.""" """Attempt to discover new lights."""
lights = await pyzerproc.discover() lights = await pyzerproc.discover()
@ -50,14 +39,9 @@ async def discover_entities(hass: HomeAssistant) -> List[Entity]:
] ]
entities = [] entities = []
connected_lights = filter( for light in new_lights:
None, await asyncio.gather(*(connect_light(light) for light in new_lights)) hass.data[DOMAIN]["addresses"].add(light.address)
) entities.append(ZerprocLight(light))
for light in connected_lights:
# Double-check the light hasn't been added in the meantime
if light.address not in hass.data[DOMAIN]["addresses"]:
hass.data[DOMAIN]["addresses"].add(light.address)
entities.append(ZerprocLight(light))
return entities return entities
@ -68,11 +52,6 @@ async def async_setup_entry(
async_add_entities: Callable[[List[Entity], bool], None], async_add_entities: Callable[[List[Entity], bool], None],
) -> None: ) -> None:
"""Set up Zerproc light devices.""" """Set up Zerproc light devices."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
if "addresses" not in hass.data[DOMAIN]:
hass.data[DOMAIN]["addresses"] = set()
warned = False warned = False
async def discover(*args): async def discover(*args):
@ -120,7 +99,7 @@ class ZerprocLight(LightEntity):
await self._light.disconnect() await self._light.disconnect()
except pyzerproc.ZerprocException: except pyzerproc.ZerprocException:
_LOGGER.debug( _LOGGER.debug(
"Exception disconnected from %s", self.entity_id, exc_info=True "Exception disconnecting from %s", self._light.address, exc_info=True
) )
@property @property
@ -198,11 +177,11 @@ class ZerprocLight(LightEntity):
state = await self._light.get_state() state = await self._light.get_state()
except pyzerproc.ZerprocException: except pyzerproc.ZerprocException:
if self._available: if self._available:
_LOGGER.warning("Unable to connect to %s", self.entity_id) _LOGGER.warning("Unable to connect to %s", self._light.address)
self._available = False self._available = False
return return
if self._available is False: if self._available is False:
_LOGGER.info("Reconnected to %s", self.entity_id) _LOGGER.info("Reconnected to %s", self._light.address)
self._available = True self._available = True
self._is_on = state.is_on self._is_on = state.is_on
hsv = color_util.color_RGB_to_hsv(*state.color) hsv = color_util.color_RGB_to_hsv(*state.color)

View file

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zerproc", "documentation": "https://www.home-assistant.io/integrations/zerproc",
"requirements": [ "requirements": [
"pyzerproc==0.4.7" "pyzerproc==0.4.8"
], ],
"codeowners": [ "codeowners": [
"@emlove" "@emlove"

View file

@ -1920,7 +1920,7 @@ pyxeoma==1.4.1
pyzbar==0.1.7 pyzbar==0.1.7
# homeassistant.components.zerproc # homeassistant.components.zerproc
pyzerproc==0.4.7 pyzerproc==0.4.8
# homeassistant.components.qnap # homeassistant.components.qnap
qnapstats==0.3.0 qnapstats==0.3.0

View file

@ -993,7 +993,7 @@ pywemo==0.6.3
pywilight==0.0.68 pywilight==0.0.68
# homeassistant.components.zerproc # homeassistant.components.zerproc
pyzerproc==0.4.7 pyzerproc==0.4.8
# homeassistant.components.rachio # homeassistant.components.rachio
rachiopy==1.0.3 rachiopy==1.0.3

View file

@ -118,6 +118,8 @@ async def test_init(hass, mock_entry):
assert mock_light_1.disconnect.called assert mock_light_1.disconnect.called
assert mock_light_2.disconnect.called assert mock_light_2.disconnect.called
assert hass.data[DOMAIN]["addresses"] == {"AA:BB:CC:DD:EE:FF", "11:22:33:44:55:66"}
async def test_discovery_exception(hass, mock_entry): async def test_discovery_exception(hass, mock_entry):
"""Test platform setup.""" """Test platform setup."""
@ -136,42 +138,15 @@ async def test_discovery_exception(hass, mock_entry):
assert len(hass.data[DOMAIN]["addresses"]) == 0 assert len(hass.data[DOMAIN]["addresses"]) == 0
async def test_connect_exception(hass, mock_entry):
"""Test platform setup."""
await setup.async_setup_component(hass, "persistent_notification", {})
mock_entry.add_to_hass(hass)
mock_light_1 = MagicMock(spec=pyzerproc.Light)
mock_light_1.address = "AA:BB:CC:DD:EE:FF"
mock_light_1.name = "LEDBlue-CCDDEEFF"
mock_light_1.is_connected.return_value = False
mock_light_2 = MagicMock(spec=pyzerproc.Light)
mock_light_2.address = "11:22:33:44:55:66"
mock_light_2.name = "LEDBlue-33445566"
mock_light_2.is_connected.return_value = False
with patch(
"homeassistant.components.zerproc.light.pyzerproc.discover",
return_value=[mock_light_1, mock_light_2],
), patch.object(
mock_light_1, "connect", side_effect=pyzerproc.ZerprocException("TEST")
):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
# The exception connecting to light 1 should be captured, but light 2
# should still be added
assert len(hass.data[DOMAIN]["addresses"]) == 1
async def test_remove_entry(hass, mock_light, mock_entry): async def test_remove_entry(hass, mock_light, mock_entry):
"""Test platform setup.""" """Test platform setup."""
assert hass.data[DOMAIN]["addresses"] == {"AA:BB:CC:DD:EE:FF"}
with patch.object(mock_light, "disconnect") as mock_disconnect: with patch.object(mock_light, "disconnect") as mock_disconnect:
await hass.config_entries.async_remove(mock_entry.entry_id) await hass.config_entries.async_remove(mock_entry.entry_id)
assert mock_disconnect.called assert mock_disconnect.called
assert DOMAIN not in hass.data
async def test_remove_entry_exceptions_caught(hass, mock_light, mock_entry): async def test_remove_entry_exceptions_caught(hass, mock_light, mock_entry):