Only log xiaomi_miio update exceptions once (#41226)

* xiaomi_miio: only log update exceptions once

Replaces #37695

* add som more missed exception logger cases + do not change the control flow as pointed out by @cgtobi

* Use patch&MagickMock from tests.async_mock

* Fix linting for alarm_control_panel

* update the test to verify that the warning on update is only logged when the device was previously available
pull/42326/head
Teemu R 2020-10-25 01:53:36 +02:00 committed by GitHub
parent 51915c86cb
commit bdb71c9159
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 127 additions and 87 deletions

View File

@ -195,8 +195,9 @@ class AirMonitorS1(AirMonitorB1):
self._humidity = state.humidity
self._available = True
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
class AirMonitorV1(AirMonitorB1):
@ -210,8 +211,9 @@ class AirMonitorV1(AirMonitorB1):
self._air_quality_index = state.aqi
self._available = True
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def unit_of_measurement(self):

View File

@ -3,7 +3,7 @@
from functools import partial
import logging
from miio.gateway import GatewayException
from miio import DeviceException
from homeassistant.components.alarm_control_panel import (
SUPPORT_ALARM_ARM_AWAY,
@ -103,7 +103,7 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity):
partial(func, *args, **kwargs)
)
_LOGGER.debug("Response received from miio device: %s", result)
except GatewayException as exc:
except DeviceException as exc:
_LOGGER.error(mask_error, exc)
async def async_alarm_arm_away(self, code=None):
@ -122,9 +122,11 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity):
"""Fetch state from the device."""
try:
state = await self.hass.async_add_executor_job(self._gateway.alarm.status)
except GatewayException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
except DeviceException as ex:
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)

View File

@ -655,8 +655,10 @@ class XiaomiGenericDevice(FanEntity):
return result == SUCCESS
except DeviceException as exc:
_LOGGER.error(mask_error, exc)
self._available = False
if self._available:
_LOGGER.error(mask_error, exc)
self._available = False
return False
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
@ -785,8 +787,9 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def speed_list(self) -> list:
@ -1029,8 +1032,9 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def speed_list(self) -> list:
@ -1138,8 +1142,9 @@ class XiaomiAirFresh(XiaomiGenericDevice):
)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def speed_list(self) -> list:

View File

@ -65,7 +65,7 @@ class XiaomiGatewayDevice(Entity):
self._entry = entry
self._unique_id = sub_device.sid
self._name = f"{sub_device.name} ({sub_device.sid})"
self._available = None
self._available = False
@property
def unique_id(self):
@ -100,5 +100,6 @@ class XiaomiGatewayDevice(Entity):
await self.hass.async_add_executor_job(self._sub_device.update)
self._available = True
except gateway.GatewayException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)

View File

@ -324,8 +324,10 @@ class XiaomiPhilipsAbstractLight(LightEntity):
return result == SUCCESS
except DeviceException as exc:
_LOGGER.error(mask_error, exc)
self._available = False
if self._available:
_LOGGER.error(mask_error, exc)
self._available = False
return False
async def async_turn_on(self, **kwargs):
@ -356,8 +358,10 @@ class XiaomiPhilipsAbstractLight(LightEntity):
try:
state = await self.hass.async_add_executor_job(self._light.status)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
@ -380,8 +384,10 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight):
try:
state = await self.hass.async_add_executor_job(self._light.status)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
@ -536,8 +542,10 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight):
try:
state = await self.hass.async_add_executor_job(self._light.status)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
@ -593,8 +601,10 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb):
try:
state = await self.hass.async_add_executor_job(self._light.status)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
@ -637,8 +647,10 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight):
try:
state = await self.hass.async_add_executor_job(self._light.status)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
@ -778,8 +790,10 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight):
try:
state = await self.hass.async_add_executor_job(self._light.status)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)
@ -932,8 +946,10 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb):
try:
state = await self.hass.async_add_executor_job(self._light.status)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
return
_LOGGER.debug("Got new state: %s", state)

View File

@ -233,8 +233,9 @@ class XiaomiAirQualityMonitor(Entity):
)
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
class XiaomiGatewaySensor(XiaomiGatewayDevice):

View File

@ -280,8 +280,10 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
return result == SUCCESS
except DeviceException as exc:
_LOGGER.error(mask_error, exc)
self._available = False
if self._available:
_LOGGER.error(mask_error, exc)
self._available = False
return False
async def async_turn_on(self, **kwargs):
@ -316,8 +318,9 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
self._state_attrs[ATTR_TEMPERATURE] = state.temperature
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
async def async_set_wifi_led_on(self):
"""Turn the wifi led on."""
@ -402,8 +405,9 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
self._state_attrs[ATTR_POWER_PRICE] = state.power_price
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
async def async_set_power_mode(self, mode: str):
"""Set the power mode."""
@ -492,8 +496,9 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
self._state_attrs[ATTR_LOAD_POWER] = state.load_power
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch):
@ -541,5 +546,6 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch):
self._state_attrs[ATTR_LOAD_POWER] = state.load_power
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
if self._available:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)

View File

@ -485,10 +485,10 @@ class MiroboVacuum(StateVacuumEntity):
self.dnd_state = self._vacuum.dnd_status()
self._available = True
except OSError as exc:
_LOGGER.error("Got OSError while fetching the state: %s", exc)
except DeviceException as exc:
_LOGGER.warning("Got exception while fetching the state: %s", exc)
except (OSError, DeviceException) as exc:
if self._available:
self._available = False
_LOGGER.warning("Got exception while fetching the state: %s", exc)
# Fetch timers separately, see #38285
try:

View File

@ -2,6 +2,7 @@
from datetime import datetime, time, timedelta
from unittest import mock
from miio import DeviceException
import pytest
from pytz import utc
@ -52,9 +53,12 @@ from homeassistant.const import (
CONF_PLATFORM,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.setup import async_setup_component
from tests.async_mock import MagicMock, patch
PLATFORM = "xiaomi_miio"
# calls made when device status is requested
@ -70,7 +74,7 @@ STATUS_CALLS = [
@pytest.fixture(name="mock_mirobo_is_got_error")
def mirobo_is_got_error_fixture():
"""Mock mock_mirobo."""
mock_vacuum = mock.MagicMock()
mock_vacuum = MagicMock()
mock_vacuum.status().data = {"test": "raw"}
mock_vacuum.status().is_on = False
mock_vacuum.status().fanspeed = 38
@ -98,21 +102,19 @@ def mirobo_is_got_error_fixture():
mock_vacuum.dnd_status().start = time(hour=22, minute=0)
mock_vacuum.dnd_status().end = time(hour=6, minute=0)
mock_timer_1 = mock.MagicMock()
mock_timer_1 = MagicMock()
mock_timer_1.enabled = True
mock_timer_1.cron = "5 5 1 8 1"
mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc)
mock_timer_2 = mock.MagicMock()
mock_timer_2 = MagicMock()
mock_timer_2.enabled = False
mock_timer_2.cron = "5 5 1 8 2"
mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc)
mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2]
with mock.patch(
"homeassistant.components.xiaomi_miio.vacuum.Vacuum"
) as mock_vaccum_cls:
with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vaccum_cls:
mock_vaccum_cls.return_value = mock_vacuum
yield mock_vacuum
@ -135,14 +137,12 @@ new_fanspeeds = {
@pytest.fixture(name="mock_mirobo_fanspeeds", params=[old_fanspeeds, new_fanspeeds])
def mirobo_old_speeds_fixture(request):
"""Fixture for testing both types of fanspeeds."""
mock_vacuum = mock.MagicMock()
mock_vacuum = MagicMock()
mock_vacuum.status().battery = 32
mock_vacuum.fan_speed_presets.return_value = request.param
mock_vacuum.status().fanspeed = list(request.param.values())[0]
with mock.patch(
"homeassistant.components.xiaomi_miio.vacuum.Vacuum"
) as mock_vaccum_cls:
with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vaccum_cls:
mock_vaccum_cls.return_value = mock_vacuum
yield mock_vacuum
@ -150,7 +150,7 @@ def mirobo_old_speeds_fixture(request):
@pytest.fixture(name="mock_mirobo_is_on")
def mirobo_is_on_fixture():
"""Mock mock_mirobo."""
mock_vacuum = mock.MagicMock()
mock_vacuum = MagicMock()
mock_vacuum.status().data = {"test": "raw"}
mock_vacuum.status().is_on = True
mock_vacuum.status().fanspeed = 99
@ -176,46 +176,53 @@ def mirobo_is_on_fixture():
mock_vacuum.status().state_code = 5
mock_vacuum.dnd_status().enabled = False
mock_timer_1 = mock.MagicMock()
mock_timer_1 = MagicMock()
mock_timer_1.enabled = True
mock_timer_1.cron = "5 5 1 8 1"
mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc)
mock_timer_2 = mock.MagicMock()
mock_timer_2 = MagicMock()
mock_timer_2.enabled = False
mock_timer_2.cron = "5 5 1 8 2"
mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc)
mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2]
with mock.patch(
"homeassistant.components.xiaomi_miio.vacuum.Vacuum"
) as mock_vaccum_cls:
with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vaccum_cls:
mock_vaccum_cls.return_value = mock_vacuum
yield mock_vacuum
@pytest.fixture(name="mock_mirobo_errors")
def mirobo_errors_fixture():
"""Mock mock_mirobo_errors to simulate a bad vacuum status request."""
mock_vacuum = mock.MagicMock()
mock_vacuum.status.side_effect = OSError()
with mock.patch(
"homeassistant.components.xiaomi_miio.vacuum.Vacuum"
) as mock_vaccum_cls:
mock_vaccum_cls.return_value = mock_vacuum
yield mock_vacuum
async def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors):
"""Test vacuum supported features."""
async def test_xiaomi_exceptions(hass, caplog, mock_mirobo_is_on):
"""Test error logging on exceptions."""
entity_name = "test_vacuum_cleaner_error"
await setup_component(hass, entity_name)
entity_id = await setup_component(hass, entity_name)
def is_available():
state = hass.states.get(entity_id)
return state.state != STATE_UNAVAILABLE
# The initial setup has to be done successfully
assert "Initializing with host 192.168.1.100 (token 12345...)" in caplog.text
assert mock_mirobo_errors.status.call_count == 1
assert "ERROR" in caplog.text
assert "Got OSError while fetching the state" in caplog.text
assert "WARNING" not in caplog.text
assert is_available()
# Second update causes an exception, which should be logged
mock_mirobo_is_on.status.side_effect = DeviceException("dummy exception")
await hass.helpers.entity_component.async_update_entity(entity_id)
assert "WARNING" in caplog.text
assert "Got exception while fetching the state" in caplog.text
assert not is_available()
# Third update does not get logged as the device is already unavailable,
# so we clear the log and reset the status to test that
caplog.clear()
mock_mirobo_is_on.status.reset_mock()
await hass.helpers.entity_component.async_update_entity(entity_id)
assert "Got exception while fetching the state" not in caplog.text
assert not is_available()
assert mock_mirobo_is_on.status.call_count == 1
async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error):
@ -463,7 +470,7 @@ async def test_xiaomi_vacuum_fanspeeds(hass, caplog, mock_mirobo_fanspeeds):
{"entity_id": entity_id, "fan_speed": "invent"},
blocking=True,
)
assert "ERROR" in caplog.text
assert "Fan speed step not recognized" in caplog.text
async def test_xiaomi_vacuum_goto_service(hass, caplog, mock_mirobo_is_on):