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

Add camera platform to tplink integration #129180

Merged
merged 36 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
590509d
Add camera entity to tplink integration
sdb9696 Oct 25, 2024
8b790a1
Merge remote-tracking branch 'upstream/dev' into tplink/feat/camera
sdb9696 Nov 21, 2024
46901c6
Set camera module earlier
sdb9696 Nov 21, 2024
69cc6ef
Merge branch 'dev' into tplink/feat/camera
sdb9696 Nov 26, 2024
c04af52
Fix smartcam namespace
sdb9696 Nov 27, 2024
c4331ec
Update post review
sdb9696 Nov 27, 2024
e1be0c1
Add tests
sdb9696 Nov 27, 2024
f23da7a
Update post review
sdb9696 Nov 28, 2024
aae5066
Missed a docstring fix
sdb9696 Nov 28, 2024
476e9ae
Exclude pan/tilt features
sdb9696 Nov 28, 2024
06070a0
Add optionsflow for tplink camera account
sdb9696 Dec 2, 2024
13a405e
Set video url in init
sdb9696 Dec 2, 2024
4d474fd
Configure camera account as part of main ConfigFlow
sdb9696 Dec 3, 2024
9082907
Cleanup and add camera auth flow to all discovery flows
sdb9696 Dec 3, 2024
0ac0256
Fix tests
sdb9696 Dec 3, 2024
db982ae
Merge remote-tracking branch 'upstream/dev' into tplink/feat/camera
sdb9696 Dec 3, 2024
5943282
Add tests
sdb9696 Dec 4, 2024
fd1f601
Merge remote-tracking branch 'upstream/dev' into tplink/feat/camera
sdb9696 Dec 4, 2024
3962423
Apply spacing suggestions from code review
sdb9696 Dec 4, 2024
0c3907e
Apply wording/strings suggestions from code review
sdb9696 Dec 4, 2024
914ba65
Reduce nesting and function size
sdb9696 Dec 4, 2024
5503b4f
Finish test coverage
sdb9696 Dec 4, 2024
f395dcf
Update post review
sdb9696 Dec 4, 2024
2ca2544
Revert conftest change
sdb9696 Dec 4, 2024
347c03b
Merge branch 'dev' into tplink/feat/camera
sdb9696 Dec 10, 2024
f7e6d41
Merge remote-tracking branch 'upstream/dev' into tplink/feat/camera
sdb9696 Dec 20, 2024
6684173
Merge remote-tracking branch 'origin/tplink/feat/camera' into tplink/…
sdb9696 Dec 20, 2024
a6e8701
Use stream.async_check_stream_client_error
sdb9696 Dec 20, 2024
c2b878a
Try mpeg stream on av.open error not 401
sdb9696 Dec 21, 2024
71d9ed1
Try mpeg stream on all errors
sdb9696 Dec 22, 2024
3da886b
Add test for mpeg fallback
sdb9696 Dec 22, 2024
f9f671f
Merge branch 'dev' into tplink/feat/camera
sdb9696 Dec 22, 2024
25e71dd
Remove uv.lock
sdb9696 Dec 22, 2024
7c0c12f
Merge branch 'dev' into tplink/feat/camera
sdb9696 Dec 22, 2024
378cc2f
Use time.mononic
sdb9696 Dec 22, 2024
950f662
Merge remote-tracking branch 'origin/tplink/feat/camera' into tplink/…
sdb9696 Dec 22, 2024
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
150 changes: 150 additions & 0 deletions homeassistant/components/tplink/camera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""Component providing support to the Ring Door Bell camera."""
sdb9696 marked this conversation as resolved.
Show resolved Hide resolved

from __future__ import annotations
sdb9696 marked this conversation as resolved.
Show resolved Hide resolved

from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any

from aiohttp import web
from haffmpeg.camera import CameraMjpeg
from kasa import Device, Module
from kasa.experimental.modules.camera import Camera as CameraModule

Check failure on line 13 in homeassistant/components/tplink/camera.py

View workflow job for this annotation

GitHub Actions / Check pylint

E0611: No name 'experimental' in module 'kasa' (no-name-in-module)

from homeassistant.components import ffmpeg
from homeassistant.components.camera import (
Camera,
CameraEntityDescription,
CameraEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import TPLinkConfigEntry, legacy_device_id
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import CoordinatedTPLinkEntity, TPLinkModuleEntityDescription

FORCE_REFRESH_INTERVAL = timedelta(minutes=3)
MOTION_DETECTION_CAPABILITY = "motion_detection"
sdb9696 marked this conversation as resolved.
Show resolved Hide resolved

_LOGGER = logging.getLogger(__name__)


@dataclass(frozen=True, kw_only=True)
class TPLinkCameraEntityDescription(
CameraEntityDescription, TPLinkModuleEntityDescription
):
"""Base class for event entity description."""
sdb9696 marked this conversation as resolved.
Show resolved Hide resolved


CAMERA_DESCRIPTIONS: tuple[TPLinkCameraEntityDescription, ...] = (
TPLinkCameraEntityDescription(
key="live",
translation_key="live",
),
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: TPLinkConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up climate entities."""
sdb9696 marked this conversation as resolved.
Show resolved Hide resolved
data = config_entry.runtime_data
parent_coordinator = data.parent_coordinator
device = parent_coordinator.device
ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass)

async_add_entities(
TPLinkCameraEntity(
device,
parent_coordinator,
description,
camera_module=camera_module,
parent=None,
ffmpeg_manager=ffmpeg_manager,
)
for description in CAMERA_DESCRIPTIONS
if (camera_module := device.modules.get(Module.Camera))

Check failure on line 71 in homeassistant/components/tplink/camera.py

View workflow job for this annotation

GitHub Actions / Check mypy

"type[Module]" has no attribute "Camera" [attr-defined]
)


class TPLinkCameraEntity(CoordinatedTPLinkEntity, Camera):
"""Representation of a TPLink thermostat."""

_attr_name = None
_attr_supported_features = CameraEntityFeature.STREAM | CameraEntityFeature.ON_OFF

def __init__(
self,
device: Device,
coordinator: TPLinkDataUpdateCoordinator,
description: TPLinkCameraEntityDescription,
*,
camera_module: CameraModule,
parent: Device | None = None,
ffmpeg_manager: ffmpeg.FFmpegManager,
) -> None:
"""Initialize a Ring Door Bell camera."""
sdb9696 marked this conversation as resolved.
Show resolved Hide resolved
self._description = description
super().__init__(device, coordinator, parent=parent)
sdb9696 marked this conversation as resolved.
Show resolved Hide resolved
Camera.__init__(self)
self._camera_module = camera_module
self._ffmpeg_manager = ffmpeg_manager

self._video_url: str | None = None
self._image: bytes | None = None

def _get_unique_id(self) -> str:
"""Return unique ID for the entity."""
return f"{legacy_device_id(self._device)}-{self._description}"

sdb9696 marked this conversation as resolved.
Show resolved Hide resolved
@callback
def _async_update_attrs(self) -> None:
"""Update the entity's attributes."""
self._attr_is_on = self._camera_module.is_on
self._video_url = self._camera_module.stream_rtsp_url()
sdb9696 marked this conversation as resolved.
Show resolved Hide resolved

@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return {}

sdb9696 marked this conversation as resolved.
Show resolved Hide resolved
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
if self._image is None and (video_url := self._video_url):
image = await ffmpeg.async_get_image(
self.hass,
video_url,
width=width,
height=height,
)
if image:
self._image = image
return self._image

async def handle_async_mjpeg_stream(
self, request: web.Request
) -> web.StreamResponse | None:
"""Generate an HTTP MJPEG stream from the camera."""
if self._video_url is None:
return None

stream = CameraMjpeg(self._ffmpeg_manager.binary)
await stream.open_camera(self._video_url)

try:
stream_reader = await stream.get_reader()
return await async_aiohttp_proxy_stream(
self.hass,
request,
stream_reader,
self._ffmpeg_manager.ffmpeg_stream_content_type,
)
finally:
await stream.close()
1 change: 1 addition & 0 deletions homeassistant/components/tplink/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
PLATFORMS: Final = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CAMERA,
Platform.CLIMATE,
Platform.FAN,
Platform.LIGHT,
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/tplink/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ class TPLinkFeatureEntityDescription(EntityDescription):
deprecated_info: DeprecatedInfo | None = None


@dataclass(frozen=True, kw_only=True)
class TPLinkModuleEntityDescription(EntityDescription):
"""Base class for a TPLink module based entity description."""

deprecated_info: DeprecatedInfo | None = None


def async_refresh_after[_T: CoordinatedTPLinkEntity, **_P](
func: Callable[Concatenate[_T, _P], Awaitable[None]],
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/tplink/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "TP-Link Smart Home",
"codeowners": ["@rytilahti", "@bdraco", "@sdb9696"],
"config_flow": true,
"dependencies": ["network"],
"dependencies": ["network", "ffmpeg"],
"dhcp": [
{
"registered_devices": true
Expand Down
Loading