Bump pydroid-ipcam to 2.0.0 (#76906)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
324f5555ed
commit
63dcd8ec08
12 changed files with 91 additions and 37 deletions
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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%]"
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue