diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index a2dc25d825b..6f17616a216 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -57,4 +57,4 @@ class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return if motion is detected.""" - return self.cam.export_sensor(MOTION_ACTIVE)[0] == 1.0 + return self.cam.get_sensor_value(MOTION_ACTIVE) == 1.0 diff --git a/homeassistant/components/android_ip_webcam/config_flow.py b/homeassistant/components/android_ip_webcam/config_flow.py index 09f0fdaa3a2..c41a998ff54 100644 --- a/homeassistant/components/android_ip_webcam/config_flow.py +++ b/homeassistant/components/android_ip_webcam/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any from pydroid_ipcam import PyDroidIPCam +from pydroid_ipcam.exceptions import PyDroidIPCamException, Unauthorized import voluptuous as vol from homeassistant import config_entries @@ -33,7 +34,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect.""" websession = async_get_clientsession(hass) @@ -45,8 +46,16 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: password=data.get(CONF_PASSWORD), ssl=False, ) - await cam.update() - return cam.available + errors = {} + try: + await cam.update() + except Unauthorized: + errors[CONF_USERNAME] = "invalid_auth" + errors[CONF_PASSWORD] = "invalid_auth" + except PyDroidIPCamException: + errors["base"] = "cannot_connect" + + return errors class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -68,13 +77,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) # to be removed when YAML import is removed title = user_input.get(CONF_NAME) or user_input[CONF_HOST] - if await validate_input(self.hass, user_input): + if not (errors := await validate_input(self.hass, user_input)): return self.async_create_entry(title=title, data=user_input) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, - errors={"base": "cannot_connect"}, + errors=errors, ) async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: diff --git a/homeassistant/components/android_ip_webcam/coordinator.py b/homeassistant/components/android_ip_webcam/coordinator.py index 3940c6df7e4..1647b6890c1 100644 --- a/homeassistant/components/android_ip_webcam/coordinator.py +++ b/homeassistant/components/android_ip_webcam/coordinator.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging from pydroid_ipcam import PyDroidIPCam +from pydroid_ipcam.exceptions import PyDroidIPCamException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -37,6 +38,7 @@ class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Update Android IP Webcam entities.""" - await self.cam.update() - if not self.cam.available: - raise UpdateFailed + try: + await self.cam.update() + except PyDroidIPCamException as err: + raise UpdateFailed(err) from err diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 0023454728a..29a077443c0 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", - "requirements": ["pydroid-ipcam==1.3.1"], + "requirements": ["pydroid-ipcam==2.0.0"], "codeowners": ["@engrbm87"], "iot_class": "local_polling" } diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index d699121d6c9..43a4a0c828c 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -54,8 +54,8 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda ipcam: ipcam.export_sensor("battery_level")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("battery_level")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("battery_level"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_level"), ), AndroidIPWebcamSensorEntityDescription( key="battery_temp", @@ -63,56 +63,56 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = ( icon="mdi:thermometer", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("battery_temp"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_temp"), ), AndroidIPWebcamSensorEntityDescription( key="battery_voltage", name="Battery voltage", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("battery_voltage"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_voltage"), ), AndroidIPWebcamSensorEntityDescription( key="light", name="Light level", icon="mdi:flashlight", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("light")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("light")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("light"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("light"), ), AndroidIPWebcamSensorEntityDescription( key="motion", name="Motion", icon="mdi:run", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("motion")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("motion")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("motion"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("motion"), ), AndroidIPWebcamSensorEntityDescription( key="pressure", name="Pressure", icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("pressure")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("pressure")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("pressure"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("pressure"), ), AndroidIPWebcamSensorEntityDescription( key="proximity", name="Proximity", icon="mdi:map-marker-radius", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("proximity")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("proximity")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("proximity"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("proximity"), ), AndroidIPWebcamSensorEntityDescription( key="sound", name="Sound", icon="mdi:speaker", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("sound")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("sound")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("sound"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("sound"), ), AndroidIPWebcamSensorEntityDescription( key="video_connections", diff --git a/homeassistant/components/android_ip_webcam/strings.json b/homeassistant/components/android_ip_webcam/strings.json index a9ade78a413..6f6639cecb4 100644 --- a/homeassistant/components/android_ip_webcam/strings.json +++ b/homeassistant/components/android_ip_webcam/strings.json @@ -11,7 +11,8 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index b09b0de4be8..9d2175fe3d3 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -55,8 +55,8 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = ( name="Focus", icon="mdi:image-filter-center-focus", entity_category=EntityCategory.CONFIG, - on_func=lambda ipcam: ipcam.torch(activate=True), - off_func=lambda ipcam: ipcam.torch(activate=False), + on_func=lambda ipcam: ipcam.focus(activate=True), + off_func=lambda ipcam: ipcam.focus(activate=False), ), AndroidIPWebcamSwitchEntityDescription( key="gps_active", @@ -111,8 +111,8 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = ( name="Video recording", icon="mdi:record-rec", entity_category=EntityCategory.CONFIG, - on_func=lambda ipcam: ipcam.record(activate=True), - off_func=lambda ipcam: ipcam.record(activate=False), + on_func=lambda ipcam: ipcam.record(record=True), + off_func=lambda ipcam: ipcam.record(record=False), ), ) diff --git a/homeassistant/components/android_ip_webcam/translations/en.json b/homeassistant/components/android_ip_webcam/translations/en.json index 775263225ea..be6416341c2 100644 --- a/homeassistant/components/android_ip_webcam/translations/en.json +++ b/homeassistant/components/android_ip_webcam/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" }, "step": { "user": { diff --git a/requirements_all.txt b/requirements_all.txt index 733d82c9668..88174881477 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1476,7 +1476,7 @@ pydexcom==0.2.3 pydoods==1.0.2 # homeassistant.components.android_ip_webcam -pydroid-ipcam==1.3.1 +pydroid-ipcam==2.0.0 # homeassistant.components.ebox pyebox==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef6a03ddcd9..1291ff55669 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1025,7 +1025,7 @@ pydeconz==103 pydexcom==0.2.3 # homeassistant.components.android_ip_webcam -pydroid-ipcam==1.3.1 +pydroid-ipcam==2.0.0 # homeassistant.components.econet pyeconet==0.1.15 diff --git a/tests/components/android_ip_webcam/test_config_flow.py b/tests/components/android_ip_webcam/test_config_flow.py index 1ede523ecd2..d203ef15e63 100644 --- a/tests/components/android_ip_webcam/test_config_flow.py +++ b/tests/components/android_ip_webcam/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Android IP Webcam config flow.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import Mock, patch import aiohttp @@ -99,6 +99,27 @@ async def test_device_already_configured( assert result2["reason"] == "already_configured" +async def test_form_invalid_auth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we handle invalid auth error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientResponseError(Mock(), (), status=401), + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1", "port": 8080, "username": "user", "password": "wrong-pass"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"username": "invalid_auth", "password": "invalid_auth"} + + async def test_form_cannot_connect( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/android_ip_webcam/test_init.py b/tests/components/android_ip_webcam/test_init.py index e0c21445d71..1fee1a5c388 100644 --- a/tests/components/android_ip_webcam/test_init.py +++ b/tests/components/android_ip_webcam/test_init.py @@ -3,6 +3,7 @@ from collections.abc import Awaitable from typing import Callable +from unittest.mock import Mock import aiohttp @@ -19,6 +20,8 @@ MOCK_CONFIG_DATA = { "name": "IP Webcam", "host": "1.1.1.1", "port": 8080, + "username": "user", + "password": "pass", } @@ -50,10 +53,10 @@ async def test_successful_config_entry( assert entry.state == ConfigEntryState.LOADED -async def test_setup_failed( +async def test_setup_failed_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: - """Test integration failed due to an error.""" + """Test integration failed due to connection error.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) @@ -67,6 +70,23 @@ async def test_setup_failed( assert entry.state == ConfigEntryState.SETUP_RETRY +async def test_setup_failed_invalid_auth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test integration failed due to invalid auth.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientResponseError(Mock(), (), status=401), + ) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.SETUP_RETRY + + async def test_unload_entry(hass: HomeAssistant, aioclient_mock_fixture) -> None: """Test removing integration.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA)