156 lines
5.3 KiB
Python
156 lines
5.3 KiB
Python
"""Helper bot module."""
|
|
import asyncio
|
|
import json
|
|
import ssl
|
|
from typing import Any, MutableMapping, Union
|
|
|
|
from cachetools import TTLCache
|
|
from gmqtt import Client, Subscription
|
|
from gmqtt.mqtt.constants import MQTTv311
|
|
|
|
from bumper.util import get_logger
|
|
|
|
_LOGGER = get_logger("helperbot")
|
|
|
|
|
|
class CommandDto:
|
|
"""Command DTO."""
|
|
|
|
def __init__(self, payload_type: str) -> None:
|
|
self._payload_type = payload_type
|
|
self._event = asyncio.Event()
|
|
self._response: str | bytes
|
|
|
|
async def wait_for_response(self) -> str | dict[str, Any]:
|
|
"""Wait for the response to be received."""
|
|
await self._event.wait()
|
|
if self._payload_type == "j":
|
|
return json.loads(self._response) # type:ignore[no-any-return]
|
|
|
|
return str(self._response)
|
|
|
|
def add_response(self, response: str | bytes) -> None:
|
|
"""Add received response."""
|
|
self._response = response
|
|
self._event.set()
|
|
|
|
|
|
class HelperBot:
|
|
"""Helper bot, which converts commands from the rest api to mqtt ones."""
|
|
|
|
def __init__(self, host: str, port: int, timeout: float = 60):
|
|
self._commands: MutableMapping[str, CommandDto] = TTLCache(
|
|
maxsize=timeout * 60, ttl=timeout * 1.1
|
|
)
|
|
self._host = host
|
|
self._port = port
|
|
self._client_id = "helperbot@bumper/helperbot"
|
|
self._timeout = timeout
|
|
self._client = Client("helperbot@bumper/helperbot")
|
|
|
|
# pylint: disable=unused-argument
|
|
async def _on_message(
|
|
client: Client, topic: str, payload: bytes, qos: int, properties: dict
|
|
) -> None:
|
|
try:
|
|
_LOGGER.debug(
|
|
"Got message: topic=%s; payload=%s;", topic, payload.decode()
|
|
)
|
|
topic_split = topic.split("/")
|
|
data_decoded = str(payload.decode())
|
|
if topic_split[10] in self._commands:
|
|
self._commands[topic_split[10]].add_response(data_decoded)
|
|
except Exception:
|
|
_LOGGER.error(
|
|
"An exception occured during handling message.", exc_info=True
|
|
)
|
|
|
|
self._client.on_message = _on_message
|
|
|
|
@property
|
|
def is_connected(self) -> bool:
|
|
"""Return True if client is connected successfully."""
|
|
return self._client.is_connected # type: ignore[no-any-return]
|
|
|
|
async def start(self) -> None:
|
|
"""Connect and subscribe helper bot."""
|
|
try:
|
|
if self.is_connected:
|
|
return
|
|
|
|
ssl_ctx = ssl.create_default_context()
|
|
ssl_ctx.check_hostname = False
|
|
ssl_ctx.verify_mode = ssl.CERT_NONE
|
|
await self._client.connect(
|
|
self._host, self._port, ssl=ssl_ctx, version=MQTTv311
|
|
)
|
|
self._client.subscribe(
|
|
Subscription("iot/p2p/+/+/+/+/helperbot/bumper/helperbot/+/+/+")
|
|
)
|
|
except Exception:
|
|
_LOGGER.exception("An exception occurred during startup", exc_info=True)
|
|
raise
|
|
|
|
async def _wait_for_resp(
|
|
self, command_dto: CommandDto, request_id: str
|
|
) -> dict[str, Any]:
|
|
try:
|
|
payload = await asyncio.wait_for(
|
|
command_dto.wait_for_response(), timeout=self._timeout
|
|
)
|
|
return {"id": request_id, "ret": "ok", "resp": payload}
|
|
except asyncio.TimeoutError:
|
|
_LOGGER.debug("wait_for_resp timeout reached")
|
|
except asyncio.CancelledError:
|
|
_LOGGER.debug("wait_for_resp cancelled by asyncio", exc_info=True)
|
|
except Exception: # pylint: disable=broad-except
|
|
_LOGGER.exception("An unknown error occurred", exc_info=True)
|
|
|
|
return {
|
|
"id": request_id,
|
|
"errno": 500,
|
|
"ret": "fail",
|
|
"debug": "wait for response timed out",
|
|
}
|
|
|
|
async def send_command(
|
|
self, cmdjson: dict[str, Any], request_id: str
|
|
) -> dict[str, Any]:
|
|
"""Send command over MQTT."""
|
|
if not self.is_connected:
|
|
await self.start()
|
|
|
|
try:
|
|
topic = (
|
|
f"iot/p2p/{cmdjson['cmdName']}/helperbot/bumper/helperbot/{cmdjson['toId']}/"
|
|
f"{cmdjson['toType']}/{cmdjson['toRes']}/q/{request_id}/{cmdjson['payloadType']}"
|
|
)
|
|
|
|
if cmdjson["payloadType"] == "j":
|
|
payload = json.dumps(cmdjson["payload"])
|
|
else:
|
|
payload = str(cmdjson["payload"])
|
|
|
|
command_dto = CommandDto(cmdjson["payloadType"])
|
|
self._commands[request_id] = command_dto
|
|
|
|
_LOGGER.debug("Sending message %s", topic)
|
|
self._client.publish(topic, payload.encode())
|
|
|
|
resp = await self._wait_for_resp(command_dto, request_id)
|
|
return resp
|
|
except Exception: # pylint: disable=broad-except
|
|
_LOGGER.exception("Could not send command.", exc_info=True)
|
|
return {
|
|
"id": request_id,
|
|
"errno": 500,
|
|
"ret": "fail",
|
|
"debug": "exception occurred please check bumper logs",
|
|
}
|
|
finally:
|
|
self._commands.pop(request_id, None)
|
|
|
|
async def disconnect(self) -> None:
|
|
"""Disconnect client."""
|
|
if self.is_connected:
|
|
await self._client.disconnect()
|