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, Credentials,
Device, Device,
) )
from kasa.eventtype import EventType
from .common import echo, error, pass_dev_or_child from .common import echo, error, pass_dev_or_child
@ -35,8 +36,38 @@ async def aioinput(string: str):
envvar="KASA_CAMERA_PASSWORD", envvar="KASA_CAMERA_PASSWORD",
help="Camera account password to use to authenticate to device.", 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 @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.""" """Commands to control light settings."""
try: try:
import onvif # type: ignore[import-untyped] # noqa: F401 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}") echo(f"Device {dev.host} received event {event}")
creds = Credentials(cam_username, cam_password) 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") 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 logging
import os import os
import socket import socket
from collections.abc import Callable import sys
import uuid
from collections.abc import Callable, Iterable
from datetime import timedelta from datetime import timedelta
from enum import StrEnum, auto
from subprocess import check_output from subprocess import check_output
import onvif # type: ignore[import-untyped] import onvif # type: ignore[import-untyped]
@ -16,18 +17,12 @@ from aiohttp import web
from onvif.managers import NotificationManager # type: ignore[import-untyped] from onvif.managers import NotificationManager # type: ignore[import-untyped]
from ...credentials import Credentials from ...credentials import Credentials
from ...eventtype import EventType
from ..smartcammodule import SmartCamModule from ..smartcammodule import SmartCamModule
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_LISTEN_PORT = 28002
class EventType(StrEnum):
"""Listen event types."""
MOTION_DETECTED = auto()
PERSON_DETECTED = auto()
TAMPER_DETECTED = auto()
BABY_CRY_DETECTED = auto()
TOPIC_EVENT_TYPE = { TOPIC_EVENT_TYPE = {
@ -42,10 +37,11 @@ class Listen(SmartCamModule):
manager: NotificationManager manager: NotificationManager
callback: Callable[[EventType], None] callback: Callable[[EventType], None]
topics: set[EventType] | None event_types: Iterable[EventType] | None
listening = False listening = False
site: web.TCPSite site: web.TCPSite
runner: web.AppRunner runner: web.AppRunner
instance_id: str
async def _invoke_callback(self, event: EventType) -> None: async def _invoke_callback(self, event: EventType) -> None:
self.callback(event) self.callback(event)
@ -55,7 +51,7 @@ class Listen(SmartCamModule):
result = self.manager.process(content) result = self.manager.process(content)
for msg in result.NotificationMessage: for msg in result.NotificationMessage:
if (event := TOPIC_EVENT_TYPE.get(msg.Topic._value_1)) and ( 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)) asyncio.create_task(self._invoke_callback(event))
return web.Response() return web.Response()
@ -65,15 +61,21 @@ class Listen(SmartCamModule):
callback: Callable[[EventType], None], callback: Callable[[EventType], None],
camera_credentials: Credentials, camera_credentials: Credentials,
*, *,
topics: set[EventType] | None = None, event_types: Iterable[EventType] | None = None,
listen_ip: str | None = None, listen_ip: str | None = None,
listen_port: int | None = None,
) -> None: ) -> None:
"""Start listening for events.""" """Start listening for events."""
self.callback = callback 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: 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/" wsdl = f"{os.path.dirname(onvif.__file__)}/wsdl/"
@ -86,7 +88,7 @@ class Listen(SmartCamModule):
) )
await mycam.update_xaddrs() 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( self.manager = await mycam.create_notification_manager(
address=address, address=address,
@ -100,40 +102,53 @@ class Listen(SmartCamModule):
async def stop(self) -> None: async def stop(self) -> None:
"""Stop the listener.""" """Stop the listener."""
if not self.listening: if not self.listening:
_LOGGER.debug("Listener for %s already stopped", self._device.host)
return return
_LOGGER.debug("Stopping listener for %s", self._device.host)
self.listening = False self.listening = False
await self.site.stop() await self.site.stop()
await self.runner.shutdown() await self.runner.shutdown()
async def _get_host_port(self, listen_ip: str | None) -> tuple[str, int]: async def _get_host_ip(self) -> str:
def _create_socket(listen_ip: str | None) -> tuple[str, int]: def _get_host() -> str:
if not listen_ip: if sys.platform == "win32":
return socket.gethostbyname(socket.gethostname())
else:
res = check_output(["hostname", "-I"]) # noqa: S603, S607 res = check_output(["hostname", "-I"]) # noqa: S603, S607
listen_ip, _, _ = res.decode().partition(" ") listen_ip, _, _ = res.decode().partition(" ")
return listen_ip
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() 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 = 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) self.runner = web.AppRunner(app)
await self.runner.setup() 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() await self.site.start()
except Exception:
_LOGGER.debug( _LOGGER.exception(
"Listen handler for %s running on %s:%s", self._device.host, listen_ip, port "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}/"
)