Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev Pull #14

Merged
merged 5 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .vscode/extensions.js

This file was deleted.

5 changes: 5 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"ms-python.black-formatter"
]
}
11 changes: 10 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,14 @@
"editor.formatOnPaste": true,
"python.analysis.inlayHints.functionReturnTypes": true,
"python.analysis.inlayHints.variableTypes": true,
"python.analysis.typeCheckingMode": "off"
"python.analysis.typeCheckingMode": "off",
"python.formatting.blackArgs": [
"--line-length",
"100"
],
"python.formatting.provider": "black",
"[python]": {
"editor.formatOnPaste": false,
"editor.formatOnSaveMode": "file"
}
}
3 changes: 3 additions & 0 deletions custom_components/reolink_rest/__version__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""version"""

__version__ = "0.6.0"
18 changes: 10 additions & 8 deletions custom_components/reolink_rest/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,10 @@ def _cleanup():
entities = []
data = coordinator.data
push_setup = True
abilities = data.abilities

for channel in data.channels.keys():
ability = coordinator.data.abilities.channels[channel]
ability = abilities.channels[channel]
if not ability.alarm.motion:
continue

Expand Down Expand Up @@ -220,15 +221,15 @@ def _cleanup():

ai_types = []
# if ability.support.ai: <- in my tests this ability was not set
if ability.support.ai.animal:
if ability.supports.ai.animal:
ai_types.append(AITypes.ANIMAL)
if ability.support.ai.face:
if ability.supports.ai.face:
ai_types.append(AITypes.FACE)
if ability.support.ai.people:
if ability.supports.ai.people:
ai_types.append(AITypes.PEOPLE)
if ability.support.ai.pet:
if ability.supports.ai.pet:
ai_types.append(AITypes.PET)
if ability.support.ai.vehicle:
if ability.supports.ai.vehicle:
ai_types.append(AITypes.VEHICLE)

for description in SENSORS:
Expand All @@ -241,7 +242,7 @@ def _cleanup():
if entities:
async_add_entities(entities)

if not push_setup and data.abilities.onvif:
if not push_setup and abilities.onvif:
webhooks = async_get_webhook_manager(hass)
if webhooks is not None:
webhook = webhooks.async_register(hass, config_entry)
Expand Down Expand Up @@ -269,7 +270,7 @@ def _sub_failure(entry_id: str):
)
hass.create_task(_coordinator.async_request_refresh())
subscription = None
if not bool(coordinator.data.ports.get("onvifEnable", True)):
if not coordinator.data.ports.onvif.enabled:
coordinator.logger.warning(
"ONVIF not enabled for device %s, forcing polling mode",
coordinator.data.device.name,
Expand Down Expand Up @@ -309,6 +310,7 @@ def __init__(

def _handle_coordinator_update(self) -> None:
data = self.coordinator.data.motion[self.entity_description.channel]
_LOGGER.info("Motion<-%r", data)
if self.entity_description.ai_type is None:
self._attr_is_on = data.detected
else:
Expand Down
73 changes: 44 additions & 29 deletions custom_components/reolink_rest/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@

from homeassistant.const import CONF_USERNAME, CONF_PASSWORD

from async_reolink.api.system import abilities

from async_reolink.api.errors import ReolinkResponseError

from async_reolink.api.const import IntStreamTypes as StreamTypes
from async_reolink.api.const import (
IntStreamTypes as StreamTypes,
DEFAULT_USERNAME,
DEFAULT_PASSWORD,
)

from async_reolink.api.system import capabilities as abilities
from async_reolink.api.ptz import typings as ptz

from .entity import (
ReolinkEntityData,
Expand Down Expand Up @@ -189,13 +194,14 @@ async def async_setup_entry(

entities: list[ReolinkCamera] = []
data = coordinator.data
_abilities = data.abilities
for channel in data.channels.keys():
ability = coordinator.data.abilities.channels[channel]
ability = _abilities.channels[channel]

features: int = 0

has_ptz = False
if ability.ptz.type == abilities.channel.PTZTypeValues.AF:
if ability.ptz.type == abilities.PTZType.AF:
features = (
ReolinkCameraEntityFeature.ZOOM.value | ReolinkCameraEntityFeature.FOCUS
)
Expand All @@ -204,28 +210,28 @@ async def async_setup_entry(
has_ptz = True
features = ReolinkCameraEntityFeature.PAN_TILT.value
if ability.ptz.type in (
abilities.channel.PTZTypeValues.PTZ,
abilities.channel.PTZTypeValues.PTZ_NO_SPEED,
abilities.PTZType.PTZ,
abilities.PTZType.PTZ_NO_SPEED,
):
features |= ReolinkCameraEntityFeature.ZOOM.value

otypes: list[OutputStreamTypes] = []
if ability.snap:
otypes.append(OutputStreamTypes.JPEG)
if stream:
if data.abilities.rtmp:
if _abilities.rtmp:
otypes.append(OutputStreamTypes.RTMP)
if data.abilities.rtsp:
if _abilities.rtsp:
otypes.append(OutputStreamTypes.RTSP)

stypes: list[StreamTypes] = []
if ability.live in (
abilities.channel.LiveValues.MAIN_EXTERN_SUB,
abilities.channel.LiveValues.MAIN_SUB,
abilities.Live.MAIN_EXTERN_SUB,
abilities.Live.MAIN_SUB,
):
stypes.append(StreamTypes.MAIN)
stypes.append(StreamTypes.SUB)
if ability.live == abilities.channel.LiveValues.MAIN_EXTERN_SUB:
if ability.live == abilities.Live.MAIN_EXTERN_SUB:
stypes.append(StreamTypes.EXT)

if not otypes or not stypes:
Expand Down Expand Up @@ -295,7 +301,7 @@ def _cleanup():
if (
description.output_type == OutputStreamTypes.RTMP
and description.stream_type == StreamTypes.MAIN
and ability.mainEncType == abilities.channel.EncodingTypeValues.H265
and ability.main_encoding == abilities.EncodingType.H265
):
continue
if (
Expand Down Expand Up @@ -349,16 +355,18 @@ def __init__(
ReolinkEntity.__init__(self, coordinator, description, context)
self._attr_supported_features = supported_features
self._snapshot_task: Task[bytes | None] = None
if self.entity_description.output_type == OutputStreamTypes.RTSP and not bool(
self.coordinator.data.ports.get("rtspEnable", True)
if (
self.entity_description.output_type == OutputStreamTypes.RTSP
and not self.coordinator.data.ports.rtsp.enabled
):
self._attr_available = False
self.coordinator.logger.error(
"RTSP is disabled on device (%s) camera will be unavailable",
self.coordinator.data.device.name,
)
elif self.entity_description.output_type == OutputStreamTypes.RTMP and not bool(
self.coordinator.data.ports.get("rtmpEnable", True)
elif (
self.entity_description.output_type == OutputStreamTypes.RTMP
and not self.coordinator.data.ports.rtmp.enabled
):
self._attr_available = False
self.coordinator.logger.error(
Expand All @@ -383,9 +391,9 @@ async def stream_source(self) -> str | None:

# rtsp uses separate auth handlers so we have to "inject" the auth with http basic
data = self.coordinator.config_entry.data
auth = quote(data[CONF_USERNAME])
auth = quote(data.get(CONF_USERNAME, DEFAULT_USERNAME))
auth += ":"
auth += quote(data[CONF_PASSWORD])
auth += quote(data.get(CONF_PASSWORD, DEFAULT_PASSWORD))
idx = url.index("://")
url = f"{url[:idx+3]}{auth}@{url[idx+3:]}"
elif self.entity_description.output_type == OutputStreamTypes.RTMP:
Expand All @@ -406,6 +414,12 @@ async def _async_use_rtsp_to_webrtc(self) -> bool:
return False
return await super()._async_use_rtsp_to_webrtc()

async def _async_use_rtsp_to_webrtc(self) -> bool:
# Force falce since the RTMP stream does not seem to work with webrtc
if self.entity_description.output_type != OutputStreamTypes.RTSP:
return False
return await super()._async_use_rtsp_to_webrtc()

async def _async_camera_image(self):
domain_data: ReolinkDomainData = self.hass.data[DOMAIN]
client = domain_data[self.coordinator.config_entry.entry_id][
Expand All @@ -430,9 +444,10 @@ async def _async_camera_image(self):
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
if not self.coordinator.data.abilities.channels[
ability = self.coordinator.data.abilities.channels[
self.entity_description.channel
].snap:
]
if not ability.snap:
return await super().async_camera_image(width, height)

# throttle calls to one per channel at a time
Expand All @@ -451,21 +466,21 @@ def available(self) -> bool:

def _handle_coordinator_update(self) -> None:
if self.entity_description.output_type == OutputStreamTypes.RTSP:
self._attr_available = bool(
self.coordinator.data.ports.get("rtspEnable", True)
)
self._attr_available = self.coordinator.data.ports.rtsp.enabled
elif self.entity_description.output_type == OutputStreamTypes.RTMP:
self._attr_available = bool(
self.coordinator.data.ports.get("rtmpEnable", True)
)
self._attr_available = self.coordinator.data.ports.rtmp.enabled
return super()._handle_coordinator_update()

async def async_set_zoom(self, position: int):
"""Set Zoom"""
client = self.coordinator.data.client
await client.set_ptz_zoom(position, self.entity_description.channel)
await client.set_ptz_zoomfocus(
ptz.ZoomOperation.ZOOM, position, self.entity_description.channel
)

async def async_set_focus(self, position: int):
"""Set Focus"""
client = self.coordinator.data.client
await client.set_ptz_focus(position, self.entity_description.channel)
await client.set_ptz_zoomfocus(
ptz.ZoomOperation.FOCUS, position, self.entity_description.channel
)
44 changes: 26 additions & 18 deletions custom_components/reolink_rest/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from __future__ import annotations

import logging
from typing import TypeVar
from typing import Mapping, TypeVar
from urllib.parse import urlparse

import voluptuous as vol
Expand All @@ -22,7 +22,7 @@
from async_reolink.api.const import DEFAULT_USERNAME, DEFAULT_PASSWORD

from async_reolink.api import errors as reo_errors
from async_reolink.api.network import ChannelStatusType
from async_reolink.api.network.typings import ChannelStatus
from async_reolink.rest import Client as RestClient
from async_reolink.rest.connection import Encryption
from async_reolink.rest.errors import AUTH_ERRORCODES
Expand Down Expand Up @@ -115,9 +115,9 @@ def _auth_schema(require_password: bool = False, **defaults: UserDataType):
}


def _simple_channels(channels: list[ChannelStatusType]):
def _simple_channels(channels: Mapping[int, ChannelStatus]):
return (
{channel["channel"]: channel["name"] for channel in channels}
{i: channel.name for i, channel in channels.items()}
if channels is not None
else None
)
Expand Down Expand Up @@ -218,25 +218,26 @@ async def async_step_user(
abilities = await client.get_ability(
data.get(CONF_USERNAME, DEFAULT_USERNAME)
)
if abilities.devInfo:

if abilities.device.info:
devinfo = await client.get_device_info()
title: str = devinfo.get("name", title)
title: str = devinfo.name or title
if self.unique_id is None:
if abilities.p2p:
p2p = await client.get_p2p()
if abilities.localLink:
if abilities.local_link:
link = await client.get_local_link()
unique_id = _create_unique_id(
uuid=p2p["uid"] if p2p is not None else None,
device_type=devinfo["type"] if devinfo is not None else None,
serial=devinfo["serial"] if devinfo is not None else None,
mac=link["mac"] if link is not None else None,
uuid=p2p.uid if p2p is not None else None,
device_type=devinfo.type if devinfo is not None else None,
serial=devinfo.serial if devinfo is not None else None,
mac=link.mac if link is not None else None,
)
if unique_id is not None:
self.async_set_unique_id(unique_id)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()

if devinfo["channelNum"] > 1:
if devinfo.channels > 1:
channels = await client.get_channel_status()
if channels is not None:
self.context["channels"] = _simple_channels(channels)
Expand All @@ -251,7 +252,12 @@ async def async_step_user(
return await self.async_step_connection(data, errors)
except reo_errors.ReolinkResponseError as resp_error:
if resp_error.code in AUTH_ERRORCODES:
errors = {"base": "invalid_auth"}
errors = (
{"base": "invalid_auth"}
if data.get(CONF_USERNAME, DEFAULT_USERNAME) != DEFAULT_USERNAME
or data.get(CONF_PASSWORD, DEFAULT_PASSWORD) != DEFAULT_PASSWORD
else {"base": "auth_required"}
)
return await self.async_step_auth(data, errors)
_LOGGER.exception(
"An internal device error occurred on %s, configuration aborting",
Expand Down Expand Up @@ -312,12 +318,14 @@ async def async_step_connection(

if user_input is not None and errors is None:
if _validate_connection_data(user_input):
user_input = {dslice(user_input, CONF_HOST, CONF_PORT, CONF_USE_HTTPS)}
user_input = dict(
dslice(user_input, CONF_HOST, CONF_PORT, CONF_USE_HTTPS)
)
if self.data is not None:
self.data.update(user_input)
else:
self.data = user_input
return await self.async_step_user()
return await self.async_step_user(user_input)

schema = _connection_schema(**(user_input or {}))

Expand All @@ -338,7 +346,7 @@ async def async_step_auth(
if user_input is not None and errors is None:
user_input = dict(dslice(user_input, CONF_USERNAME, CONF_PASSWORD))
self.data.update(user_input)
return await self.async_step_user()
return await self.async_step_user(user_input)

schema = _auth_schema(errors is not None, **(user_input or self.data or {}))

Expand Down Expand Up @@ -367,7 +375,7 @@ async def async_step_channels(
if not self.options:
self.options = {}
self.options.update(user_input)
return await self.async_step_user()
return await self.async_step_user(user_input)

schema = _channels_schema(
self.context["channels"], **(user_input or self.options or {})
Expand Down
Loading