Source code for miio.push_server.serverprotocol

import calendar
import datetime
import logging
import struct

from ..protocol import Message

_LOGGER = logging.getLogger(__name__)

HELO_BYTES = bytes.fromhex(
    "21310020ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
)

ERR_INVALID = -1
ERR_UNSUPPORTED = -2
ERR_METHOD_EXEC_FAILED = -3


[docs] class ServerProtocol: """Handle responding to UDP packets.""" def __init__(self, loop, udp_socket, server): """Initialize the class.""" self.transport = None self._loop = loop self._sock = udp_socket self.server = server self._connected = False def _build_ack(self): # Original devices are using year 1970, but it seems current datetime is fine timestamp = calendar.timegm(datetime.datetime.now().timetuple()) # ACK packet not signed, 16 bytes header + 16 bytes of zeroes return struct.pack( ">HHIII16s", 0x2131, 32, 0, self.server.device_id, timestamp, bytes(16) )
[docs] def connection_made(self, transport): """Set the transport.""" self.transport = transport self._connected = True _LOGGER.info( "Miio push server started with address=%s server_id=%s", self.server._address, self.server.device_id, )
[docs] def connection_lost(self, exc): """Handle connection lost.""" if self._connected: _LOGGER.error("Connection unexpectedly lost in Miio push server: %s", exc)
[docs] def send_ping_ACK(self, host, port): _LOGGER.debug("%s:%s=>PING", host, port) m = self._build_ack() self.transport.sendto(m, (host, port)) _LOGGER.debug("%s:%s<=ACK(server_id=%s)", host, port, self.server.device_id)
def _create_message(self, data, token, device_id): """Create a message to be sent to the client.""" header = { "length": 0, "unknown": 0, "device_id": device_id, "ts": datetime.datetime.now(), } msg = { "data": {"value": data}, "header": {"value": header}, "checksum": 0, } response = Message.build(msg, token=token) return response
[docs] def send_response(self, host, port, msg_id, token, payload=None): if payload is None: payload = {} data = {**payload, "id": msg_id} msg = self._create_message(data, token, device_id=self.server.device_id) self.transport.sendto(msg, (host, port)) _LOGGER.debug(">> %s:%s: %s", host, port, data)
[docs] def send_error(self, host, port, msg_id, token, code, message): """Send error message with given code and message to the client.""" return self.send_response( host, port, msg_id, token, {"error": {"code": code, "error": message}} )
def _handle_datagram_from_registered_device(self, host, port, data): """Handle requests from registered eventing devices.""" token = self.server._registered_devices[host]["token"] callback = self.server._registered_devices[host]["callback"] msg = Message.parse(data, token=token) msg_value = msg.data.value msg_id = msg_value["id"] _LOGGER.debug("<< %s:%s: %s", host, port, msg_value) # Send OK # This result means OK, but some methods return ['ok'] instead of 0 # might be necessary to use different results for different methods payload = {"result": 0} self.send_response(host, port, msg_id, token, payload=payload) # Parse message action, device_call_id = msg_value["method"].rsplit(":", 1) source_device_id = device_call_id.replace("_", ".") callback(source_device_id, action, msg_value.get("params")) def _handle_datagram_from_client(self, host: str, port: int, data): """Handle datagram from a regular client.""" token = bytes.fromhex(32 * "0") # TODO: make token configurable? msg = Message.parse(data, token=token) msg_value = msg.data.value msg_id = msg_value["id"] _LOGGER.debug( "Received datagram #%s from regular client: %s: %s", msg_id, host, msg_value, ) if "method" not in msg_value: return self.send_error( host, port, msg_id, token, ERR_INVALID, "missing method" ) methods = self.server.methods if msg_value["method"] not in methods: return self.send_error( host, port, msg_id, token, ERR_UNSUPPORTED, "unsupported method" ) _LOGGER.debug("Got method call: %s", msg_value["method"]) method = methods[msg_value["method"]] if callable(method): try: response = method(msg_value) except Exception as ex: _LOGGER.exception(ex) return self.send_error( host, port, msg_id, token, ERR_METHOD_EXEC_FAILED, f"Exception {type(ex)}: {ex}", ) else: response = method _LOGGER.debug("Responding %s with %s", msg_id, response) return self.send_response(host, port, msg_id, token, payload=response)
[docs] def datagram_received(self, data, addr): """Handle received messages.""" try: (host, port) = addr if data == HELO_BYTES: return self.send_ping_ACK(host, port) if host in self.server._registered_devices: return self._handle_datagram_from_registered_device(host, port, data) else: return self._handle_datagram_from_client(host, port, data) except Exception: _LOGGER.exception( "Cannot process Miio push server packet: '%s' from %s:%s", data, host, port, )
[docs] def error_received(self, exc): """Log UDP errors.""" _LOGGER.error("UDP error received in Miio push server: %s", exc)
[docs] def close(self): """Stop the server.""" _LOGGER.debug("Miio push server shutting down") self._connected = False if self.transport: self.transport.close() self._sock.close() _LOGGER.info("Miio push server stopped")