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
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return if motion is detected."""
|
"""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 typing import Any
|
||||||
|
|
||||||
from pydroid_ipcam import PyDroidIPCam
|
from pydroid_ipcam import PyDroidIPCam
|
||||||
|
from pydroid_ipcam.exceptions import PyDroidIPCamException, Unauthorized
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
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."""
|
"""Validate the user input allows us to connect."""
|
||||||
|
|
||||||
websession = async_get_clientsession(hass)
|
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),
|
password=data.get(CONF_PASSWORD),
|
||||||
ssl=False,
|
ssl=False,
|
||||||
)
|
)
|
||||||
await cam.update()
|
errors = {}
|
||||||
return cam.available
|
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):
|
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
|
# to be removed when YAML import is removed
|
||||||
title = user_input.get(CONF_NAME) or user_input[CONF_HOST]
|
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_create_entry(title=title, data=user_input)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
errors={"base": "cannot_connect"},
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
||||||
|
|
|
@ -4,6 +4,7 @@ from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pydroid_ipcam import PyDroidIPCam
|
from pydroid_ipcam import PyDroidIPCam
|
||||||
|
from pydroid_ipcam.exceptions import PyDroidIPCamException
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
|
@ -37,6 +38,7 @@ class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
|
||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Update Android IP Webcam entities."""
|
"""Update Android IP Webcam entities."""
|
||||||
await self.cam.update()
|
try:
|
||||||
if not self.cam.available:
|
await self.cam.update()
|
||||||
raise UpdateFailed
|
except PyDroidIPCamException as err:
|
||||||
|
raise UpdateFailed(err) from err
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["repairs"],
|
"dependencies": ["repairs"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||||
"requirements": ["pydroid-ipcam==1.3.1"],
|
"requirements": ["pydroid-ipcam==2.0.0"],
|
||||||
"codeowners": ["@engrbm87"],
|
"codeowners": ["@engrbm87"],
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,8 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = (
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
value_fn=lambda ipcam: ipcam.export_sensor("battery_level")[0],
|
value_fn=lambda ipcam: ipcam.get_sensor_value("battery_level"),
|
||||||
unit_fn=lambda ipcam: ipcam.export_sensor("battery_level")[1],
|
unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_level"),
|
||||||
),
|
),
|
||||||
AndroidIPWebcamSensorEntityDescription(
|
AndroidIPWebcamSensorEntityDescription(
|
||||||
key="battery_temp",
|
key="battery_temp",
|
||||||
|
@ -63,56 +63,56 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = (
|
||||||
icon="mdi:thermometer",
|
icon="mdi:thermometer",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
value_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[0],
|
value_fn=lambda ipcam: ipcam.get_sensor_value("battery_temp"),
|
||||||
unit_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[1],
|
unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_temp"),
|
||||||
),
|
),
|
||||||
AndroidIPWebcamSensorEntityDescription(
|
AndroidIPWebcamSensorEntityDescription(
|
||||||
key="battery_voltage",
|
key="battery_voltage",
|
||||||
name="Battery voltage",
|
name="Battery voltage",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
value_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[0],
|
value_fn=lambda ipcam: ipcam.get_sensor_value("battery_voltage"),
|
||||||
unit_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[1],
|
unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_voltage"),
|
||||||
),
|
),
|
||||||
AndroidIPWebcamSensorEntityDescription(
|
AndroidIPWebcamSensorEntityDescription(
|
||||||
key="light",
|
key="light",
|
||||||
name="Light level",
|
name="Light level",
|
||||||
icon="mdi:flashlight",
|
icon="mdi:flashlight",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda ipcam: ipcam.export_sensor("light")[0],
|
value_fn=lambda ipcam: ipcam.get_sensor_value("light"),
|
||||||
unit_fn=lambda ipcam: ipcam.export_sensor("light")[1],
|
unit_fn=lambda ipcam: ipcam.get_sensor_unit("light"),
|
||||||
),
|
),
|
||||||
AndroidIPWebcamSensorEntityDescription(
|
AndroidIPWebcamSensorEntityDescription(
|
||||||
key="motion",
|
key="motion",
|
||||||
name="Motion",
|
name="Motion",
|
||||||
icon="mdi:run",
|
icon="mdi:run",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda ipcam: ipcam.export_sensor("motion")[0],
|
value_fn=lambda ipcam: ipcam.get_sensor_value("motion"),
|
||||||
unit_fn=lambda ipcam: ipcam.export_sensor("motion")[1],
|
unit_fn=lambda ipcam: ipcam.get_sensor_unit("motion"),
|
||||||
),
|
),
|
||||||
AndroidIPWebcamSensorEntityDescription(
|
AndroidIPWebcamSensorEntityDescription(
|
||||||
key="pressure",
|
key="pressure",
|
||||||
name="Pressure",
|
name="Pressure",
|
||||||
icon="mdi:gauge",
|
icon="mdi:gauge",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda ipcam: ipcam.export_sensor("pressure")[0],
|
value_fn=lambda ipcam: ipcam.get_sensor_value("pressure"),
|
||||||
unit_fn=lambda ipcam: ipcam.export_sensor("pressure")[1],
|
unit_fn=lambda ipcam: ipcam.get_sensor_unit("pressure"),
|
||||||
),
|
),
|
||||||
AndroidIPWebcamSensorEntityDescription(
|
AndroidIPWebcamSensorEntityDescription(
|
||||||
key="proximity",
|
key="proximity",
|
||||||
name="Proximity",
|
name="Proximity",
|
||||||
icon="mdi:map-marker-radius",
|
icon="mdi:map-marker-radius",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda ipcam: ipcam.export_sensor("proximity")[0],
|
value_fn=lambda ipcam: ipcam.get_sensor_value("proximity"),
|
||||||
unit_fn=lambda ipcam: ipcam.export_sensor("proximity")[1],
|
unit_fn=lambda ipcam: ipcam.get_sensor_unit("proximity"),
|
||||||
),
|
),
|
||||||
AndroidIPWebcamSensorEntityDescription(
|
AndroidIPWebcamSensorEntityDescription(
|
||||||
key="sound",
|
key="sound",
|
||||||
name="Sound",
|
name="Sound",
|
||||||
icon="mdi:speaker",
|
icon="mdi:speaker",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda ipcam: ipcam.export_sensor("sound")[0],
|
value_fn=lambda ipcam: ipcam.get_sensor_value("sound"),
|
||||||
unit_fn=lambda ipcam: ipcam.export_sensor("sound")[1],
|
unit_fn=lambda ipcam: ipcam.get_sensor_unit("sound"),
|
||||||
),
|
),
|
||||||
AndroidIPWebcamSensorEntityDescription(
|
AndroidIPWebcamSensorEntityDescription(
|
||||||
key="video_connections",
|
key="video_connections",
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"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": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
|
|
@ -55,8 +55,8 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = (
|
||||||
name="Focus",
|
name="Focus",
|
||||||
icon="mdi:image-filter-center-focus",
|
icon="mdi:image-filter-center-focus",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
on_func=lambda ipcam: ipcam.torch(activate=True),
|
on_func=lambda ipcam: ipcam.focus(activate=True),
|
||||||
off_func=lambda ipcam: ipcam.torch(activate=False),
|
off_func=lambda ipcam: ipcam.focus(activate=False),
|
||||||
),
|
),
|
||||||
AndroidIPWebcamSwitchEntityDescription(
|
AndroidIPWebcamSwitchEntityDescription(
|
||||||
key="gps_active",
|
key="gps_active",
|
||||||
|
@ -111,8 +111,8 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = (
|
||||||
name="Video recording",
|
name="Video recording",
|
||||||
icon="mdi:record-rec",
|
icon="mdi:record-rec",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
on_func=lambda ipcam: ipcam.record(activate=True),
|
on_func=lambda ipcam: ipcam.record(record=True),
|
||||||
off_func=lambda ipcam: ipcam.record(activate=False),
|
off_func=lambda ipcam: ipcam.record(record=False),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"already_configured": "Device is already configured"
|
"already_configured": "Device is already configured"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Failed to connect"
|
"cannot_connect": "Failed to connect",
|
||||||
|
"invalid_auth": "Invalid authentication"
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
|
|
@ -1476,7 +1476,7 @@ pydexcom==0.2.3
|
||||||
pydoods==1.0.2
|
pydoods==1.0.2
|
||||||
|
|
||||||
# homeassistant.components.android_ip_webcam
|
# homeassistant.components.android_ip_webcam
|
||||||
pydroid-ipcam==1.3.1
|
pydroid-ipcam==2.0.0
|
||||||
|
|
||||||
# homeassistant.components.ebox
|
# homeassistant.components.ebox
|
||||||
pyebox==1.1.4
|
pyebox==1.1.4
|
||||||
|
|
|
@ -1025,7 +1025,7 @@ pydeconz==103
|
||||||
pydexcom==0.2.3
|
pydexcom==0.2.3
|
||||||
|
|
||||||
# homeassistant.components.android_ip_webcam
|
# homeassistant.components.android_ip_webcam
|
||||||
pydroid-ipcam==1.3.1
|
pydroid-ipcam==2.0.0
|
||||||
|
|
||||||
# homeassistant.components.econet
|
# homeassistant.components.econet
|
||||||
pyeconet==0.1.15
|
pyeconet==0.1.15
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Test the Android IP Webcam config flow."""
|
"""Test the Android IP Webcam config flow."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
@ -99,6 +99,27 @@ async def test_device_already_configured(
|
||||||
assert result2["reason"] == "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(
|
async def test_form_cannot_connect(
|
||||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
@ -19,6 +20,8 @@ MOCK_CONFIG_DATA = {
|
||||||
"name": "IP Webcam",
|
"name": "IP Webcam",
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
|
"username": "user",
|
||||||
|
"password": "pass",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,10 +53,10 @@ async def test_successful_config_entry(
|
||||||
assert entry.state == ConfigEntryState.LOADED
|
assert entry.state == ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_failed(
|
async def test_setup_failed_connection_error(
|
||||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test integration failed due to an error."""
|
"""Test integration failed due to connection error."""
|
||||||
|
|
||||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA)
|
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
@ -67,6 +70,23 @@ async def test_setup_failed(
|
||||||
assert entry.state == ConfigEntryState.SETUP_RETRY
|
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:
|
async def test_unload_entry(hass: HomeAssistant, aioclient_mock_fixture) -> None:
|
||||||
"""Test removing integration."""
|
"""Test removing integration."""
|
||||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA)
|
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA)
|
||||||
|
|
Loading…
Add table
Reference in a new issue