Mercurial > louis > mq > lightsd
changeset 420:8d707d43a54d
Finish the patches on the documentation and lightsc.py
author | Louis Opter <kalessin@kalessin.fr> |
---|---|
date | Mon, 04 Jan 2016 16:11:47 +0100 |
parents | b6ec1933ec85 |
children | fc431cab531c |
files | doc_semver.patch fix_lightscpy_readloop.patch series |
diffstat | 3 files changed, 0 insertions(+), 270 deletions(-) [+] |
line wrap: on
line diff
--- a/doc_semver.patch Mon Jan 04 15:34:40 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,24 +0,0 @@ -# HG changeset patch -# Parent bfa59be534ab65b5caff5b25d70a1d9fa961bfbb -Document semver a little bit better - -diff --git a/docs/changelog.rst b/docs/changelog.rst ---- a/docs/changelog.rst -+++ b/docs/changelog.rst -@@ -1,7 +1,15 @@ - Changelog - ========= - --lightsd uses `semantic versionning <http://semver.org/>`_. -+lightsd uses `semantic versionning <http://semver.org/>`_, here is the summary: -+ -+Given a version number MAJOR.MINOR.PATCH: -+ -+- MAJOR version gets incremented when you may need to modify your lightsd -+ configuration to keep your setup running; -+- MINOR version gets incremented when functionality or substantial improvements -+ are added in a backwards-compatible manner; -+- PATCH version gets incremented for backwards-compatible bug fixes. - - 1.1.2 (2015-11-30) - ------------------
--- a/fix_lightscpy_readloop.patch Mon Jan 04 15:34:40 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,244 +0,0 @@ -# HG changeset patch -# Parent d65cba5a051329c02b6d339ff3ba0685c8a94376 -Fix lightsc.py's read loop and document it - -It now handles arbitrarily large and partial responses properly. - -diff --git a/docs/protocol.rst b/docs/protocol.rst ---- a/docs/protocol.rst -+++ b/docs/protocol.rst -@@ -1,7 +1,12 @@ - The lights daemon protocol - ========================== - --The lightsd protocol is implemented on top of `JSON-RPC 2.0`_. -+The lightsd protocol is implemented on top of `JSON-RPC 2.0`_. This section -+covers the available methods and how to target bulbs. -+ -+Since lightsd implements JSON-RPC without any kind of framing like it usually is -+the case (using HTTP), this section also explains how to implement your own -+lightsd client in `Writing a client for lightsd`_. - - .. _JSON-RPC 2.0: http://www.jsonrpc.org/specification - -@@ -19,7 +24,7 @@ - | ``#TagName`` | targets bulbs tagged with *TagName* | - +-----------------------------+------------------------------------------------+ - | ``124f31a5`` | directly target the bulb with the given id | --| | (mac addr, see below) | -+| | (that's the bulb mac address, see below) | - +-----------------------------+------------------------------------------------+ - | ``label`` | directly target the bulb with the given Label | - +-----------------------------+------------------------------------------------+ -@@ -144,9 +149,105 @@ - - untag("#myexistingtag", "myexistingtag") - -+Writing a client for lightsd -+---------------------------- -+ -+lightsd does JSON-RPC directly over TCP, requests and responses aren't framed in -+any way like it is usually done by using HTTP. -+ -+This means that you will very likely need to write a JSON-RPC client -+specifically for lightsd. You're actually encouraged to do that as lightsd will -+probably augment JSON-RPC via lightsd specific `JSON-RPC extensions`_ in the -+future. -+ -+.. _JSON-RPC extensions: http://www.jsonrpc.org/specification#extensions -+ -+JSON-RPC over TCP -+~~~~~~~~~~~~~~~~~ -+ -+JSON-RPC works in a request/response fashion: the socket (network connection) is -+never used in a full-duplex fashion (data never flows in both direction at the -+same time): -+ -+#. Write (send) a request on the socket; -+#. Read (receive) the response on the socket; -+#. Repeat. -+ -+Writing the request is easy: do successive write (send) calls until you have -+successfully sent the whole request. The next step (reading/receiving) is a bit -+more complex. And that said, if the response isn't useful to you, you can ask -+lightsd to omit it by turning your request into a `notification`_: if you remove -+the JSON-RPC id, then you can just send your requests (now notifications) on the -+socket in a fire and forget fashion. -+ -+.. _notification: http://www.jsonrpc.org/specification#notification -+ -+Otherwise to successfully read and decode JSON-RPC over TCP you will need to -+implement your own read loop, the algorithm follows. It focuses on the low-level -+details, adapt it for the language and platform you are using: -+ -+#. Prepare an empty buffer that you can grow, we will accumulate received data -+ in it; -+#. Start an infinite loop and start a read (receive) for a chunk of data (e.g: -+ 4KiB), accumulate the received data in the previous buffer, then try to -+ interpret the data as JSON: -+ -+ - if valid JSON can be decoded then break out of the loop; -+ - else data is missing and continue the loop; -+#. Decode the JSON data. -+ -+Here is a complete Python 3 request/response example: -+ -+.. code-block:: python -+ :linenos: -+ -+ import json -+ import socket -+ import uuid -+ -+ READ_SIZE = 4096 -+ ENCODING = "utf-8" -+ -+ # Connect to lightsd, here using an Unix socket. The rest of the example is -+ # valid for TCP sockets too. Replace /run/lightsd/socket by the output of: -+ # echo $(lightsd --rundir)/socket -+ lightsd_socket = socket.socket(socket.AF_UNIX) -+ lightsd_socket.connect("/run/lightsd/socket") -+ lightsd_socket.settimeout(2) # seconds -+ -+ # Prepare the request: -+ request = json.dumps({ -+ "method": "get_light_state", -+ "params": ["*"], -+ "jsonrpc": "2.0", -+ "id": str(uuid.uuid4()), -+ }).encode(ENCODING, "surrogateescape") -+ -+ # Send it: -+ lightsd_socket.sendall(request) -+ -+ # Prepare an empty buffer to accumulate the received data: -+ response = bytearray() -+ while True: -+ # Read a chunk of data, and accumulate it in the response buffer: -+ response += lightsd_socket.recv(READ_SIZE) -+ try: -+ # Try to load the received the data, we ignore encoding errors -+ # since we only wanna know if the received data is complete. -+ json.loads(response.decode(ENCODING, "ignore")) -+ break # Decoding was successful, we have received everything. -+ except Exception: -+ continue # Decoding failed, data must be missing. -+ -+ response = response.decode(ENCODING, "surrogateescape") -+ print(json.loads(response)) -+ - Notes ------- -+~~~~~ - -+- Use an incremental JSON parser if you have one handy: for responses multiple -+ times the size of your receive window it will let you avoid decoding the whole -+ response at each iteration of the read loop; - - lightsd supports batch JSON-RPC requests, use them! - - .. vim: set tw=80 spelllang=en spell: -diff --git a/examples/lightsc.py b/examples/lightsc.py ---- a/examples/lightsc.py -+++ b/examples/lightsc.py -@@ -42,8 +42,28 @@ - - class LightsClient: - -- def __init__(self, url): -+ READ_SIZE = 4096 -+ TIMEOUT = 2 # seconds -+ ENCODING = "utf-8" -+ -+ class Error(Exception): -+ pass -+ -+ class TimeoutError(Error): -+ pass -+ -+ class JSONError(Error): -+ -+ def __init__(self, response): -+ self._response = response -+ -+ def __str__(self): -+ return "received invalid JSON: {}".format(self._response) -+ -+ def __init__(self, url, encoding=ENCODING, timeout=TIMEOUT, -+ read_size=READ_SIZE): - self.url = url -+ self.encoding = encoding - - parts = urllib.parse.urlparse(args.url) - -@@ -55,7 +75,9 @@ - self._socket = socket.create_connection((parts.hostname, parts.port)) - else: - raise ValueError("Unsupported url {}".format(url)) -+ self._socket.settimeout(timeout) - -+ self._read_size = read_size - self._pipeline = [] - self._batch = False - -@@ -75,15 +97,24 @@ - } - - def _execute_payload(self, payload): -- self._socket.send(json.dumps(payload).encode("utf-8")) -- # FIXME: proper read loop -- response = self._socket.recv(64 * 1024).decode("utf-8") -+ response = bytearray() - try: -- response = json.loads(response) -- except Exception: -- print("received invalid json: {}".format(response)) -+ payload = json.dumps(payload) -+ payload = payload.encode(self.encoding, "surrogateescape") -+ self._socket.sendall(payload) - -- return response -+ while True: -+ response += self._socket.recv(self._read_size) -+ try: -+ return json.loads(response.decode( -+ self.encoding, "surrogateescape" -+ )) -+ except Exception: -+ continue -+ except socket.timeout: -+ if not response: -+ raise self.TimeoutError -+ raise self.JSONError(response) - - def _jsonrpc_call(self, method, params): - payload = self._make_payload(method, params) -@@ -199,11 +230,6 @@ - - def _drop_to_shell(lightsc): - c = lightsc # noqa -- nb = "d073d501a0d5" # noqa -- fugu = "d073d500603b" # noqa -- neko = "d073d5018fb6" # noqa -- middle = "d073d502e530" # noqa -- - banner = ( - "Connected to {}, use the variable c to interact with your " - "bulbs:\n\n>>> r = c.get_light_state(\"*\")".format(c.url) -@@ -227,7 +253,7 @@ - lightsdrundir = subprocess.check_output(["lightsd", "--rundir"]) - except Exception as ex: - print( -- "Couldn't infer lightsd's runtime directory is lightsd installed? " -+ "Couldn't infer lightsd's runtime directory, is lightsd installed? " - "({})\nTrying build/socket...".format(ex), - file=sys.stderr - ) -@@ -238,7 +264,7 @@ - lightsdrundir = lightsdrundir.decode(encoding).strip() - - parser = argparse.ArgumentParser( -- description="lightsc.py is an interactive lightsd Python client" -+ description="Interactive lightsd Python client" - ) - parser.add_argument( - "-u", "--url", type=str,
--- a/series Mon Jan 04 15:34:40 2016 +0100 +++ b/series Mon Jan 04 16:11:47 2016 +0100 @@ -1,8 +1,6 @@ -doc_semver.patch optional_jsonrpc_args.patch network_discovery.patch dont_use_ev_assign.patch -fix_lightscpy_readloop.patch add_power_transition.patch open_gateway_on_any_bulb_response.patch #+future make_gateway_write_callbacks_low_priority.patch #+future