async def transform(collector, target, reference, **kwargs): """ Do a http-api like transformation over whatever target you specify ``target:transform d073d5000000 -- '{"color": "red", "effect": "pulse"}'`` It takes in ``color``, ``effect``, ``power`` and valid options for a ``SetWaveformOptional``. You may also specify ``transform_options`` that change how the transform works. keep_brightness Ignore brightness options in the request transition_color If the light is off and we power on, setting this to True will mean the color of the light is not set to the new color before we make it appear to be on. This defaults to False, which means it will appear to turn on with the new color """ extra = collector.photons_app.extra_as_json extra = sb.dictionary_spec().normalise(Meta.empty(), extra) transform_options = sb.set_options( transform_options=sb.dictionary_spec()).normalise( Meta.empty(), extra)["transform_options"] msg = Transformer.using(extra, **transform_options) if not msg: raise PhotonsAppError( 'Please specify valid options after --. For example ``transform -- \'{"power": "on", "color": "red"}\'``' ) await target.send(msg, reference)
async def set_tile_positions(collector, target, reference, **kwargs): """ Set the positions of the tiles in your chain. ``lan:set_tile_positions d073d5f09124 -- '[[0, 0], [-1, 0], [-1, 1]]'`` """ extra = collector.photons_app.extra_as_json positions = sb.listof(sb.listof(sb.float_spec())).normalise( Meta.empty(), extra) if any(len(position) != 2 for position in positions): raise PhotonsAppError( "Please enter positions as a list of two item lists of user_x, user_y" ) async def gen(reference, sender, **kwargs): ps = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather( ps, reference, **kwargs): if info["cap"].has_matrix: for i, (user_x, user_y) in enumerate(positions): yield TileMessages.SetUserPosition( tile_index=i, user_x=user_x, user_y=user_y, res_required=False, target=serial, ) await target.send(FromGenerator(gen), reference)
async def animate( self, target, sender, final_future, reference, options, global_options=None, **kwargs ): options = self.optionskls.FieldSpec().normalise(Meta.empty(), options) return await self.animationkls(target, sender, options, global_options).animate( reference, final_future, **kwargs )
def __new__(*args, **kwargs): kls = type.__new__(*args, **kwargs) if kls.min_extended_fw is not None: spec = sb.tuple_spec(sb.integer_spec(), sb.integer_spec()) kls.min_extended_fw = spec.normalise(Meta.empty(), kls.min_extended_fw) return kls
def __init__(self, mn, mx, minimum_mn=None, maximum_mx=None, spec=None, multiplier=None): self.mn = round(float(mn), 3) self.mx = round(float(mx), 3) self.spec = (spec or self.default_normaliser)() self.multiplier = multiplier if multiplier is not None else self.default_multiplier mmn = minimum_mn if minimum_mn is not None else self.default_minimum_min mmx = maximum_mx if maximum_mx is not None else self.default_maximum_max if mmn not in (None, False) and self.mn < mmn: self.mn = mmn if mmx not in (None, False) and self.mx > mmx: self.mx = mmx self.meta = Meta.empty() self.constant = None if self.mn == self.mx: self.constant = self.mn
async def attr(collector, target, reference, artifact, **kwargs): """ Send a message to your bulb and print out all the replies. This is the same as the get_attr and set_attr commands but doesn't prefix the wanted message with get or set ``target:attr d073d5000000 get_host_firmware`` """ protocol_register = collector.configuration["protocol_register"] if artifact in (None, "", sb.NotSpecified): raise BadOption( "Please specify what you want to get\nUsage: {0} <target>:get_attr <reference> <attr_to_get>" .format(sys.argv[0])) kls = find_packet(protocol_register, artifact, "") if kls is None: raise BadOption("Sorry, couldn't a class for this message", prefix="", want=artifact) photons_app = collector.photons_app extra = photons_app.extra_as_json if "extra_payload_kwargs" in kwargs: extra.update(kwargs["extra_payload_kwargs"]) msg = kls.normalise(Meta.empty(), extra) async for pkt in target.send(msg, reference, **kwargs): print("{0}: {1}".format(pkt.serial, repr(pkt.payload)))
def fields_description(kls): """ yield (name, type_info, help) for all the fields on our kls Where type_info looks something like `integer (required)` or `string (default "blah")` and fields that have no help are skipped """ final_specs = ( kls.FieldSpec(formatter=MergedOptionStringFormatter).make_spec(Meta.empty()).kwargs ) for name, field in kls.fields.items(): hlp = "" if type(field) is tuple: hlp, field = field else: hlp = field.help spec = final_specs[name] if isinstance(field, dictobj.NullableField): spec = spec.spec.specs[1] if hlp: yield name, " ".join(signature(spec)), dedent(hlp).strip()
async def execute_task(self, **kwargs): extra = self.photons_app.extra_as_json positions = sb.listof(sb.listof(sb.float_spec())).normalise( Meta.empty(), extra) if any(len(position) != 2 for position in positions): raise PhotonsAppError( "Please enter positions as a list of two item lists of user_x, user_y" ) async def gen(reference, sender, **kwargs): ps = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather( ps, reference, **kwargs): if info["cap"].has_matrix: for i, (user_x, user_y) in enumerate(positions): yield TileMessages.SetUserPosition( tile_index=i, user_x=user_x, user_y=user_y, res_required=False, target=serial, ) await self.target.send(FromGenerator(gen), self.reference)
def from_options(kls, options): """Create a Filter based on the provided dictionary""" if isinstance(options, dict): for option in options: if option not in kls.fields: log.warning(hp.lc("Unknown option provided for filter", wanted=option)) return kls.FieldSpec().normalise(Meta.empty(), options)
async def execute_task(self, **kwargs): options = sb.set_options( indication=sb.required(sb.boolean()), duration_s=sb.required(sb.integer_spec()), ).normalise(Meta.empty(), self.photons_app.extra_as_json) await self.target.send(SetCleanConfig(**options), self.reference, **kwargs)
def make_animation(final_future, pauser=None): options = self.animator.Options.FieldSpec().normalise(Meta.empty(), self.options) animation = self.animator.Animation(final_future, options, pauser=pauser) if self.options is not sb.NotSpecified: for attr in animation.overridable: if attr in self.options: setattr(animation, attr, self.options[attr]) return animation
def assertProperties(self, thing, checker): checker(thing) bts = thing.pack() thing2 = type(thing).unpack(bts) checker(thing2) thing3 = type(thing).normalise(Meta.empty(), json.loads(repr(thing))) checker(thing3)
async def make_crontab(collector, target, reference, artifact, **kwargs): """ Make a crontab file executing our day dusk options. Usage is:: ./generate-crontab.py """ collector.register_converters( {"daydusk": DayDusk.FieldSpec(formatter=MergedOptionStringFormatter)}) daydusk = collector.configuration["daydusk"] spec = sb.set_options(path=sb.defaulted(sb.string_spec(), '/config/daydusk.crontab'), lifx_script=sb.defaulted(sb.string_spec(), '/usr/local/bin/lifx')) extra = collector.configuration["photons_app"].extra_as_json kwargs = { k: v for k, v in spec.normalise(Meta.empty(), extra).items() if v is not sb.NotSpecified } cronfile = kwargs['path'] lifx_script = kwargs['lifx_script'] if not daydusk.schedules: raise NoSchedules() cron = CronTab() extra_script_args = ["--silent"] for name, options in daydusk.schedules.items(): script_args = {**options.hsbk, **options.extra} command = [ lifx_script, options.task, options.reference, *extra_script_args, "--", json.dumps(script_args), ] command = str(" ".join([shlex.quote(part) for part in command])) job = cron.new(command=command) job.dow.on(*options.dow) job.minute.on(options.minute) job.hour.on(options.hour) if os.path.exists(cronfile): os.remove(cronfile) cron.write(cronfile) print(f"Generated crontab at {cronfile}")
def make_message(protocol_register, pkt_type, pkt_args): """ Find the packet class for this ``pkt_type`` and instantiate it with the provided ``pkt_args``. """ kls = find_packet(protocol_register, pkt_type) if pkt_args is not None: return kls.normalise(Meta.empty(), pkt_args) else: return kls()
def pack_payload(kls, pkt_type, data, messages_register=None): """ Given some payload data as a dictionary and it's ``pkt_type``, return a hexlified string of the payload. """ for k in messages_register or [kls]: if int(pkt_type) in k.by_type: return k.by_type[int(pkt_type)].Payload.normalise( Meta.empty(), data).pack() raise BadConversion("Unknown message type!", pkt_type=pkt_type)
async def set_chain_state(collector, target, reference, artifact, **kwargs): """ Set the state of colors on your tile ``lan:set_chain_state d073d5f09124 -- '{"colors": [[[0, 0, 0, 3500], [0, 0, 0, 3500], ...], [[0, 0, 1, 3500], ...], ...], "tile_index": 1, "length": 1, "x": 0, "y": 0, "width": 8}'`` Where the colors is a grid of 8 rows of 8 ``[h, s, b, k]`` values. """ # noqa options = collector.photons_app.extra_as_json if "colors" in options: spec = sb.listof( sb.listof( list_spec(sb.integer_spec(), sb.float_spec(), sb.float_spec(), sb.integer_spec()))) colors = spec.normalise(Meta.empty().at("colors"), options["colors"]) row_lengths = [len(row) for row in colors] if len(set(row_lengths)) != 1: raise PhotonsAppError( "Please specify colors as a grid with the same length rows", got=row_lengths) num_cells = sum(len(row) for row in colors) if num_cells != 64: raise PhotonsAppError("Please specify 64 colors", got=num_cells) cells = [] for row in colors: for col in row: cells.append({ "hue": col[0], "saturation": col[1], "brightness": col[2], "kelvin": col[3] }) options["colors"] = cells else: raise PhotonsAppError( "Please specify colors in options after -- as a grid of [h, s, b, k]" ) missing = [] for field in TileMessages.Set64.Payload.Meta.all_names: if field not in options and field not in ("duration", "reserved6"): missing.append(field) if missing: raise PhotonsAppError("Missing options for the SetTileState message", missing=missing) options["res_required"] = False msg = TileMessages.Set64.empty_normalise(**options) await target.send(msg, reference)
async def execute_task(self, **kwargs): broadcast = True if self.artifact is not sb.NotSpecified: broadcast = self.artifact options = sb.set_options( cli_output=sb.defaulted(sb.boolean(), None), env_output=sb.defaulted(sb.boolean(), False), settings_output=sb.defaulted(sb.boolean(), False), ).normalise(Meta.empty(), self.collector.photons_app.extra_as_json) if options["env_output"] is False and options[ "settings_output"] is False: if options["cli_output"] is None: options["cli_output"] = True env_output = options["env_output"] cli_output = options["cli_output"] settings_output = options["settings_output"] ips = {} async with self.target.session() as sender: found, serials = await self.reference.find(sender, timeout=20, broadcast=broadcast) for serial in serials: services = found[binascii.unhexlify(serial)] if Services.UDP in services: ip = services[Services.UDP].host ips[serial] = ip sorted_ips = sorted(ips.items(), key=lambda item: ipaddress.ip_address(item[1])) if cli_output: for serial, ip in sorted_ips: print(f"{serial}: {ip}") if cli_output and (env_output or settings_output): print() if env_output: print(f"export HARDCODED_DISCOVERY='{json.dumps(sorted_ips)}'") if settings_output: print() if settings_output: print("discovery_options:") print(" hardcoded_discovery:") for serial, ip in sorted_ips: print(f' {serial}: "{ip}"')
async def help(collector, tasks, reference, target, **kwargs): """ Display more help information for specified target:task This task takes an extra argument that can be: <target> A specific target, will show associated tasks for that target <target type> Will show what targets are available for this type and their associated tasks <task> Will show expanded help information for this task You can also be tricky and do something like ``<target>:help`` instead of ``help <target>`` """ if reference in ("", sb.NotSpecified, None): task_name = "help" target_name = sb.NotSpecified else: target_name, task_name = task_specifier_spec().normalise(Meta.empty(), reference) target_register = collector.configuration["target_register"] if task_name == "help": task_name = sb.NotSpecified if task_name in target_register.targets or task_name in target_register.types: target_name = task_name task_name = sb.NotSpecified if target_name is not sb.NotSpecified: if target_name in target_register.targets or target_name in target_register.types: kwargs["specific_target"] = target_name if target_name not in target_register.targets and target_name not in target_register.types: raise PhotonsAppError( "Sorry, cannot find help for non existing target", wanted=target_name ) if task_name is not sb.NotSpecified: kwargs["specific_task"] = task_name if task_name not in tasks: raise PhotonsAppError("Sorry, cannot find help for non existing task", wanted=task_name) await list_tasks(collector, tasks, **kwargs)
def create(kls, *args, **kwargs): """Create an instance of this class from either a single position argument or keyword arguments""" if (args and kwargs) or (args and len(args) != 1): raise PhotonsAppError( "Creating a packet must be done with one positional argument, or just keyword arguments", args=args, kwargs=kwargs, ) val = kwargs or sb.NotSpecified if args: val = args[0] if isinstance(val, (bitarray, bytes)): return PacketPacking.unpack(kls, val) return kls.spec().normalise(Meta.empty(), val)
async def execute_task(self, **kwargs): task_name = sb.NotSpecified target_name = self.target if self.reference is not sb.NotSpecified: if ":" in self.reference: target_name, task_name = task_specifier_spec().normalise( Meta.empty(), self.reference) else: task_name = self.reference target_register = self.collector.configuration["target_register"] if task_name in target_register.registered or task_name in target_register.types: target_name = task_name task_name = sb.NotSpecified for name, target in target_register.created.items(): if target is target_name: target_name = name break if target_name is not sb.NotSpecified: if target_name in target_register.registered or target_name in target_register.types: kwargs["specific_target"] = target_name if (target_name not in target_register.registered and target_name not in target_register.types): raise PhotonsAppError( "Sorry, cannot find help for non existing target", wanted=target_name) if task_name is not sb.NotSpecified: kwargs["specific_task"] = task_name if task_name not in task_register: raise PhotonsAppError( "Sorry, cannot find help for non existing task", wanted=task_name, available=task_register.names, ) await task_register.fill_task( self.collector, list_tasks, specific_task_groups=self.specific_task_groups, **kwargs).run()
def getitem_spec(self, typ, key, actual, parent, serial, do_transform, allow_bitarray, unpacking): """ Used by __getitem__ to use the spec on the type to transform the ``actual`` value """ if typ._allow_callable: if callable(actual): actual = actual(parent or self, serial) spec = typ.spec(self, unpacking, transform=False) res = spec.normalise(Meta.empty().at(key), actual) if do_transform and unpacking and res is not sb.NotSpecified and res is not Optional: res = typ.untransform(self, res) if type(res) is bitarray and not allow_bitarray: return res.tobytes() else: return res
async def attr_actual(collector, target, reference, artifact, **kwargs): """ Same as the attr command but prints out the actual values on the replies rather than transformed values """ protocol_register = collector.configuration["protocol_register"] if artifact in (None, "", sb.NotSpecified): raise BadOption( "Please specify what you want to get\nUsage: {0} <target>:get_attr <reference> <attr_to_get>" .format(sys.argv[0])) kls = find_packet(protocol_register, artifact, "") if kls is None: raise BadOption("Sorry, couldn't a class for this message", prefix="", want=artifact) extra = collector.photons_app.extra_as_json if "extra_payload_kwargs" in kwargs: extra.update(kwargs["extra_payload_kwargs"]) def lines(pkt, indent=" "): for field in pkt.Meta.all_names: val = pkt[field] if isinstance(val, list): yield f"{indent}{field}:" for item in val: ind = f"{indent} " ls = list(lines(item, ind)) first = list(ls[0]) first[len(indent) + 2] = "*" ls[0] = "".join(first) yield from ls else: yield f"{indent}{field}: {pkt.actual(field)}" msg = kls.normalise(Meta.empty(), extra) async for pkt in target.send(msg, reference, **kwargs): print() print(f"""{"=" * 10}: {pkt.serial}""") for line in lines(pkt): print(line)
def pack(kls, data, protocol_register, unknown_ok=False): """ Return a hexlified string of the data. This uses ``pkt_type`` and ``protocol`` in the data, along with the protocol_register to find the appropriate class to use to perform the packing. """ protocol, pkt_type, Packet, PacketKls, data = kls.get_packet_type( data, protocol_register) if PacketKls is None: if not unknown_ok: raise BadConversion("Unknown message type!", protocol=protocol, pkt_type=pkt_type) PacketKls = Packet return PacketKls.normalise(Meta.empty(), data).pack()
def msg(kls, components, overrides=None): """ Create a :ref:`SetWaveformOptional <LightMessages.SetWaveformOptional>` message that may be used to change the state of a device to what has been specified. .. code-block:: python from photons_control.colour import ColourParser async def my_action(target, reference): msg = ColourParser.msg("green") await target.send(msg, reference) """ h, s, b, k = kls.hsbk(components, overrides) colour = dict( hue=0 if h is None else h, set_hue=h is not None, saturation=0 if s is None else s, set_saturation=s is not None, brightness=0 if b is None else b, set_brightness=b is not None, kelvin=0 if k is None else int(k), set_kelvin=k is not None, ) other = dict( transient=0, cycles=1, skew_ratio=0, waveform=Waveform.SAW, period=0 if not overrides else overrides.get("duration", 0), ) other_override = Effects.make(**(overrides or {})) options = MergedOptions.using(other, other_override, overrides or {}, colour) return LightMessages.SetWaveformOptional.normalise( Meta.empty(), options)
async def set_attr(collector, target, reference, artifact, broadcast=False, **kwargs): """ Set attributes on your globes ``target:set_attr d073d5000000 color -- '{"hue": 360, "saturation": 1, "brightness": 1}'`` This does the same thing as ``get_attr`` but will look for ``Set<Attr>`` message and initiates it with the options found after the ``--``. So in this case it will create ``SetColor(hue=360, saturation=1, brightness=1)`` and send that to the device. """ protocol_register = collector.configuration["protocol_register"] if artifact in (None, "", sb.NotSpecified): raise BadOption( "Please specify what you want to get\nUsage: {0} <target>:set_attr <reference> <attr_to_get> -- '{{<options>}}'" .format(sys.argv[0])) setter = find_packet(protocol_register, artifact, "Set") if setter is None: raise BadOption("Sorry, couldn't a class for this message", prefix="set", want=artifact) extra = collector.photons_app.extra_as_json if "extra_payload_kwargs" in kwargs: extra.update(kwargs["extra_payload_kwargs"]) msg = setter.normalise(Meta.empty(), extra) async for pkt in target.send(msg, reference, broadcast=broadcast): print("{0}: {1}".format(pkt.serial, repr(pkt.payload)))
def __setitem__(self, key, val): """ Set values on the object This will unpack dictionaries if we are setting a dictionary for a group field. We also see if a field has a transform option and use it if it's there """ if key in self.Meta.groups: if val is Initial: # Special case because of the logic in dictobj that sets default values on initialization # Should only happen for group fields where it makes no sense return self._set_group_item(key, val) return # If our type has a transformation, apply it to our value # The assumption is that if there is a transformation, # The value will always be given with the transformation in mind # untransform will be used when extracting the value typ = self.Meta.all_field_types_dict.get(key) if typ and typ._transform is not sb.NotSpecified and val not in ( sb.NotSpecified, Optional): val = typ.do_transform(self, val) # If we have multiple, then we want to make sure we create actual objects # So that if we modify an object in place, it's updated on the packet if typ and hasattr( typ, "_multiple") and typ._multiple and val is not sb.NotSpecified: val = typ.spec(self, unpacking=True).normalise(Meta.empty().at(key), val) # Otherwise we set directly on the packet dictobj.__setitem__(self, key, val)
async def get_attr(collector, target, reference, artifact, **kwargs): """ Get attributes from your globes ``target:get_attr d073d5000000 color`` Where ``d073d5000000`` is replaced with the serial of the device you are addressing and ``color`` is replaced with the attribute you want. This task works by looking at all the loaded LIFX binary protocol messages defined for the 1024 protocol and looks for ``Get<Attr>``. So if you want the ``color`` attribute, it will look for the ``GetColor`` message and send that to the device and print out the reply packet we get back. """ protocol_register = collector.configuration["protocol_register"] if artifact in (None, "", sb.NotSpecified): raise BadOption( "Please specify what you want to get\nUsage: {0} <target>:get_attr <reference> <attr_to_get>" .format(sys.argv[0])) getter = find_packet(protocol_register, artifact, "Get") if getter is None: raise BadOption("Sorry, couldn't a class for this message", prefix="get", want=artifact) extra = collector.photons_app.extra_as_json if "extra_payload_kwargs" in kwargs: extra.update(kwargs["extra_payload_kwargs"]) msg = getter.normalise(Meta.empty(), extra) async for pkt in target.send(msg, reference, **kwargs): print("{0}: {1}".format(pkt.serial, repr(pkt.payload)))
describe "from_options": it "normalises the options": normalised = mock.Mock(name="normalised") spec = mock.Mock(name="spec") spec.normalise.return_value = normalised FieldSpec = mock.Mock(name="FieldSpec", return_value=spec) options = mock.Mock(name="options") with mock.patch.object(Filter, "FieldSpec", FieldSpec): assert Filter.from_options(options) is normalised FieldSpec.assert_called_once_with() spec.normalise.assert_called_once_with(Meta.empty(), options) it "works": want = { "label": ["bathroom", "hallway"], "location_id": ["identifier1"], "saturation": [(0.7, 0.7), (0.8, 1.0)], } filtr = Filter.from_options(want) self.assertFltrMatches(filtr, want) describe "empty": it "gives back a filter with just refresh options": self.assertFltrMatches( Filter.empty(), {"refresh_info": False, "refresh_discovery": False} )
] @hp.memoized_property def Together(s): class Together(dictobj.PacketSpec): parent_packet = True fields = [("g1", s.Group1), ("g2", s.Group2), ("three", T.String)] return Together return V() it "works", V: spec = V.Together.spec() t = spec.normalise( Meta.empty(), {"one": False, "two": 1.2, "three": b"whatever\x00\x00", "sb": 24, "bts": b"\x01"}, ) assert t.one is False assert t.two == 1.2 assert t.mod == "1.2.modified" assert t.three == "whatever" assert t.sb == 24 assert t.bts == b"\x01\x00\x00" assert t.actual("two") == 65538 assert t.actual("three").tobytes() == b"whatever\x00\x00" it "works when value given as groups", V: spec = V.Together.spec() t = spec.normalise(
def make_photons_app(self, **kwargs): meta = Meta.empty().at("photons_app") return PhotonsApp.FieldSpec(formatter=MergedOptionStringFormatter).normalise(meta, kwargs)