Mercurial > louis > mq > lightsd
view add_monolight.patch @ 499:2da15caf4d44
Get monolight to work again with the refactored client
author | Louis Opter <kalessin@kalessin.fr> |
---|---|
date | Tue, 18 Oct 2016 10:15:19 -0700 |
parents | ada36e135d0d |
children | d250169c1a69 |
line wrap: on
line source
# HG changeset patch # Parent 7e908ae2e0c7088791fb4a442d86d7687a52b956 Start an experimental GUI for a Monome 128 Varibright Written in Python >= 3.5. diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -2,3 +2,4 @@ .*\.py[co]$ ^build ^pcaps +.*\.egg-info$ diff --git a/apps/monolight/monolight/__init__.py b/apps/monolight/monolight/__init__.py new file mode 100644 diff --git a/apps/monolight/monolight/cli.py b/apps/monolight/monolight/cli.py new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/cli.py @@ -0,0 +1,65 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + +import asyncio +import click +import functools +import lightsc +import locale +import logging +import monome +import signal + +from . import osc +from . import ui + +ENCODING = locale.getpreferredencoding() + +FADERS_MAX_VALUE = 100 + +logging.basicConfig(level=logging.INFO) + + +@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") +def main(serialoscd_host: str, serialoscd_port: int, lightsd_url: str): + loop = asyncio.get_event_loop() + + tasks = asyncio.gather( + loop.create_task(lightsc.create_async_lightsd_connection(lightsd_url)), + loop.create_task(monome.create_serialosc_connection( + functools.partial(osc.MonomeApplication, ui.submit_keypress) + )) + ) + loop.run_until_complete(tasks) + lightsd, serialosc = tasks.result() + + 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) + + 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 +++ b/apps/monolight/monolight/osc.py @@ -0,0 +1,45 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + +import monome + +MONOME_KEYPRESS_DOWN = 1 +MONOME_KEYPRESS_UP = 0 +MONOME_KEYPRESS_STATES = frozenset({ + MONOME_KEYPRESS_DOWN, + MONOME_KEYPRESS_UP, +}) + + +class MonomeApplication(monome.Monome): + + def __init__(self, keypress_callback): + self._keypress_callback = keypress_callback + monome.Monome.__init__(self, "/monolight") + + def ready(self): + self.led_all(0) + + def grid_key(self, x, y, s): + self._keypress_callback(x, y, s) + + +def monome_apply(serialosc, method, *args, **kwargs): + for device in serialosc.app_instances.values(): + for app in device: + if isinstance(app, MonomeApplication): + method(app, *args, **kwargs) diff --git a/apps/monolight/monolight/ui.py b/apps/monolight/monolight/ui.py new file mode 100644 --- /dev/null +++ b/apps/monolight/monolight/ui.py @@ -0,0 +1,114 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + +import asyncio +import collections +import monome +import logging + +from lightsc.requests import ( + SetLightFromHSBK, + PowerOff, + PowerOn, + PowerToggle, +) + +from .osc import ( + MONOME_KEYPRESS_DOWN, + monome_apply, +) +logger = logging.getLogger("monolight.ui") + +_event_queue = None + +_KeyPress = collections.namedtuple("_KeyPress", ("x", "y", "state")) + +_STOP_SENTINEL = object() + + +def draw(serialosc): + buf = monome.LedBuffer(8, 8) + buf.led_set(0, 0, 1) + for x in range(0, 5): + buf.led_set(x, 7, 1) + monome_apply(serialosc, buf.render) + + +def hide(serialosc): + monome_apply(serialosc, monome.Monome.led_all, 0) + + +async def start(loop, lightsd, serialosc): + global _event_queue + + _event_queue = asyncio.Queue() + + hidden = True + + while True: + keypress = await _event_queue.get() + if keypress is _STOP_SENTINEL: + hide(serialosc) + _event_queue = None + return + + if not hidden: + draw(serialosc) + + logger.info("keypress: x={}, y={}, state={}".format(*keypress)) + + if keypress.state != MONOME_KEYPRESS_DOWN: + continue + if keypress.y != 7 and keypress.y != 0: + continue + if keypress.x == 0: + if keypress.y == 0: + hidden = not hidden + if hidden: + hide(serialosc) + continue + await lightsd.apply(PowerOff(["*"])) + if keypress.y != 7: + continue + if keypress.x == 1: + await lightsd.apply(PowerOn(["*"])) + elif keypress.x == 2: + await lightsd.apply(PowerToggle(["neko"])) + elif keypress.x == 3: + await lightsd.apply(PowerToggle(["fugu"])) + elif keypress.x == 4: + async with lightsd.batch() as batch: + batch.apply(SetLightFromHSBK( + ["#tower"], 37.469443, 1.0, 0.25, 3500, 600 + )) + batch.apply(SetLightFromHSBK( + ["fugu", "buzz"], 47.469443, 0.2, 0.2, 3500, 600 + )) + batch.apply(SetLightFromHSBK( + ["candle"], 47.469443, 0.2, 0.15, 3500, 600 + )) + batch.apply(PowerOn(["#br"])) + + +def stop(): + if _event_queue is not None: + _event_queue.put_nowait(_STOP_SENTINEL) + + +def submit_keypress(x, y, state): + if _event_queue is not None: + _event_queue.put_nowait(_KeyPress(x, y, state)) diff --git a/apps/monolight/setup.py b/apps/monolight/setup.py new file mode 100644 --- /dev/null +++ b/apps/monolight/setup.py @@ -0,0 +1,52 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# This file is part of lighstd. +# +# lighstd 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. +# +# lighstd 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 lighstd. If not, see <http://www.gnu.org/licenses/>. + +import setuptools + +version = "0.0.1.dev0" + +setuptools.setup( + name="monolight", + version=version, + description="A Monome UI to control smart bulbs using lightsd", + 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", + ], + }, + install_requires=[ + "click~=6.6", + "pymonome~=0.8.2", + ], + tests_require=[ + "doubles~=1.1.3", + "freezegun~=0.3.5", + "pytest~=3.0", + ], + extras_require={ + "dev": [ + "flake8", + "ipython", + "pdbpp", + "pep8", + ], + }, +) diff --git a/clients/python/lightsc/README.rst b/clients/python/lightsc/README.rst new file mode 100644 --- /dev/null +++ b/clients/python/lightsc/README.rst @@ -0,0 +1,63 @@ +A Python client to control your smart bulbs through lightsd +=========================================================== + +lightsd_ is a daemon (background service) to control your LIFX_ WiFi "smart" +bulbs. This package allows you to make RPC calls to lightsd to control your +light bulbs from Python. It is built on top of the ``asyncio`` module and +requires Python ≥ 3.5: + +.. code-block:: python + + import asyncio + import click + + 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)) + + 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) + + 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/python/lightsc/lightsc/__init__.py b/clients/python/lightsc/lightsc/__init__.py new file mode 100644 --- /dev/null +++ b/clients/python/lightsc/lightsc/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + +from .client import ( # noqa + create_lightsd_connection, + create_async_lightsd_connection, +) +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/python/lightsc/lightsc/client.py @@ -0,0 +1,321 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + +import asyncio +import functools +import json +import locale +import logging +import os +import urllib +import uuid + +from typing import ( + Any, + Callable, + Dict, + List, + NamedTuple, + Sequence, +) +from typing import ( # noqa + Tuple, + Type, +) + +from . import ( + exceptions, + requests, + responses, + structs, +) + +logger = logging.getLogger("lightsd.client") + + +_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: 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": self.id, + "jsonrpc": "2.0", + "method": method, + "params": params, + } + self.response = asyncio.Future() # type: asyncio.futures.Future + + +class AsyncJSONRPCLightsClient: + + READ_SIZE = 8192 + TIMEOUT = 2 # seconds + ENCODING = "utf-8" + + 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._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_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) + + async def _jsonrpc_execute( + self, pipeline: List[_JSONRPCCall] + ) -> Dict[str, Any]: + if not pipeline: + return {} + + requests = [call.request for call in pipeline] + for req in requests: + logger.info("Request {id}: {method}({params})".format(**req)) + + 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: + call.timeout_handle = self._loop.call_later( + call.timeout, + functools.partial( + self._handle_response, call.id, response=None, timeout=True + ) + ) + self._pending_calls[call.id] = call + + 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() + self._listen_task = None + + if self._writer is not None: + if self._writer.can_write_eof(): + self._writer.write_eof() + self._writer.close() + if self._reader is not None: + self._reader.feed_eof() + if not self._reader.at_eof(): + await self._reader.read() + self._reader = self._writer = None + + self._pending_calls = {} + + async def _reconnect(self) -> None: + await self.close() + await self.connect() + + 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) -> None: + parts = urllib.parse.urlparse(self.url) + 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+jsonrpc": + open_connection = functools.partial( + asyncio.open_connection, parts.hostname, parts.port + ) + else: + raise ValueError("Unsupported url {}".format(self.url)) + + try: + self._reader, self._writer = await asyncio.wait_for( + open_connection(limit=self.read_size, loop=self._loop), + self.timeout, + loop=self._loop, + ) + self._listen_task = self._loop.create_task(self._listen()) + except Exception: + logger.error("Couldn't open {}".format(self.url)) + raise + + async def _listen(self) -> None: + buf = bytearray() + + while True: + chunk = await self._reader.read(self.READ_SIZE) + if not len(chunk): # EOF, reconnect + logger.info("EOF, reconnecting...") + await self._reconnect() + return + + buf += chunk + try: + json.loads(buf.decode(self.encoding, "ignore")) + except Exception: + continue + response = json.loads(buf.decode(self.encoding, "surrogateescape")) + buf = bytearray() + + batch = response if isinstance(response, list) else [response] + for response in batch: + id = response["id"] + + error = response.get("error") + if error is not None: + 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 + + logger.info("Response {}: {}".format(id, response["result"])) + self._handle_response(id, response["result"]) + + def batch(self) -> "_AsyncJSONRPCBatch": + return _AsyncJSONRPCBatch(self) + + +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 = AsyncJSONRPCLightsClient(url, loop=loop) + await c.connect() + return c + + +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/python/lightsc/lightsc/exceptions.py @@ -0,0 +1,28 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + + +class LightsError(Exception): + pass + + +class LightsClientError(LightsError): + pass + + +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 <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + +from typing import ( + Any, + List, + TypeVar, +) + + +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) + + +class GetLightState(Request): + + def __init__(self, targets: List[str]) -> None: + Request.__init__(self, targets) + + +class PowerOff(Request): + + 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 PowerToggle(Request): + + 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 <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + +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 LightsState(Response): + + 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 <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + +from typing import ( + List, +) + + +class Struct: + pass + + +class LightBulb(Struct): + + 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/python/lightsc/lightsc/view.py @@ -0,0 +1,32 @@ +# Copyright (c) 2016, Louis Opter <louis@opter.org> +# +# 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 <http://www.gnu.org/licenses/>. + +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 <louis@opter.org> +# +# This file is part of lighstd. +# +# lighstd 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. +# +# lighstd 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 lighstd. If not, see <http://www.gnu.org/licenses/>. + +import setuptools + +version = "0.0.1.dev0" + +with open("README.rst", "r") as fp: + long_description = fp.read() + +setuptools.setup( + name="lightsc", + version=version, + 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": [], + }, + install_requires=[], + tests_require=[ + "doubles~=1.1.3", + "freezegun~=0.3.5", + "pytest~=3.0", + ], + extras_require={ + "dev": [ + "flake8", + "mypy-lang", + "ipython", + "pdbpp", + "pep8", + "typed-ast", + ], + }, +)