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 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)
class Fields(dictobj.Spec): uuid = dictobj.Field(sb.string_spec, wrapper=sb.required) matcher = dictobj.Field( json_string_spec(sb.dictionary_spec(), storing), wrapper=sb.required ) power = dictobj.NullableField(sb.boolean) color = dictobj.NullableField(sb.string_spec) zones = dictobj.NullableField(json_string_spec(sb.listof(hsbk()), storing)) chain = dictobj.NullableField(json_string_spec(sb.listof(chain_spec), storing)) duration = dictobj.NullableField(sb.integer_spec)
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)
class Options(dictobj.Spec): chain = dictobj.Field(sb.listof(TileChild.FieldSpec())) chain_length = dictobj.NullableField(sb.integer_spec) palette = dictobj.Field(sb.listof(color_spec())) palette_count = dictobj.NullableField(sb.integer_spec) matrix_effect = dictobj.Field(enum_spec(None, TileEffectType, unpacking=True), default=TileEffectType.OFF)
def setup_addon_register(self, photons_app, __main__): """Setup our addon register""" # Create the addon getter and register the crosshair namespace self.addon_getter = AddonGetter() self.addon_getter.add_namespace("lifx.photons", Result.FieldSpec(), Addon.FieldSpec()) # Initiate the addons from our configuration register = Register(self.addon_getter, self) if "addons" in photons_app: addons = photons_app["addons"] if type(addons) in (MergedOptions, dict) or getattr(addons, "is_dict", False): spec = sb.dictof(sb.string_spec(), sb.listof(sb.string_spec())) meta = Meta(photons_app, []).at("addons") for namespace, adns in spec.normalise(meta, addons).items(): register.add_pairs(*[(namespace, adn) for adn in adns]) elif photons_app.get("default_activate"): for comp in photons_app["default_activate"]: register.add_pairs(("lifx.photons", comp)) if __main__ is not None: register.add_pairs(("lifx.photons", "__main__")) # Import our addons register.recursive_import_known() # Resolve our addons register.recursive_resolve_imported() return register
def normalise_filled(self, meta, val): val = sb.listof(self.spec).normalise(meta, val) if len(val) != self.length: raise BadSpecValue( "Expected certain number of parts", want=self.length, got=len(val), meta=meta ) return val
class Options(dictobj.Spec): zones = dictobj.Field(sb.listof(color_spec())) zones_count = dictobj.NullableField(sb.integer_spec) zones_effect = dictobj.Field(enum_spec(None, MultiZoneEffectType, unpacking=True), default=MultiZoneEffectType.OFF)
def normalise(self, meta, val): if "SERIAL_FILTER" in os.environ and self.see_env: val = os.environ["SERIAL_FILTER"].split(",") if val == ["null"]: return None meta = Meta(meta.everything, []).at("${SERIAL_FILTER}") if val in (None, sb.NotSpecified): return val return sb.listof(serial_spec()).normalise(meta, val)
class MemoryTarget(Target): """ Knows how to talk to fake devices as if they were on the network. """ devices = dictobj.Field(sb.listof(sb.any_spec()), wrapper=sb.required) default_broadcast = dictobj.Field( sb.defaulted(sb.string_spec(), "255.255.255.255")) session_kls = makeMemorySession(NetworkSession)
class Schedule(dictobj.Spec): days = dictobj.NullableField( sb.listof(enum_spec(None, Days, unpacking=True))) hour = dictobj.Field(range_spec(sb.integer_spec(), 0, 23), wrapper=sb.required) minute = dictobj.Field(range_spec(sb.integer_spec(), 0, 59), wrapper=sb.required) task = dictobj.Field(task_spec) hue = dictobj.Field(range_spec(sb.float_spec(), 0, 360), default=0) saturation = dictobj.Field(range_spec(sb.float_spec(), 0, 1), default=0) brightness = dictobj.Field(range_spec(sb.float_spec(), 0, 1), default=1) kelvin = dictobj.Field(range_spec(sb.integer_spec(), 1500, 9000), default=3500) transform_options = dictobj.NullableField( sb.dictof(sb.string_spec(), sb.boolean())) duration = dictobj.NullableField(sb.float_spec) power = dictobj.NullableField(power_spec) colors = dictobj.NullableField(colors_spec) override = dictobj.NullableField( sb.dictof(sb.string_spec(), range_spec(sb.float_spec(), 0, 1))) reference = dictobj.Field(reference_spec) @property def hsbk(self): if self.task == 'lan:transform': keys = ["hue", "saturation", "brightness", "kelvin"] options = {k: v for k, v in self.as_dict().items() if k in keys} return {k: v for k, v in options.items() if v is not None} else: return {} @property def extra(self): keys_except = [ "days", "hour", "minute", "reference", "task", "hue", "saturation", "brightness", "kelvin" ] options = { k: v for k, v in self.as_dict().items() if k not in keys_except } return {k: v for k, v in options.items() if v is not None} @property def dow(self): days = self.days if not self.days: days = list(Days) return [day.value for day in days]
class AnimationPauseCommand(store.Command): """ Pause an animation """ animations_runner = store.injected("animations") pause = dictobj.Field( sb.listof(sb.string_spec()), help="The animation identities to pause", ) async def execute(self): return await self.animations_runner.pause(*self.pause)
class AnimationStopCommand(store.Command): """ Stop an animation """ animations_runner = store.injected("animations") stop = dictobj.Field( sb.listof(sb.string_spec()), help="The animation identities to stop", ) async def execute(self): return await self.animations_runner.stop(*self.stop)
class TileChild(dictobj.Spec): accel_meas_x = dictobj.Field(sb.integer_spec, default=0) accel_meas_y = dictobj.Field(sb.integer_spec, default=0) accel_meas_z = dictobj.Field(sb.integer_spec, default=0) user_x = dictobj.Field(sb.float_spec, default=0) user_y = dictobj.Field(sb.float_spec, default=0) width = dictobj.Field(sb.integer_spec, default=8) height = dictobj.Field(sb.integer_spec, default=8) device_version_vendor = dictobj.Field(sb.integer_spec, default=1) device_version_product = dictobj.Field(sb.integer_spec, default=55) device_version_version = dictobj.Field(sb.integer_spec, default=0) firmware_version_minor = dictobj.Field(sb.integer_spec, default=50) firmware_version_major = dictobj.Field(sb.integer_spec, default=3) firmware_build = dictobj.Field(sb.integer_spec, default=0) colors = dictobj.Field(sb.listof(color_spec()))
class MemoryTarget(LanTarget): """ Knows how to talk to fake devices as if they were on the network. """ gaps = dictobj.Field( Gaps(gap_between_results=0.05, gap_between_ack_and_res=0.05, timeouts=[(0.2, 0.2)])) io_service = dictobj.Field(sb.any_spec, default=MemoryService) transport_kls = dictobj.Field(sb.any_spec, default=MemoryTransport) devices = dictobj.Field(sb.listof(sb.any_spec()), wrapper=sb.required) default_broadcast = dictobj.Field( sb.defaulted(sb.string_spec(), "255.255.255.255")) session_kls = makeMemorySession(NetworkSession)
def normalise_filled(self, meta, val): if type(val) is str: val = val.split(",") if type(val) is list: res = [] for pair in val: if type(pair) is not str: res.append(pair) else: if "-" in pair: res.append(tuple(v.strip() for v in pair.split("-", 1))) else: res.append((pair.strip(), pair.strip())) val = res return sb.listof(sb.tuple_spec(sb.float_spec(), sb.float_spec())).normalise(meta, val)
class SceneInfoCommand(store.Command): """ Retrieve information about scenes in the database """ db_queue = store.injected("db_queue") uuid = dictobj.NullableField( sb.listof(sb.string_spec()), help="Only get information for scene with these uuid" ) only_meta = dictobj.Field( sb.boolean, default=False, help="Only return meta info about the scenes" ) async def execute(self): def get(db): info = defaultdict(lambda: {"meta": {}, "scene": []}) fs = [] ifs = [] if self.uuid: fs.append(Scene.uuid.in_(self.uuid)) ifs.append(SceneInfo.uuid.in_(self.uuid)) for sinfo in db.query(SceneInfo).filter(*ifs): info[sinfo.uuid]["meta"] = sinfo.as_dict(ignore=["uuid"]) for scene in db.query(Scene).filter(*fs): # Make sure there is an entry if no SceneInfo for this scene info[scene.uuid] if not self.only_meta: dct = scene.as_dict(ignore=["uuid"]) info[scene.uuid]["scene"].append(dct) if self.only_meta: for _, data in info.items(): del data["scene"] return dict(info) return await self.db_queue.request(get)
class SceneChangeCommand(store.Command): """ Set all the options for a scene """ db_queue = store.injected("db_queue") uuid = dictobj.NullableField( sb.string_spec, help="The uuid of the scene to change, if None we create a new scene" ) label = dictobj.NullableField(sb.string_spec, help="The label to give this scene") description = dictobj.NullableField(sb.string_spec, help="The description to give this scene") scene = dictobj.NullableField( sb.listof(Scene.DelayedSpec(storing=True)), help="The options for the scene" ) async def execute(self): def make(db): scene_uuid = self.uuid or str(uuid.uuid4()) if self.scene is not None: for thing in db.queries.get_scenes(uuid=scene_uuid).all(): db.delete(thing) for part in self.scene: made = db.queries.create_scene(**part(scene_uuid).as_dict()) db.add(made) info, _ = db.queries.get_or_create_scene_info(uuid=scene_uuid) if self.label is not None: info.label = self.label if self.description is not None: info.description = self.description db.add(info) return scene_uuid return await self.db_queue.request(make)
def normalise_filled(self, meta, val): options_spec = sb.set_options( filename=sb.required(sb.string_spec()), optional=sb.defaulted(sb.boolean(), False) ) lst_spec = sb.listof(sb.or_spec(sb.string_spec(), options_spec)) if isinstance(val, list): val = {"after": lst_spec.normalise(meta, val)} val = sb.set_options(after=lst_spec, before=lst_spec).normalise(meta, val) formatted = sb.formatted(sb.string_spec(), formatter=MergedOptionStringFormatter) for key in ("after", "before"): result = [] for i, thing in enumerate(val[key]): filename = thing optional = False if isinstance(thing, dict): filename = thing["filename"] optional = thing["optional"] filename = formatted.normalise(meta.at(key).indexed_at(i), filename) if optional and not os.path.exists(filename): log.warning(hp.lc("Ignoring optional configuration", filename=filename)) continue if not os.path.exists(filename): raise BadConfiguration( "Specified extra file doesn't exist", source=self.source, filename=filename, meta=meta, ) result.append(filename) val[key] = result return self.extras_spec.normalise(meta, val)
class TransitionOptions(dictobj.Spec): run_first = dictobj.Field( sb.boolean, default=True, help="Whether to run a transition before feature animations") run_last = dictobj.Field( sb.boolean, default=True, help="Whether to run a transition after limit of feature animations", ) run_between = dictobj.Field( sb.boolean, default=True, help="Whether to run a transitions between animations") animations = dictobj.Field( sb.listof(animation_spec()), help="Same option as in the ``animations`` option of the root options", )
class Not(dictobj.Spec): there = dictobj.Field(sb.listof(sb.integer_spec()))
class One(dictobj.Spec): list1 = dictobj.Field(sb.listof(Two.FieldSpec())) list2 = dictobj.Field(sb.listof(sb.integer_spec())) thing3 = dictobj.Field(sb.string_spec())
self.specs = [ range_spec(0, 360), range_spec(0, 1), range_spec(0, 1), range_spec(1500, 9000, spec=sb.integer_spec()), ] def normalise_filled(self, meta, val): val = sized_list_spec(sb.any_spec(), 4).normalise(meta, val) res = [] for i, (v, s) in enumerate(zip(val, self.specs)): res.append(s.normalise(meta.at(i), v)) return res chain_spec = sb.listof(hsbk()) class json_string_spec(sb.Spec): def __init__(self, spec, storing): self.spec = spec self.storing = storing def normalise_filled(self, meta, val): if type(val) is str: try: v = json.loads(val) except (TypeError, ValueError) as error: raise BadSpecValue("Value was not valid json", error=error, meta=meta)
def normalise_filled(self, meta, val): return ",".join(sb.listof(sb.string_spec()).normalise(meta, val))
self.assertSignature(sb.string_choice_spec(["one", "two"]), "choice of (one | two)") it "knows about optional_spec": self.assertSignature(sb.optional_spec(sb.integer_spec()), "integer (optional)") self.assertSignature(sb.optional_spec(sb.any_spec()), "(optional)") it "knows about defaulted": self.assertSignature(sb.defaulted(sb.integer_spec(), 20), "integer (default 20)") self.assertSignature(sb.defaulted(sb.any_spec(), True), "(default True)") it "knows about required": self.assertSignature(sb.required(sb.integer_spec()), "integer (required)") self.assertSignature(sb.required(sb.any_spec()), "(required)") it "knows about listof": self.assertSignature(sb.listof(sb.integer_spec()), "[ integer , ... ]") self.assertSignature(sb.listof(sb.any_spec()), "[ <item> , ... ]") it "knows about dictof": self.assertSignature(sb.dictof(sb.string_spec(), sb.integer_spec()), "{ string : integer }") self.assertSignature(sb.dictof(sb.string_spec(), sb.any_spec()), "{ string : <item> }") it "knows about container_spec": class Container: def __init__(self, value): pass self.assertSignature(sb.container_spec(Container, sb.string_spec()), "string") it "knows about formatted":
class Options(dictobj.Spec): relays = dictobj.NullableField(sb.listof(Relay.FieldSpec())) relays_count = dictobj.NullableField(sb.integer_spec())
async def execute_task(self, **kwargs): options = self.photons_app.extra_as_json width = options.get("width", 8) options["width"] = width 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) cells = [] for row in colors: while len(row) < width: row.append(None) for col in row: if col is None: cells.append({ "hue": 0, "saturation": 0, "brightness": 0, "kelvin": 3500 }) continue 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.create(**options) await self.target.send(msg, self.reference)
class Filter(dictobj.Spec): refresh_info = dictobj.Field(boolean, default=False) refresh_discovery = dictobj.Field(boolean, default=False) serial = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) label = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) power = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) group_id = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) group_name = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) location_id = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) location_name = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) hue = dictobj.Field(str_ranges, wrapper=sb.optional_spec) saturation = dictobj.Field(str_ranges, wrapper=sb.optional_spec) brightness = dictobj.Field(str_ranges, wrapper=sb.optional_spec) kelvin = dictobj.Field(str_ranges, wrapper=sb.optional_spec) firmware_version = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) product_id = dictobj.Field(sb.listof(sb.integer_spec()), wrapper=sb.optional_spec) product_identifier = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) cap = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) @classmethod def from_json_str(kls, s): """ Interpret s as a json string and use it to create a Filter using from_options """ try: options = json.loads(s) except (TypeError, ValueError) as error: raise InvalidJson(error=error) else: if type(options) is not dict: raise InvalidJson("Expected a dictionary", got=type(options)) return kls.from_options(options) @classmethod def from_key_value_str(kls, s): """ Create a Filter based on the ``key=value key2=value2`` string provided. Each key=value pair is separated by a space and arrays are formed by separating values by a comma. Note that values may not have spaces in them because of how we split the key=value pairs. If you need values to have spaces use from_json_str or from_options. """ options = {} for part in s.split(" "): m = regexes["key_value"].match(part) if m: groups = m.groupdict() if groups["key"] not in ( "hue", "saturation", "brightness", "kelvin", "refresh_info", "refresh_discovery", ): options[groups["key"]] = groups["value"].split(",") else: options[groups["key"]] = groups["value"] return kls.from_options(options) @classmethod def from_url_str(kls, s): """ Create a Filter based on ``key=value&otherkey=value2`` string provided Where the string is url encoded. """ return kls.from_options(parse_qs(s)) @classmethod def from_kwargs(kls, **kwargs): """Create a Filter based on the provided kwarg arguments""" return kls.from_options(kwargs) @classmethod def empty(kls, refresh_info=False, refresh_discovery=False): """Create an empty filter""" return kls.from_options({ "refresh_info": refresh_info, "refresh_discovery": refresh_discovery }) @classmethod 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) def has(self, field): """Say whether the filter has an opinion on this field""" return field in self.fields and self[field] != sb.NotSpecified def matches(self, field_name, val): """ Says whether this filter matches against provided filed_name/val pair * Always say False for ``refresh_info`` and ``refresh_discovery`` * Say False if the value on the filter for field_name is NotSpecified * Say True if a hsbk value and we are within the range specified in val * Say True if value on the filter is a list, and val exists in that list * Say True if value on the filter is not a list and matches val """ if field_name in ("refresh_info", "refresh_discovery"): return False if field_name in self.fields: f = self[field_name] if f is not sb.NotSpecified: if field_name in ("hue", "saturation", "brightness", "kelvin"): return any(val >= pair[0] and val <= pair[1] for pair in f) if field_name in self.label_fields and type(val) is str: if type(f) is list: return any(fnmatch.fnmatch(val, pat) for pat in f) else: return fnmatch.fnmatch(val, f) if type(f) is list: if type(val) is list: return any(v in val for v in f) else: return val in f else: return val == f return False @property def matches_all(self): """True if this Filter matches against any device""" for field in self.fields: if field not in ("refresh_info", "refresh_discovery"): if self[field] != sb.NotSpecified: return False return True @property def points(self): """Provide InfoPoints enums that match the keys on this filter with values""" for e in InfoPoints: for key in e.value.keys: if self[key] != sb.NotSpecified: yield e @property def label_fields(self): return ("product_identifier", "label", "location_name", "group_name")
class Device(dictobj.Spec): """ An object representing a single device. Users shouldn't have to interact with these directly """ limit = dictobj.NullableField(sb.any_spec) serial = dictobj.Field(sb.string_spec, wrapper=sb.required) label = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec) power = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec) group = dictobj.Field(sb.any_spec, wrapper=sb.optional_spec) location = dictobj.Field(sb.any_spec, wrapper=sb.optional_spec) hue = dictobj.Field(sb.integer_spec, wrapper=sb.optional_spec) saturation = dictobj.Field(sb.float_spec, wrapper=sb.optional_spec) brightness = dictobj.Field(sb.float_spec, wrapper=sb.optional_spec) kelvin = dictobj.Field(sb.integer_spec, wrapper=sb.optional_spec) firmware_version = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec) product_id = dictobj.Field(sb.integer_spec, wrapper=sb.optional_spec) product_identifier = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec) cap = dictobj.Field(sb.listof(sb.string_spec()), wrapper=sb.optional_spec) def setup(self, *args, **kwargs): super().setup(*args, **kwargs) self.point_futures = {e: hp.ResettableFuture() for e in InfoPoints} self.point_futures[None] = hp.ResettableFuture() @hp.memoized_property def final_future(self): return asyncio.Future() @property def property_fields(self): return ["group_id", "group_name", "location_name", "location_id"] @property def group_id(self): if self.group is sb.NotSpecified: return sb.NotSpecified return self.group.uuid @property def group_name(self): if self.group is sb.NotSpecified: return sb.NotSpecified return self.group.name @property def location_name(self): if self.location is sb.NotSpecified: return sb.NotSpecified return self.location.name @property def location_id(self): if self.location is sb.NotSpecified: return sb.NotSpecified return self.location.uuid def as_dict(self): actual = super(Device, self).as_dict() del actual["group"] del actual["limit"] del actual["location"] for key in self.property_fields: actual[key] = self[key] return actual @property def info(self): return { k: v for k, v in self.as_dict().items() if v is not sb.NotSpecified } def matches_fltr(self, fltr): """ Say whether we match against the provided filter """ if fltr.matches_all: return True fields = [f for f in self.fields if f != "limit"] + self.property_fields has_atleast_one_field = False for field in fields: val = self[field] if val is not sb.NotSpecified: has_field = fltr.has(field) if has_field: has_atleast_one_field = True if has_field and not fltr.matches(field, val): return False return has_atleast_one_field def set_from_pkt(self, pkt, collections): """ Set information from the provided pkt. collections is used for determining the group/location based on the pkt. We return a InfoPoints enum representing what type of information was set. """ if pkt | LightMessages.LightState: self.label = pkt.label self.power = "off" if pkt.power == 0 else "on" self.hue = pkt.hue self.saturation = pkt.saturation self.brightness = pkt.brightness self.kelvin = pkt.kelvin return InfoPoints.LIGHT_STATE elif pkt | DeviceMessages.StateGroup: uuid = binascii.hexlify(pkt.group).decode() self.group = collections.add_group(uuid, pkt.updated_at, pkt.label) return InfoPoints.GROUP elif pkt | DeviceMessages.StateLocation: uuid = binascii.hexlify(pkt.location).decode() self.location = collections.add_location(uuid, pkt.updated_at, pkt.label) return InfoPoints.LOCATION elif pkt | DeviceMessages.StateHostFirmware: self.firmware_version = f"{pkt.version_major}.{pkt.version_minor}" return InfoPoints.FIRMWARE elif pkt | DeviceMessages.StateVersion: self.product_id = pkt.product product = Products[pkt.vendor, pkt.product] self.product_identifier = product.identifier cap = [] for prop in ( "has_ir", "has_color", "has_chain", "has_matrix", "has_multizone", "has_variable_color_temp", ): if getattr(product.cap, prop): cap.append(prop[4:]) else: cap.append("not_{}".format(prop[4:])) self.cap = sorted(cap) return InfoPoints.VERSION def points_from_fltr(self, fltr): """Return the relevant messages from this filter""" for e in InfoPoints: if fltr is None or any( fltr.has(key) for key in e.value.keys) or fltr.matches_all: if fltr is not None and fltr.refresh_info: if e.value.refresh is not None: self.point_futures[e].reset() yield e async def finish(self): self.final_future.cancel() if hasattr(self, "_refresh_information_loop"): self._refresh_information_loop.cancel() await asyncio.wait([self._refresh_information_loop]) del self.final_future def ensure_refresh_information_loop(self, sender, time_between_queries, collections): loop = getattr(self, "_refresh_information_loop", None) if not loop or loop.done(): self._refresh_information_loop = hp.async_as_background( self.refresh_information_loop(sender, time_between_queries, collections)) return self._refresh_information_loop async def refresh_information_loop(self, sender, time_between_queries, collections): points = iter(itertools.cycle(list(InfoPoints))) time_between_queries = time_between_queries or {} refreshes = {} for e in InfoPoints: if e.value.refresh is None: refreshes[e] = None else: refreshes[e] = time_between_queries.get( e.name, e.value.refresh) async def gen(reference, sender, **kwargs): async for _ in hp.tick(1): if self.final_future.done(): return e = next(points) fut = self.point_futures[e] if fut.done(): refresh = refreshes[e] if refresh is None: continue if time.time() - fut.result() < refresh: continue yield e.value.msg msg = FromGenerator(gen, reference_override=self.serial) async for pkt in sender(msg, self.serial, limit=self.limit): point = self.set_from_pkt(pkt, collections) self.point_futures[point].reset() self.point_futures[point].set_result(time.time()) async def matches(self, sender, fltr, collections): if fltr is None: return True async def gen(reference, sender, **kwargs): for e in self.points_from_fltr(fltr): if self.final_future.done(): return if not self.point_futures[e].done(): yield e.value.msg msg = FromGenerator(gen, reference_override=self.serial) async for pkt in sender(msg, self.serial, limit=self.limit): point = self.set_from_pkt(pkt, collections) self.point_futures[point].reset() self.point_futures[point].set_result(time.time()) return self.matches_fltr(fltr)
("Uint32", 32, "<I", int), ("Int64", 64, "<q", int), ("Uint64", 64, "<Q", int), ("Float", 32, "<f", float), ("Double", 64, "<d", float), ("Bytes", None, None, bytes), ("String", None, None, str), ("Reserved", None, None, bytes), ("CSV", None, None, (list, str, ",")), ("JSON", None, None, json), ) json_spec = sb.match_spec( (bool, sb.any_spec()), (int, sb.any_spec()), (float, sb.any_spec()), (str, sb.any_spec()), (list, lambda: sb.listof(json_spec)), (type(None), sb.any_spec()), fallback=lambda: sb.dictof(sb.string_spec(), json_spec), ) # Here so we don't have to instantiate these every time we get a value from a packet static_conversion_from_spec = { any: sb.any_spec(), bool: boolean(), float: float_spec(), (bool, int): boolean_as_int_spec(), json: json_spec, }