PK|L 7##yeelight/__init__.py# flake8: noqa """A Python library for controlling YeeLight RGB bulbs.""" from yeelight.main import Bulb, BulbType, BulbException, discover_bulbs from yeelight.flow import Flow, HSVTransition, RGBTransition, TemperatureTransition, SleepTransition from yeelight.version import __version__ PK(L#SKg=g=yeelight/decorator.py# ######################### LICENSE ############################ # # flake8: noqa # Copyright (c) 2005-2016, Michele Simionato # All rights reserved. # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # Redistributions in bytecode form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. """ Decorator module, see http://pypi.python.org/pypi/decorator for the documentation. """ from __future__ import print_function import collections import inspect import itertools import operator import re import sys __version__ = "4.0.10" if sys.version >= "3": from inspect import getfullargspec def get_init(cls): return cls.__init__ else: class getfullargspec(object): "A quick and dirty replacement for getfullargspec for Python 2.X" def __init__(self, f): self.args, self.varargs, self.varkw, self.defaults = inspect.getargspec(f) self.kwonlyargs = [] self.kwonlydefaults = None def __iter__(self): yield self.args yield self.varargs yield self.varkw yield self.defaults getargspec = inspect.getargspec def get_init(cls): return cls.__init__.__func__ # getargspec has been deprecated in Python 3.5 ArgSpec = collections.namedtuple("ArgSpec", "args varargs varkw defaults") def getargspec(f): """A replacement for inspect.getargspec""" spec = getfullargspec(f) return ArgSpec(spec.args, spec.varargs, spec.varkw, spec.defaults) DEF = re.compile("\s*def\s*([_\w][_\w\d]*)\s*\(") # basic functionality class FunctionMaker(object): """ An object with the ability to create functions with a given signature. It has attributes name, doc, module, signature, defaults, dict and methods update and make. """ # Atomic get-and-increment provided by the GIL _compile_count = itertools.count() def __init__(self, func=None, name=None, signature=None, defaults=None, doc=None, module=None, funcdict=None): self.shortsignature = signature if func: # func can be a class or a callable, but not an instance method self.name = func.__name__ if self.name == "": # small hack for lambda functions self.name = "_lambda_" self.doc = func.__doc__ self.module = func.__module__ if inspect.isfunction(func): argspec = getfullargspec(func) self.annotations = getattr(func, "__annotations__", {}) for a in ("args", "varargs", "varkw", "defaults", "kwonlyargs", "kwonlydefaults"): setattr(self, a, getattr(argspec, a)) for i, arg in enumerate(self.args): setattr(self, "arg%d" % i, arg) if sys.version < "3": # easy way self.shortsignature = self.signature = inspect.formatargspec(formatvalue=lambda val: "", *argspec)[ 1:-1 ] else: # Python 3 way allargs = list(self.args) allshortargs = list(self.args) if self.varargs: allargs.append("*" + self.varargs) allshortargs.append("*" + self.varargs) elif self.kwonlyargs: allargs.append("*") # single star syntax for a in self.kwonlyargs: allargs.append("%s=None" % a) allshortargs.append("%s=%s" % (a, a)) if self.varkw: allargs.append("**" + self.varkw) allshortargs.append("**" + self.varkw) self.signature = ", ".join(allargs) self.shortsignature = ", ".join(allshortargs) self.dict = func.__dict__.copy() # func=None happens when decorating a caller if name: self.name = name if signature is not None: self.signature = signature if defaults: self.defaults = defaults if doc: self.doc = doc if module: self.module = module if funcdict: self.dict = funcdict # check existence required attributes assert hasattr(self, "name") if not hasattr(self, "signature"): raise TypeError("You are decorating a non function: %s" % func) def update(self, func, **kw): "Update the signature of func with the data in self" func.__name__ = self.name func.__doc__ = getattr(self, "doc", None) func.__dict__ = getattr(self, "dict", {}) func.__defaults__ = getattr(self, "defaults", ()) func.__kwdefaults__ = getattr(self, "kwonlydefaults", None) func.__annotations__ = getattr(self, "annotations", None) try: frame = sys._getframe(3) except AttributeError: # for IronPython and similar implementations callermodule = "?" else: callermodule = frame.f_globals.get("__name__", "?") func.__module__ = getattr(self, "module", callermodule) func.__dict__.update(kw) def make(self, src_templ, evaldict=None, addsource=False, **attrs): "Make a new function from a given template and update the signature" src = src_templ % vars(self) # expand name and signature evaldict = evaldict or {} mo = DEF.match(src) if mo is None: raise SyntaxError("not a valid function template\n%s" % src) name = mo.group(1) # extract the function name names = set([name] + [arg.strip(" *") for arg in self.shortsignature.split(",")]) for n in names: if n in ("_func_", "_call_"): raise NameError("%s is overridden in\n%s" % (n, src)) if not src.endswith("\n"): # add a newline for old Pythons src += "\n" # Ensure each generated function has a unique filename for profilers # (such as cProfile) that depend on the tuple of (, # , ) being unique. filename = "" % (next(self._compile_count),) try: code = compile(src, filename, "single") exec(code, evaldict) except: print("Error in generated code:", file=sys.stderr) print(src, file=sys.stderr) raise func = evaldict[name] if addsource: attrs["__source__"] = src self.update(func, **attrs) return func @classmethod def create(cls, obj, body, evaldict, defaults=None, doc=None, module=None, addsource=True, **attrs): """ Create a function from the strings name, signature and body. evaldict is the evaluation dictionary. If addsource is true an attribute __source__ is added to the result. The attributes attrs are added, if any. """ if isinstance(obj, str): # "name(signature)" name, rest = obj.strip().split("(", 1) signature = rest[:-1] # strip a right parens func = None else: # a function name = None signature = None func = obj self = cls(func, name, signature, defaults, doc, module) ibody = "\n".join(" " + line for line in body.splitlines()) return self.make("def %(name)s(%(signature)s):\n" + ibody, evaldict, addsource, **attrs) def decorate(func, caller): """ decorate(func, caller) decorates a function using a caller. """ evaldict = dict(_call_=caller, _func_=func) fun = FunctionMaker.create(func, "return _call_(_func_, %(shortsignature)s)", evaldict, __wrapped__=func) if hasattr(func, "__qualname__"): fun.__qualname__ = func.__qualname__ return fun def decorator(caller, _func=None): """decorator(caller) converts a caller function into a decorator""" if _func is not None: # return a decorated function # this is obsolete behavior; you should use decorate instead return decorate(_func, caller) # else return a decorator function if inspect.isclass(caller): name = caller.__name__.lower() doc = "decorator(%s) converts functions/generators into " "factories of %s objects" % ( caller.__name__, caller.__name__, ) elif inspect.isfunction(caller): if caller.__name__ == "": name = "_lambda_" else: name = caller.__name__ doc = caller.__doc__ else: # assume caller is an object with a __call__ method name = caller.__class__.__name__.lower() doc = caller.__call__.__doc__ evaldict = dict(_call_=caller, _decorate_=decorate) return FunctionMaker.create( "%s(func)" % name, "return _decorate_(func, _call_)", evaldict, doc=doc, module=caller.__module__, __wrapped__=caller, ) # ####################### contextmanager ####################### # try: # Python >= 3.2 from contextlib import _GeneratorContextManager except ImportError: # Python >= 2.5 from contextlib import GeneratorContextManager as _GeneratorContextManager class ContextManager(_GeneratorContextManager): def __call__(self, func): """Context manager decorator""" return FunctionMaker.create( func, "with _self_: return _func_(%(shortsignature)s)", dict(_self_=self, _func_=func), __wrapped__=func ) init = getfullargspec(_GeneratorContextManager.__init__) n_args = len(init.args) if n_args == 2 and not init.varargs: # (self, genobj) Python 2.7 def __init__(self, g, *a, **k): return _GeneratorContextManager.__init__(self, g(*a, **k)) ContextManager.__init__ = __init__ elif n_args == 2 and init.varargs: # (self, gen, *a, **k) Python 3.4 pass elif n_args == 4: # (self, gen, args, kwds) Python 3.5 def __init__(self, g, *a, **k): return _GeneratorContextManager.__init__(self, g, a, k) ContextManager.__init__ = __init__ contextmanager = decorator(ContextManager) # ############################ dispatch_on ############################ # def append(a, vancestors): """ Append ``a`` to the list of the virtual ancestors, unless it is already included. """ add = True for j, va in enumerate(vancestors): if issubclass(va, a): add = False break if issubclass(a, va): vancestors[j] = a add = False if add: vancestors.append(a) # inspired from simplegeneric by P.J. Eby and functools.singledispatch def dispatch_on(*dispatch_args): """ Factory of decorators turning a function into a generic function dispatching on the given arguments. """ assert dispatch_args, "No dispatch args passed" dispatch_str = "(%s,)" % ", ".join(dispatch_args) def check(arguments, wrong=operator.ne, msg=""): """Make sure one passes the expected number of arguments""" if wrong(len(arguments), len(dispatch_args)): raise TypeError("Expected %d arguments, got %d%s" % (len(dispatch_args), len(arguments), msg)) def gen_func_dec(func): """Decorator turning a function into a generic function""" # first check the dispatch arguments argset = set(getfullargspec(func).args) if not set(dispatch_args) <= argset: raise NameError("Unknown dispatch arguments %s" % dispatch_str) typemap = {} def vancestors(*types): """ Get a list of sets of virtual ancestors for the given types """ check(types) ras = [[] for _ in range(len(dispatch_args))] for types_ in typemap: for t, type_, ra in zip(types, types_, ras): if issubclass(t, type_) and type_ not in t.__mro__: append(type_, ra) return [set(ra) for ra in ras] def ancestors(*types): """ Get a list of virtual MROs, one for each type """ check(types) lists = [] for t, vas in zip(types, vancestors(*types)): n_vas = len(vas) if n_vas > 1: raise RuntimeError("Ambiguous dispatch for %s: %s" % (t, vas)) elif n_vas == 1: va, = vas mro = type("t", (t, va), {}).__mro__[1:] else: mro = t.__mro__ lists.append(mro[:-1]) # discard t and object return lists def register(*types): """ Decorator to register an implementation for the given types """ check(types) def dec(f): check(getfullargspec(f).args, operator.lt, " in " + f.__name__) typemap[types] = f return f return dec def dispatch_info(*types): """ An utility to introspect the dispatch algorithm """ check(types) lst = [] for anc in itertools.product(*ancestors(*types)): lst.append(tuple(a.__name__ for a in anc)) return lst def _dispatch(dispatch_args, *args, **kw): types = tuple(type(arg) for arg in dispatch_args) try: # fast path f = typemap[types] except KeyError: pass else: return f(*args, **kw) combinations = itertools.product(*ancestors(*types)) next(combinations) # the first one has been already tried for types_ in combinations: f = typemap.get(types_) if f is not None: return f(*args, **kw) # else call the default implementation return func(*args, **kw) return FunctionMaker.create( func, "return _f_(%s, %%(shortsignature)s)" % dispatch_str, dict(_f_=_dispatch), register=register, default=func, typemap=typemap, vancestors=vancestors, ancestors=ancestors, dispatch_info=dispatch_info, __wrapped__=func, ) gen_func_dec.__name__ = "dispatch_on" + dispatch_str return gen_func_dec PKhqLS|J<yeelight/enums.pyfrom enum import Enum, IntEnum class CronType(Enum): """The type of event in cron.""" off = 0 class PowerMode(IntEnum): """Power mode of the light.""" LAST = 0 NORMAL = 1 RGB = 2 HSV = 3 COLOR_FLOW = 4 MOONLIGHT = 5 PK|Lyʁyeelight/flow.pyimport colorsys import logging from enum import Enum from itertools import chain from .utils import _clamp _LOGGER = logging.getLogger(__name__) class Action(Enum): """ The Flow action enumeration. Use this as the ``action`` parameter in a flow, to specify what should happen after the flow ends. """ recover = 0 stay = 1 off = 2 class Flow(object): actions = Action def __init__(self, count=0, action=Action.recover, transitions=None): """ A complete flow, consisting of one or multiple transitions. Example: >>> transitions = [RGBTransition(255, 0, 0), SleepTransition(400)] >>> Flow(3, Flow.actions.recover, transitions) :param int count: The number of times to run this flow (0 to run forever). :param action action: The action to take after the flow stops. Can be ``Flow.actions.recover`` to go back to the state before the flow, ``Flow.actions.stay`` to stay at the last state, and ``Flow.actions.off`` to turn off. :param list transitions: A list of :py:class:`FlowTransition ` instances that describe the flow transitions to perform. """ if transitions is None: transitions = [] self.count = count self.action = action self.transitions = transitions # Note, main depends on us, so we cannot import BulbException here. if len(self.transitions) > 9: _LOGGER.warning( "The bulb seems to support up to 9 transitions. Your %s might fail." % len(self.transitions) ) @property def expression(self): """ Return a YeeLight-compatible expression that implements this flow. :rtype: list """ expr = chain.from_iterable(transition.as_list() for transition in self.transitions) expr = ", ".join(str(value) for value in expr) return expr class FlowTransition(object): """A single transition in the flow.""" def as_list(self): """ Return a YeeLight-compatible expression that implements this transition. :rtype: list """ brightness = min(int(self.brightness), 100) # Duration must be at least 50, otherwise there's an error. return [max(50, self.duration), self._mode, self._value, brightness] class RGBTransition(FlowTransition): def __init__(self, red, green, blue, duration=300, brightness=100): """ An RGB transition. :param int red: The value of red (0-255). :param int green: The value of green (0-255). :param int blue: The value of blue (0-255). :param int duration: The duration of the effect, in milliseconds. The minimum is 50. :param int brightness: The brightness value to transition to (1-100). """ self.red = red self.green = green self.blue = blue # The mode value the YeeLight protocol mandates. self._mode = 1 self.duration = duration self.brightness = brightness @property def _value(self): """The YeeLight-compatible value for this transition.""" red = _clamp(self.red, 0, 255) green = _clamp(self.green, 0, 255) blue = _clamp(self.blue, 0, 255) return red * 65536 + green * 256 + blue def __repr__(self): return "<%s(%s,%s,%s) duration %s, brightness %s>" % ( self.__class__.__name__, self.red, self.green, self.blue, self.duration, self.brightness, ) class HSVTransition(FlowTransition): def __init__(self, hue, saturation, duration=300, brightness=100): """ An HSV transition. :param int hue: The color hue to transition to (0-359). :param int saturation: The color saturation to transition to (0-100). :param int duration: The duration of the effect, in milliseconds. The minimum is 50. :param int brightness: The brightness value to transition to (1-100). """ self.hue = hue self.saturation = saturation # The mode value the YeeLight protocol mandates. self._mode = 1 self.duration = duration self.brightness = brightness @property def _value(self): """The YeeLight-compatible value for this transition.""" hue = _clamp(self.hue, 0, 359) / 359.0 saturation = max(0, min(100, self.saturation)) / 100.0 red, green, blue = [int(round(col * 255)) for col in colorsys.hsv_to_rgb(hue, saturation, 1)] return red * 65536 + green * 256 + blue def __repr__(self): return "<%s(%s,%s) duration %s, brightness %s>" % ( self.__class__.__name__, self.hue, self.saturation, self.duration, self.brightness, ) class TemperatureTransition(FlowTransition): def __init__(self, degrees, duration=300, brightness=100): """ A Color Temperature transition. :param int degrees: The degrees to set the color temperature to (1700-6500). :param int duration: The duration of the effect, in milliseconds. The minimum is 50. :param int brightness: The brightness value to transition to (1-100). """ self.degrees = degrees # The mode value the YeeLight protocol mandates. self._mode = 2 self.duration = duration self.brightness = _clamp(brightness, 1, 100) @property def _value(self): """The YeeLight-compatible value for this transition.""" return max(1700, min(6500, self.degrees)) def __repr__(self): return "<%s(%sK) duration %s, brightness %s>" % ( self.__class__.__name__, self.degrees, self.duration, self.brightness, ) class SleepTransition(FlowTransition): def __init__(self, duration=300): """ A Sleep transition. :param int duration: The duration of the effect, in milliseconds. The minimum is 50. """ # The mode value the YeeLight protocol mandates. self._mode = 7 # Ignored by YeeLight. self._value = 1 self.brightness = 2 self.duration = duration def __repr__(self): return "<%s: duration %s>" % (self.__class__.__name__, self.duration) PK&MxmVVyeelight/main.pyimport colorsys import json import logging import os import socket import struct from enum import Enum from future.utils import raise_from from .decorator import decorator from .enums import PowerMode from .flow import Flow from .utils import _clamp if os.name == "nt": import win32api as fcntl else: import fcntl try: from urllib.parse import urlparse except ImportError: from urlparse import urlparse _LOGGER = logging.getLogger(__name__) _MODEL_SPECS = { "mono": {"color_temp": {"min": 2700, "max": 2700}}, "mono1": {"color_temp": {"min": 2700, "max": 2700}}, "color": {"color_temp": {"min": 1700, "max": 6500}}, "color1": {"color_temp": {"min": 1700, "max": 6500}}, "strip1": {"color_temp": {"min": 1700, "max": 6500}}, "bslamp1": {"color_temp": {"min": 1700, "max": 6500}}, "ceiling1": {"color_temp": {"min": 2700, "max": 6500}}, "ceiling2": {"color_temp": {"min": 2700, "max": 6500}}, "ceiling3": {"color_temp": {"min": 2700, "max": 6000}}, "ceiling4": {"color_temp": {"min": 2700, "max": 6500}}, "color2": {"color_temp": {"min": 2700, "max": 6500}}, } @decorator def _command(f, *args, **kw): """A decorator that wraps a function and enables effects.""" self = args[0] effect = kw.get("effect", self.effect) duration = kw.get("duration", self.duration) power_mode = kw.get("power_mode", self.power_mode) method, params = f(*args, **kw) if method in ["set_ct_abx", "set_rgb", "set_hsv", "set_bright", "set_power", "toggle"]: if self._music_mode: # Mapping calls to their properties. # Used to keep music mode cache up to date. action_property_map = { "set_ct_abx": ["ct"], "set_rgb": ["rgb"], "set_hsv": ["hue", "sat"], "set_bright": ["bright"], "set_power": ["power"], } # Handle toggling separately, as it depends on a previous power state. if method == "toggle": self._last_properties["power"] = "on" if self._last_properties["power"] == "off" else "off" elif method in action_property_map: set_prop = action_property_map[method] update_props = {set_prop[prop]: params[prop] for prop in range(len(set_prop))} _LOGGER.debug("Music mode cache update: %s", update_props) self._last_properties.update(update_props) # Add the effect parameters. params += [effect, duration] # Add power_mode parameter. if method == "set_power" and params[0] == "on" and power_mode.value != PowerMode.LAST: params += [power_mode.value] result = self.send_command(method, params).get("result", []) if result: return result[0] def get_ip_address(ifname): """ Returns the IPv4 address of the requested interface (thanks Martin Konecny, https://stackoverflow.com/a/24196955) :param string interface: The interface to get the IPv4 address of. :returns: The interface's IPv4 address. """ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) return socket.inet_ntoa( fcntl.ioctl(s.fileno(), 0x8915, struct.pack("256s", bytes(ifname[:15], "utf-8")))[20:24] ) # SIOCGIFADDR def discover_bulbs(timeout=2, interface=False): """ Discover all the bulbs in the local network. :param int timeout: How many seconds to wait for replies. Discovery will always take exactly this long to run, as it can't know when all the bulbs have finished responding. :param string interface: The interface that should be used for multicast packets. Note: it *has* to have a valid IPv4 address. IPv6-only interfaces are not supported (at the moment). The default one will be used if this is not specified. :returns: A list of dictionaries, containing the ip, port and capabilities of each of the bulbs in the network. """ msg = "\r\n".join(["M-SEARCH * HTTP/1.1", "HOST: 239.255.255.250:1982", 'MAN: "ssdp:discover"', "ST: wifi_bulb"]) # Set up UDP socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 32) if interface: s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(get_ip_address(interface))) s.settimeout(timeout) s.sendto(msg.encode(), ("239.255.255.250", 1982)) bulbs = [] bulb_ips = set() while True: try: data, addr = s.recvfrom(65507) except socket.timeout: break capabilities = dict([x.strip("\r").split(": ") for x in data.decode().split("\n") if ":" in x]) parsed_url = urlparse(capabilities["Location"]) bulb_ip = (parsed_url.hostname, parsed_url.port) if bulb_ip in bulb_ips: continue capabilities = {key: value for key, value in capabilities.items() if key.islower()} bulbs.append({"ip": bulb_ip[0], "port": bulb_ip[1], "capabilities": capabilities}) bulb_ips.add(bulb_ip) return bulbs class BulbException(Exception): """ A generic yeelight exception. This exception is raised when bulb informs about errors, e.g., when trying to issue unsupported commands to the bulb. """ pass class BulbType(Enum): """ The bulb's type. This is either `White` (for monochrome bulbs), `Color` (for color bulbs), `WhiteTemp` (for white bulbs with configurable color temperature), or `Unknown` if the properties have not been fetched yet. """ Unknown = -1 White = 0 Color = 1 WhiteTemp = 2 class Bulb(object): def __init__( self, ip, port=55443, effect="smooth", duration=300, auto_on=False, power_mode=PowerMode.LAST, model=None ): """ The main controller class of a physical YeeLight bulb. :param str ip: The IP of the bulb. :param int port: The port to connect to on the bulb. :param str effect: The type of effect. Can be "smooth" or "sudden". :param int duration: The duration of the effect, in milliseconds. The minimum is 30. This is ignored for sudden effects. :param bool auto_on: Whether to call :py:meth:`ensure_on() ` to turn the bulb on automatically before each operation, if it is off. This renews the properties of the bulb before each message, costing you one extra message per command. Turn this off and do your own checking with :py:meth:`get_properties() ` or run :py:meth:`ensure_on() ` yourself if you're worried about rate-limiting. :param yeelight.enums.PowerMode power_mode: The mode for the light set when powering on. :param str model: The model name of the yeelight (e.g. "color", "mono", etc). The setting is used to enable model specific features (e.g. a particular color temperature range). """ self._ip = ip self._port = port self.effect = effect self.duration = duration self.auto_on = auto_on self.power_mode = power_mode self.model = model self.__cmd_id = 0 # The last command id we used. self._last_properties = {} # The last set of properties we've seen. self._music_mode = False # Whether we're currently in music mode. self.__socket = None # The socket we use to communicate. @property def _cmd_id(self): """ Return the next command ID and increment the counter. :rtype: int """ self.__cmd_id += 1 return self.__cmd_id - 1 @property def _socket(self): """Return, optionally creating, the communication socket.""" if self.__socket is None: self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.__socket.settimeout(5) self.__socket.connect((self._ip, self._port)) return self.__socket def ensure_on(self): """Turn the bulb on if it is off.""" if self._music_mode is True or self.auto_on is False: return self.get_properties() if self._last_properties["power"] != "on": self.turn_on() @property def last_properties(self): """ The last properties we've seen the bulb have. This might potentially be out of date, as there's no background listener for the bulb's notifications. To update it, call :py:meth:`get_properties `. """ return self._last_properties @property def bulb_type(self): """ The type of bulb we're communicating with. Returns a :py:class:`BulbType ` describing the bulb type. When trying to access before properties are known, the bulb type is unknown. :rtype: yeelight.BulbType :return: The bulb's type. """ if not self._last_properties or any(name not in self.last_properties for name in ["ct", "rgb"]): return BulbType.Unknown if self.last_properties["rgb"] is None and self.last_properties["ct"]: return BulbType.WhiteTemp if all( name in self.last_properties and self.last_properties[name] is None for name in ["ct", "rgb", "hue", "sat"] ): return BulbType.White else: return BulbType.Color @property def music_mode(self): """ Return whether the music mode is active. :rtype: bool :return: True if music mode is on, False otherwise. """ return self._music_mode def get_properties( self, requested_properties=[ "power", "bright", "ct", "rgb", "hue", "sat", "color_mode", "flowing", "delayoff", "music_on", "name", ], ): """ Retrieve and return the properties of the bulb. This method also updates ``last_properties`` when it is called. :param list requested_properties: The list of properties to request from the bulb. By default, this does not include ``flow_params``. :returns: A dictionary of param: value items. :rtype: dict """ # When we are in music mode, the bulb does not respond to queries # therefore we need to keep the state up-to-date ourselves if self._music_mode: return self._last_properties response = self.send_command("get_prop", requested_properties) properties = response["result"] properties = [x if x else None for x in properties] self._last_properties = dict(zip(requested_properties, properties)) return self._last_properties def send_command(self, method, params=None): """ Send a command to the bulb. :param str method: The name of the method to send. :param list params: The list of parameters for the method. :raises BulbException: When the bulb indicates an error condition. :returns: The response from the bulb. """ command = {"id": self._cmd_id, "method": method, "params": params} _LOGGER.debug("%s > %s", self, command) try: self._socket.send((json.dumps(command) + "\r\n").encode("utf8")) except socket.error as ex: # Some error occurred, remove this socket in hopes that we can later # create a new one. self.__socket.close() self.__socket = None raise_from(BulbException("A socket error occurred when sending the command."), ex) if self._music_mode: # We're in music mode, nothing else will happen. return {"result": ["ok"]} # The bulb will send us updates on its state in addition to responses, # so we want to make sure that we read until we see an actual response. response = None while response is None: try: data = self._socket.recv(16 * 1024) except socket.error: # An error occured, let's close and abort... self.__socket.close() self.__socket = None response = {"error": "Bulb closed the connection."} break for line in data.split(b"\r\n"): if not line: continue try: line = json.loads(line.decode("utf8")) _LOGGER.debug("%s < %s", self, line) except ValueError: line = {"result": ["invalid command"]} if line.get("method") != "props": # This is probably the response we want. response = line else: self._last_properties.update(line["params"]) if "error" in response: raise BulbException(response["error"]) return response @_command def set_color_temp(self, degrees, **kwargs): """ Set the bulb's color temperature. :param int degrees: The degrees to set the color temperature to (1700-6500). """ self.ensure_on() degrees = _clamp(degrees, 1700, 6500) return "set_ct_abx", [degrees] @_command def set_rgb(self, red, green, blue, **kwargs): """ Set the bulb's RGB value. :param int red: The red value to set (0-255). :param int green: The green value to set (0-255). :param int blue: The blue value to set (0-255). """ self.ensure_on() red = _clamp(red, 0, 255) green = _clamp(green, 0, 255) blue = _clamp(blue, 0, 255) return "set_rgb", [red * 65536 + green * 256 + blue] @_command def set_adjust(self, action, prop, **kwargs): """ Adjust a parameter. I don't know what this is good for. I don't know how to use it, or why. I'm just including it here for completeness, and because it was easy, but it won't get any particular love. :param str action: The direction of adjustment. Can be "increase", "decrease" or "circle". :param str prop: The property to adjust. Can be "bright" for brightness, "ct" for color temperature and "color" for color. The only action for "color" can be "circle". Why? Who knows. """ return "set_adjust", [action, prop] @_command def set_hsv(self, hue, saturation, value=None, **kwargs): """ Set the bulb's HSV value. :param int hue: The hue to set (0-359). :param int saturation: The saturation to set (0-100). :param int value: The value to set (0-100). If omitted, the bulb's brightness will remain the same as before the change. """ self.ensure_on() # We fake this using flow so we can add the `value` parameter. hue = _clamp(hue, 0, 359) saturation = _clamp(saturation, 0, 100) if value is None: # If no value was passed, use ``set_hsv`` to preserve luminance. return "set_hsv", [hue, saturation] else: # Otherwise, use flow. value = _clamp(value, 0, 100) if kwargs.get("effect", self.effect) == "sudden": duration = 50 else: duration = kwargs.get("duration", self.duration) hue = _clamp(hue, 0, 359) / 359.0 saturation = _clamp(saturation, 0, 100) / 100.0 red, green, blue = [int(round(col * 255)) for col in colorsys.hsv_to_rgb(hue, saturation, 1)] rgb = red * 65536 + green * 256 + blue return "start_cf", [1, 1, "%s, 1, %s, %s" % (duration, rgb, value)] @_command def set_brightness(self, brightness, **kwargs): """ Set the bulb's brightness. :param int brightness: The brightness value to set (1-100). """ self.ensure_on() brightness = _clamp(brightness, 1, 100) return "set_bright", [brightness] @_command def turn_on(self, **kwargs): """Turn the bulb on.""" return "set_power", ["on"] @_command def turn_off(self, **kwargs): """Turn the bulb off.""" return "set_power", ["off"] @_command def toggle(self, **kwargs): """Toggle the bulb on or off.""" return "toggle", [] @_command def set_default(self): """Set the bulb's current state as default.""" return "set_default", [] @_command def set_name(self, name): """ Set the bulb's name. :param str name: The string you want to set as the bulb's name. """ return "set_name", [name] @_command def start_flow(self, flow): """ Start a flow. :param yeelight.Flow flow: The Flow instance to start. """ if not isinstance(flow, Flow): raise ValueError("Argument is not a Flow instance.") self.ensure_on() return ("start_cf", [flow.count * len(flow.transitions), flow.action.value, flow.expression]) @_command def stop_flow(self): """Stop a flow.""" return "stop_cf", [] def start_music(self, port=0): """ Start music mode. Music mode essentially upgrades the existing connection to a reverse one (the bulb connects to the library), removing all limits and allowing you to send commands without being rate-limited. Starting music mode will start a new listening socket, tell the bulb to connect to that, and then close the old connection. If the bulb cannot connect to the host machine for any reason, bad things will happen (such as library freezes). :param int port: The port to listen on. If none is specified, a random port will be chosen. """ if self._music_mode: raise AssertionError("Already in music mode, please stop music mode first.") # Force populating the cache in case we are being called directly # without ever fetching properties beforehand self.get_properties() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Reuse sockets so we don't hit "address already in use" errors. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(("", port)) host, port = s.getsockname() s.listen(3) local_ip = self._socket.getsockname()[0] self.send_command("set_music", [1, local_ip, port]) s.settimeout(5) conn, _ = s.accept() s.close() # Close the listening socket. self.__socket.close() self.__socket = conn self._music_mode = True return "ok" @_command def stop_music(self): """ Stop music mode. Stopping music mode will close the previous connection. Calling ``stop_music`` more than once, or while not in music mode, is safe. """ if self.__socket: self.__socket.close() self.__socket = None self._music_mode = False return "set_music", [0] @_command def cron_add(self, event_type, value): """ Add an event to cron. Example:: >>> bulb.cron_add(CronType.off, 10) :param yeelight.enums.CronType event_type: The type of event. Currently, only ``CronType.off``. """ return "cron_add", [event_type.value, value] @_command def cron_get(self, event_type): """ Retrieve an event from cron. :param yeelight.enums.CronType event_type: The type of event. Currently, only ``CronType.off``. """ return "cron_get", [event_type.value] @_command def cron_del(self, event_type): """ Remove an event from cron. :param yeelight.enums.CronType event_type: The type of event. Currently, only ``CronType.off``. """ return "cron_del", [event_type.value] def __repr__(self): return "Bulb<{ip}:{port}, type={type}>".format(ip=self._ip, port=self._port, type=self.bulb_type) def set_power_mode(self, mode): """ Set the light power mode. If the light is off it will be turned on. :param yeelight.enums.PowerMode mode: The mode to swith to. """ return self.turn_on(power_mode=mode) def get_model_specs(self, **kwargs): """ Return the specifications (e.g. color temperature min/max) of the bulb. """ if self.model is not None and self.model in _MODEL_SPECS: return _MODEL_SPECS[self.model] _LOGGER.debug("Model unknown (%s). Providing a fallback", self.model) if self.bulb_type is BulbType.White: return _MODEL_SPECS["mono"] if self.bulb_type is BulbType.WhiteTemp: return _MODEL_SPECS["ceiling1"] # BulbType.Color and BulbType.Unknown return _MODEL_SPECS["color"] PKhqL҃WWyeelight/metadata.py# -*- coding: utf-8 -*- """Project metadata. Information describing the project. """ from .version import __version__ # The package name, which is also the "UNIX name" for the project. package = "yeelight" project = "python-yeelight" project_no_spaces = project.replace(" ", "") version = __version__ description = "yeelight is a Python library for controlling YeeLight WiFi RGB" " LED bulbs." authors = ["Stavros Korokithakis"] authors_string = ", ".join(authors) emails = ["hi@stavros.io"] license = "BSD" copyright = "2016 " + authors_string url = "https://gitlab.com/stavros/python-yeelight" PK|Li܅yeelight/tests.pyimport json import os import sys import unittest from yeelight import Bulb # noqa from yeelight import enums sys.path.insert(0, os.path.abspath(__file__ + "/../..")) class SocketMock(object): def __init__(self, received=b'{"id": 0, "result": ["ok"]}'): self.received = received def send(self, data): self.sent = json.loads(data.decode("utf8")) def recv(self, length): return self.received class Tests(unittest.TestCase): def setUp(self): self.socket = SocketMock() self.bulb = Bulb(ip="", auto_on=True) self.bulb._Bulb__socket = self.socket def test_rgb1(self): self.bulb.set_rgb(255, 255, 0) self.assertEqual(self.socket.sent["method"], "set_rgb") self.assertEqual(self.socket.sent["params"], [16776960, "smooth", 300]) def test_rgb2(self): self.bulb.effect = "sudden" self.bulb.set_rgb(255, 255, 0) self.assertEqual(self.socket.sent["method"], "set_rgb") self.assertEqual(self.socket.sent["params"], [16776960, "sudden", 300]) def test_rgb3(self): self.bulb.set_rgb(255, 255, 0, effect="sudden") self.assertEqual(self.socket.sent["method"], "set_rgb") self.assertEqual(self.socket.sent["params"], [16776960, "sudden", 300]) def test_hsv1(self): self.bulb.set_hsv(200, 100, effect="sudden") self.assertEqual(self.socket.sent["method"], "set_hsv") self.assertEqual(self.socket.sent["params"], [200, 100, "sudden", 300]) def test_hsv2(self): self.bulb.set_hsv(200, 100, 10, effect="sudden", duration=500) self.assertEqual(self.socket.sent["method"], "start_cf") self.assertEqual(self.socket.sent["params"], [1, 1, "50, 1, 43263, 10"]) def test_hsv3(self): self.bulb.set_hsv(200, 100, 10, effect="smooth", duration=1000) self.assertEqual(self.socket.sent["method"], "start_cf") self.assertEqual(self.socket.sent["params"], [1, 1, "1000, 1, 43263, 10"]) def test_hsv4(self): self.bulb.effect = "sudden" self.bulb.set_hsv(200, 100, 10, effect="smooth", duration=1000) self.assertEqual(self.socket.sent["method"], "start_cf") self.assertEqual(self.socket.sent["params"], [1, 1, "1000, 1, 43263, 10"]) def test_toggle1(self): self.bulb.toggle() self.assertEqual(self.socket.sent["method"], "toggle") self.assertEqual(self.socket.sent["params"], ["smooth", 300]) self.bulb.toggle(duration=3000) self.assertEqual(self.socket.sent["params"], ["smooth", 3000]) def test_turn_off1(self): self.bulb.turn_off() self.assertEqual(self.socket.sent["method"], "set_power") self.assertEqual(self.socket.sent["params"], ["off", "smooth", 300]) self.bulb.turn_off(duration=3000) self.assertEqual(self.socket.sent["params"], ["off", "smooth", 3000]) def test_turn_on1(self): self.bulb.turn_on() self.assertEqual(self.socket.sent["method"], "set_power") self.assertEqual(self.socket.sent["params"], ["on", "smooth", 300, enums.PowerMode.LAST.value]) self.bulb.turn_on(duration=3000) self.assertEqual(self.socket.sent["params"], ["on", "smooth", 3000, enums.PowerMode.LAST.value]) def test_turn_on2(self): self.bulb.effect = "sudden" self.bulb.turn_on() self.assertEqual(self.socket.sent["method"], "set_power") self.assertEqual(self.socket.sent["params"], ["on", "sudden", 300, enums.PowerMode.LAST.value]) def test_turn_on3(self): self.bulb.turn_on(effect="sudden", duration=50) self.assertEqual(self.socket.sent["method"], "set_power") self.assertEqual(self.socket.sent["params"], ["on", "sudden", 50, enums.PowerMode.LAST.value]) def test_turn_on4(self): self.bulb.power_mode = enums.PowerMode.MOONLIGHT self.bulb.turn_on() self.assertEqual(self.socket.sent["method"], "set_power") self.assertEqual(self.socket.sent["params"], ["on", "smooth", 300, enums.PowerMode.MOONLIGHT.value]) def test_turn_on5(self): self.bulb.turn_on(power_mode=enums.PowerMode.MOONLIGHT) self.assertEqual(self.socket.sent["method"], "set_power") self.assertEqual(self.socket.sent["params"], ["on", "smooth", 300, enums.PowerMode.MOONLIGHT.value]) def test_set_power_mode1(self): self.bulb.set_power_mode(enums.PowerMode.MOONLIGHT) self.assertEqual(self.socket.sent["method"], "set_power") self.assertEqual(self.socket.sent["params"], ["on", "smooth", 300, enums.PowerMode.MOONLIGHT.value]) def test_set_power_mode2(self): self.bulb.set_power_mode(enums.PowerMode.NORMAL) self.assertEqual(self.socket.sent["method"], "set_power") self.assertEqual(self.socket.sent["params"], ["on", "smooth", 300, enums.PowerMode.NORMAL.value]) def test_set_power_mode3(self): self.bulb.set_power_mode(enums.PowerMode.LAST) self.assertEqual(self.socket.sent["method"], "set_power") self.assertEqual(self.socket.sent["params"], ["on", "smooth", 300, enums.PowerMode.LAST.value]) def test_color_temp1(self): self.bulb.set_color_temp(1400) self.assertEqual(self.socket.sent["method"], "set_ct_abx") self.assertEqual(self.socket.sent["params"], [1700, "smooth", 300]) self.bulb.set_color_temp(1400, duration=3000) self.assertEqual(self.socket.sent["params"], [1700, "smooth", 3000]) def test_color_temp2(self): self.bulb.set_color_temp(8400, effect="sudden") self.assertEqual(self.socket.sent["method"], "set_ct_abx") self.assertEqual(self.socket.sent["params"], [6500, "sudden", 300]) if __name__ == "__main__": unittest.main() PKu'LYq~~yeelight/transitions.py"""Pre-made transitions, for your strobing pleasure.""" import random from .flow import HSVTransition, RGBTransition, SleepTransition, TemperatureTransition from .utils import _clamp def disco(bpm=120): """ Color changes to the beat. :param int bpm: The beats per minute to pulse to. :returns: A list of transitions. :rtype: list """ duration = int(60000 / bpm) transitions = [ HSVTransition(0, 100, duration=duration, brightness=100), HSVTransition(0, 100, duration=duration, brightness=1), HSVTransition(90, 100, duration=duration, brightness=100), HSVTransition(90, 100, duration=duration, brightness=1), HSVTransition(180, 100, duration=duration, brightness=100), HSVTransition(180, 100, duration=duration, brightness=1), HSVTransition(270, 100, duration=duration, brightness=100), HSVTransition(270, 100, duration=duration, brightness=1), ] return transitions def temp(): """ Slowly-changing color temperature. :returns: A list of transitions. :rtype: list """ transitions = [TemperatureTransition(1700, duration=40000), TemperatureTransition(6500, duration=40000)] return transitions def strobe(): """ Rapid flashing on and off. :returns: A list of transitions. :rtype: list """ transitions = [HSVTransition(0, 0, duration=50, brightness=100), HSVTransition(0, 0, duration=50, brightness=1)] return transitions def pulse(red, green, blue, duration=250, brightness=100): """ Pulse a single color once (mainly to be used for notifications). :param int red: The red color component to pulse (0-255). :param int green: The green color component to pulse (0-255). :param int blue: The blue color component to pulse (0-255). :param int duration: The duration to pulse for, in milliseconds. :param int brightness: The brightness to pulse at (1-100). :returns: A list of transitions. :rtype: list """ red = _clamp(red, 0, 255) green = _clamp(green, 0, 255) blue = _clamp(blue, 0, 255) transitions = [ RGBTransition(red, green, blue, duration=duration, brightness=brightness), RGBTransition(red, green, blue, duration=duration, brightness=1), ] return transitions def strobe_color(brightness=100): """ Rapid flashing colors. :param int brightness: The brightness of the transition. :returns: A list of transitions. :rtype: list """ transitions = [ HSVTransition(240, 100, duration=50, brightness=brightness), HSVTransition(60, 100, duration=50, brightness=brightness), HSVTransition(330, 100, duration=50, brightness=brightness), HSVTransition(0, 100, duration=50, brightness=brightness), HSVTransition(173, 100, duration=50, brightness=brightness), HSVTransition(30, 100, duration=50, brightness=brightness), ] return transitions def alarm(duration=250): """ Red alarm; flashing bright red to dark red. :param int duration: The duration between hi/lo brightness,in milliseconds. :returns: A list of transitions. :rtype: list """ transitions = [ HSVTransition(0, 100, duration=duration, brightness=100), HSVTransition(0, 100, duration=duration, brightness=60), ] return transitions def police(duration=300, brightness=100): """ Color changes from red to blue, like police lights. :param int duration: The duration between red and blue, in milliseconds. :param int brightness: The brightness of the transition. :returns: A list of transitions. :rtype: list """ transitions = [ RGBTransition(255, 0, 0, duration=duration, brightness=brightness), RGBTransition(0, 0, 255, duration=duration, brightness=brightness), ] return transitions def police2(duration=250, brightness=100): """ Color flashes red and then blue, like urgent police lights. :param int duration: The duration to fade to next color, in milliseconds. :param int brightness: The brightness of the transition. :returns: A list of transitions. :rtype: list """ transitions = [ RGBTransition(255, 0, 0, brightness=brightness, duration=duration), RGBTransition(255, 0, 0, brightness=1, duration=duration), RGBTransition(255, 0, 0, brightness=brightness, duration=duration), SleepTransition(duration=duration), RGBTransition(0, 0, 255, brightness=brightness, duration=duration), RGBTransition(0, 0, 255, brightness=1, duration=duration), RGBTransition(0, 0, 255, brightness=brightness, duration=duration), SleepTransition(duration=duration), ] return transitions def lsd(duration=3000, brightness=100): """ Gradual changes to a pleasing, trippy palette. :param int brightness: The brightness of the transition. :returns: A list of transitions. :rtype: list """ hs_values = [(3, 85), (20, 90), (55, 95), (93, 50), (198, 97)] return [HSVTransition(hue, saturation, duration=duration, brightness=brightness) for hue, saturation in hs_values] def christmas(duration=250, brightness=100, sleep=3000): """ Color changes from red to green, like christmas lights. :param int duration: The duration between red and green, in milliseconds. :param int brightness: The brightness of the transition. :param int sleep: The time to sleep between colors, in milliseconds. :returns: A list of transitions. :rtype: list """ transitions = [ HSVTransition(0, 100, duration=duration, brightness=brightness), SleepTransition(duration=sleep), HSVTransition(120, 100, duration=duration, brightness=brightness), SleepTransition(duration=sleep), ] return transitions def rgb(duration=250, brightness=100, sleep=3000): """ Color changes from red to green to blue. :param int duration: The duration to fade to next color, in milliseconds. :param int brightness: The brightness of the transition. :param int sleep: The time to sleep between colors, in milliseconds :returns: A list of transitions. :rtype: list """ transitions = [ HSVTransition(0, 100, duration=duration, brightness=brightness), SleepTransition(duration=sleep), HSVTransition(120, 100, duration=duration, brightness=brightness), SleepTransition(duration=sleep), HSVTransition(240, 100, duration=duration, brightness=brightness), SleepTransition(duration=sleep), ] return transitions def randomloop(duration=750, brightness=100, count=9): """ Color changes between `count` randomly chosen colors. :param int duration: The duration to fade to next color, in milliseconds. :param int brightness: The brightness of the transition. :param int count: The number of random chosen colors in transition. :returns: A list of transitions. :rtype: list """ count = _clamp(count, 1, 9) transitions = [HSVTransition(random.randint(0, 360), 100, duration=duration) for _ in range(count)] return transitions def slowdown(duration=2000, brightness=100, count=8): """ Changes between `count` random chosen colors with increasing transition time. :param int duration: The duration to fade to next color, in milliseconds. :param int brightness: The brightness of the transition. :param int count: The number of random chosen colors in transition. :returns: A list of transitions. :rtype: list """ count = _clamp(count, 1, 8) transitions = [HSVTransition(random.randint(0, 360), 100, duration=(duration * x)) for x in range(1, count + 1)] return transitions PK{J.yeelight/utils.pydef _clamp(value, minx, maxx): """ Constrain a value between a minimum and a maximum. If the value is larger than the maximum or lower than the minimum, the maximum or minimum will be returned instead. :param int value: The value to clamp. :param int minx: The minimum the value can take. :param int maxx: The maximum the value can take. """ return max(minx, min(maxx, value)) PKn&M%&&yeelight/version.py# flake8: noqa __version__ = "0.4.3" PK=JR yeelight-0.4.3.dist-info/LICENSECopyright (c) 2016, Stavros Korokithakis All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. PK!HJVSayeelight-0.4.3.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,rzd&Y)r$[)T&UD"PK!Hj !yeelight-0.4.3.dist-info/METADATAVr6}Wl<\Tqz8iODHD l}wy%Lz=ݳxQ*e'ÓahtQF{>K˦_/c,=3!w6zgx'0m4WaR˂(c8 »խdE\ha>^xB7dpnc): VR:FڻMrHJ^W1Z:mnxedzN菆]q]{@'wU18hl߅kS$qGΈ;#xry@R筪4 ?oj nǐǀO-[&6Z5r/~Jpx{ {w[%!zYFjcBղ{JJ}u}VJ|TI,¯) 58\6jk])芫}rϦRJ>6ڨ4, o/0* :'9]i(5}<:[dgiwf|сWcN #\tn65́nz{:h#i˩n %]?6Adp; !P \M$1&50MO)E0dTqf1-l*J%etMc!c a8CQ@9F(`l =ʰpf0@ɑq78@vBJiy qzrˉ5Ƨ 1  @`iRt}f+puN*#ud]|; gk3@[JDW0̑b1Ln{9c8Q=tR~6j)-M'-'zG"}>;2IikgX72> Z2 U5Gp3ڔeu4nd#e*zi85zD _(lz( ¶8p)9#.,;g:lׄ>4轢oyb١@.4hC<:H>z}p5i%k!^̳g:-jqJKv.W4) FG9R%2&vtyn}&rsUˈ&fs`S(oDa۷ͦRtF)WȞR1-w%ZҰ`=$2& `"}Et*o!%uRԗ-Z JAc9w~IJ PK!HX}ێyeelight-0.4.3.dist-info/RECORDur@}h] †j&iA lR |u?]+$E$I>=-`?Ls d.F;>qʋ 9L Fδ => y. FyqËvlrzNV}7"{\kd֛6ٞqM^zC彛l; w=~~*Yf@C وDC D4աC&n^2i/G!ZUK 0.4Iؖ(Һ'cZ9OdS-E4s&*$7Sm)?PK|L 7##yeelight/__init__.pyPK(L#SKg=g=Uyeelight/decorator.pyPKhqLS|J<>yeelight/enums.pyPK|Lyʁ @yeelight/flow.pyPK&MxmVVZyeelight/main.pyPKhqL҃WWرyeelight/metadata.pyPK|Li܅ayeelight/tests.pyPKu'LYq~~2yeelight/transitions.pyPK{J.yeelight/utils.pyPKn&M%&&yeelight/version.pyPK=JR yeelight-0.4.3.dist-info/LICENSEPK!HJVSa8yeelight-0.4.3.dist-info/WHEELPK!Hj !yeelight-0.4.3.dist-info/METADATAPK!HX}ێyeelight-0.4.3.dist-info/RECORDPK