Merge pull request #57793 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-10-15 13:57:41 -07:00 committed by GitHub
commit eb30fb1f3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 388 additions and 64 deletions

View File

@ -404,8 +404,8 @@ class DlnaDmrEntity(MediaPlayerEntity):
try: try:
do_ping = self.poll_availability or self.check_available do_ping = self.poll_availability or self.check_available
await self._device.async_update(do_ping=do_ping) await self._device.async_update(do_ping=do_ping)
except UpnpError: except UpnpError as err:
_LOGGER.debug("Device unavailable") _LOGGER.debug("Device unavailable: %r", err)
await self._device_disconnect() await self._device_disconnect()
return return
finally: finally:

View File

@ -1,7 +1,7 @@
"""Support for testing internet speed via Fast.com.""" """Support for testing internet speed via Fast.com."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import datetime, timedelta
import logging import logging
from typing import Any from typing import Any
@ -67,7 +67,7 @@ class SpeedtestData:
self.data: dict[str, Any] | None = None self.data: dict[str, Any] | None = None
self._hass = hass self._hass = hass
def update(self) -> None: def update(self, now: datetime | None = None) -> None:
"""Get the latest data from fast.com.""" """Get the latest data from fast.com."""
_LOGGER.debug("Executing fast.com speedtest") _LOGGER.debug("Executing fast.com speedtest")

View File

@ -2,7 +2,7 @@
"domain": "mill", "domain": "mill",
"name": "Mill", "name": "Mill",
"documentation": "https://www.home-assistant.io/integrations/mill", "documentation": "https://www.home-assistant.io/integrations/mill",
"requirements": ["millheater==0.6.1"], "requirements": ["millheater==0.6.2"],
"codeowners": ["@danielhiversen"], "codeowners": ["@danielhiversen"],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"

View File

@ -151,7 +151,7 @@ class NotionEntity(CoordinatorEntity):
"identifiers": {(DOMAIN, sensor["hardware_id"])}, "identifiers": {(DOMAIN, sensor["hardware_id"])},
"manufacturer": "Silicon Labs", "manufacturer": "Silicon Labs",
"model": sensor["hardware_revision"], "model": sensor["hardware_revision"],
"name": sensor["name"], "name": str(sensor["name"]),
"sw_version": sensor["firmware_version"], "sw_version": sensor["firmware_version"],
"via_device": (DOMAIN, bridge.get("hardware_id")), "via_device": (DOMAIN, bridge.get("hardware_id")),
} }

View File

@ -311,7 +311,9 @@ def setup_connection_for_dialect(
result = query_on_connection(dbapi_connection, "SELECT VERSION()") result = query_on_connection(dbapi_connection, "SELECT VERSION()")
version = result[0][0] version = result[0][0]
major, minor, _patch = version.split(".", 2) major, minor, _patch = version.split(".", 2)
if int(major) == 5 and int(minor) < 8: if (int(major) == 5 and int(minor) < 8) or (
int(major) == 10 and int(minor) < 2
):
instance._db_supports_row_number = ( # pylint: disable=[protected-access] instance._db_supports_row_number = ( # pylint: disable=[protected-access]
False False
) )

View File

@ -182,7 +182,7 @@ SENSORS: Final = {
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=sensor.DEVICE_CLASS_HUMIDITY, device_class=sensor.DEVICE_CLASS_HUMIDITY,
state_class=sensor.STATE_CLASS_MEASUREMENT, state_class=sensor.STATE_CLASS_MEASUREMENT,
available=lambda block: cast(int, block.extTemp) != 999, available=lambda block: cast(int, block.humidity) != 999,
), ),
("sensor", "luminosity"): BlockAttributeDescription( ("sensor", "luminosity"): BlockAttributeDescription(
name="Luminosity", name="Luminosity",

View File

@ -3,7 +3,7 @@
"name": "SimpliSafe", "name": "SimpliSafe",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe", "documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==11.0.6"], "requirements": ["simplisafe-python==11.0.7"],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -2,7 +2,7 @@
"domain": "spider", "domain": "spider",
"name": "Itho Daalderop Spider", "name": "Itho Daalderop Spider",
"documentation": "https://www.home-assistant.io/integrations/spider", "documentation": "https://www.home-assistant.io/integrations/spider",
"requirements": ["spiderpy==1.4.2"], "requirements": ["spiderpy==1.4.3"],
"codeowners": ["@peternijssen"], "codeowners": ["@peternijssen"],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"

View File

@ -264,5 +264,5 @@ class UpnpEntity(CoordinatorEntity):
def available(self) -> bool: def available(self) -> bool:
"""Return if entity is available.""" """Return if entity is available."""
return super().available and ( return super().available and (
self.coordinator.data.get(self.entity_description.key) or False self.coordinator.data.get(self.entity_description.key) is not None
) )

View File

@ -190,7 +190,10 @@ class DerivedUpnpSensor(UpnpSensor):
# Calculate derivative. # Calculate derivative.
delta_value = current_value - self._last_value delta_value = current_value - self._last_value
if self.entity_description.native_unit_of_measurement == DATA_BYTES: if (
self.entity_description.native_unit_of_measurement
== DATA_RATE_KIBIBYTES_PER_SECOND
):
delta_value /= KIBIBYTE delta_value /= KIBIBYTE
delta_time = current_timestamp - self._last_timestamp delta_time = current_timestamp - self._last_timestamp
if delta_time.total_seconds() == 0: if delta_time.total_seconds() == 0:

View File

@ -6,7 +6,6 @@ import contextlib
from datetime import timedelta from datetime import timedelta
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
import logging import logging
import socket
from urllib.parse import urlparse from urllib.parse import urlparse
from async_upnp_client.search import SsdpSearchListener from async_upnp_client.search import SsdpSearchListener
@ -163,9 +162,6 @@ UPDATE_REQUEST_PROPERTIES = [
"active_mode", "active_mode",
] ]
BULB_NETWORK_EXCEPTIONS = (socket.error,)
BULB_EXCEPTIONS = (BulbException, asyncio.TimeoutError, *BULB_NETWORK_EXCEPTIONS)
PLATFORMS = ["binary_sensor", "light"] PLATFORMS = ["binary_sensor", "light"]
@ -270,7 +266,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
device = await _async_get_device(hass, entry.data[CONF_HOST], entry) device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
await _async_initialize(hass, entry, device) await _async_initialize(hass, entry, device)
except BULB_EXCEPTIONS as ex: except (asyncio.TimeoutError, OSError, BulbException) as ex:
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
@ -594,13 +590,20 @@ class YeelightDevice:
self._available = True self._available = True
if not self._initialized: if not self._initialized:
self._initialized = True self._initialized = True
except BULB_NETWORK_EXCEPTIONS as ex: except OSError as ex:
if self._available: # just inform once if self._available: # just inform once
_LOGGER.error( _LOGGER.error(
"Unable to update device %s, %s: %s", self._host, self.name, ex "Unable to update device %s, %s: %s", self._host, self.name, ex
) )
self._available = False self._available = False
except BULB_EXCEPTIONS as ex: except asyncio.TimeoutError as ex:
_LOGGER.debug(
"timed out while trying to update device %s, %s: %s",
self._host,
self.name,
ex,
)
except BulbException as ex:
_LOGGER.debug( _LOGGER.debug(
"Unable to update device %s, %s: %s", self._host, self.name, ex "Unable to update device %s, %s: %s", self._host, self.name, ex
) )

View File

@ -1,6 +1,7 @@
"""Light platform support for yeelight.""" """Light platform support for yeelight."""
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
import math import math
@ -8,6 +9,7 @@ import voluptuous as vol
import yeelight import yeelight
from yeelight import Bulb, Flow, RGBTransition, SleepTransition, flows from yeelight import Bulb, Flow, RGBTransition, SleepTransition, flows
from yeelight.enums import BulbType, LightType, PowerMode, SceneClass from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
from yeelight.main import BulbException
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -51,8 +53,6 @@ from . import (
ATTR_COUNT, ATTR_COUNT,
ATTR_MODE_MUSIC, ATTR_MODE_MUSIC,
ATTR_TRANSITIONS, ATTR_TRANSITIONS,
BULB_EXCEPTIONS,
BULB_NETWORK_EXCEPTIONS,
CONF_FLOW_PARAMS, CONF_FLOW_PARAMS,
CONF_MODE_MUSIC, CONF_MODE_MUSIC,
CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH,
@ -243,23 +243,33 @@ def _async_cmd(func):
"""Define a wrapper to catch exceptions from the bulb.""" """Define a wrapper to catch exceptions from the bulb."""
async def _async_wrap(self, *args, **kwargs): async def _async_wrap(self, *args, **kwargs):
try: for attempts in range(2):
_LOGGER.debug("Calling %s with %s %s", func, args, kwargs) try:
return await func(self, *args, **kwargs) _LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
except BULB_NETWORK_EXCEPTIONS as ex: return await func(self, *args, **kwargs)
# A network error happened, the bulb is likely offline now except asyncio.TimeoutError as ex:
self.device.async_mark_unavailable() # The wifi likely dropped, so we want to retry once since
self.async_state_changed() # python-yeelight will auto reconnect
exc_message = str(ex) or type(ex) exc_message = str(ex) or type(ex)
raise HomeAssistantError( if attempts == 0:
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" continue
) from ex raise HomeAssistantError(
except BULB_EXCEPTIONS as ex: f"Timed out when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
# The bulb likely responded but had an error ) from ex
exc_message = str(ex) or type(ex) except OSError as ex:
raise HomeAssistantError( # A network error happened, the bulb is likely offline now
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" self.device.async_mark_unavailable()
) from ex self.async_state_changed()
exc_message = str(ex) or type(ex)
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
) from ex
except BulbException as ex:
# The bulb likely responded but had an error
exc_message = str(ex) or type(ex)
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
) from ex
return _async_wrap return _async_wrap
@ -621,7 +631,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
"""Set bulb's color.""" """Set bulb's color."""
if not hs_color or COLOR_MODE_HS not in self.supported_color_modes: if not hs_color or COLOR_MODE_HS not in self.supported_color_modes:
return return
if self.color_mode == COLOR_MODE_HS and self.hs_color == hs_color: if (
not self.device.is_color_flow_enabled
and self.color_mode == COLOR_MODE_HS
and self.hs_color == hs_color
):
_LOGGER.debug("HS already set to: %s", hs_color) _LOGGER.debug("HS already set to: %s", hs_color)
# Already set, and since we get pushed updates # Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not # we avoid setting it again to ensure we do not
@ -638,7 +652,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
"""Set bulb's color.""" """Set bulb's color."""
if not rgb or COLOR_MODE_RGB not in self.supported_color_modes: if not rgb or COLOR_MODE_RGB not in self.supported_color_modes:
return return
if self.color_mode == COLOR_MODE_RGB and self.rgb_color == rgb: if (
not self.device.is_color_flow_enabled
and self.color_mode == COLOR_MODE_RGB
and self.rgb_color == rgb
):
_LOGGER.debug("RGB already set to: %s", rgb) _LOGGER.debug("RGB already set to: %s", rgb)
# Already set, and since we get pushed updates # Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not # we avoid setting it again to ensure we do not
@ -657,7 +675,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
return return
temp_in_k = mired_to_kelvin(colortemp) temp_in_k = mired_to_kelvin(colortemp)
if self.color_mode == COLOR_MODE_COLOR_TEMP and self.color_temp == colortemp: if (
not self.device.is_color_flow_enabled
and self.color_mode == COLOR_MODE_COLOR_TEMP
and self.color_temp == colortemp
):
_LOGGER.debug("Color temp already set to: %s", temp_in_k) _LOGGER.debug("Color temp already set to: %s", temp_in_k)
# Already set, and since we get pushed updates # Already set, and since we get pushed updates
# we avoid setting it again to ensure we do not # we avoid setting it again to ensure we do not

View File

@ -2,7 +2,7 @@
"domain": "yeelight", "domain": "yeelight",
"name": "Yeelight", "name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.7.7", "async-upnp-client==0.22.8"], "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.8"],
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
"config_flow": true, "config_flow": true,
"dependencies": ["network"], "dependencies": ["network"],

View File

@ -5,6 +5,7 @@ from youless_api.youless_sensor import YoulessSensor
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL,
STATE_CLASS_TOTAL_INCREASING, STATE_CLASS_TOTAL_INCREASING,
SensorEntity, SensorEntity,
) )
@ -40,9 +41,9 @@ async def async_setup_entry(
async_add_entities( async_add_entities(
[ [
GasSensor(coordinator, device), GasSensor(coordinator, device),
PowerMeterSensor(coordinator, device, "low"), PowerMeterSensor(coordinator, device, "low", STATE_CLASS_TOTAL_INCREASING),
PowerMeterSensor(coordinator, device, "high"), PowerMeterSensor(coordinator, device, "high", STATE_CLASS_TOTAL_INCREASING),
PowerMeterSensor(coordinator, device, "total"), PowerMeterSensor(coordinator, device, "total", STATE_CLASS_TOTAL),
CurrentPowerSensor(coordinator, device), CurrentPowerSensor(coordinator, device),
DeliveryMeterSensor(coordinator, device, "low"), DeliveryMeterSensor(coordinator, device, "low"),
DeliveryMeterSensor(coordinator, device, "high"), DeliveryMeterSensor(coordinator, device, "high"),
@ -168,7 +169,11 @@ class PowerMeterSensor(YoulessBaseSensor):
_attr_state_class = STATE_CLASS_TOTAL_INCREASING _attr_state_class = STATE_CLASS_TOTAL_INCREASING
def __init__( def __init__(
self, coordinator: DataUpdateCoordinator, device: str, dev_type: str self,
coordinator: DataUpdateCoordinator,
device: str,
dev_type: str,
state_class: str,
) -> None: ) -> None:
"""Instantiate a power meter sensor.""" """Instantiate a power meter sensor."""
super().__init__( super().__init__(
@ -177,6 +182,7 @@ class PowerMeterSensor(YoulessBaseSensor):
self._device = device self._device = device
self._type = dev_type self._type = dev_type
self._attr_name = f"Power {dev_type}" self._attr_name = f"Power {dev_type}"
self._attr_state_class = state_class
@property @property
def get_sensor(self) -> YoulessSensor | None: def get_sensor(self) -> YoulessSensor | None:

View File

@ -5,7 +5,7 @@ from typing import Final
MAJOR_VERSION: Final = 2021 MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 10 MINOR_VERSION: Final = 10
PATCH_VERSION: Final = "4" PATCH_VERSION: Final = "5"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@ -1005,7 +1005,7 @@ micloud==0.3
miflora==0.7.0 miflora==0.7.0
# homeassistant.components.mill # homeassistant.components.mill
millheater==0.6.1 millheater==0.6.2
# homeassistant.components.minio # homeassistant.components.minio
minio==4.0.9 minio==4.0.9
@ -2149,7 +2149,7 @@ simplehound==0.3
simplepush==1.1.4 simplepush==1.1.4
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==11.0.6 simplisafe-python==11.0.7
# homeassistant.components.sisyphus # homeassistant.components.sisyphus
sisyphus-control==3.0 sisyphus-control==3.0
@ -2214,7 +2214,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3 speedtest-cli==2.1.3
# homeassistant.components.spider # homeassistant.components.spider
spiderpy==1.4.2 spiderpy==1.4.3
# homeassistant.components.spotify # homeassistant.components.spotify
spotipy==2.18.0 spotipy==2.18.0
@ -2459,7 +2459,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13 yalexs==1.1.13
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.7 yeelight==0.7.8
# homeassistant.components.yeelightsunflower # homeassistant.components.yeelightsunflower
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10

View File

@ -585,7 +585,7 @@ mficlient==0.3.0
micloud==0.3 micloud==0.3
# homeassistant.components.mill # homeassistant.components.mill
millheater==0.6.1 millheater==0.6.2
# homeassistant.components.minio # homeassistant.components.minio
minio==4.0.9 minio==4.0.9
@ -1224,7 +1224,7 @@ sharkiqpy==0.1.8
simplehound==0.3 simplehound==0.3
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==11.0.6 simplisafe-python==11.0.7
# homeassistant.components.slack # homeassistant.components.slack
slackclient==2.5.0 slackclient==2.5.0
@ -1263,7 +1263,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3 speedtest-cli==2.1.3
# homeassistant.components.spider # homeassistant.components.spider
spiderpy==1.4.2 spiderpy==1.4.3
# homeassistant.components.spotify # homeassistant.components.spotify
spotipy==2.18.0 spotipy==2.18.0
@ -1403,7 +1403,7 @@ yalesmartalarmclient==0.3.4
yalexs==1.1.13 yalexs==1.1.13
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.7 yeelight==0.7.8
# homeassistant.components.youless # homeassistant.components.youless
youless-api==0.14 youless-api==0.14

View File

@ -125,7 +125,8 @@ async def test_last_run_was_recently_clean(hass):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mysql_version, db_supports_row_number", "mysql_version, db_supports_row_number",
[ [
("10.0.0", True), ("10.2.0", True),
("10.1.0", False),
("5.8.0", True), ("5.8.0", True),
("5.7.0", False), ("5.7.0", False),
], ],

View File

@ -9,6 +9,9 @@ from homeassistant.components import ssdp
from homeassistant.components.upnp.const import ( from homeassistant.components.upnp.const import (
BYTES_RECEIVED, BYTES_RECEIVED,
BYTES_SENT, BYTES_SENT,
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
DOMAIN,
PACKETS_RECEIVED, PACKETS_RECEIVED,
PACKETS_SENT, PACKETS_SENT,
ROUTER_IP, ROUTER_IP,
@ -19,6 +22,8 @@ from homeassistant.components.upnp.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util import dt from homeassistant.util import dt
from tests.common import MockConfigEntry
TEST_UDN = "uuid:device" TEST_UDN = "uuid:device"
TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
TEST_USN = f"{TEST_UDN}::{TEST_ST}" TEST_USN = f"{TEST_UDN}::{TEST_ST}"
@ -115,8 +120,8 @@ class MockDevice:
self.status_times_polled += 1 self.status_times_polled += 1
return { return {
WAN_STATUS: "Connected", WAN_STATUS: "Connected",
ROUTER_UPTIME: 0, ROUTER_UPTIME: 10,
ROUTER_IP: "192.168.0.1", ROUTER_IP: "8.9.10.11",
} }
@ -185,3 +190,24 @@ async def ssdp_no_discovery():
return_value=[], return_value=[],
) as mock_get_info: ) as mock_get_info:
yield (mock_register, mock_get_info) yield (mock_register, mock_get_info)
@pytest.fixture
async def setup_integration(
hass: HomeAssistant, mock_get_source_ip, ssdp_instant_discovery, mock_upnp_device
):
"""Create an initialized integration."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONFIG_ENTRY_UDN: TEST_UDN,
CONFIG_ENTRY_ST: TEST_ST,
},
)
# Load config_entry.
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
yield entry

View File

@ -0,0 +1,42 @@
"""Tests for UPnP/IGD binary_sensor."""
from datetime import timedelta
from unittest.mock import AsyncMock
from homeassistant.components.upnp.const import (
DOMAIN,
ROUTER_IP,
ROUTER_UPTIME,
WAN_STATUS,
)
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
from .conftest import MockDevice
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_upnp_binary_sensors(
hass: HomeAssistant, setup_integration: MockConfigEntry
):
"""Test normal sensors."""
mock_device: MockDevice = hass.data[DOMAIN][setup_integration.entry_id].device
# First poll.
wan_status_state = hass.states.get("binary_sensor.mock_name_wan_status")
assert wan_status_state.state == "on"
# Second poll.
mock_device.async_get_status = AsyncMock(
return_value={
WAN_STATUS: "Disconnected",
ROUTER_UPTIME: 100,
ROUTER_IP: "",
}
)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31))
await hass.async_block_till_done()
wan_status_state = hass.states.get("binary_sensor.mock_name_wan_status")
assert wan_status_state.state == "off"

View File

@ -25,6 +25,7 @@ from .conftest import (
TEST_ST, TEST_ST,
TEST_UDN, TEST_UDN,
TEST_USN, TEST_USN,
MockDevice,
) )
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@ -196,7 +197,7 @@ async def test_options_flow(hass: HomeAssistant):
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) is True assert await hass.config_entries.async_setup(config_entry.entry_id) is True
await hass.async_block_till_done() await hass.async_block_till_done()
mock_device = hass.data[DOMAIN][config_entry.entry_id].device mock_device: MockDevice = hass.data[DOMAIN][config_entry.entry_id].device
# Reset. # Reset.
mock_device.traffic_times_polled = 0 mock_device.traffic_times_polled = 0

View File

@ -9,7 +9,6 @@ from homeassistant.components.upnp.const import (
DOMAIN, DOMAIN,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .conftest import TEST_ST, TEST_UDN from .conftest import TEST_ST, TEST_UDN
@ -28,10 +27,6 @@ async def test_async_setup_entry_default(hass: HomeAssistant):
}, },
) )
# Initialisation of component, no device discovered.
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
# Load config_entry. # Load config_entry.
entry.add_to_hass(hass) entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) is True assert await hass.config_entries.async_setup(entry.entry_id) is True

View File

@ -0,0 +1,114 @@
"""Tests for UPnP/IGD sensor."""
from datetime import timedelta
from unittest.mock import AsyncMock
from homeassistant.components.upnp.const import (
BYTES_RECEIVED,
BYTES_SENT,
DOMAIN,
PACKETS_RECEIVED,
PACKETS_SENT,
ROUTER_IP,
ROUTER_UPTIME,
TIMESTAMP,
UPDATE_INTERVAL,
WAN_STATUS,
)
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
from .conftest import MockDevice
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_upnp_sensors(hass: HomeAssistant, setup_integration: MockConfigEntry):
"""Test normal sensors."""
mock_device: MockDevice = hass.data[DOMAIN][setup_integration.entry_id].device
# First poll.
b_received_state = hass.states.get("sensor.mock_name_b_received")
b_sent_state = hass.states.get("sensor.mock_name_b_sent")
packets_received_state = hass.states.get("sensor.mock_name_packets_received")
packets_sent_state = hass.states.get("sensor.mock_name_packets_sent")
external_ip_state = hass.states.get("sensor.mock_name_external_ip")
wan_status_state = hass.states.get("sensor.mock_name_wan_status")
assert b_received_state.state == "0"
assert b_sent_state.state == "0"
assert packets_received_state.state == "0"
assert packets_sent_state.state == "0"
assert external_ip_state.state == "8.9.10.11"
assert wan_status_state.state == "Connected"
# Second poll.
mock_device.async_get_traffic_data = AsyncMock(
return_value={
TIMESTAMP: dt_util.utcnow() + UPDATE_INTERVAL,
BYTES_RECEIVED: 10240,
BYTES_SENT: 20480,
PACKETS_RECEIVED: 30,
PACKETS_SENT: 40,
}
)
mock_device.async_get_status = AsyncMock(
return_value={
WAN_STATUS: "Disconnected",
ROUTER_UPTIME: 100,
ROUTER_IP: "",
}
)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31))
await hass.async_block_till_done()
b_received_state = hass.states.get("sensor.mock_name_b_received")
b_sent_state = hass.states.get("sensor.mock_name_b_sent")
packets_received_state = hass.states.get("sensor.mock_name_packets_received")
packets_sent_state = hass.states.get("sensor.mock_name_packets_sent")
external_ip_state = hass.states.get("sensor.mock_name_external_ip")
wan_status_state = hass.states.get("sensor.mock_name_wan_status")
assert b_received_state.state == "10240"
assert b_sent_state.state == "20480"
assert packets_received_state.state == "30"
assert packets_sent_state.state == "40"
assert external_ip_state.state == ""
assert wan_status_state.state == "Disconnected"
async def test_derived_upnp_sensors(
hass: HomeAssistant, setup_integration: MockConfigEntry
):
"""Test derived sensors."""
mock_device: MockDevice = hass.data[DOMAIN][setup_integration.entry_id].device
# First poll.
kib_s_received_state = hass.states.get("sensor.mock_name_kib_s_received")
kib_s_sent_state = hass.states.get("sensor.mock_name_kib_s_sent")
packets_s_received_state = hass.states.get("sensor.mock_name_packets_s_received")
packets_s_sent_state = hass.states.get("sensor.mock_name_packets_s_sent")
assert kib_s_received_state.state == "unknown"
assert kib_s_sent_state.state == "unknown"
assert packets_s_received_state.state == "unknown"
assert packets_s_sent_state.state == "unknown"
# Second poll.
mock_device.async_get_traffic_data = AsyncMock(
return_value={
TIMESTAMP: dt_util.utcnow() + UPDATE_INTERVAL,
BYTES_RECEIVED: int(10240 * UPDATE_INTERVAL.total_seconds()),
BYTES_SENT: int(20480 * UPDATE_INTERVAL.total_seconds()),
PACKETS_RECEIVED: int(30 * UPDATE_INTERVAL.total_seconds()),
PACKETS_SENT: int(40 * UPDATE_INTERVAL.total_seconds()),
}
)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31))
await hass.async_block_till_done()
kib_s_received_state = hass.states.get("sensor.mock_name_kib_s_received")
kib_s_sent_state = hass.states.get("sensor.mock_name_kib_s_sent")
packets_s_received_state = hass.states.get("sensor.mock_name_packets_s_received")
packets_s_sent_state = hass.states.get("sensor.mock_name_packets_s_sent")
assert kib_s_received_state.state == "10.0"
assert kib_s_sent_state.state == "20.0"
assert packets_s_received_state.state == "30.0"
assert packets_s_sent_state.state == "40.0"

View File

@ -1,7 +1,9 @@
"""Test Yeelight.""" """Test Yeelight."""
import asyncio
from datetime import timedelta from datetime import timedelta
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest
from yeelight import BulbException, BulbType from yeelight import BulbException, BulbType
from yeelight.aio import KEY_CONNECTED from yeelight.aio import KEY_CONNECTED
@ -507,3 +509,51 @@ async def test_connection_dropped_resyncs_properties(hass: HomeAssistant):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 2 assert len(mocked_bulb.async_get_properties.mock_calls) == 2
async def test_oserror_on_first_update_results_in_unavailable(hass: HomeAssistant):
"""Test that an OSError on first update results in unavailable."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id=ID,
data={CONF_HOST: "127.0.0.1"},
options={CONF_NAME: "Test name"},
)
config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb()
mocked_bulb.async_get_properties = AsyncMock(side_effect=OSError)
with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("light.test_name").state == STATE_UNAVAILABLE
@pytest.mark.parametrize("exception", [BulbException, asyncio.TimeoutError])
async def test_non_oserror_exception_on_first_update(
hass: HomeAssistant, exception: Exception
):
"""Test that an exceptions other than OSError on first update do not result in unavailable.
The unavailable state will come as a push update in this case
"""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id=ID,
data={CONF_HOST: "127.0.0.1"},
options={CONF_NAME: "Test name"},
)
config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb()
mocked_bulb.async_get_properties = AsyncMock(side_effect=exception)
with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch(
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("light.test_name").state != STATE_UNAVAILABLE

View File

@ -625,6 +625,22 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant):
assert mocked_bulb.async_set_brightness.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == []
mocked_bulb.async_set_rgb.reset_mock() mocked_bulb.async_set_rgb.reset_mock()
mocked_bulb.last_properties["flowing"] = "1"
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: (red, green, blue)},
blocking=True,
)
assert mocked_bulb.async_set_hsv.mock_calls == []
assert mocked_bulb.async_set_rgb.mock_calls == [
call(255, 0, 0, duration=350, light_type=ANY)
]
assert mocked_bulb.async_set_color_temp.mock_calls == []
assert mocked_bulb.async_set_brightness.mock_calls == []
mocked_bulb.async_set_rgb.reset_mock()
mocked_bulb.last_properties["flowing"] = "0"
await hass.services.async_call( await hass.services.async_call(
"light", "light",
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -666,6 +682,22 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant):
assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == []
assert mocked_bulb.async_set_brightness.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == []
mocked_bulb.last_properties["flowing"] = "1"
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250},
blocking=True,
)
assert mocked_bulb.async_set_hsv.mock_calls == []
assert mocked_bulb.async_set_rgb.mock_calls == []
assert mocked_bulb.async_set_color_temp.mock_calls == [
call(4000, duration=350, light_type=ANY)
]
assert mocked_bulb.async_set_brightness.mock_calls == []
mocked_bulb.async_set_color_temp.reset_mock()
mocked_bulb.last_properties["flowing"] = "0"
mocked_bulb.last_properties["color_mode"] = 3 mocked_bulb.last_properties["color_mode"] = 3
# This last change should generate a call even though # This last change should generate a call even though
# the color mode is the same since the HSV has changed # the color mode is the same since the HSV has changed
@ -681,6 +713,33 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant):
assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_rgb.mock_calls == []
assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == []
assert mocked_bulb.async_set_brightness.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == []
mocked_bulb.async_set_hsv.reset_mock()
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (100, 35)},
blocking=True,
)
assert mocked_bulb.async_set_hsv.mock_calls == []
assert mocked_bulb.async_set_rgb.mock_calls == []
assert mocked_bulb.async_set_color_temp.mock_calls == []
assert mocked_bulb.async_set_brightness.mock_calls == []
mocked_bulb.last_properties["flowing"] = "1"
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (100, 35)},
blocking=True,
)
assert mocked_bulb.async_set_hsv.mock_calls == [
call(100.0, 35.0, duration=350, light_type=ANY)
]
assert mocked_bulb.async_set_rgb.mock_calls == []
assert mocked_bulb.async_set_color_temp.mock_calls == []
assert mocked_bulb.async_set_brightness.mock_calls == []
mocked_bulb.last_properties["flowing"] = "0"
async def test_device_types(hass: HomeAssistant, caplog): async def test_device_types(hass: HomeAssistant, caplog):