diff --git a/.coveragerc b/.coveragerc index 06f6bca0eec..dc35999768f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -633,6 +633,9 @@ omit = homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mochad/* + homeassistant/components/modbus/base_platform.py + homeassistant/components/modbus/binary_sensor.py + homeassistant/components/modbus/cover.py homeassistant/components/modbus/climate.py homeassistant/components/modbus/modbus.py homeassistant/components/modem_callerid/sensor.py diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 963cbb9be33..d4eb322f4d7 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.7.7"], + "requirements": ["pyatv==0.8.1"], "zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."], "after_dependencies": ["discovery"], "codeowners": ["@postlund"], diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 980ffa8549b..e9cfdb87983 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -1,4 +1,5 @@ """Support for the CO2signal platform.""" +from datetime import timedelta import logging import CO2Signal @@ -17,6 +18,7 @@ import homeassistant.helpers.config_validation as cv CONF_COUNTRY_CODE = "country_code" _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=3) ATTRIBUTION = "Data provided by CO2signal" diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 5d0b31c8788..7003038593b 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -248,10 +248,10 @@ class DeviceTrackerWatcher(WatcherBase): return ip_address = attributes.get(ATTR_IP) - hostname = attributes.get(ATTR_HOST_NAME) + hostname = attributes.get(ATTR_HOST_NAME, "") mac_address = attributes.get(ATTR_MAC) - if ip_address is None or hostname is None or mac_address is None: + if ip_address is None or mac_address is None: return self.process_client(ip_address, hostname, _format_mac(mac_address)) @@ -328,10 +328,10 @@ class DHCPWatcher(WatcherBase): return ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) or packet[IP].src - hostname = _decode_dhcp_option(options, HOSTNAME) + hostname = _decode_dhcp_option(options, HOSTNAME) or "" mac_address = _format_mac(packet[Ether].src) - if ip_address is None or hostname is None or mac_address is None: + if ip_address is None or mac_address is None: return self.process_client(ip_address, hostname, mac_address) diff --git a/homeassistant/components/fireservicerota/manifest.json b/homeassistant/components/fireservicerota/manifest.json index f35be9e839f..1eea9fbfbf1 100644 --- a/homeassistant/components/fireservicerota/manifest.json +++ b/homeassistant/components/fireservicerota/manifest.json @@ -3,7 +3,7 @@ "name": "FireServiceRota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fireservicerota", - "requirements": ["pyfireservicerota==0.0.42"], + "requirements": ["pyfireservicerota==0.0.43"], "codeowners": ["@cyberjunky"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index 954203e9b10..718900533aa 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -133,7 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name="home_plus_control_module", update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=60), + update_interval=timedelta(seconds=300), ) hass_entry_data[DATA_COORDINATOR] = coordinator diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 353cd55c747..4643a8c662a 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -3,7 +3,7 @@ "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ - "pyinsteon==1.0.11" + "pyinsteon==1.0.12" ], "codeowners": [ "@teharris1" diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 5b57e2b0b4c..5b92f9f1f6a 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -100,10 +100,8 @@ class KNXExposeSensor: def _init_expose_state(self) -> None: """Initialize state of the exposure.""" init_state = self.hass.states.get(self.entity_id) - init_value = self._get_expose_value(init_state) - self.device.sensor_value.value = ( - init_value if init_value is not None else self.expose_default - ) + state_value = self._get_expose_value(init_state) + self.device.sensor_value.value = state_value @callback def shutdown(self) -> None: @@ -116,12 +114,13 @@ class KNXExposeSensor: def _get_expose_value(self, state: State | None) -> StateType: """Extract value from state.""" if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): - return None - value = ( - state.state - if self.expose_attribute is None - else state.attributes.get(self.expose_attribute) - ) + value = self.expose_default + else: + value = ( + state.state + if self.expose_attribute is None + else state.attributes.get(self.expose_attribute, self.expose_default) + ) if self.type == "binary": if value in (1, STATE_ON, "True"): return True @@ -150,9 +149,7 @@ class KNXExposeSensor: async def _async_set_knx_value(self, value: StateType) -> None: """Set new value on xknx ExposeSensor.""" if value is None: - if self.expose_default is None: - return - value = self.expose_default + return await self.device.set(value) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 092e07eb5d2..1adc407d692 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,7 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.9"], + "requirements": ["pypck==0.7.10"], "codeowners": ["@alengwenus"], "iot_class": "local_push" } diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index e2a8a94a74a..778203a1c93 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -265,6 +265,17 @@ turn_on: min: -100 max: 100 unit_of_measurement: "%" + white: + name: White + description: + Set the light to white mode and change its brightness, where 0 turns + the light off, 1 is the minimum brightness and 255 is the maximum + brightness supported by the light. + advanced: true + selector: + number: + min: 0 + max: 255 profile: name: Profile description: Name of a light profile to use. diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index ed2f6e69863..4ff7adbdd29 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -51,6 +51,7 @@ class BasePlatform(Entity): self._value = None self._available = True self._scan_interval = int(entry[CONF_SCAN_INTERVAL]) + self._call_active = False @abstractmethod async def async_update(self, now=None): @@ -160,9 +161,14 @@ class BaseSwitch(BasePlatform, RestoreEntity): self.async_write_ha_state() return + # do not allow multiple active calls to the same platform + if self._call_active: + return + self._call_active = True result = await self._hub.async_pymodbus_call( self._slave, self._verify_address, 1, self._verify_type ) + self._call_active = False if result is None: self._available = False self.async_write_ha_state() diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index bc586e2f24d..0188210be8a 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -54,9 +54,15 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): async def async_update(self, now=None): """Update the state of the sensor.""" + + # do not allow multiple active calls to the same platform + if self._call_active: + return + self._call_active = True result = await self._hub.async_pymodbus_call( self._slave, self._address, 1, self._input_type ) + self._call_active = False if result is None: self._available = False self.async_write_ha_state() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 5c99ac86d6c..42430f609cf 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -185,13 +185,18 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): """Update Target & Current Temperature.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval + + # do not allow multiple active calls to the same platform + if self._call_active: + return + self._call_active = True self._target_temperature = await self._async_read_register( CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register ) self._current_temperature = await self._async_read_register( self._input_type, self._address ) - + self._call_active = False self.async_write_ha_state() async def _async_read_register(self, register_type, register) -> float | None: diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 88c8fd77ae8..bd150434dc1 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -149,9 +149,14 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): """Update the state of the cover.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval + # do not allow multiple active calls to the same platform + if self._call_active: + return + self._call_active = True result = await self._hub.async_pymodbus_call( self._slave, self._address, 1, self._input_type ) + self._call_active = False if result is None: self._available = False self.async_write_ha_state() diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 0826f4d5794..f2b033bec3e 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -1,5 +1,6 @@ """Support for Modbus.""" import asyncio +from copy import deepcopy import logging from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient @@ -196,7 +197,7 @@ class ModbusHub: self._config_name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] self._config_delay = client_config[CONF_DELAY] - self._pb_call = PYMODBUS_CALL.copy() + self._pb_call = deepcopy(PYMODBUS_CALL) self._pb_class = { CONF_SERIAL: ModbusSerialClient, CONF_TCP: ModbusTcpClient, diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 95ba0a65ef0..7b01d48c862 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -2,6 +2,7 @@ import logging from plexapi.exceptions import NotFound +import requests.exceptions from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.debounce import Debouncer @@ -171,6 +172,13 @@ class PlexLibrarySectionSensor(SensorEntity): self._available = True except NotFound: self._available = False + except requests.exceptions.RequestException as err: + _LOGGER.error( + "Could not update library sensor for '%s': %s", + self.library_section.title, + err, + ) + self._available = False self.async_write_ha_state() def _update_state_and_attrs(self): diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 120e38e8058..d7d3c064ad7 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -2,7 +2,7 @@ "domain": "rainbird", "name": "Rain Bird", "documentation": "https://www.home-assistant.io/integrations/rainbird", - "requirements": ["pyrainbird==0.4.2"], + "requirements": ["pyrainbird==0.4.3"], "codeowners": ["@konikvranik"], "iot_class": "local_polling" } diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 4ffe940f946..133baccf4fb 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -18,7 +18,11 @@ "dhcp": [ { "hostname": "tizen*" - } + }, + {"macaddress": "8CC8CD*"}, + {"macaddress": "606BBD*"}, + {"macaddress": "F47B5E*"}, + {"macaddress": "4844F7*"} ], "codeowners": [ "@escoand", diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 66fdd7d95be..9150099656c 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -6,6 +6,8 @@ from typing import Any from pysiaalarm import SIAEvent +from homeassistant.util.dt import utcnow + from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE PING_INTERVAL_MARGIN = 30 @@ -23,7 +25,9 @@ def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: ATTR_CODE: event.code, ATTR_MESSAGE: event.message, ATTR_ID: event.id, - ATTR_TIMESTAMP: event.timestamp.isoformat(), + ATTR_TIMESTAMP: event.timestamp.isoformat() + if event.timestamp + else utcnow().isoformat(), } @@ -42,7 +46,9 @@ def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: "code": event.code, "message": event.message, "x_data": event.x_data, - "timestamp": event.timestamp.isoformat(), + "timestamp": event.timestamp.isoformat() + if event.timestamp + else utcnow().isoformat(), "event_qualifier": event.event_qualifier, "event_type": event.event_type, "partition": event.partition, diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index a48b9ba74ce..985a0506574 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.2"], + "requirements": ["pysma==0.6.4"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 1c92e2ce51a..c88aa453d2c 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -266,6 +266,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): "manufacturer": "Spotify AB", "model": model, "name": self._name, + "entry_type": "service", } @property diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 0c572bfba8a..c6166419e39 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -183,11 +183,12 @@ class ZHADevice(LogMixin): return self._zigpy_device.model @property - def manufacturer_code(self): + def manufacturer_code(self) -> int | None: """Return the manufacturer code for the device.""" - if self._zigpy_device.node_desc.is_valid: - return self._zigpy_device.node_desc.manufacturer_code - return None + if self._zigpy_device.node_desc is None: + return None + + return self._zigpy_device.node_desc.manufacturer_code @property def nwk(self): @@ -210,17 +211,20 @@ class ZHADevice(LogMixin): return self._zigpy_device.last_seen @property - def is_mains_powered(self): + def is_mains_powered(self) -> bool | None: """Return true if device is mains powered.""" + if self._zigpy_device.node_desc is None: + return None + return self._zigpy_device.node_desc.is_mains_powered @property - def device_type(self): + def device_type(self) -> str: """Return the logical device type for the device.""" - node_descriptor = self._zigpy_device.node_desc - return ( - node_descriptor.logical_type.name if node_descriptor.is_valid else UNKNOWN - ) + if self._zigpy_device.node_desc is None: + return UNKNOWN + + return self._zigpy_device.node_desc.logical_type.name @property def power_source(self): @@ -230,18 +234,27 @@ class ZHADevice(LogMixin): ) @property - def is_router(self): + def is_router(self) -> bool | None: """Return true if this is a routing capable device.""" + if self._zigpy_device.node_desc is None: + return None + return self._zigpy_device.node_desc.is_router @property - def is_coordinator(self): + def is_coordinator(self) -> bool | None: """Return true if this device represents the coordinator.""" + if self._zigpy_device.node_desc is None: + return None + return self._zigpy_device.node_desc.is_coordinator @property - def is_end_device(self): + def is_end_device(self) -> bool | None: """Return true if this device is an end device.""" + if self._zigpy_device.node_desc is None: + return None + return self._zigpy_device.node_desc.is_end_device @property diff --git a/homeassistant/const.py b/homeassistant/const.py index 2c90713249c..2fb83613a3f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __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/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 82b09e5f7ef..dbdaaf6da5e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -175,6 +175,22 @@ DHCP = [ "domain": "samsungtv", "hostname": "tizen*" }, + { + "domain": "samsungtv", + "macaddress": "8CC8CD*" + }, + { + "domain": "samsungtv", + "macaddress": "606BBD*" + }, + { + "domain": "samsungtv", + "macaddress": "F47B5E*" + }, + { + "domain": "samsungtv", + "macaddress": "4844F7*" + }, { "domain": "screenlogic", "hostname": "pentair: *", diff --git a/requirements_all.txt b/requirements_all.txt index 8634b6957ea..f57ac367ca3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1318,7 +1318,7 @@ pyatmo==5.2.0 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.7.7 +pyatv==0.8.1 # homeassistant.components.bbox pybbox==0.0.5-alpha @@ -1423,7 +1423,7 @@ pyezviz==0.1.8.9 pyfido==2.1.1 # homeassistant.components.fireservicerota -pyfireservicerota==0.0.42 +pyfireservicerota==0.0.43 # homeassistant.components.flexit pyflexit==0.3 @@ -1490,7 +1490,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.11 +pyinsteon==1.0.12 # homeassistant.components.intesishome pyintesishome==1.7.6 @@ -1669,7 +1669,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.9 +pypck==0.7.10 # homeassistant.components.pjlink pypjlink2==1.2.1 @@ -1696,7 +1696,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.4.2 +pyrainbird==0.4.3 # homeassistant.components.recswitch pyrecswitch==1.0.2 @@ -1749,7 +1749,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.2 +pysma==0.6.4 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9799999a79..1bec3389bbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -743,7 +743,7 @@ pyatag==0.3.5.3 pyatmo==5.2.0 # homeassistant.components.apple_tv -pyatv==0.7.7 +pyatv==0.8.1 # homeassistant.components.blackbird pyblackbird==0.5 @@ -791,7 +791,7 @@ pyezviz==0.1.8.9 pyfido==2.1.1 # homeassistant.components.fireservicerota -pyfireservicerota==0.0.42 +pyfireservicerota==0.0.43 # homeassistant.components.flume pyflume==0.5.5 @@ -837,7 +837,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.11 +pyinsteon==1.0.12 # homeassistant.components.ipma pyipma==2.0.5 @@ -950,7 +950,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.9 +pypck==0.7.10 # homeassistant.components.plaato pyplaato==0.0.15 @@ -991,7 +991,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.2 +pysma==0.6.4 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py index db543007fb2..f07fa7d70bb 100644 --- a/tests/components/apple_tv/conftest.py +++ b/tests/components/apple_tv/conftest.py @@ -2,7 +2,8 @@ from unittest.mock import patch -from pyatv import conf, net +from pyatv import conf +from pyatv.support.http import create_session import pytest from .common import MockPairingHandler, create_conf @@ -39,7 +40,7 @@ def pairing(): async def _pair(config, protocol, loop, session=None, **kwargs): handler = MockPairingHandler( - await net.create_session(session), config.get_service(protocol) + await create_session(session), config.get_service(protocol) ) handler.always_fail = mock_pair.always_fail return handler @@ -121,11 +122,7 @@ def dmap_device_with_credentials(mock_scan): @pytest.fixture -def airplay_device(mock_scan): +def device_with_no_services(mock_scan): """Mock pyatv.scan.""" - mock_scan.result.append( - create_conf( - "127.0.0.1", "AirPlay Device", conf.AirPlayService("airplayid", port=7777) - ) - ) + mock_scan.result.append(create_conf("127.0.0.1", "Invalid Device")) yield mock_scan diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 615a1f404f5..45edaa36251 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -236,15 +236,15 @@ async def test_user_adds_existing_device(hass, mrp_device): assert result2["errors"] == {"base": "already_configured"} -async def test_user_adds_unusable_device(hass, airplay_device): - """Test that it is not possible to add pure AirPlay device.""" +async def test_user_adds_unusable_device(hass, device_with_no_services): + """Test that it is not possible to add device with no services.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"device_input": "AirPlay Device"}, + {"device_input": "Invalid Device"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "no_usable_service"} diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 122e81786c2..0da383c758a 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -81,6 +81,47 @@ RAW_DHCP_RENEWAL = ( b"\x43\x37\x08\x01\x21\x03\x06\x1c\x33\x3a\x3b\xff" ) +# 60:6b:bd:59:e4:b4 192.168.107.151 +RAW_DHCP_REQUEST_WITHOUT_HOSTNAME = ( + b"\xff\xff\xff\xff\xff\xff\x60\x6b\xbd\x59\xe4\xb4\x08\x00\x45\x00" + b"\x02\x40\x00\x00\x00\x00\x40\x11\x78\xae\x00\x00\x00\x00\xff\xff" + b"\xff\xff\x00\x44\x00\x43\x02\x2c\x02\x04\x01\x01\x06\x00\xff\x92" + b"\x7e\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x60\x6b\xbd\x59\xe4\xb4\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01\x03\x3d\x07\x01" + b"\x60\x6b\xbd\x59\xe4\xb4\x3c\x25\x75\x64\x68\x63\x70\x20\x31\x2e" + b"\x31\x34\x2e\x33\x2d\x56\x44\x20\x4c\x69\x6e\x75\x78\x20\x56\x44" + b"\x4c\x69\x6e\x75\x78\x2e\x31\x2e\x32\x2e\x31\x2e\x78\x32\x04\xc0" + b"\xa8\x6b\x97\x36\x04\xc0\xa8\x6b\x01\x37\x07\x01\x03\x06\x0c\x0f" + b"\x1c\x2a\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) + async def test_dhcp_match_hostname_and_macaddress(hass): """Test matching based on hostname and macaddress.""" @@ -182,6 +223,29 @@ async def test_dhcp_match_macaddress(hass): } +async def test_dhcp_match_macaddress_without_hostname(hass): + """Test matching based on macaddress only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "macaddress": "606BBD*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST_WITHOUT_HOSTNAME) + + with patch.object(hass.config_entries.flow, "async_init") as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.107.151", + dhcp.HOSTNAME: "", + dhcp.MAC_ADDRESS: "606bbd59e4b4", + } + + async def test_dhcp_nomatch(hass): """Test not matching based on macaddress only.""" dhcp_watcher = dhcp.DHCPWatcher( diff --git a/tests/components/home_plus_control/test_switch.py b/tests/components/home_plus_control/test_switch.py index aec23f0d32a..75d416ba2b1 100644 --- a/tests/components/home_plus_control/test_switch.py +++ b/tests/components/home_plus_control/test_switch.py @@ -146,7 +146,7 @@ async def test_plant_topology_reduction_change( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -208,7 +208,7 @@ async def test_plant_topology_increase_change( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -268,7 +268,7 @@ async def test_module_status_unavailable(hass, mock_config_entry, mock_modules): return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -339,7 +339,7 @@ async def test_module_status_available( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -443,7 +443,7 @@ async def test_update_with_api_error( side_effect=HomePlusControlApiError, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=100) + hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 diff --git a/tests/components/plex/test_sensor.py b/tests/components/plex/test_sensor.py index 5fa50892f32..39a2901e72d 100644 --- a/tests/components/plex/test_sensor.py +++ b/tests/components/plex/test_sensor.py @@ -1,6 +1,8 @@ """Tests for Plex sensors.""" from datetime import timedelta +import requests.exceptions + from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er @@ -15,6 +17,7 @@ LIBRARY_UPDATE_PAYLOAD = {"StatusNotification": [{"title": "Library scan complet async def test_library_sensor_values( hass, + caplog, setup_plex_server, mock_websocket, requests_mock, @@ -63,6 +66,34 @@ async def test_library_sensor_values( assert library_tv_sensor.attributes["seasons"] == 1 assert library_tv_sensor.attributes["shows"] == 1 + # Handle `requests` exception + requests_mock.get( + "/library/sections/2/all?includeCollections=0&type=2", + exc=requests.exceptions.ReadTimeout, + ) + trigger_plex_update( + mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD + ) + await hass.async_block_till_done() + + library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows") + assert library_tv_sensor.state == STATE_UNAVAILABLE + + assert "Could not update library sensor" in caplog.text + + # Ensure sensor updates properly when it recovers + requests_mock.get( + "/library/sections/2/all?includeCollections=0&type=2", + text=library_tvshows_size, + ) + trigger_plex_update( + mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD + ) + await hass.async_block_till_done() + + library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows") + assert library_tv_sensor.state == "10" + # Handle library deletion requests_mock.get( "/library/sections/2/all?includeCollections=0&type=2", status_code=404 diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index a0d2875ca59..1c9fdbcd0c5 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -3,7 +3,7 @@ import socket from unittest.mock import Mock, PropertyMock, call, patch from samsungctl.exceptions import AccessDenied, UnhandledResponse -from samsungtvws.exceptions import ConnectionFailure +from samsungtvws.exceptions import ConnectionFailure, HttpApiError from websocket import WebSocketException, WebSocketProtocolException from homeassistant import config_entries @@ -86,6 +86,7 @@ MOCK_SSDP_DATA_WRONGMODEL = { ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", } MOCK_DHCP_DATA = {IP_ADDRESS: "fake_host", MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"} +EXISTING_IP = "192.168.40.221" MOCK_ZEROCONF_DATA = { CONF_HOST: "fake_host", CONF_PORT: 1234, @@ -99,7 +100,13 @@ MOCK_ZEROCONF_DATA = { MOCK_OLD_ENTRY = { CONF_HOST: "fake_host", CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", - CONF_IP_ADDRESS: "fake_ip_old", + CONF_IP_ADDRESS: EXISTING_IP, + CONF_METHOD: "legacy", + CONF_PORT: None, +} +MOCK_LEGACY_ENTRY = { + CONF_HOST: EXISTING_IP, + CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", CONF_METHOD: "legacy", CONF_PORT: None, } @@ -306,17 +313,22 @@ async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock): assert result["type"] == "form" assert result["step_id"] == "confirm" - # entry was added - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input="whatever" - ) - assert result["type"] == "create_entry" - assert result["title"] == "fake2_model" - assert result["data"][CONF_HOST] == "fake2_host" - assert result["data"][CONF_NAME] == "fake2_model" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" - assert result["data"][CONF_MODEL] == "fake2_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" + with patch( + "homeassistant.components.samsungtv.bridge.Remote.__enter__", + return_value=True, + ): + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake2_model" + assert result["data"][CONF_HOST] == "fake2_host" + assert result["data"][CONF_NAME] == "fake2_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" + assert result["data"][CONF_MODEL] == "fake2_model" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remote: Mock): @@ -867,7 +879,7 @@ async def test_update_old_entry(hass: HomeAssistant, remote: Mock): assert len(config_entries_domain) == 1 assert entry is config_entries_domain[0] assert entry.data[CONF_ID] == "0d1cef00-00dc-1000-9c80-4844f7b172de_old" - assert entry.data[CONF_IP_ADDRESS] == "fake_ip_old" + assert entry.data[CONF_IP_ADDRESS] == EXISTING_IP assert not entry.unique_id assert await async_setup_component(hass, DOMAIN, {}) is True @@ -998,6 +1010,69 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" +async def test_update_legacy_missing_mac_from_dhcp(hass, remote: Mock): + """Test missing mac added.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_LEGACY_ENTRY, + unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={IP_ADDRESS: EXISTING_IP, MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"}, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + + +async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(hass, remote: Mock): + """Test missing mac added when there is no unique id.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_LEGACY_ENTRY, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS.rest_device_info", + side_effect=HttpApiError, + ), patch( + "homeassistant.components.samsungtv.bridge.Remote.__enter__", + return_value=True, + ), patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={IP_ADDRESS: EXISTING_IP, MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"}, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id is None + + async def test_form_reauth_legacy(hass, remote: Mock): """Test reauthenticate legacy.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) @@ -1068,9 +1143,6 @@ async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): ) await hass.async_block_till_done() - import pprint - - pprint.pprint(result2) assert result2["type"] == "form" assert result2["errors"] == {"base": RESULT_AUTH_MISSING} diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index eb65cc4fd2e..5180e9dbc07 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -3,8 +3,8 @@ import asyncio import time from unittest.mock import AsyncMock, Mock -from zigpy.device import Device as zigpy_dev -from zigpy.endpoint import Endpoint as zigpy_ep +import zigpy.device as zigpy_dev +import zigpy.endpoint as zigpy_ep import zigpy.profiles.zha import zigpy.types import zigpy.zcl @@ -27,7 +27,7 @@ class FakeEndpoint: self.out_clusters = {} self._cluster_attr = {} self.member_of = {} - self.status = 1 + self.status = zigpy_ep.Status.ZDO_INIT self.manufacturer = manufacturer self.model = model self.profile_id = zigpy.profiles.zha.PROFILE_ID @@ -57,7 +57,7 @@ class FakeEndpoint: @property def __class__(self): """Fake being Zigpy endpoint.""" - return zigpy_ep + return zigpy_ep.Endpoint @property def unique_id(self): @@ -65,8 +65,8 @@ class FakeEndpoint: return self.device.ieee, self.endpoint_id -FakeEndpoint.add_to_group = zigpy_ep.add_to_group -FakeEndpoint.remove_from_group = zigpy_ep.remove_from_group +FakeEndpoint.add_to_group = zigpy_ep.Endpoint.add_to_group +FakeEndpoint.remove_from_group = zigpy_ep.Endpoint.remove_from_group def patch_cluster(cluster): @@ -125,12 +125,11 @@ class FakeDevice: self.lqi = 255 self.rssi = 8 self.last_seen = time.time() - self.status = 2 + self.status = zigpy_dev.Status.ENDPOINTS_INIT self.initializing = False self.skip_configuration = False self.manufacturer = manufacturer self.model = model - self.node_desc = zigpy.zdo.types.NodeDescriptor() self.remove_from_group = AsyncMock() if node_desc is None: node_desc = b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00" @@ -138,7 +137,7 @@ class FakeDevice: self.neighbors = [] -FakeDevice.add_to_group = zigpy_dev.add_to_group +FakeDevice.add_to_group = zigpy_dev.Device.add_to_group def get_zha_gateway(hass):