Use specific listen port and add cli parameters

This commit is contained in:
Steven B 2024-12-19 11:29:03 +00:00
parent 99e8a2fd87
commit 6b411700b7
No known key found for this signature in database
GPG Key ID: 6D5B46B3679F2A43
3 changed files with 99 additions and 35 deletions

View File

@ -10,6 +10,7 @@ from kasa import (
Credentials,
Device,
)
from kasa.eventtype import EventType
from .common import echo, error, pass_dev_or_child
@ -35,8 +36,38 @@ async def aioinput(string: str):
envvar="KASA_CAMERA_PASSWORD",
help="Camera account password to use to authenticate to device.",
)
@click.option(
"--listen-port",
default=None,
required=False,
envvar="KASA_LISTEN_PORT",
help="Port to listen on for onvif notifications.",
)
@click.option(
"--listen-ip",
default=None,
required=False,
envvar="KASA_LISTEN_IP",
help="Ip address to listen on for onvif notifications.",
)
@click.option(
"-et",
"--event-types",
default=None,
required=False,
multiple=True,
type=click.Choice([et for et in EventType], case_sensitive=False),
help="Event types to listen to.",
)
@pass_dev_or_child
async def listen(dev: Device, cam_username: str, cam_password: str) -> None:
async def listen(
dev: Device,
cam_username: str,
cam_password: str,
listen_port: int | None,
listen_ip: str | None,
event_types: list[EventType] | None,
) -> None:
"""Commands to control light settings."""
try:
import onvif # type: ignore[import-untyped] # noqa: F401
@ -53,7 +84,13 @@ async def listen(dev: Device, cam_username: str, cam_password: str) -> None:
echo(f"Device {dev.host} received event {event}")
creds = Credentials(cam_username, cam_password)
await listen.listen(on_event, creds)
await listen.listen(
on_event,
creds,
listen_ip=listen_ip,
listen_port=listen_port,
event_types=event_types,
)
await aioinput("Listening, press enter to cancel\n")

12
kasa/eventtype.py Normal file
View File

@ -0,0 +1,12 @@
"""Module for listen event types."""
from enum import StrEnum, auto
class EventType(StrEnum):
"""Listen event types."""
MOTION_DETECTED = auto()
PERSON_DETECTED = auto()
TAMPER_DETECTED = auto()
BABY_CRY_DETECTED = auto()

View File

@ -6,9 +6,10 @@ import asyncio
import logging
import os
import socket
from collections.abc import Callable
import sys
import uuid
from collections.abc import Callable, Iterable
from datetime import timedelta
from enum import StrEnum, auto
from subprocess import check_output
import onvif # type: ignore[import-untyped]
@ -16,18 +17,12 @@ from aiohttp import web
from onvif.managers import NotificationManager # type: ignore[import-untyped]
from ...credentials import Credentials
from ...eventtype import EventType
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()
DEFAULT_LISTEN_PORT = 28002
TOPIC_EVENT_TYPE = {
@ -42,10 +37,11 @@ class Listen(SmartCamModule):
manager: NotificationManager
callback: Callable[[EventType], None]
topics: set[EventType] | None
event_types: Iterable[EventType] | None
listening = False
site: web.TCPSite
runner: web.AppRunner
instance_id: str
async def _invoke_callback(self, event: EventType) -> None:
self.callback(event)
@ -55,7 +51,7 @@ class Listen(SmartCamModule):
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
not self.event_types or event in self.event_types
):
asyncio.create_task(self._invoke_callback(event))
return web.Response()
@ -65,15 +61,21 @@ class Listen(SmartCamModule):
callback: Callable[[EventType], None],
camera_credentials: Credentials,
*,
topics: set[EventType] | None = None,
event_types: Iterable[EventType] | None = None,
listen_ip: str | None = None,
listen_port: int | None = None,
) -> None:
"""Start listening for events."""
self.callback = callback
self.topics = topics
self.event_types = event_types
self.instance_id = str(uuid.uuid4())
if listen_port is None:
listen_port = DEFAULT_LISTEN_PORT
def subscription_lost() -> None:
pass
_LOGGER.debug("Notification subscription lost for %s", self._device.host)
asyncio.create_task(self.stop())
wsdl = f"{os.path.dirname(onvif.__file__)}/wsdl/"
@ -86,7 +88,7 @@ class Listen(SmartCamModule):
)
await mycam.update_xaddrs()
address = await self._start_server(listen_ip)
address = await self._start_server(listen_ip, listen_port)
self.manager = await mycam.create_notification_manager(
address=address,
@ -100,40 +102,53 @@ class Listen(SmartCamModule):
async def stop(self) -> None:
"""Stop the listener."""
if not self.listening:
_LOGGER.debug("Listener for %s already stopped", self._device.host)
return
_LOGGER.debug("Stopping listener for %s", self._device.host)
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:
async def _get_host_ip(self) -> str:
def _get_host() -> str:
if sys.platform == "win32":
return socket.gethostbyname(socket.gethostname())
else:
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
return listen_ip
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, _create_socket, listen_ip)
return await loop.run_in_executor(None, _get_host)
async def _start_server(self, listen_ip: str | None) -> str:
async def _start_server(self, listen_ip: str | None, listen_port: int) -> str:
app = web.Application()
app.add_routes([web.post("/", self._handle_event)])
app.add_routes(
[web.post(f"/{self._device.host}/{self.instance_id}/", self._handle_event)]
)
self.runner = web.AppRunner(app)
await self.runner.setup()
listen_ip, port = await self._get_host_port(listen_ip)
if not listen_ip:
listen_ip = await self._get_host_ip()
self.site = web.TCPSite(self.runner, listen_ip, port)
self.site = web.TCPSite(self.runner, listen_ip, listen_port)
try:
await self.site.start()
_LOGGER.debug(
"Listen handler for %s running on %s:%s", self._device.host, listen_ip, port
except Exception:
_LOGGER.exception(
"Error trying to start listener for %s: ", self._device.host
)
return f"http://{listen_ip}:{port}"
_LOGGER.debug(
"Listen handler for %s running on %s:%s",
self._device.host,
listen_ip,
listen_port,
)
return (
f"http://{listen_ip}:{listen_port}/{self._device.host}/{self.instance_id}/"
)