# HG changeset patch # User Louis Opter # Date 1476810919 25200 # Node ID 2da15caf4d44ca2053dca4c9911ece8e6e6a1c7b # Parent ada36e135d0d06248306fac5b67b4ffd47429752 Get monolight to work again with the refactored client diff -r ada36e135d0d -r 2da15caf4d44 add_monolight.patch --- a/add_monolight.patch Fri Oct 14 17:10:24 2016 -0700 +++ b/add_monolight.patch Tue Oct 18 10:15:19 2016 -0700 @@ -1,5 +1,5 @@ # HG changeset patch -# Parent 703102c9539ab4968c7cde3313c2f33ce0070a8a +# Parent 7e908ae2e0c7088791fb4a442d86d7687a52b956 Start an experimental GUI for a Monome 128 Varibright Written in Python >= 3.5. @@ -18,7 +18,7 @@ new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/cli.py -@@ -0,0 +1,101 @@ +@@ -0,0 +1,65 @@ +# Copyright (c) 2016, Louis Opter +# +# This file is part of lightsd. @@ -38,16 +38,13 @@ + +import asyncio +import click -+import contextlib +import functools ++import lightsc +import locale +import logging +import monome -+import os +import signal -+import subprocess + -+from . import lightsc +from . import osc +from . import ui + @@ -58,53 +55,15 @@ +logging.basicConfig(level=logging.INFO) + + -+def get_lightsd_rundir(): -+ try: -+ lightsdrundir = subprocess.check_output(["lightsd", "--rundir"]) -+ except Exception as ex: -+ click.echo( -+ "Couldn't infer lightsd's runtime directory, is lightsd installed? " -+ "({})\nTrying build/socket…".format(ex), -+ err=True -+ ) -+ lightscdir = os.path.realpath(__file__).split(os.path.sep)[:-2] -+ lightsdrundir = os.path.join(*[os.path.sep] + lightscdir + ["build"]) -+ else: -+ lightsdrundir = lightsdrundir.decode(ENCODING).strip() -+ -+ return lightsdrundir -+ -+LIGHTSD_SOCKET = "unix://" + os.path.join(get_lightsd_rundir(), "socket") -+ -+ -+@contextlib.contextmanager -+def handle_unix_signals(loop): -+ SIGNALS = (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT) -+ if hasattr(loop, "add_signal_handler"): -+ for signum in SIGNALS: -+ loop.add_signal_handler(signum, ui.stop) -+ yield -+ # Workaround dumb bug in Python: -+ # Traceback (most recent call last): -+ # File "/usr/lib64/python3.5/asyncio/base_events.py", line 431, in __del__ -+ # File "/usr/lib64/python3.5/asyncio/unix_events.py", line 58, in close -+ # File "/usr/lib64/python3.5/asyncio/unix_events.py", line 139, in remove_signal_handler # noqa -+ # File "/usr/lib64/python3.5/signal.py", line 47, in signal -+ # TypeError: signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable object # noqa -+ if hasattr(loop, "remove_signal_handler"): -+ for signum in SIGNALS: -+ loop.remove_signal_handler(signum) -+ -+ +@click.command() +@click.option("--serialoscd-host", default="127.0.0.1") +@click.option("--serialoscd-port", type=click.IntRange(0, 2**16 - 1)) -+@click.option("--lightsd-url", default=LIGHTSD_SOCKET) -+def main(serialoscd_host, serialoscd_port, lightsd_url): ++@click.option("--lightsd-url") ++def main(serialoscd_host: str, serialoscd_port: int, lightsd_url: str): + loop = asyncio.get_event_loop() + + tasks = asyncio.gather( -+ loop.create_task(lightsc.create_lightsd_connection(lightsd_url)), ++ loop.create_task(lightsc.create_async_lightsd_connection(lightsd_url)), + loop.create_task(monome.create_serialosc_connection( + functools.partial(osc.MonomeApplication, ui.submit_keypress) + )) @@ -112,14 +71,19 @@ + loop.run_until_complete(tasks) + lightsd, serialosc = tasks.result() + -+ with handle_unix_signals(loop): -+ # TODO: make which monome instance to use something configurable -+ ui_task = loop.create_task(ui.start(loop, lightsd, serialosc)) ++ if hasattr(loop, "add_signal_handler"): ++ for signum in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT): ++ loop.add_signal_handler(signum, ui.stop) ++ ++ # TODO: make which monome instance to use something configurable ++ ui_task = loop.create_task(ui.start(loop, lightsd, serialosc)) + -+ loop.run_until_complete(ui_task) ++ loop.run_until_complete(ui_task) + -+ serialosc.disconnect() -+ loop.run_until_complete(lightsd.close()) ++ serialosc.disconnect() ++ loop.run_until_complete(lightsd.close()) ++ ++ loop.close() diff --git a/apps/monolight/monolight/osc.py b/apps/monolight/monolight/osc.py new file mode 100644 --- /dev/null @@ -197,17 +161,17 @@ +import monome +import logging + -+from .osc import ( -+ MONOME_KEYPRESS_DOWN, -+ monome_apply, -+) -+from .lightsc.commands import ( ++from lightsc.requests import ( + SetLightFromHSBK, + PowerOff, + PowerOn, + PowerToggle, +) + ++from .osc import ( ++ MONOME_KEYPRESS_DOWN, ++ monome_apply, ++) +logger = logging.getLogger("monolight.ui") + +_event_queue = None @@ -346,11 +310,11 @@ + ], + }, +) -diff --git a/clients/lightsd-python/README.rst b/clients/lightsd-python/README.rst +diff --git a/clients/python/lightsc/README.rst b/clients/python/lightsc/README.rst new file mode 100644 --- /dev/null -+++ b/clients/lightsd-python/README.rst -@@ -0,0 +1,30 @@ ++++ b/clients/python/lightsc/README.rst +@@ -0,0 +1,63 @@ +A Python client to control your smart bulbs through lightsd +=========================================================== + @@ -362,30 +326,63 @@ +.. code-block:: python + + import asyncio ++ import click + -+ from lightsd import create_lightsd_connection -+ from lightsd.commands import PowerOff, PowerOn, SetLightFromHSBK ++ from lightsc import LightsView, create_async_lightsd_connection ++ from lightsc.requests import ( ++ GetLightState, ++ PowerOff, ++ PowerOn, ++ SetLightFromHSBK, ++ ) ++ ++ async def example(url, targets): ++ async with create_async_lightsd_connection(url) as client: ++ click.echo("Connected to lightsd running at {}".format(client.url)) ++ ++ view = LightsView() ++ view.update(await client.apply(GetLightState(targets)) ++ click.echo("Discovered bulbs: {}".format(view)) + -+ async def example(): -+ client = await create_lightsd_connection("unix:///run/lightsd/socket") -+ async with client.batch() as batch: -+ batch.apply(PowerOn(targets=["*"])) -+ batch.apply(SetLightFromHSBK(["*"], 0., 1., 1., 3500, transition=600)) -+ client.apply(PowerOff(["*"])) -+ await client.close() ++ transition_ms = 600 ++ red_hsbk = (0., 1., 1., 3500) ++ click.echo("Turning all bulbs to red in {}ms...".format(transition_ms)) ++ async with client.batch() as batch: ++ batch.apply(PowerOn(targets)) ++ batch.apply(SetLightFromHSBK(targets, *red_hsbk, transition_ms=transition_ms)) ++ ++ click.echo("Restoring original state") ++ async with client.batch() as batch: ++ for b in view.bulbs: ++ PowerState = PowerOn if b.power else PowerOff ++ hsbk = (b.h, b.s, b.b, b.k) + -+ loop = asyncio.get_event_loop() -+ loop.run_until_complete(loop.create_task(example())) ++ batch.apply(PowerState([b.label])) ++ batch.apply(SetLightFromHSBK([b.label], *hsbk, transition_ms=transition_ms)) ++ ++ @click.command() ++ @click.option("--lightsd-url", help="supported schemes: tcp+jsonrpc://, unix+jsonrpc://") ++ @click.argument("bulb_targets", nargs=-1, required=True) ++ def main(lightsd_url, bulb_targets) ++ """This example will turn all your bulbs to red before restoring their ++ original state. ++ ++ If an URL is not provided this script will attempt to connect to ++ lightsd's UNIX socket. ++ """ ++ ++ evl = asyncio.get_event_loop() ++ evl.run_until_complete(evl.create_task(example(lightsd_url, bulb_targets))) + +.. _lightsd: https://www.lightsd.io/ +.. _LIFX: http://lifx.co/ + +.. vim: set tw=80 spelllang=en spell: -diff --git a/clients/lightsd-python/lightsd/__init__.py b/clients/lightsd-python/lightsd/__init__.py +diff --git a/clients/python/lightsc/lightsc/__init__.py b/clients/python/lightsc/lightsc/__init__.py new file mode 100644 --- /dev/null -+++ b/clients/lightsd-python/lightsd/__init__.py -@@ -0,0 +1,21 @@ ++++ b/clients/python/lightsc/lightsc/__init__.py +@@ -0,0 +1,24 @@ +# Copyright (c) 2016, Louis Opter +# +# This file is part of lightsd. @@ -404,14 +401,17 @@ +# along with lightsd. If not, see . + +from .client import ( # noqa -+ LightsClient, + create_lightsd_connection, ++ create_async_lightsd_connection, +) -diff --git a/clients/lightsd-python/lightsd/client.py b/clients/lightsd-python/lightsd/client.py ++from .view import ( # noqa ++ LightsView, ++) +diff --git a/clients/python/lightsc/lightsc/client.py b/clients/python/lightsc/lightsc/client.py new file mode 100644 --- /dev/null -+++ b/clients/lightsd-python/lightsd/client.py -@@ -0,0 +1,248 @@ ++++ b/clients/python/lightsc/lightsc/client.py +@@ -0,0 +1,321 @@ +# Copyright (c) 2016, Louis Opter +# +# This file is part of lightsd. @@ -430,134 +430,154 @@ +# along with lightsd. If not, see . + +import asyncio -+import collections +import functools +import json ++import locale +import logging +import os +import urllib +import uuid + -+from . import commands ++from typing import ( ++ Any, ++ Callable, ++ Dict, ++ List, ++ NamedTuple, ++ Sequence, ++) ++from typing import ( # noqa ++ Tuple, ++ Type, ++) + -+logger = logging.getLogger("monolight.lightsc") ++from . import ( ++ exceptions, ++ requests, ++ responses, ++ structs, ++) ++ ++logger = logging.getLogger("lightsd.client") ++ + -+PendingRequestEntry = collections.namedtuple( -+ "PendingRequestEntry", ("handler_cb", "timeout_handle") -+) ++_JSONRPCMethod = NamedTuple("_JSONRPCMethod", [ ++ ("name", str), ++ ("map_result", Callable[[Any], responses.Response]), ++]) ++_JSONRPC_API = { ++ requests.GetLightState: _JSONRPCMethod( ++ name="get_light_state", ++ map_result=lambda result: responses.LightsState([ ++ structs.LightBulb( ++ b["label"], b["power"], *b["hsbk"], tags=b["tags"] ++ ) for b in result ++ ]) ++ ), ++ requests.SetLightFromHSBK: _JSONRPCMethod( ++ name="set_light_from_hsbk", ++ map_result=lambda result: responses.Bool(result) ++ ), ++ requests.PowerOn: _JSONRPCMethod( ++ name="power_on", ++ map_result=lambda result: responses.Bool(result) ++ ), ++ requests.PowerOff: _JSONRPCMethod( ++ name="power_off", ++ map_result=lambda result: responses.Bool(result) ++ ), ++ requests.PowerToggle: _JSONRPCMethod( ++ name="power_toggle", ++ map_result=lambda result: responses.Bool(result) ++ ), ++} # type: Dict[Type[requests.RequestClass], _JSONRPCMethod] + + +class _JSONRPCCall: + -+ def __init__(self, method, params, response_handler): ++ def __init__( ++ self, method: str, params: Sequence[Any], timeout: int = None ++ ) -> None: ++ self.id = str(uuid.uuid4()) ++ self.method = method ++ self.params = params ++ self.timeout = timeout ++ self.timeout_handle = None # type: asyncio.Handle + self.request = { -+ "id": str(uuid.uuid4()), ++ "id": self.id, + "jsonrpc": "2.0", + "method": method, + "params": params, + } -+ self.response_handler = response_handler ++ self.response = asyncio.Future() # type: asyncio.futures.Future + + -+class _JSONRPCBatch: -+ -+ def __init__(self, client): -+ self._client = client -+ self._batch = [] -+ -+ async def __aenter__(self): -+ return self -+ -+ async def __aexit__(self, exc_type, exc_val, exc_tb): -+ if exc_type is None: -+ await self._client._jsonrpc_execute(self._batch) -+ -+ def apply(self, command): -+ self._batch.append(_JSONRPCCall( -+ command.METHOD, -+ command.params, -+ response_handler=self._client._HANDLERS.get(command.__class__) -+ )) -+ -+ -+class LightsClient: ++class AsyncJSONRPCLightsClient: + + READ_SIZE = 8192 -+ REFRESH_INTERVAL = 250 / 1000 # seconds + TIMEOUT = 2 # seconds + ENCODING = "utf-8" + -+ def __init__(self, -+ url, -+ encoding=ENCODING, -+ timeout=TIMEOUT, -+ read_size=READ_SIZE, -+ refresh_interval=REFRESH_INTERVAL, -+ loop=None): ++ def __init__( ++ self, ++ url: str, ++ encoding: str = ENCODING, ++ timeout: int = TIMEOUT, ++ read_size: int = READ_SIZE, ++ loop: asyncio.AbstractEventLoop = None ++ ) -> None: + self.url = url + self.encoding = encoding + self.timeout = timeout + self.read_size = read_size -+ self.refresh_interval = refresh_interval -+ self._pending_requests = {} -+ self._reader = self._writer = None -+ self._listen_task = self._poll_task = None ++ self._listen_task = None # type: asyncio.Task ++ self._pending_calls = {} # type: Dict[str, _JSONRPCCall] ++ self._reader = None # type: asyncio.StreamReader ++ self._writer = None # type: asyncio.StreamWriter + self._loop = loop or asyncio.get_event_loop() + -+ def _handle_light_state(self, response): -+ logger.info("Updating bulbs state") -+ self._bulbs = {b["label"]: b for b in response} -+ -+ _HANDLERS = { -+ commands.GetLightState: _handle_light_state -+ } -+ -+ def _handle_response(self, id, response): -+ handler_cb, timeout_handle = self._pending_requests.pop(id) -+ timeout_handle.cancel() -+ -+ if response is None: -+ logger.info("Timeout on request {}".format(id)) ++ def _handle_response( ++ self, id: str, response: Any, timeout: bool = False ++ ) -> None: ++ call = self._pending_calls.pop(id) ++ if timeout is True: ++ call.response.set_exception(exceptions.LightsClientTimeoutError()) + return ++ call.timeout_handle.cancel() ++ call.response.set_result(response) + -+ if handler_cb is not None: -+ handler_cb(self, response) -+ return -+ -+ logger.info("No handler for response {}: {}".format(id, response)) ++ async def _jsonrpc_execute( ++ self, pipeline: List[_JSONRPCCall] ++ ) -> Dict[str, Any]: ++ if not pipeline: ++ return {} + -+ async def _jsonrpc_execute(self, pipeline): -+ calls = [call.request for call in pipeline] -+ payload = json.dumps(calls[0] if len(calls) == 1 else calls) -+ payload = payload.encode(self.encoding, "surrogateescape") ++ requests = [call.request for call in pipeline] ++ for req in requests: ++ logger.info("Request {id}: {method}({params})".format(**req)) + -+ self._writer.write(payload) -+ -+ for call in calls: -+ logger.info("Request {id}: {method}({params})".format(**call)) ++ payload = json.dumps(requests[0] if len(requests) == 1 else requests) ++ self._writer.write(payload.encode(self.encoding, "surrogateescape")) + + await self._writer.drain() + + for call in pipeline: -+ id = call.request["id"] -+ timeout_cb = functools.partial( -+ self._handle_response, id, response=None ++ call.timeout_handle = self._loop.call_later( ++ call.timeout, ++ functools.partial( ++ self._handle_response, call.id, response=None, timeout=True ++ ) + ) -+ self._pending_requests[id] = PendingRequestEntry( -+ handler_cb=call.response_handler, -+ timeout_handle=self._loop.call_later(self.TIMEOUT, timeout_cb) -+ ) ++ self._pending_calls[call.id] = call + -+ async def close(self): -+ futures = [] -+ if self._poll_task is not None: -+ self._poll_task.cancel() -+ futures.append(self._poll_task) ++ futures = [call.response for call in pipeline] ++ await asyncio.wait(futures, loop=self._loop) ++ return {call.id: call.response.result() for call in pipeline} ++ ++ async def close(self) -> None: + if self._listen_task is not None: + self._listen_task.cancel() -+ futures.append(self._listen_task) -+ await asyncio.wait(futures, loop=self._loop) -+ self._poll_task = self._listen_task = None ++ self._listen_task = None + + if self._writer is not None: + if self._writer.can_write_eof(): @@ -569,25 +589,26 @@ + await self._reader.read() + self._reader = self._writer = None + -+ async def _reconnect(self): ++ self._pending_calls = {} ++ ++ async def _reconnect(self) -> None: + await self.close() + await self.connect() + -+ async def apply(self, command): -+ await self._jsonrpc_execute([_JSONRPCCall( -+ command.METHOD, -+ command.params, -+ response_handler=self._HANDLERS.get(command.__class__), -+ )]) ++ async def apply(self, req: requests.Request, timeout: int = TIMEOUT): ++ method = _JSONRPC_API[req.__class__] ++ call = _JSONRPCCall(method.name, req.params, timeout=timeout) ++ reps_by_id = await self._jsonrpc_execute([call]) ++ return method.map_result(reps_by_id[call.id]) + -+ async def connect(self): ++ async def connect(self) -> None: + parts = urllib.parse.urlparse(self.url) -+ if parts.scheme == "unix": ++ if parts.scheme == "unix+jsonrpc": + path = os.path.join(parts.netloc, parts.path).rstrip(os.path.sep) + open_connection = functools.partial( + asyncio.open_unix_connection, path + ) -+ elif parts.scheme == "tcp": ++ elif parts.scheme == "tcp+jsonrpc": + open_connection = functools.partial( + asyncio.open_connection, parts.hostname, parts.port + ) @@ -600,18 +621,12 @@ + self.timeout, + loop=self._loop, + ) -+ self._listen_task = self._loop.create_task(self.listen()) -+ self._poll_task = self._loop.create_task(self.poll()) ++ self._listen_task = self._loop.create_task(self._listen()) + except Exception: + logger.error("Couldn't open {}".format(self.url)) + raise + -+ async def poll(self): -+ while True: -+ await self.apply(commands.GetLightState(["*"])) -+ await asyncio.sleep(self.refresh_interval) -+ -+ async def listen(self): ++ async def _listen(self) -> None: + buf = bytearray() + + while True: @@ -625,46 +640,104 @@ + try: + json.loads(buf.decode(self.encoding, "ignore")) + except Exception: -+ # Decoding or parsing failed, data must be missing, try again: + continue -+ -+ # Decoding and JSON parsing were successful, we have received -+ # a full response: + response = json.loads(buf.decode(self.encoding, "surrogateescape")) + buf = bytearray() + -+ # Convert the response to a batch response if needed so we -+ # can always loop over it: + batch = response if isinstance(response, list) else [response] ++ for response in batch: ++ id = response["id"] + -+ for response in batch: + error = response.get("error") + if error is not None: -+ id = response["id"] -+ del self._pending_requests[id] -+ logger.warning("Error {}: {code} - {message}".format( -+ id, **error -+ )) ++ code = error.get("code") ++ msg = error.get("msg") ++ logger.warning("Error {}: {} - {}".format(id, code, msg)) ++ call = self._pending_calls.pop(id) ++ ex = exceptions.LightsClientError(msg) ++ call.response.set_exception(ex) ++ call.timeout_handle.cancel() + continue + -+ self._handle_response(response["id"], response["result"]) ++ logger.info("Response {}: {}".format(id, response["result"])) ++ self._handle_response(id, response["result"]) + -+ def batch(self): -+ return _JSONRPCBatch(self) ++ def batch(self) -> "_AsyncJSONRPCBatch": ++ return _AsyncJSONRPCBatch(self) + + -+async def create_lightsd_connection(url, loop=None): ++class _AsyncJSONRPCBatch: ++ ++ def __init__(self, client: AsyncJSONRPCLightsClient) -> None: ++ self.responses = None # type: Tuple[responses.Response, ...] ++ self._client = client ++ self._batch = [] # type: List[_JSONRPCCall] ++ ++ async def __aenter__(self) -> "_AsyncJSONRPCBatch": ++ return self ++ ++ async def __aexit__(self, exc_type, exc_val, exc_tb): ++ if exc_type is None: ++ reps_by_id = await self._client._jsonrpc_execute(self._batch) ++ self.responses = ( ++ _JSONRPC_API[req.__class__].map_result(reps_by_id[req.id]) ++ for req in self._batch ++ ) ++ ++ def apply( ++ self, ++ req: requests.Request, ++ timeout: int = AsyncJSONRPCLightsClient.TIMEOUT ++ ) -> None: ++ method = _JSONRPC_API[req.__class__] ++ call = _JSONRPCCall(method.name, req.params, timeout=timeout) ++ self._batch.append(call) ++ ++ ++async def get_lightsd_unix_socket_async( ++ loop: asyncio.AbstractEventLoop = None, ++) -> str: ++ process = await asyncio.create_subprocess_exec( ++ "lightsd", "--rundir", ++ stdout=asyncio.subprocess.PIPE, ++ stderr=asyncio.subprocess.DEVNULL, ++ loop=loop, ++ ) ++ stdout, stderr = await process.communicate() ++ stdout = stdout.decode(locale.getpreferredencoding()).strip() ++ if process.returncode == 0 and stdout: ++ lightsdrundir = stdout ++ else: ++ lightsdrundir = "build" ++ logger.warning( ++ "Couldn't infer lightsd's runtime directory, is " ++ "lightsd installed? Trying {}…".format(lightsdrundir) ++ ) ++ ++ return "unix+jsonrpc://" + os.path.join(lightsdrundir, "socket") ++ ++ ++async def create_async_lightsd_connection( ++ url: str = None, ++ loop: asyncio.AbstractEventLoop = None ++) -> AsyncJSONRPCLightsClient: + if loop is None: + loop = asyncio.get_event_loop() ++ if url is None: ++ url = await get_lightsd_unix_socket_async(loop) + -+ c = LightsClient(url, loop=loop) ++ c = AsyncJSONRPCLightsClient(url, loop=loop) + await c.connect() + return c -diff --git a/clients/lightsd-python/lightsd/commands.py b/clients/lightsd-python/lightsd/commands.py ++ ++ ++def create_lightsd_connection(url: str = None) -> None: ++ raise NotImplementedError("Sorry, no synchronous client available yet") +diff --git a/clients/python/lightsc/lightsc/exceptions.py b/clients/python/lightsc/lightsc/exceptions.py new file mode 100644 --- /dev/null -+++ b/clients/lightsd-python/lightsd/commands.py -@@ -0,0 +1,64 @@ ++++ b/clients/python/lightsc/lightsc/exceptions.py +@@ -0,0 +1,28 @@ +# Copyright (c) 2016, Louis Opter +# +# This file is part of lightsd. @@ -683,57 +756,221 @@ +# along with lightsd. If not, see . + + -+class Command: ++class LightsError(Exception): ++ pass + -+ METHOD = None + -+ def __init__(self, *args): -+ self.params = args ++class LightsClientError(LightsError): ++ pass + + -+class SetLightFromHSBK(Command): ++class LightsClientTimeoutError(LightsClientError): ++ pass +diff --git a/clients/python/lightsc/lightsc/requests.py b/clients/python/lightsc/lightsc/requests.py +new file mode 100644 +--- /dev/null ++++ b/clients/python/lightsc/lightsc/requests.py +@@ -0,0 +1,65 @@ ++# Copyright (c) 2016, Louis Opter ++# ++# This file is part of lightsd. ++# ++# lightsd is free software: you can redistribute it and/or modify ++# it under the terms of the GNU General Public License as published by ++# the Free Software Foundation, either version 3 of the License, or ++# (at your option) any later version. ++# ++# lightsd is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with lightsd. If not, see . + -+ METHOD = "set_light_from_hsbk" -+ -+ def __init__(self, targets, h, s, b, k, transition): -+ Command.__init__(self, targets, h, s, b, k, transition) ++from typing import ( ++ Any, ++ List, ++ TypeVar, ++) + + -+class GetLightState(Command): ++class Request: ++ ++ def __init__(self, *args: Any) -> None: ++ self.params = args ++ ++RequestClass = TypeVar("RequestClass", bound=Request) ++ ++ ++class SetLightFromHSBK(Request): ++ ++ def __init__( ++ self, ++ targets: List[str], ++ h: float, s: float, b: float, k: int, ++ transition_ms: int ++ ) -> None: ++ Request.__init__(self, targets, h, s, b, k, transition_ms) + -+ METHOD = "get_light_state" ++ ++class GetLightState(Request): ++ ++ def __init__(self, targets: List[str]) -> None: ++ Request.__init__(self, targets) ++ ++ ++class PowerOff(Request): + -+ def __init__(self, targets): -+ Command.__init__(self, targets) ++ def __init__(self, targets: List[str]) -> None: ++ Request.__init__(self, targets) ++ ++ ++class PowerOn(Request): ++ ++ def __init__(self, targets: List[str]) -> None: ++ Request.__init__(self, targets) + + -+class PowerOff(Command): ++class PowerToggle(Request): + -+ METHOD = "power_off" ++ def __init__(self, targets: List[str]) -> None: ++ Request.__init__(self, targets) +diff --git a/clients/python/lightsc/lightsc/responses.py b/clients/python/lightsc/lightsc/responses.py +new file mode 100644 +--- /dev/null ++++ b/clients/python/lightsc/lightsc/responses.py +@@ -0,0 +1,41 @@ ++# Copyright (c) 2016, Louis Opter ++# ++# This file is part of lightsd. ++# ++# lightsd is free software: you can redistribute it and/or modify ++# it under the terms of the GNU General Public License as published by ++# the Free Software Foundation, either version 3 of the License, or ++# (at your option) any later version. ++# ++# lightsd is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with lightsd. If not, see . + -+ def __init__(self, targets): -+ Command.__init__(self, targets) ++from typing import ( ++ List, ++ TypeVar, ++) ++ ++from . import structs ++ ++ ++class Response: ++ pass ++ ++ResponseClass = TypeVar("ResponseClass", bound=Response) ++ ++ ++class Bool(Response): ++ ++ def __init__(self, bool: bool) -> None: ++ self.value = bool + + -+class PowerOn(Command): ++class LightsState(Response): + -+ METHOD = "power_on" ++ def __init__(self, bulbs: List[structs.LightBulb]) -> None: ++ self.bulbs = bulbs +diff --git a/clients/python/lightsc/lightsc/structs.py b/clients/python/lightsc/lightsc/structs.py +new file mode 100644 +--- /dev/null ++++ b/clients/python/lightsc/lightsc/structs.py +@@ -0,0 +1,42 @@ ++# Copyright (c) 2016, Louis Opter ++# ++# This file is part of lightsd. ++# ++# lightsd is free software: you can redistribute it and/or modify ++# it under the terms of the GNU General Public License as published by ++# the Free Software Foundation, either version 3 of the License, or ++# (at your option) any later version. ++# ++# lightsd is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with lightsd. If not, see . + -+ def __init__(self, targets): -+ Command.__init__(self, targets) ++from typing import ( ++ List, ++) ++ ++ ++class Struct: ++ pass + + -+class PowerToggle(Command): -+ -+ METHOD = "power_toggle" ++class LightBulb(Struct): + -+ def __init__(self, targets): -+ Command.__init__(self, targets) -diff --git a/clients/lightsd-python/setup.py b/clients/lightsd-python/setup.py ++ def __init__( ++ self, ++ label: str, ++ power: bool, ++ h: float, s: float, b: float, k: int, ++ tags: List[str] ++ ) -> None: ++ self.label = label ++ self.power = power ++ self.h = h ++ self.s = s ++ self.b = b ++ self.k = k ++ self.tags = tags +diff --git a/clients/python/lightsc/lightsc/view.py b/clients/python/lightsc/lightsc/view.py new file mode 100644 --- /dev/null -+++ b/clients/lightsd-python/setup.py -@@ -0,0 +1,56 @@ ++++ b/clients/python/lightsc/lightsc/view.py +@@ -0,0 +1,32 @@ ++# Copyright (c) 2016, Louis Opter ++# ++# This file is part of lightsd. ++# ++# lightsd is free software: you can redistribute it and/or modify ++# it under the terms of the GNU General Public License as published by ++# the Free Software Foundation, either version 3 of the License, or ++# (at your option) any later version. ++# ++# lightsd is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with lightsd. If not, see . ++ ++import pprint ++ ++ ++class LightsView: ++ ++ def __init__(self): ++ self.bulbs = [] ++ self.bulbs_by_label = {} ++ ++ def __str__(self): ++ return pprint.pformat(self.bulbs_by_label) ++ ++ def update(self, lights_state): ++ self.bulbs = lights_state.bulbs ++ self.bulbs_by_label = {b.label for b in lights_state.bulbs.items()} +diff --git a/clients/python/lightsc/setup.py b/clients/python/lightsc/setup.py +new file mode 100644 +--- /dev/null ++++ b/clients/python/lightsc/setup.py +@@ -0,0 +1,53 @@ +# Copyright (c) 2016, Louis Opter +# +# This file is part of lighstd. @@ -759,23 +996,18 @@ + long_description = fp.read() + +setuptools.setup( -+ name="lightsd", ++ name="lightsc", + version=version, -+ description="A client to interact with lighsd ", ++ description="A client to interact with lightsd", + long_description=long_description, + author="Louis Opter", + author_email="louis@opter.org", + packages=setuptools.find_packages(exclude=['tests', 'tests.*']), + include_package_data=True, + entry_points={ -+ "console_scripts": [ -+ "monolight = monolight.cli:main", -+ ], ++ "console_scripts": [], + }, -+ install_requires=[ -+ "click~=6.6", -+ "pymonome~=0.8.2", -+ ], ++ install_requires=[], + tests_require=[ + "doubles~=1.1.3", + "freezegun~=0.3.5", @@ -784,9 +1016,11 @@ + extras_require={ + "dev": [ + "flake8", ++ "mypy-lang", + "ipython", + "pdbpp", + "pep8", ++ "typed-ast", + ], + }, +) diff -r ada36e135d0d -r 2da15caf4d44 add_slides.patch --- a/add_slides.patch Fri Oct 14 17:10:24 2016 -0700 +++ b/add_slides.patch Tue Oct 18 10:15:19 2016 -0700 @@ -1,5 +1,5 @@ # HG changeset patch -# Parent 31f8c4917304aea7d17e8c79a2a985082b3f9831 +# Parent 37f2e84f1c449f77606c68f2a74452dfc883fa5e Start to setup some slides for 33C3 Hopefully my talk proposal will be selected! @@ -20,7 +20,7 @@ FIND_PACKAGE(Virtualenv) FIND_PACKAGE(Xz) -+IF (BUILD_SLIDES) ++IF (WITH_SLIDES) + FIND_PACKAGE(LATEX REQUIRED) + INCLUDE(UseLATEX) +ENDIF () @@ -32,9 +32,9 @@ ) ENDIF () -+IF (BUILD_SLIDES) ++IF (WITH_SLIDES) + ADD_SUBDIRECTORY(slides) -+ENDIF (BUILD_SLIDES) ++ENDIF (WITH_SLIDES) + +### Install rules ############################################################## + @@ -1692,13 +1692,17 @@ new file mode 100644 --- /dev/null +++ b/slides/33c3/33c3.tex -@@ -0,0 +1,28 @@ +@@ -0,0 +1,54 @@ +\documentclass[xcolor={usenames,svgnames}]{beamer} + +\usepackage[american]{babel} ++\usepackage{pdfcomment} +\usepackage{tikz} ++ +\usetikzlibrary{shapes,fit} + ++\newcommand{\pdfnote}[1]{\marginnote{\pdfcomment[icon=note]{#1}}} ++ +\title{Making-of lightsd: a daemon to control your (LIFX) smart-bulbs.} +\date{33C3 --- December 2016} +\author{Louis Opter \\ \texttt{www.lightsd.io}} @@ -1707,18 +1711,40 @@ + +\begin{frame}\titlepage\end{frame} + -+%%% Supporting more bulbs products %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -+ -+% I think this is a pretty obvious idea, avoid getting locked with one -+% manufacturer or product. ++\section[design]{Design choices} + -+% lightsd has been architectured around a minimal set of bulb commands, meaning -+% that while abstractions/integrations are missing to make lightsd work with -+% other bulbs, it at least relies on a very limited set of commands (get/set -+% the state of the bulb and that's about it). For example lightsd can totally -+% implement transitions internally. ++\begin{frame}{Being a daemon} ++\pdfnote{% ++Pros:\ ++\ ++- Instant devices ``discovery'';\ ++- Conflict resolution;\ ++- Good network location;\ ++\ ++Cons:\ ++\ ++- Applications aren't self-contained: side-car process though, or even tighter\ ++ if you're GPLv3;\ ++- Discovery: avahi? dns?\ ++} ++\end{frame} + -+% Modules that would need heavy changes to support other bulb models: ++\section[tbd]{TBD} ++ ++\begin{frame}{Device suppport} ++\pdfnote{% ++I think this is a pretty obvious idea, avoid getting locked with one\ ++manufacturer or product.\ ++\ ++lightsd has been architectured around a minimal set of bulb commands, meaning\ ++that while abstractions/integrations are missing to make lightsd work with\ ++other bulbs, it at least relies on a very limited set of commands (get/set\ ++the state of the bulb and that's about it). For example lightsd can totally\ ++implement transitions internally.\ ++\ ++Modules that would need heavy changes to support other bulb models:\ ++} ++\end{frame} + +\end{document} diff --git a/slides/33c3/CMakeLists.txt b/slides/33c3/CMakeLists.txt