mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-06 10:44:04 +00:00
Add listen module to smartcam
This commit is contained in:
139
kasa/smartcam/modules/listen.py
Normal file
139
kasa/smartcam/modules/listen.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Implementation of motion detection module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from enum import StrEnum, auto
|
||||
from subprocess import check_output
|
||||
|
||||
import onvif # type: ignore[import-untyped]
|
||||
from aiohttp import web
|
||||
from onvif.managers import NotificationManager # type: ignore[import-untyped]
|
||||
|
||||
from ...credentials import Credentials
|
||||
from ..smartcammodule import SmartCamModule
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventType(StrEnum):
|
||||
"""Listen event types."""
|
||||
|
||||
MOTION_DETECTED = auto()
|
||||
PERSON_DETECTED = auto()
|
||||
TAMPER_DETECTED = auto()
|
||||
BABY_CRY_DETECTED = auto()
|
||||
|
||||
|
||||
TOPIC_EVENT_TYPE = {
|
||||
"tns1:RuleEngine/CellMotionDetector/Motion": EventType.MOTION_DETECTED,
|
||||
"tns1:RuleEngine/CellMotionDetector/People": EventType.PERSON_DETECTED,
|
||||
"tns1:RuleEngine/TamperDetector/Tamper": EventType.TAMPER_DETECTED,
|
||||
}
|
||||
|
||||
|
||||
class Listen(SmartCamModule):
|
||||
"""Implementation of lens mask module."""
|
||||
|
||||
manager: NotificationManager
|
||||
callback: Callable[[EventType], None]
|
||||
topics: set[EventType] | None
|
||||
listening = False
|
||||
site: web.TCPSite
|
||||
runner: web.AppRunner
|
||||
|
||||
async def _invoke_callback(self, event: EventType) -> None:
|
||||
self.callback(event)
|
||||
|
||||
async def _handle_event(self, request: web.Request) -> web.Response:
|
||||
content = await request.read()
|
||||
result = self.manager.process(content)
|
||||
for msg in result.NotificationMessage:
|
||||
if (event := TOPIC_EVENT_TYPE.get(msg.Topic._value_1)) and (
|
||||
not self.topics or event in self.topics
|
||||
):
|
||||
asyncio.create_task(self._invoke_callback(event))
|
||||
return web.Response()
|
||||
|
||||
async def listen(
|
||||
self,
|
||||
callback: Callable[[EventType], None],
|
||||
camera_credentials: Credentials,
|
||||
*,
|
||||
topics: set[EventType] | None = None,
|
||||
listen_ip: str | None = None,
|
||||
) -> None:
|
||||
"""Start listening for events."""
|
||||
self.callback = callback
|
||||
self.topics = topics
|
||||
|
||||
def subscription_lost() -> None:
|
||||
pass
|
||||
|
||||
wsdl = f"{os.path.dirname(onvif.__file__)}/wsdl/"
|
||||
|
||||
mycam = onvif.ONVIFCamera(
|
||||
self._device.host,
|
||||
2020,
|
||||
camera_credentials.username,
|
||||
camera_credentials.password,
|
||||
wsdl,
|
||||
)
|
||||
await mycam.update_xaddrs()
|
||||
|
||||
address = await self._start_server(listen_ip)
|
||||
|
||||
self.manager = await mycam.create_notification_manager(
|
||||
address=address,
|
||||
interval=timedelta(minutes=10),
|
||||
subscription_lost_callback=subscription_lost,
|
||||
)
|
||||
|
||||
self.listening = True
|
||||
_LOGGER.debug("Listener started for %s", self._device.host)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the listener."""
|
||||
if not self.listening:
|
||||
return
|
||||
self.listening = False
|
||||
await self.site.stop()
|
||||
await self.runner.shutdown()
|
||||
|
||||
async def _get_host_port(self, listen_ip: str | None) -> tuple[str, int]:
|
||||
def _create_socket(listen_ip: str | None) -> tuple[str, int]:
|
||||
if not listen_ip:
|
||||
res = check_output(["hostname", "-I"]) # noqa: S603, S607
|
||||
listen_ip, _, _ = res.decode().partition(" ")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind((listen_ip, 0))
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
return listen_ip, port
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(None, _create_socket, listen_ip)
|
||||
|
||||
async def _start_server(self, listen_ip: str | None) -> str:
|
||||
app = web.Application()
|
||||
app.add_routes([web.post("/", self._handle_event)])
|
||||
|
||||
self.runner = web.AppRunner(app)
|
||||
await self.runner.setup()
|
||||
|
||||
listen_ip, port = await self._get_host_port(listen_ip)
|
||||
|
||||
self.site = web.TCPSite(self.runner, listen_ip, port)
|
||||
await self.site.start()
|
||||
|
||||
_LOGGER.debug(
|
||||
"Listen handler for %s running on %s:%s", self._device.host, listen_ip, port
|
||||
)
|
||||
|
||||
return f"http://{listen_ip}:{port}"
|
@@ -11,7 +11,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Motion(SmartCamModule):
|
||||
"""Implementation of lens mask module."""
|
||||
"""Implementation of motion detection module."""
|
||||
|
||||
REQUIRED_COMPONENT = "detection"
|
||||
|
||||
|
@@ -11,7 +11,7 @@ from ..module import Module
|
||||
from ..protocols.smartcamprotocol import _ChildCameraProtocolWrapper
|
||||
from ..smart import SmartChildDevice, SmartDevice
|
||||
from ..smart.smartdevice import ComponentsRaw
|
||||
from .modules import ChildDevice, DeviceModule
|
||||
from .modules import Camera, ChildDevice, DeviceModule, Time
|
||||
from .smartcammodule import SmartCamModule
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -128,22 +128,37 @@ class SmartCamDevice(SmartDevice):
|
||||
|
||||
self._children = children
|
||||
|
||||
def _try_add_listen_module(self) -> None:
|
||||
try:
|
||||
import onvif # type: ignore[import-untyped] # noqa: F401
|
||||
except ImportError:
|
||||
return
|
||||
from .modules.listen import Listen
|
||||
|
||||
self._modules[Listen._module_name()] = Listen(self, Listen._module_name())
|
||||
|
||||
async def _initialize_modules(self) -> None:
|
||||
"""Initialize modules based on component negotiation response."""
|
||||
for mod in SmartCamModule.REGISTERED_MODULES.values():
|
||||
required_component = cast(str, mod.REQUIRED_COMPONENT)
|
||||
if (
|
||||
mod.REQUIRED_COMPONENT
|
||||
and mod.REQUIRED_COMPONENT not in self._components
|
||||
# Always add Camera module to cameras
|
||||
and (
|
||||
mod._module_name() != Module.Camera
|
||||
or self._device_type is not DeviceType.Camera
|
||||
required_component in self._components
|
||||
or any(
|
||||
self.sys_info.get(key) is not None
|
||||
for key in mod.SYSINFO_LOOKUP_KEYS
|
||||
)
|
||||
or mod in self.FIRST_UPDATE_MODULES
|
||||
or mod is Time
|
||||
):
|
||||
continue
|
||||
module = mod(self, mod._module_name())
|
||||
if await module._check_supported():
|
||||
self._modules[module.name] = module
|
||||
module = mod(self, mod._module_name())
|
||||
if await module._check_supported():
|
||||
self._modules[module.name] = module
|
||||
|
||||
if self._device_type is DeviceType.Camera:
|
||||
self._modules[Camera._module_name()] = Camera(self, Camera._module_name())
|
||||
|
||||
if Module.Motion in self._modules:
|
||||
self._try_add_listen_module()
|
||||
|
||||
async def _initialize_features(self) -> None:
|
||||
"""Initialize device features."""
|
||||
|
Reference in New Issue
Block a user