webhooks: Rework get_status() subscriptions
Implement a new subscription system for get_status() updates. Subscriptions are per-client. After an initial update, only changes will be transmitted. Responses are only transmitted to the client that issued the subscription. Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
parent
16a53e6918
commit
b0e3effb53
|
@ -357,128 +357,101 @@ class GCodeHelper:
|
||||||
|
|
||||||
SUBSCRIPTION_REFRESH_TIME = .25
|
SUBSCRIPTION_REFRESH_TIME = .25
|
||||||
|
|
||||||
class StatusHandler:
|
class QueryStatusHelper:
|
||||||
def __init__(self, printer):
|
def __init__(self, printer):
|
||||||
self.printer = printer
|
self.printer = printer
|
||||||
self.webhooks = webhooks = printer.lookup_object('webhooks')
|
self.clients = {}
|
||||||
self.ready = self.timer_started = False
|
self.pending_queries = []
|
||||||
self.reactor = self.printer.get_reactor()
|
self.query_timer = None
|
||||||
self.available_objects = {}
|
self.last_query = {}
|
||||||
self.subscriptions = {}
|
|
||||||
self.subscription_timer = self.reactor.register_timer(
|
|
||||||
self._batch_subscription_handler, self.reactor.NEVER)
|
|
||||||
|
|
||||||
# Register events
|
|
||||||
self.printer.register_event_handler(
|
|
||||||
"klippy:ready", self._handle_ready)
|
|
||||||
self.printer.register_event_handler(
|
|
||||||
"gcode:request_restart", self._handle_restart)
|
|
||||||
|
|
||||||
# Register webhooks
|
# Register webhooks
|
||||||
webhooks.register_endpoint("objects/list", self._handle_object_request)
|
webhooks = printer.lookup_object('webhooks')
|
||||||
webhooks.register_endpoint("objects/status",
|
webhooks.register_endpoint("objects/list", self._handle_list)
|
||||||
self._handle_status_request)
|
webhooks.register_endpoint("objects/query", self._handle_query)
|
||||||
webhooks.register_endpoint("objects/subscription",
|
webhooks.register_endpoint("objects/subscribe", self._handle_subscribe)
|
||||||
self._handle_subscription_request)
|
def _handle_list(self, web_request):
|
||||||
webhooks.register_endpoint("objects/list_subscription",
|
objects = [n for n, o in self.printer.lookup_objects()
|
||||||
self._handle_list_subscription_request)
|
if hasattr(o, 'get_status')]
|
||||||
|
web_request.send({'objects': objects})
|
||||||
def _handle_ready(self):
|
def _do_query(self, eventtime):
|
||||||
eventtime = self.reactor.monotonic()
|
last_query = self.last_query
|
||||||
self.available_objects = {}
|
query = self.last_query = {}
|
||||||
objs = self.printer.lookup_objects()
|
msglist = self.pending_queries
|
||||||
status_objs = {n: o for n, o in objs if hasattr(o, "get_status")}
|
self.pending_queries = []
|
||||||
for name, obj in status_objs.items():
|
msglist.extend(self.clients.values())
|
||||||
attrs = obj.get_status(eventtime)
|
# Generate get_status() info for each client
|
||||||
self.available_objects[name] = attrs.keys()
|
for cconn, subscription, send_func, template in msglist:
|
||||||
self.ready = True
|
is_query = cconn is None
|
||||||
|
if not is_query and cconn.is_closed():
|
||||||
def _handle_restart(self, eventtime):
|
del self.clients[cconn]
|
||||||
self.ready = False
|
|
||||||
self.reactor.update_timer(self.subscription_timer, self.reactor.NEVER)
|
|
||||||
|
|
||||||
def _batch_subscription_handler(self, eventtime):
|
|
||||||
status = self._process_status_request(self.subscriptions, eventtime)
|
|
||||||
self.webhooks.call_remote_method(
|
|
||||||
"process_status_update", status=status)
|
|
||||||
return eventtime + SUBSCRIPTION_REFRESH_TIME
|
|
||||||
|
|
||||||
def _process_status_request(self, requested_objects, eventtime):
|
|
||||||
result = {}
|
|
||||||
if self.ready:
|
|
||||||
for name, req_items in requested_objects.items():
|
|
||||||
obj = self.printer.lookup_object(name, None)
|
|
||||||
if obj is not None and name in self.available_objects:
|
|
||||||
status = obj.get_status(eventtime)
|
|
||||||
if not req_items:
|
|
||||||
# return all items excluding callables
|
|
||||||
result[name] = {k: v for k, v in status.items()
|
|
||||||
if not callable(v)}
|
|
||||||
else:
|
|
||||||
# return requested items excluding callables
|
|
||||||
result[name] = {k: v for k, v in status.items()
|
|
||||||
if k in req_items and not callable(v)}
|
|
||||||
else:
|
|
||||||
result = {"status": "Klippy Not Ready"}
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _handle_object_request(self, web_request):
|
|
||||||
web_request.send(dict(self.available_objects))
|
|
||||||
|
|
||||||
def _handle_status_request(self, web_request):
|
|
||||||
args = web_request.get_args()
|
|
||||||
eventtime = self.reactor.monotonic()
|
|
||||||
result = self._process_status_request(args, eventtime)
|
|
||||||
web_request.send(result)
|
|
||||||
|
|
||||||
def _handle_subscription_request(self, web_request):
|
|
||||||
args = web_request.get_args()
|
|
||||||
if args:
|
|
||||||
self.add_subscripton(args)
|
|
||||||
else:
|
|
||||||
raise web_request.error("Invalid argument")
|
|
||||||
|
|
||||||
def _handle_list_subscription_request(self, web_request):
|
|
||||||
web_request.send(dict(self.subscriptions))
|
|
||||||
|
|
||||||
def add_subscripton(self, new_sub):
|
|
||||||
if not new_sub:
|
|
||||||
return
|
|
||||||
for obj_name, req_items in new_sub.items():
|
|
||||||
if obj_name not in self.available_objects:
|
|
||||||
logging.info(
|
|
||||||
"webhooks: Object {%s} not available for subscription"
|
|
||||||
% (obj_name))
|
|
||||||
continue
|
continue
|
||||||
# validate requested items
|
# Query each requested printer object
|
||||||
|
cquery = {}
|
||||||
|
for obj_name, req_items in subscription.items():
|
||||||
|
res = query.get(obj_name, None)
|
||||||
|
if res is None:
|
||||||
|
po = self.printer.lookup_object(obj_name, None)
|
||||||
|
if po is None or not hasattr(po, 'get_status'):
|
||||||
|
res = query[obj_name] = {}
|
||||||
|
else:
|
||||||
|
res = query[obj_name] = po.get_status(eventtime)
|
||||||
|
if req_items is None:
|
||||||
|
req_items = list(res.keys())
|
||||||
if req_items:
|
if req_items:
|
||||||
avail_items = set(self.available_objects[obj_name])
|
subscription[obj_name] = req_items
|
||||||
invalid_items = set(req_items) - avail_items
|
lres = last_query.get(obj_name, {})
|
||||||
if invalid_items:
|
cres = {}
|
||||||
logging.info(
|
for ri in req_items:
|
||||||
"webhooks: Removed invalid items [%s] from "
|
rd = res.get(ri, None)
|
||||||
"subscription request %s" %
|
if not callable(rd) and (is_query or rd != lres.get(ri)):
|
||||||
(", ".join(invalid_items), obj_name))
|
cres[ri] = rd
|
||||||
req_items = list(set(req_items) - invalid_items)
|
if cres or is_query:
|
||||||
if not req_items:
|
cquery[obj_name] = cres
|
||||||
# No valid items remaining
|
# Send data
|
||||||
continue
|
if cquery or is_query:
|
||||||
# Add or update subscription
|
tmp = dict(template)
|
||||||
existing_items = self.subscriptions.get(obj_name, None)
|
tmp['params'] = {'eventtime': eventtime, 'status': cquery}
|
||||||
if existing_items is not None:
|
send_func(tmp)
|
||||||
if req_items == [] or existing_items == []:
|
if not query:
|
||||||
# Subscribe to all items
|
# Unregister timer if there are no longer any subscriptions
|
||||||
self.subscriptions[obj_name] = []
|
reactor = self.printer.get_reactor()
|
||||||
else:
|
reactor.unregister_timer(self.query_timer)
|
||||||
req_items = list(set(req_items) | set(existing_items))
|
self.query_timer = None
|
||||||
self.subscriptions[obj_name] = req_items
|
return reactor.NEVER
|
||||||
else:
|
return eventtime + SUBSCRIPTION_REFRESH_TIME
|
||||||
self.subscriptions[obj_name] = req_items
|
def _handle_query(self, web_request, is_subscribe=False):
|
||||||
if not self.timer_started:
|
objects = web_request.get('objects')
|
||||||
self.reactor.update_timer(self.subscription_timer, self.reactor.NOW)
|
# Validate subscription format
|
||||||
self.timer_started = True
|
if type(objects) != type({}):
|
||||||
|
raise web_request.error("Invalid argument")
|
||||||
|
for k, v in objects.items():
|
||||||
|
if type(k) != type("") or type(v) not in [type([]), type(None)]:
|
||||||
|
raise web_request.error("Invalid argument")
|
||||||
|
if v is not None:
|
||||||
|
for ri in v:
|
||||||
|
if type(ri) != type(""):
|
||||||
|
raise web_request.error("Invalid argument")
|
||||||
|
# Add to pending queries
|
||||||
|
cconn = web_request.get_client_connection()
|
||||||
|
template = web_request.get('response_template', {})
|
||||||
|
if is_subscribe and cconn in self.clients:
|
||||||
|
del self.clients[cconn]
|
||||||
|
reactor = self.printer.get_reactor()
|
||||||
|
complete = reactor.completion()
|
||||||
|
self.pending_queries.append((None, objects, complete.complete, {}))
|
||||||
|
# Start timer if needed
|
||||||
|
if self.query_timer is None:
|
||||||
|
qt = reactor.register_timer(self._do_query, reactor.NOW)
|
||||||
|
self.query_timer = qt
|
||||||
|
# Wait for data to be queried
|
||||||
|
msg = complete.wait()
|
||||||
|
web_request.send(msg['params'])
|
||||||
|
if is_subscribe:
|
||||||
|
self.clients[cconn] = (cconn, objects, cconn.send, template)
|
||||||
|
def _handle_subscribe(self, web_request):
|
||||||
|
self._handle_query(web_request, is_subscribe=True)
|
||||||
|
|
||||||
def add_early_printer_objects(printer):
|
def add_early_printer_objects(printer):
|
||||||
printer.add_object('webhooks', WebHooks(printer))
|
printer.add_object('webhooks', WebHooks(printer))
|
||||||
GCodeHelper(printer)
|
GCodeHelper(printer)
|
||||||
StatusHandler(printer)
|
QueryStatusHelper(printer)
|
||||||
|
|
Loading…
Reference in New Issue