diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 96b3770974a..8d41bd359b8 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -375,6 +375,11 @@ class AirVisualEntity(Entity): """Return the icon.""" return self._icon + @property + def should_poll(self) -> bool: + """Disable polling.""" + return False + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 44085273940..4fd7b70835d 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -452,6 +452,11 @@ class ADBDevice(MediaPlayerEntity): """Provide the last ADB command's response as an attribute.""" return {"adb_response": self._adb_response} + @property + def media_image_hash(self): + """Hash value for media image.""" + return f"{datetime.now().timestamp()}" if self._screencap else None + @property def name(self): """Return the device name.""" @@ -497,11 +502,6 @@ class ADBDevice(MediaPlayerEntity): """Raw image data.""" return self.aftv.adb_screencap() - @property - def media_image_hash(self): - """Hash value for media image.""" - return f"{datetime.now().timestamp()}" - @adb_decorator() def media_play(self): """Send play command.""" diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index 9f90b074c3d..b15346417de 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -85,9 +85,10 @@ def setup_internal_discovery(hass: HomeAssistant) -> None: _LOGGER.debug("Starting internal pychromecast discovery.") listener, browser = pychromecast.start_discovery( - internal_add_callback, internal_remove_callback + internal_add_callback, + internal_remove_callback, + ChromeCastZeroconf.get_zeroconf(), ) - ChromeCastZeroconf.set_zeroconf(browser.zc) def stop_discovery(event): """Stop discovery of new chromecasts.""" diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index ddb6697d370..0be595de549 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,8 +3,8 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==5.1.0"], - "after_dependencies": ["cloud"], + "requirements": ["pychromecast==5.3.0"], + "after_dependencies": ["cloud","zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] } diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index fd7443d1c26..84917e0194a 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -14,6 +14,7 @@ from pychromecast.socket_client import ( ) import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, @@ -170,6 +171,7 @@ async def _async_setup_platform( for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): async_cast_discovered(chromecast) + ChromeCastZeroconf.set_zeroconf(await zeroconf.async_get_instance(hass)) hass.async_add_executor_job(setup_internal_discovery, hass) diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index e3a4e2f8720..867848f89d1 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -2,7 +2,7 @@ "domain": "devolo_home_control", "name": "devolo_home_control", "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", - "requirements": ["devolo-home-control-api==0.10.0"], + "requirements": ["devolo-home-control-api==0.11.0"], "config_flow": true, "codeowners": [ "@2Fake", diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 484bd708489..4d780d48cd1 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -17,10 +17,6 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -113,7 +109,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) - update_entities_telegram = partial(async_dispatcher_send, hass, DOMAIN) + def update_entities_telegram(telegram): + """Update entities with latest telegram and trigger state update.""" + # Make all device entities aware of new telegram + for device in devices: + device.update_data(telegram) # Creates an asyncio.Protocol factory for reading DSMR telegrams from # serial and calls update_entities_telegram to update entities on arrival @@ -187,17 +187,12 @@ class DSMREntity(Entity): self._config = config self.telegram = {} - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - async_dispatcher_connect(self.hass, DOMAIN, self.update_data) - ) - @callback def update_data(self, telegram): """Update data.""" self.telegram = telegram - self.async_write_ha_state() + if self.hass: + self.async_write_ha_state() def get_dsmr_object_attr(self, attribute): """Read attribute from last received telegram for this DSMR object.""" diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 53c65a6ab07..a76a29b2ed5 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -61,6 +61,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Initialize config entry which represents the HEOS controller.""" + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) + host = entry.data[CONF_HOST] # Setting all_progress_events=False ensures that we only receive a # media position update upon start of playback or when media changes diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 91dbc19ac95..138d1c4462c 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -33,12 +33,16 @@ class HeosFlowHandler(config_entries.ConfigFlow): # Abort if other flows in progress or an entry already exists if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="already_setup") + await self.async_set_unique_id(DOMAIN) # Show selection form return self.async_show_form(step_id="user") async def async_step_import(self, user_input=None): """Occurs when an entry is setup through config.""" host = user_input[CONF_HOST] + # raise_on_progress is False here in case ssdp discovers + # heos first which would block the import + await self.async_set_unique_id(DOMAIN, raise_on_progress=False) return self.async_create_entry(title=format_title(host), data={CONF_HOST: host}) async def async_step_user(self, user_input=None): diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 25b2b0381ea..a1eaf2f3a2a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -29,11 +29,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import Event, ServiceCall, callback -from homeassistant.exceptions import ( - ConfigEntryNotReady, - HomeAssistantError, - Unauthorized, -) +from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -649,13 +645,7 @@ async def async_setup_entry(hass, entry): tls_version=tls_version, ) - result: str = await hass.data[DATA_MQTT].async_connect() - - if result == CONNECTION_FAILED: - return False - - if result == CONNECTION_FAILED_RECOVERABLE: - raise ConfigEntryNotReady + await hass.data[DATA_MQTT].async_connect() async def async_stop_mqtt(_event: Event): """Stop MQTT component.""" @@ -835,15 +825,14 @@ class MQTT: self._mqttc.connect, self.broker, self.port, self.keepalive ) except OSError as err: - _LOGGER.error("Failed to connect due to exception: %s", err) - return CONNECTION_FAILED_RECOVERABLE + _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) - if result != 0: - _LOGGER.error("Failed to connect: %s", mqtt.error_string(result)) - return CONNECTION_FAILED + if result is not None and result != 0: + _LOGGER.error( + "Failed to connect to MQTT server: %s", mqtt.error_string(result) + ) self._mqttc.loop_start() - return CONNECTION_SUCCESS async def async_disconnect(self): """Stop the MQTT client.""" @@ -898,6 +887,7 @@ class MQTT: This method is a coroutine. """ + _LOGGER.debug("Unsubscribing from %s", topic) async with self._paho_lock: result: int = None result, _ = await self.hass.async_add_executor_job( @@ -933,6 +923,7 @@ class MQTT: return self.connected = True + _LOGGER.info("Connected to MQTT server (%s)", result_code) # Group subscriptions to only re-subscribe once for each topic. keyfunc = attrgetter("topic") @@ -999,7 +990,7 @@ class MQTT: def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: """Disconnected callback.""" self.connected = False - _LOGGER.warning("Disconnected from MQTT (%s).", result_code) + _LOGGER.warning("Disconnected from MQTT server (%s)", result_code) def _raise_on_error(result_code: int) -> None: diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 4ed128f1ff3..c9fc388e1a3 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -530,7 +530,7 @@ class MqttCover( async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - position = float(kwargs[ATTR_TILT_POSITION]) + position = kwargs[ATTR_TILT_POSITION] # The position needs to be between min and max level = self.find_in_range_from_percent(position) @@ -550,10 +550,7 @@ class MqttCover( percentage_position = position if set_position_template is not None: position = set_position_template.async_render(**kwargs) - elif ( - self._config[CONF_POSITION_OPEN] != 100 - and self._config[CONF_POSITION_CLOSED] != 0 - ): + else: position = self.find_in_range_from_percent(position, COVER_PAYLOAD) mqtt.async_publish( diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 3b65243d078..7ff40ffe27d 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -138,13 +138,17 @@ class Trigger: self.remove_signal = remove_signal self.type = config[CONF_TYPE] self.subtype = config[CONF_SUBTYPE] - self.topic = config[CONF_TOPIC] self.payload = config[CONF_PAYLOAD] self.qos = config[CONF_QOS] + topic_changed = self.topic != config[CONF_TOPIC] + self.topic = config[CONF_TOPIC] - # Unsubscribe+subscribe if this trigger is in use - for trig in self.trigger_instances: - await trig.async_attach_trigger() + # Unsubscribe+subscribe if this trigger is in use and topic has changed + # If topic is same unsubscribe+subscribe will execute in the wrong order + # because unsubscribe is done with help of async_create_task + if topic_changed: + for trig in self.trigger_instances: + await trig.async_attach_trigger() def detach_trigger(self): """Remove MQTT device trigger.""" diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 0e9d3ddca98..bbce71f5d28 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -297,7 +297,7 @@ class ONVIFDevice: try: ptz_service = self.device.create_ptz_service() presets = await ptz_service.GetPresets(profile.token) - profile.ptz.presets = [preset.token for preset in presets] + profile.ptz.presets = [preset.token for preset in presets if preset] except (Fault, ServerDisconnectedError): # It's OK if Presets aren't supported profile.ptz.presets = [] diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 0deeb44dbc2..69c9e24ad89 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -222,7 +222,12 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): await self.coordinator.roku.remote("home") appl = next( - (app for app in self.coordinator.data.apps if app.name == source), None + ( + app + for app in self.coordinator.data.apps + if source in (app.name, app.app_id) + ), + None, ) if appl is not None: diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index fd246520696..0ce8101f49c 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -500,6 +500,8 @@ class TodoistProjectData: events = [] for task in project_task_data: + if task["due"] is None: + continue due_date = _parse_due_date(task["due"]) if start_date < due_date < end_date: event = { diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index d699160eed4..c2fcb88c305 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -97,7 +97,7 @@ class HaServiceBrowser(ServiceBrowser): # To avoid overwhemling the system we pre-filter here and only process # DNSPointers for the configured record name (type) # - if record.name != self.type or not isinstance(record, DNSPointer): + if record.name not in self.types or not isinstance(record, DNSPointer): return super().update_record(zc, now, record) @@ -181,6 +181,7 @@ def setup(hass, config): if not service_info: # Prevent the browser thread from collapsing as # service_info can be None + _LOGGER.debug("Failed to get info for device %s", name) return info = info_from_service(service_info) @@ -216,11 +217,12 @@ def setup(hass, config): ) ) - for service in ZEROCONF: - HaServiceBrowser(zeroconf, service, handlers=[service_update]) + types = list(ZEROCONF) if HOMEKIT_TYPE not in ZEROCONF: - HaServiceBrowser(zeroconf, HOMEKIT_TYPE, handlers=[service_update]) + types.append(HOMEKIT_TYPE) + + HaServiceBrowser(zeroconf, types, handlers=[service_update]) return True diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index a3d4d1d8399..e28594d5598 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.26.1"], + "requirements": ["zeroconf==0.26.3"], "dependencies": ["api"], "codeowners": ["@robbiet480", "@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/const.py b/homeassistant/const.py index da3d94d85b9..1c49e3447e6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 110 -PATCH_VERSION = "3" +PATCH_VERSION = "4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 52981f0a1a1..0a0fc81ae5b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.16 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.26.1 +zeroconf==0.26.3 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index c1c0323a906..01d99251e22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -469,7 +469,7 @@ deluge-client==1.7.1 denonavr==0.8.1 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.10.0 +devolo-home-control-api==0.11.0 # homeassistant.components.directv directv==0.3.0 @@ -1245,7 +1245,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==5.1.0 +pychromecast==5.3.0 # homeassistant.components.cmus pycmus==0.1.1 @@ -2239,7 +2239,7 @@ youtube_dl==2020.05.08 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.26.1 +zeroconf==0.26.3 # homeassistant.components.zha zha-quirks==0.0.39 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f75c4d8694..79af61a03e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -203,7 +203,7 @@ defusedxml==0.6.0 denonavr==0.8.1 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.10.0 +devolo-home-control-api==0.11.0 # homeassistant.components.directv directv==0.3.0 @@ -527,7 +527,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==5.1.0 +pychromecast==5.3.0 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 @@ -900,7 +900,7 @@ xmltodict==0.12.0 ya_ma==0.3.8 # homeassistant.components.zeroconf -zeroconf==0.26.1 +zeroconf==0.26.3 # homeassistant.components.zha zha-quirks==0.0.39 diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index d90c4263240..800814df013 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -6,7 +6,8 @@ from pyheos import HeosError from homeassistant import data_entry_flow from homeassistant.components import heos, ssdp from homeassistant.components.heos.config_flow import HeosFlowHandler -from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS +from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP from homeassistant.const import CONF_HOST from tests.async_mock import patch @@ -55,6 +56,7 @@ async def test_create_entry_when_host_valid(hass, controller): heos.DOMAIN, context={"source": "user"}, data=data ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == data assert controller.connect.call_count == 1 @@ -70,6 +72,7 @@ async def test_create_entry_when_friendly_name_valid(hass, controller): heos.DOMAIN, context={"source": "user"}, data=data ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == {CONF_HOST: "127.0.0.1"} assert controller.connect.call_count == 1 @@ -79,28 +82,34 @@ async def test_create_entry_when_friendly_name_valid(hass, controller): async def test_discovery_shows_create_form(hass, controller, discovery_data): """Test discovery shows form to confirm setup and subsequent abort.""" + await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": "ssdp"}, data=discovery_data ) await hass.async_block_till_done() - assert len(hass.config_entries.flow.async_progress()) == 1 + flows_in_progress = hass.config_entries.flow.async_progress() + assert flows_in_progress[0]["context"]["unique_id"] == DOMAIN + assert len(flows_in_progress) == 1 assert hass.data[DATA_DISCOVERED_HOSTS] == {"Office (127.0.0.1)": "127.0.0.1"} port = urlparse(discovery_data[ssdp.ATTR_SSDP_LOCATION]).port discovery_data[ssdp.ATTR_SSDP_LOCATION] = f"http://127.0.0.2:{port}/" discovery_data[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" + await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": "ssdp"}, data=discovery_data ) await hass.async_block_till_done() - assert len(hass.config_entries.flow.async_progress()) == 1 + flows_in_progress = hass.config_entries.flow.async_progress() + assert flows_in_progress[0]["context"]["unique_id"] == DOMAIN + assert len(flows_in_progress) == 1 assert hass.data[DATA_DISCOVERED_HOSTS] == { "Office (127.0.0.1)": "127.0.0.1", "Bedroom (127.0.0.2)": "127.0.0.2", } -async def test_disovery_flow_aborts_already_setup( +async def test_discovery_flow_aborts_already_setup( hass, controller, discovery_data, config_entry ): """Test discovery flow aborts when entry already setup.""" @@ -110,3 +119,34 @@ async def test_disovery_flow_aborts_already_setup( result = await flow.async_step_ssdp(discovery_data) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_setup" + + +async def test_discovery_sets_the_unique_id(hass, controller, discovery_data): + """Test discovery sets the unique id.""" + + port = urlparse(discovery_data[ssdp.ATTR_SSDP_LOCATION]).port + discovery_data[ssdp.ATTR_SSDP_LOCATION] = f"http://127.0.0.2:{port}/" + discovery_data[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" + + await hass.config_entries.flow.async_init( + heos.DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data + ) + await hass.async_block_till_done() + flows_in_progress = hass.config_entries.flow.async_progress() + assert flows_in_progress[0]["context"]["unique_id"] == DOMAIN + assert len(flows_in_progress) == 1 + assert hass.data[DATA_DISCOVERED_HOSTS] == {"Bedroom (127.0.0.2)": "127.0.0.2"} + + +async def test_import_sets_the_unique_id(hass, controller): + """Test import sets the unique id.""" + + with patch("homeassistant.components.heos.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + heos.DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: "127.0.0.2"}, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == DOMAIN diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index a6852e3db41..a32ea5dd08c 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -31,6 +31,7 @@ async def test_async_setup_creates_entry(hass, config): entry = entries[0] assert entry.title == "Controller (127.0.0.1)" assert entry.data == {CONF_HOST: "127.0.0.1"} + assert entry.unique_id == DOMAIN async def test_async_setup_updates_entry(hass, config_entry, config, controller): @@ -44,6 +45,7 @@ async def test_async_setup_updates_entry(hass, config_entry, config, controller) entry = entries[0] assert entry.title == "Controller (127.0.0.2)" assert entry.data == {CONF_HOST: "127.0.0.2"} + assert entry.unique_id == DOMAIN async def test_async_setup_returns_true(hass, config_entry, config): diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index f77ccca57ef..aa7dc746951 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -14,6 +14,7 @@ from tests.common import ( assert_lists_same, async_fire_mqtt_message, async_get_device_automations, + async_mock_mqtt_component, async_mock_service, mock_device_registry, mock_registry, @@ -456,7 +457,7 @@ async def test_if_fires_on_mqtt_message_after_update( await hass.async_block_till_done() assert len(calls) == 1 - # Update the trigger + # Update the trigger with different topic async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data2) await hass.async_block_till_done() @@ -468,6 +469,65 @@ async def test_if_fires_on_mqtt_message_after_update( await hass.async_block_till_done() assert len(calls) == 2 + # Update the trigger with same topic + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data2) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "foobar/triggers/button1", "") + await hass.async_block_till_done() + assert len(calls) == 2 + + async_fire_mqtt_message(hass, "foobar/triggers/buttonOne", "") + await hass.async_block_till_done() + assert len(calls) == 3 + + +async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): + """Test subscription to topics without change.""" + mock_mqtt = await async_mock_mqtt_component(hass) + config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + data1 = ( + '{ "automation_type":"trigger",' + ' "device":{"identifiers":["0AFFD2"]},' + ' "topic": "foobar/triggers/button1",' + ' "type": "button_short_press",' + ' "subtype": "button_1" }' + ) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "bla1", + "type": "button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press")}, + }, + }, + ] + }, + ) + + call_count = mock_mqtt.async_subscribe.call_count + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + assert mock_mqtt.async_subscribe.call_count == call_count + async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( hass, device_reg, calls, mqtt_mock diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9ec5e09f276..3626c5a746c 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -18,7 +18,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -678,23 +677,24 @@ async def test_setup_embedded_with_embedded(hass): assert _start.call_count == 1 -async def test_setup_fails_if_no_connect_broker(hass): +async def test_setup_logs_error_if_no_connect_broker(hass, caplog): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = lambda *args: 1 - assert not await mqtt.async_setup_entry(hass, entry) + assert await mqtt.async_setup_entry(hass, entry) + assert "Failed to connect to MQTT server:" in caplog.text -async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass): +async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplog): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = MagicMock(side_effect=OSError("Connection error")) - with pytest.raises(ConfigEntryNotReady): - await mqtt.async_setup_entry(hass, entry) + assert await mqtt.async_setup_entry(hass, entry) + assert "Failed to connect to MQTT server due to exception:" in caplog.text async def test_setup_uses_certificate_on_certificate_set_to_auto(hass, mock_mqtt): diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 9d809cae433..9ac758585e9 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -332,7 +332,7 @@ async def test_services( remote_mock.assert_called_once_with("home") - with patch("homeassistant.components.roku.Roku.launch") as remote_mock: + with patch("homeassistant.components.roku.Roku.launch") as launch_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, @@ -340,7 +340,17 @@ async def test_services( blocking=True, ) - remote_mock.assert_called_once_with("12") + launch_mock.assert_called_once_with("12") + + with patch("homeassistant.components.roku.Roku.launch") as launch_mock: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: MAIN_ENTITY_ID, ATTR_INPUT_SOURCE: 12}, + blocking=True, + ) + + launch_mock.assert_called_once_with("12") async def test_tv_services( diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 89c9d0c2643..74069fa5faa 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -29,9 +29,10 @@ def mock_zeroconf(): yield mock_zc.return_value -def service_update_mock(zeroconf, service, handlers): +def service_update_mock(zeroconf, services, handlers): """Call service update handler.""" - handlers[0](zeroconf, service, f"name.{service}", ServiceStateChange.Added) + for service in services: + handlers[0](zeroconf, service, f"name.{service}", ServiceStateChange.Added) def get_service_info_mock(service_type, name): @@ -76,7 +77,7 @@ async def test_setup(hass, mock_zeroconf): mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - assert len(mock_service_browser.mock_calls) == len(zc_gen.ZEROCONF) + assert len(mock_service_browser.mock_calls) == 1 expected_flow_calls = 0 for matching_components in zc_gen.ZEROCONF.values(): expected_flow_calls += len(matching_components)