diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 8542464e41e825..809ab71671b61a 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -404,8 +404,8 @@ async def async_update(self) -> None: try: do_ping = self.poll_availability or self.check_available await self._device.async_update(do_ping=do_ping) - except UpnpError: - _LOGGER.debug("Device unavailable") + except UpnpError as err: + _LOGGER.debug("Device unavailable: %r", err) await self._device_disconnect() return finally: diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index f2424332a0151b..34c8bda2c6c427 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -1,7 +1,7 @@ """Support for testing internet speed via Fast.com.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging from typing import Any @@ -67,7 +67,7 @@ def __init__(self, hass: HomeAssistant) -> None: self.data: dict[str, Any] | None = None self._hass = hass - def update(self) -> None: + def update(self, now: datetime | None = None) -> None: """Get the latest data from fast.com.""" _LOGGER.debug("Executing fast.com speedtest") diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index e5dbbdfc1e8d1c..55aeec305fbb2d 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.6.1"], + "requirements": ["millheater==0.6.2"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index c9f4131be1addf..2fb9339955ae71 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -151,7 +151,7 @@ def __init__( "identifiers": {(DOMAIN, sensor["hardware_id"])}, "manufacturer": "Silicon Labs", "model": sensor["hardware_revision"], - "name": sensor["name"], + "name": str(sensor["name"]), "sw_version": sensor["firmware_version"], "via_device": (DOMAIN, bridge.get("hardware_id")), } diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 567164d4325962..8277f86e9f9240 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -311,7 +311,9 @@ def setup_connection_for_dialect( result = query_on_connection(dbapi_connection, "SELECT VERSION()") version = result[0][0] 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] False ) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 9ee0712aaef688..684d20f6832d7e 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -182,7 +182,7 @@ value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_HUMIDITY, 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( name="Luminosity", diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index c6bc3ae61fa0ef..8b610c6c28c23f 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==11.0.6"], + "requirements": ["simplisafe-python==11.0.7"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index ced19db39c72b2..d25d2c979015f9 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -2,7 +2,7 @@ "domain": "spider", "name": "Itho Daalderop Spider", "documentation": "https://www.home-assistant.io/integrations/spider", - "requirements": ["spiderpy==1.4.2"], + "requirements": ["spiderpy==1.4.3"], "codeowners": ["@peternijssen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index d2d59d78c0e58e..ef3bad6da4760a 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -264,5 +264,5 @@ def __init__( def available(self) -> bool: """Return if entity is available.""" 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 ) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 334dc9e8c2266f..f7cc242f6f145f 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -190,7 +190,10 @@ def native_value(self) -> str | None: # Calculate derivative. 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_time = current_timestamp - self._last_timestamp if delta_time.total_seconds() == 0: diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 64fa7b01f28ec5..a1463daed12196 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -6,7 +6,6 @@ from datetime import timedelta from ipaddress import IPv4Address, IPv6Address import logging -import socket from urllib.parse import urlparse from async_upnp_client.search import SsdpSearchListener @@ -163,9 +162,6 @@ "active_mode", ] -BULB_NETWORK_EXCEPTIONS = (socket.error,) -BULB_EXCEPTIONS = (BulbException, asyncio.TimeoutError, *BULB_NETWORK_EXCEPTIONS) - PLATFORMS = ["binary_sensor", "light"] @@ -270,7 +266,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: device = await _async_get_device(hass, entry.data[CONF_HOST], entry) await _async_initialize(hass, entry, device) - except BULB_EXCEPTIONS as ex: + except (asyncio.TimeoutError, OSError, BulbException) as ex: raise ConfigEntryNotReady from ex hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -594,13 +590,20 @@ async def _async_update_properties(self): self._available = True if not self._initialized: self._initialized = True - except BULB_NETWORK_EXCEPTIONS as ex: + except OSError as ex: if self._available: # just inform once _LOGGER.error( "Unable to update device %s, %s: %s", self._host, self.name, ex ) 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( "Unable to update device %s, %s: %s", self._host, self.name, ex ) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 67c9dc2ba07bb4..abe1285f60986c 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,6 +1,7 @@ """Light platform support for yeelight.""" from __future__ import annotations +import asyncio import logging import math @@ -8,6 +9,7 @@ import yeelight from yeelight import Bulb, Flow, RGBTransition, SleepTransition, flows from yeelight.enums import BulbType, LightType, PowerMode, SceneClass +from yeelight.main import BulbException from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -51,8 +53,6 @@ ATTR_COUNT, ATTR_MODE_MUSIC, ATTR_TRANSITIONS, - BULB_EXCEPTIONS, - BULB_NETWORK_EXCEPTIONS, CONF_FLOW_PARAMS, CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH, @@ -243,23 +243,33 @@ def _async_cmd(func): """Define a wrapper to catch exceptions from the bulb.""" async def _async_wrap(self, *args, **kwargs): - try: - _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) - return await func(self, *args, **kwargs) - except BULB_NETWORK_EXCEPTIONS as ex: - # A network error happened, the bulb is likely offline now - self.device.async_mark_unavailable() - 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 BULB_EXCEPTIONS 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 + for attempts in range(2): + try: + _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) + return await func(self, *args, **kwargs) + except asyncio.TimeoutError as ex: + # The wifi likely dropped, so we want to retry once since + # python-yeelight will auto reconnect + exc_message = str(ex) or type(ex) + if attempts == 0: + continue + raise HomeAssistantError( + f"Timed out when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" + ) from ex + except OSError as ex: + # A network error happened, the bulb is likely offline now + self.device.async_mark_unavailable() + 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 @@ -621,7 +631,11 @@ async def async_set_hs(self, hs_color, duration) -> None: """Set bulb's color.""" if not hs_color or COLOR_MODE_HS not in self.supported_color_modes: 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) # Already set, and since we get pushed updates # we avoid setting it again to ensure we do not @@ -638,7 +652,11 @@ async def async_set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" if not rgb or COLOR_MODE_RGB not in self.supported_color_modes: 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) # Already set, and since we get pushed updates # we avoid setting it again to ensure we do not @@ -657,7 +675,11 @@ async def async_set_colortemp(self, colortemp, duration) -> None: return 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) # Already set, and since we get pushed updates # we avoid setting it again to ensure we do not diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 632fdf426f227e..4682215092b3bb 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "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"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 24983bb567b936..d4465bd0c098f6 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) @@ -40,9 +41,9 @@ async def async_setup_entry( async_add_entities( [ GasSensor(coordinator, device), - PowerMeterSensor(coordinator, device, "low"), - PowerMeterSensor(coordinator, device, "high"), - PowerMeterSensor(coordinator, device, "total"), + PowerMeterSensor(coordinator, device, "low", STATE_CLASS_TOTAL_INCREASING), + PowerMeterSensor(coordinator, device, "high", STATE_CLASS_TOTAL_INCREASING), + PowerMeterSensor(coordinator, device, "total", STATE_CLASS_TOTAL), CurrentPowerSensor(coordinator, device), DeliveryMeterSensor(coordinator, device, "low"), DeliveryMeterSensor(coordinator, device, "high"), @@ -168,7 +169,11 @@ class PowerMeterSensor(YoulessBaseSensor): _attr_state_class = STATE_CLASS_TOTAL_INCREASING def __init__( - self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + self, + coordinator: DataUpdateCoordinator, + device: str, + dev_type: str, + state_class: str, ) -> None: """Instantiate a power meter sensor.""" super().__init__( @@ -177,6 +182,7 @@ def __init__( self._device = device self._type = dev_type self._attr_name = f"Power {dev_type}" + self._attr_state_class = state_class @property def get_sensor(self) -> YoulessSensor | None: diff --git a/homeassistant/const.py b/homeassistant/const.py index 2bd8a2e771953f..02d3b6db432190 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) diff --git a/requirements_all.txt b/requirements_all.txt index bd074b3c2c000f..8a7eb79934d3a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1005,7 +1005,7 @@ micloud==0.3 miflora==0.7.0 # homeassistant.components.mill -millheater==0.6.1 +millheater==0.6.2 # homeassistant.components.minio minio==4.0.9 @@ -2149,7 +2149,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==11.0.6 +simplisafe-python==11.0.7 # homeassistant.components.sisyphus sisyphus-control==3.0 @@ -2214,7 +2214,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spider -spiderpy==1.4.2 +spiderpy==1.4.3 # homeassistant.components.spotify spotipy==2.18.0 @@ -2459,7 +2459,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.7.7 +yeelight==0.7.8 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6e24b75288bc5..349912a5b2595d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -585,7 +585,7 @@ mficlient==0.3.0 micloud==0.3 # homeassistant.components.mill -millheater==0.6.1 +millheater==0.6.2 # homeassistant.components.minio minio==4.0.9 @@ -1224,7 +1224,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==11.0.6 +simplisafe-python==11.0.7 # homeassistant.components.slack slackclient==2.5.0 @@ -1263,7 +1263,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spider -spiderpy==1.4.2 +spiderpy==1.4.3 # homeassistant.components.spotify spotipy==2.18.0 @@ -1403,7 +1403,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.7.7 +yeelight==0.7.8 # homeassistant.components.youless youless-api==0.14 diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 8b5de5cff16fa0..ff690e2427945d 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -125,7 +125,8 @@ async def test_last_run_was_recently_clean(hass): @pytest.mark.parametrize( "mysql_version, db_supports_row_number", [ - ("10.0.0", True), + ("10.2.0", True), + ("10.1.0", False), ("5.8.0", True), ("5.7.0", False), ], diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 5af99e9ac2d53c..54a7fce44fb5e4 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -9,6 +9,9 @@ from homeassistant.components.upnp.const import ( BYTES_RECEIVED, BYTES_SENT, + CONFIG_ENTRY_ST, + CONFIG_ENTRY_UDN, + DOMAIN, PACKETS_RECEIVED, PACKETS_SENT, ROUTER_IP, @@ -19,6 +22,8 @@ from homeassistant.core import HomeAssistant from homeassistant.util import dt +from tests.common import MockConfigEntry + TEST_UDN = "uuid:device" TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" TEST_USN = f"{TEST_UDN}::{TEST_ST}" @@ -115,8 +120,8 @@ async def async_get_status(self) -> Mapping[str, Any]: self.status_times_polled += 1 return { WAN_STATUS: "Connected", - ROUTER_UPTIME: 0, - ROUTER_IP: "192.168.0.1", + ROUTER_UPTIME: 10, + ROUTER_IP: "8.9.10.11", } @@ -185,3 +190,24 @@ async def register_callback(hass, callback, match_dict): return_value=[], ) as 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 diff --git a/tests/components/upnp/test_binary_sensor.py b/tests/components/upnp/test_binary_sensor.py new file mode 100644 index 00000000000000..46f0021a07b789 --- /dev/null +++ b/tests/components/upnp/test_binary_sensor.py @@ -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" diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index fa315804917d4b..a704232ef842b2 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -25,6 +25,7 @@ TEST_ST, TEST_UDN, TEST_USN, + MockDevice, ) 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) assert await hass.config_entries.async_setup(config_entry.entry_id) is True 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. mock_device.traffic_times_polled = 0 diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 6b3d2a5187f0eb..7729068a2ed528 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -9,7 +9,6 @@ DOMAIN, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component 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. entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True diff --git a/tests/components/upnp/test_sensor.py b/tests/components/upnp/test_sensor.py new file mode 100644 index 00000000000000..068b5260d45daf --- /dev/null +++ b/tests/components/upnp/test_sensor.py @@ -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" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 7ddb2845ac80f8..73d2543f9d40f7 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,7 +1,9 @@ """Test Yeelight.""" +import asyncio from datetime import timedelta from unittest.mock import AsyncMock, patch +import pytest from yeelight import BulbException, BulbType 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() 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 diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 9c5a76e4a4bae4..42ac3675548ecd 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -625,6 +625,22 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): assert mocked_bulb.async_set_brightness.mock_calls == [] 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( "light", 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_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 # This last change should generate a call even though # 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_color_temp.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):