diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a17a4dc318f..6a7708c1c5c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -46,7 +46,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip setuptools + pip install -U pip==20.1.1 setuptools pip install -r requirements.txt -r requirements_test.txt # Uninstalling typing as a workaround. Eventually we should make sure # all our dependencies drop typing. @@ -603,7 +603,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip setuptools wheel + pip install -U pip==20.1.1 setuptools wheel pip install -r requirements_all.txt pip install -r requirements_test.txt # Uninstalling typing as a workaround. Eventually we should make sure diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 85e05e89cc1..92665bc1890 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -261,6 +261,7 @@ def setup_abode_events(hass): TIMELINE.AUTOMATION_GROUP, TIMELINE.DISARM_GROUP, TIMELINE.ARM_GROUP, + TIMELINE.ARM_FAULT_GROUP, TIMELINE.TEST_GROUP, TIMELINE.CAPTURE_GROUP, TIMELINE.DEVICE_GROUP, diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index b7d5f1dbe4c..99d4fd433a7 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -82,8 +82,21 @@ class AbodeCamera(AbodeDevice, Camera): return None + def turn_on(self): + """Turn on camera.""" + self._device.privacy_mode(False) + + def turn_off(self): + """Turn off camera.""" + self._device.privacy_mode(True) + def _capture_callback(self, capture): """Update the image with the device then refresh device.""" self._device.update_image_location(capture) self.get_image() self.schedule_update_ha_state() + + @property + def is_on(self): + """Return true if on.""" + return self._device.is_on diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index d59ddd6217f..e9a871035e6 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -3,7 +3,7 @@ "name": "Abode", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", - "requirements": ["abodepy==0.19.0"], + "requirements": ["abodepy==1.1.0"], "codeowners": ["@shred86"], "homekit": { "models": ["Abode", "Iota"] diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 15d58eb4620..b17a066eba7 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -230,7 +230,13 @@ class AdsHub: hnotify = int(contents.hNotification) _LOGGER.debug("Received notification %d", hnotify) - data = contents.data + + # get dynamically sized data array + data_size = contents.cbSampleSize + data = (ctypes.c_ubyte * data_size).from_address( + ctypes.addressof(contents) + + pyads.structs.SAdsNotificationHeader.data.offset + ) try: with self._lock: @@ -241,17 +247,17 @@ class AdsHub: # Parse data to desired datatype if notification_item.plc_datatype == self.PLCTYPE_BOOL: - value = bool(struct.unpack(" self._max_journeys: break + + if not self._error_notification and _deps_not_found: + self._error_notification = True + _LOGGER.info("Destination(s) %s not found", ", ".join(_deps_not_found)) + self.departures = _deps diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py index 96e1e7ed7df..9acbedd11c7 100644 --- a/homeassistant/components/songpal/config_flow.py +++ b/homeassistant/components/songpal/config_flow.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import callback from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import @@ -74,7 +75,7 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_init(self, user_input=None): """Handle a flow start.""" # Check if already configured - if self._endpoint_already_configured(): + if self._async_endpoint_already_configured(): return self.async_abort(reason="already_configured") if user_input is None: @@ -145,9 +146,10 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_init(user_input) - def _endpoint_already_configured(self): + @callback + def _async_endpoint_already_configured(self): """See if we already have an endpoint matching user input configured.""" - existing_endpoints = [ - entry.data[CONF_ENDPOINT] for entry in self._async_current_entries() - ] - return self.conf.endpoint in existing_endpoints + for entry in self._async_current_entries(): + if entry.data.get(CONF_ENDPOINT) == self.conf.endpoint: + return True + return False diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py index 8e9722316e2..640fa9bb04e 100644 --- a/homeassistant/components/toon/coordinator.py +++ b/homeassistant/components/toon/coordinator.py @@ -71,6 +71,9 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator): self.entry.data[CONF_WEBHOOK_ID] ) + # Ensure the webhook is not registered already + webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID]) + webhook_register( self.hass, DOMAIN, diff --git a/homeassistant/const.py b/homeassistant/const.py index 5c5877c5e7b..e4c5b66526c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 113 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 50e006c3d71..7642ef3480f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,6 +27,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.18 voluptuous-serialize==2.4.0 voluptuous==0.11.7 +yarl==1.4.2 zeroconf==0.27.1 pycryptodome>=3.6.6 diff --git a/requirements.txt b/requirements.txt index 93a95112658..efae1204e11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ requests==2.24.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 voluptuous-serialize==2.4.0 +yarl==1.4.2 diff --git a/requirements_all.txt b/requirements_all.txt index ea30023f8b9..9f074b745c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ WazeRouteCalculator==0.12 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.19.0 +abodepy==1.1.0 # homeassistant.components.mcp23017 adafruit-blinka==3.9.0 @@ -112,7 +112,7 @@ adafruit-circuitpython-bmp280==3.1.1 adafruit-circuitpython-mcp230xx==2.2.2 # homeassistant.components.androidtv -adb-shell[async]==0.2.0 +adb-shell[async]==0.2.1 # homeassistant.components.alarmdecoder adext==0.3 @@ -164,7 +164,7 @@ aioftp==0.12.0 aioguardian==1.0.1 # homeassistant.components.harmony -aioharmony==0.2.5 +aioharmony==0.2.6 # homeassistant.components.homekit_controller aiohomekit[IP]==0.2.45 @@ -231,7 +231,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.46 +androidtv[async]==0.0.47 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -1190,7 +1190,7 @@ py_nextbusnext==0.1.4 # py_noaa==0.3.0 # homeassistant.components.ads -pyads==3.1.3 +pyads==3.2.1 # homeassistant.components.hisense_aehw4a1 pyaehw4a1==0.3.5 @@ -1241,7 +1241,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.1.2 +pychromecast==7.2.0 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0efa7c3664f..359ff4ce433 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,10 +43,10 @@ WSDiscovery==2.0.0 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.19.0 +abodepy==1.1.0 # homeassistant.components.androidtv -adb-shell[async]==0.2.0 +adb-shell[async]==0.2.1 # homeassistant.components.adguard adguardhome==0.4.2 @@ -89,7 +89,7 @@ aiofreepybox==0.0.8 aioguardian==1.0.1 # homeassistant.components.harmony -aioharmony==0.2.5 +aioharmony==0.2.6 # homeassistant.components.homekit_controller aiohomekit[IP]==0.2.45 @@ -132,7 +132,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.46 +androidtv[async]==0.0.47 # homeassistant.components.apns apns2==0.3.0 @@ -577,7 +577,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==7.1.2 +pychromecast==7.2.0 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 diff --git a/setup.py b/setup.py index c2042ab2459..7cf06942f32 100755 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ REQUIRES = [ "ruamel.yaml==0.15.100", "voluptuous==0.11.7", "voluptuous-serialize==2.4.0", + "yarl==1.4.2", ] MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py index 0e843c59023..9db03d90222 100644 --- a/tests/components/abode/test_camera.py +++ b/tests/components/abode/test_camera.py @@ -38,3 +38,33 @@ async def test_capture_image(hass): ) await hass.async_block_till_done() mock_capture.assert_called_once() + + +async def test_camera_on(hass): + """Test the camera turn on service.""" + await setup_platform(hass, CAMERA_DOMAIN) + + with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture: + await hass.services.async_call( + CAMERA_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: "camera.test_cam"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_capture.assert_called_once_with(False) + + +async def test_camera_off(hass): + """Test the camera turn off service.""" + await setup_platform(hass, CAMERA_DOMAIN) + + with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture: + await hass.services.async_call( + CAMERA_DOMAIN, + "turn_off", + {ATTR_ENTITY_ID: "camera.test_cam"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_capture.assert_called_once_with(True) diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 8f194668e56..24be4d53ee6 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -13,7 +13,9 @@ async def test_creating_entry_sets_up_media_player(hass): "homeassistant.components.cast.media_player.async_setup_entry", return_value=True, ) as mock_setup, patch( - "pychromecast.discovery.discover_chromecasts", return_value=True + "pychromecast.discovery.discover_chromecasts", return_value=(True, None) + ), patch( + "pychromecast.discovery.stop_discovery" ): result = await hass.config_entries.flow.async_init( cast.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -34,9 +36,7 @@ async def test_configuring_cast_creates_entry(hass): """Test that specifying config will create an entry.""" with patch( "homeassistant.components.cast.async_setup_entry", return_value=True - ) as mock_setup, patch( - "pychromecast.discovery.discover_chromecasts", return_value=True - ): + ) as mock_setup: await async_setup_component( hass, cast.DOMAIN, {"cast": {"some_config": "to_trigger_import"}} ) @@ -49,9 +49,7 @@ async def test_not_configuring_cast_not_creates_entry(hass): """Test that no config will not create an entry.""" with patch( "homeassistant.components.cast.async_setup_entry", return_value=True - ) as mock_setup, patch( - "pychromecast.discovery.discover_chromecasts", return_value=True - ): + ) as mock_setup: await async_setup_component(hass, cast.DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index d159a0f025f..acf1ffd16f1 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -153,6 +153,9 @@ async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass): ) config_entry.add_to_hass(hass) + config_entry_without_host = MockConfigEntry(domain=DOMAIN, data={"name": "other"},) + config_entry_without_host.add_to_hass(hass) + harmonyapi = _get_mock_harmonyapi(connect=True) with patch( diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 5a2155441b5..d86c0d8eb88 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -247,7 +247,25 @@ async def test_form_import_dupe(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=VALID_CONFIG + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_form_import_with_ignored_entry(hass): + """Test we get abort on duplicate import when there is an ignored one.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG) + entry.add_to_hass(hass) + ignored_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) + ignored_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG ) assert result["type"] == "abort" assert result["reason"] == "already_configured" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index eacee6d9f98..dd8e9a93ab8 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -53,6 +53,7 @@ class MockResource: self.provides = ["player"] self.device = MockPlexClient(f"http://192.168.0.1{index}:32500", index + 10) self.presence = index == 0 + self.publicAddressMatches = True def connect(self, timeout): """Mock the resource connect method.""" diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index 419eaafc5eb..b576f385173 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -48,7 +48,7 @@ VALID_CONFIG_DEST = { def get_departures_mock(): """Mock rmvtransport departures loading.""" - data = { + return { "station": "Frankfurt (Main) Hauptbahnhof", "stationId": "3000010", "filter": "11111111111", @@ -145,18 +145,16 @@ def get_departures_mock(): }, ], } - return data def get_no_departures_mock(): """Mock no departures in results.""" - data = { + return { "station": "Frankfurt (Main) Hauptbahnhof", "stationId": "3000010", "filter": "11111111111", "journeys": [], } - return data async def test_rmvtransport_min_config(hass): @@ -232,4 +230,4 @@ async def test_rmvtransport_no_departures(hass): await hass.async_block_till_done() state = hass.states.get("sensor.frankfurt_main_hauptbahnhof") - assert not state + assert state.state == "unavailable"