Skip to content

Commit

Permalink
Update: Onvif dependency to commit 0e8f139
Browse files Browse the repository at this point in the history
  • Loading branch information
JurajNyiri committed May 5, 2023
1 parent 09b0b7c commit 6c4d9a8
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 64 deletions.
38 changes: 26 additions & 12 deletions custom_components/tapo_control/lib/onvif/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""The ONVIF integration."""
from httpx import RequestError
from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError
from zeep.exceptions import Fault

from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS
from homeassistant.components.stream import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS
Expand Down Expand Up @@ -27,9 +29,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

device = ONVIFDevice(hass, entry)

if not await device.async_setup():
try:
await device.async_setup()
except RequestError as err:
await device.device.close()
raise ConfigEntryNotReady(
f"Could not connect to camera {device.device.host}:{device.device.port}: {err}"
) from err
except Fault as err:
await device.device.close()
return False
# We do no know if the credentials are wrong or the camera is
# still booting up, so we will retry later
raise ConfigEntryNotReady(
f"Could not connect to camera, verify credentials are correct: {err}"
) from err
except ONVIFError as err:
await device.device.close()
raise ConfigEntryNotReady(
f"Could not setup camera {device.device.host}:{device.device.port}: {err}"
) from err

if not device.available:
raise ConfigEntryNotReady()
Expand All @@ -39,15 +57,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

hass.data[DOMAIN][entry.unique_id] = device

platforms = [Platform.BUTTON, Platform.CAMERA]
device.platforms = [Platform.BUTTON, Platform.CAMERA]

if device.capabilities.events:
platforms += [Platform.BINARY_SENSOR, Platform.SENSOR]
device.platforms += [Platform.BINARY_SENSOR, Platform.SENSOR]

if device.capabilities.imaging:
platforms += [Platform.SWITCH]
device.platforms += [Platform.SWITCH]

await hass.config_entries.async_forward_entry_setups(entry, platforms)
await hass.config_entries.async_forward_entry_setups(entry, device.platforms)

entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop)
Expand All @@ -59,16 +77,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""

device = hass.data[DOMAIN][entry.unique_id]
platforms = ["camera"]
device: ONVIFDevice = hass.data[DOMAIN][entry.unique_id]

if device.capabilities.events and device.events.started:
platforms += [Platform.BINARY_SENSOR, Platform.SENSOR]
await device.events.async_stop()
if device.capabilities.imaging:
platforms += [Platform.SWITCH]

return await hass.config_entries.async_unload_platforms(entry, platforms)
return await hass.config_entries.async_unload_platforms(entry, device.platforms)


async def _get_snapshot_auth(device):
Expand Down
27 changes: 27 additions & 0 deletions custom_components/tapo_control/lib/onvif/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from zeep.exceptions import Fault

from homeassistant import config_entries
from homeassistant.components import dhcp
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS
from homeassistant.components.stream import (
CONF_RTSP_TRANSPORT,
Expand All @@ -27,6 +28,8 @@
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import device_registry as dr

from .const import CONF_DEVICE_ID, DEFAULT_ARGUMENTS, DEFAULT_PORT, DOMAIN, LOGGER
from .device import get_device
Expand Down Expand Up @@ -101,6 +104,30 @@ async def async_step_user(self, user_input=None):
data_schema=vol.Schema({vol.Required("auto", default=True): bool}),
)

async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Handle dhcp discovery."""
hass = self.hass
mac = discovery_info.macaddress
registry = dr.async_get(self.hass)
if not (
device := registry.async_get_device(
identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, mac)}
)
):
return self.async_abort(reason="no_devices_found")
for entry_id in device.config_entries:
if (
not (entry := hass.config_entries.async_get_entry(entry_id))
or entry.domain != DOMAIN
or entry.state is config_entries.ConfigEntryState.LOADED
):
continue
if hass.config_entries.async_update_entry(
entry, data=entry.data | {CONF_HOST: discovery_info.ip}
):
hass.async_create_task(self.hass.config_entries.async_reload(entry_id))
return self.async_abort(reason="already_configured")

async def async_step_device(self, user_input=None):
"""Handle WS-Discovery.
Expand Down
83 changes: 31 additions & 52 deletions custom_components/tapo_control/lib/onvif/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
Platform,
)
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
Expand Down Expand Up @@ -55,6 +56,7 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
self.capabilities: Capabilities = Capabilities()
self.profiles: list[Profile] = []
self.max_resolution: int = 0
self.platforms: list[Platform] = []

self._dt_diff_seconds: float = 0

Expand Down Expand Up @@ -83,7 +85,7 @@ def password(self) -> str:
"""Return the password of this device."""
return self.config_entry.data[CONF_PASSWORD]

async def async_setup(self) -> bool:
async def async_setup(self) -> None:
"""Set up the device."""
self.device = get_device(
self.hass,
Expand All @@ -94,57 +96,34 @@ async def async_setup(self) -> bool:
)

# Get all device info
try:
await self.device.update_xaddrs()
await self.async_check_date_and_time()

# Create event manager
assert self.config_entry.unique_id
self.events = EventManager(
self.hass, self.device, self.config_entry.unique_id
)

# Fetch basic device info and capabilities
self.info = await self.async_get_device_info()
LOGGER.debug("Camera %s info = %s", self.name, self.info)
self.capabilities = await self.async_get_capabilities()
LOGGER.debug("Camera %s capabilities = %s", self.name, self.capabilities)
self.profiles = await self.async_get_profiles()
LOGGER.debug("Camera %s profiles = %s", self.name, self.profiles)

# No camera profiles to add
if not self.profiles:
return False

if self.capabilities.ptz:
self.device.create_ptz_service()

# Determine max resolution from profiles
self.max_resolution = max(
profile.video.resolution.width
for profile in self.profiles
if profile.video.encoding == "H264"
)
except RequestError as err:
LOGGER.warning(
"Couldn't connect to camera '%s', but will retry later. Error: %s",
self.name,
err,
)
self.available = False
await self.device.close()
except Fault as err:
LOGGER.error(
(
"Couldn't connect to camera '%s', please verify "
"that the credentials are correct. Error: %s"
),
self.name,
err,
)
return False

return True
await self.device.update_xaddrs()
await self.async_check_date_and_time()

# Create event manager
assert self.config_entry.unique_id
self.events = EventManager(self.hass, self.device, self.config_entry.unique_id)

# Fetch basic device info and capabilities
self.info = await self.async_get_device_info()
LOGGER.debug("Camera %s info = %s", self.name, self.info)
self.capabilities = await self.async_get_capabilities()
LOGGER.debug("Camera %s capabilities = %s", self.name, self.capabilities)
self.profiles = await self.async_get_profiles()
LOGGER.debug("Camera %s profiles = %s", self.name, self.profiles)

# No camera profiles to add
if not self.profiles:
raise ONVIFError("No camera profiles found")

if self.capabilities.ptz:
self.device.create_ptz_service()

# Determine max resolution from profiles
self.max_resolution = max(
profile.video.resolution.width
for profile in self.profiles
if profile.video.encoding == "H264"
)

async def async_stop(self, event=None):
"""Shut it all down."""
Expand Down
1 change: 1 addition & 0 deletions custom_components/tapo_control/lib/onvif/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"codeowners": ["@hunterjm"],
"config_flow": true,
"dependencies": ["ffmpeg"],
"dhcp": [{ "registered_devices": true }],
"documentation": "https://www.home-assistant.io/integrations/onvif",
"iot_class": "local_push",
"loggers": ["onvif", "wsdiscovery", "zeep"],
Expand Down
100 changes: 100 additions & 0 deletions custom_components/tapo_control/lib/onvif/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,106 @@ async def async_parse_tamper_detector(uid: str, msg) -> Event | None:
return None


@PARSERS.register("tns1:RuleEngine/MyRuleDetector/DogCatDetect")
# pylint: disable=protected-access
async def async_parse_dog_cat_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MyRuleDetector/DogCatDetect
"""
try:
video_source = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "Source":
video_source = source.Value

return Event(
f"{uid}_{msg.Topic._value_1}_{video_source}",
"Pet Detection",
"binary_sensor",
"motion",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None


@PARSERS.register("tns1:RuleEngine/MyRuleDetector/VehicleDetect")
# pylint: disable=protected-access
async def async_parse_vehicle_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MyRuleDetector/VehicleDetect
"""
try:
video_source = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "Source":
video_source = source.Value

return Event(
f"{uid}_{msg.Topic._value_1}_{video_source}",
"Vehicle Detection",
"binary_sensor",
"motion",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None


@PARSERS.register("tns1:RuleEngine/MyRuleDetector/PeopleDetect")
# pylint: disable=protected-access
async def async_parse_person_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MyRuleDetector/PeopleDetect
"""
try:
video_source = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "Source":
video_source = source.Value

return Event(
f"{uid}_{msg.Topic._value_1}_{video_source}",
"Person Detection",
"binary_sensor",
"motion",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None


@PARSERS.register("tns1:RuleEngine/MyRuleDetector/FaceDetect")
# pylint: disable=protected-access
async def async_parse_face_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MyRuleDetector/FaceDetect
"""
try:
video_source = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "Source":
video_source = source.Value

return Event(
f"{uid}_{msg.Topic._value_1}_{video_source}",
"Face Detection",
"binary_sensor",
"motion",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None


@PARSERS.register("tns1:Device/Trigger/DigitalInput")
# pylint: disable=protected-access
async def async_parse_digital_input(uid: str, msg) -> Event | None:
Expand Down
1 change: 1 addition & 0 deletions custom_components/tapo_control/lib/onvif/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"onvif_error": "Error setting up ONVIF device. Check logs for more information.",
"no_h264": "There were no H264 streams available. Check the profile configuration on your device.",
Expand Down

0 comments on commit 6c4d9a8

Please sign in to comment.