mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-23 03:33:35 +00:00
Use specific listen port and add cli parameters
This commit is contained in:
parent
99e8a2fd87
commit
6b411700b7
@ -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
12
kasa/eventtype.py
Normal 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()
|
@ -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)
|
||||||
await self.site.start()
|
try:
|
||||||
|
await self.site.start()
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Error trying to start listener for %s: ", self._device.host
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Listen handler for %s running on %s:%s", self._device.host, listen_ip, port
|
"Listen handler for %s running on %s:%s",
|
||||||
|
self._device.host,
|
||||||
|
listen_ip,
|
||||||
|
listen_port,
|
||||||
)
|
)
|
||||||
|
|
||||||
return f"http://{listen_ip}:{port}"
|
return (
|
||||||
|
f"http://{listen_ip}:{listen_port}/{self._device.host}/{self.instance_id}/"
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user