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): if isinstance(val, list): val = tuple(val) if isinstance(val, tuple): return sb.tuple_spec(sb.optional_spec(sb.string_spec()), sb.required(sb.string_spec())).normalise( meta, val) task = sb.or_spec(sb.string_spec(), sb.none_spec()).normalise(meta, val) if not task: task = "list_tasks" target = "" if ":" in task: target, task = val.rsplit(":", 1) if "(" not in target: return target or sb.NotSpecified, task tokens = self.initial_parse(val) chosen_task = tokens.pop().string tokens.pop() collector = meta.everything["collector"] target_register = collector.configuration["target_register"] target_name = tokens.pop(0).string target_name = self.parse_overridden_target(meta, val, collector, target_register, target_name, tokens) return target_name, chosen_task
def make_command(self, meta, val, existing): v = sb.set_options(path=sb.required(sb.string_spec()), allow_ws_only=sb.defaulted(sb.boolean(), False)).normalise( meta, val) path = v["path"] allow_ws_only = v["allow_ws_only"] if path not in self.paths: raise NoSuchPath(path, sorted(self.paths)) val = sb.set_options(body=sb.required( sb.set_options(args=sb.dictionary_spec(), command=sb.required(sb.string_spec())))).normalise( meta, val) args = val["body"]["args"] name = val["body"]["command"] if existing: name = val["body"]["command"] = f"{existing['path']}:{name}" extra_context = {} if existing: extra_context["_parent_command"] = existing["command"] everything = meta.everything if isinstance(meta.everything, MergedOptions): everything = meta.everything.wrapped() everything.update(extra_context) meta = Meta(everything, []).at("body") available_commands = self.paths[path] if name not in available_commands: raise BadSpecValue( "Unknown command", wanted=name, available=self.available(available_commands, allow_ws_only=allow_ws_only), meta=meta.at("command"), ) command = available_commands[name]["spec"].normalise( meta.at("args"), args) if not allow_ws_only and command.__whirlwind_ws_only__: raise BadSpecValue( "Command is for websockets only", wanted=name, available=self.available(available_commands, allow_ws_only=allow_ws_only), meta=meta.at("command"), ) return command, name
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}")
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]
def normalise_filled(self, meta, val): """ Convert to and from a string into an integer """ if self.unpacking: if type(val) is str: if not regexes["version_number"].match(val): raise BadSpecValue(r"Expected string to match \d+.\d+", got=val, meta=meta) return val val = sb.integer_spec().normalise(meta, val) major = val >> 0x10 minor = val & 0xFFFF return f"{major}.{minor}" else: if type(val) is int: return val val = sb.string_spec().normalise(meta, val) m = regexes["version_number"].match(val) if not m: raise BadSpecValue( r"Expected version string to match (\d+.\d+)", wanted=val, meta=meta) groups = m.groupdict() major = int(groups["major"]) minor = int(groups["minor"]) return (major << 0x10) + minor
class V: spec = sb.formatted(sb.string_spec(), formatter=MergedOptionStringFormatter) target_register = mock.Mock(name="target_register") @hp.memoized_property def meta(s): options = MergedOptions.using( {"target_register": s.target_register}, dont_prefix=[mock.Mock] ) return Meta(options, [])
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)
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 LanTarget(Target): """ Knows how to talk to a device over the local network. It's one configuration option is default_broadcast which says what address to broadcast discovery if broadcast is given to run calls as True. """ default_broadcast = dictobj.Field( sb.defaulted(sb.string_spec(), "255.255.255.255")) discovery_options = dictobj.Field(discovery_options_spec) session_kls = NetworkSession
def normalise_filled(self, meta, val): val = sb.string_spec().normalise(meta, val) val = urlparse(val).path while len(val) > 1 and val[:2] == "//": val = val[1:] directory = os.path.dirname(val) if directory and not os.path.exists(directory): os.makedirs(directory) return f"sqlite:///{val}"
def normalise_filled(self, meta, val): val = sb.string_spec().normalise(meta, val) if len(val) != 12: raise BadSpecValue("serial must be 12 characters long", got=len(val)) try: binascii.unhexlify(val) except binascii.Error: raise BadSpecValue("serial must be a hex value") return val
def normalise(self, meta, val): if val is sb.NotSpecified: """Get the currently active/routing IP address of the local machine.""" s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(("10.255.255.255", 1)) val = s.getsockname()[0] except Exception: val = "127.0.0.1" finally: s.close() return sb.string_spec().normalise(meta, val)
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 AnimationHelpCommand(store.Command): """ Return help information for animations """ animations_runner = store.injected("animations") animation_name = dictobj.NullableField( sb.string_spec(), help="Optionally the specific name of an animation to get help about", ) async def execute(self): return await self.animations_runner.help(self.animation_name)
def normalise(self, meta, val): class Options: force = False fresh = False if val is sb.NotSpecified: return Options val = [a.strip() for a in sb.string_spec().normalise(meta, val).split(",")] if "fresh" in val: Options.fresh = True if "force" in val: Options.force = True return Options
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(self, meta, val): if isinstance(val, Services): return val val = sb.string_spec().normalise(meta, val) available = [] for name, member in Services.__members__.items(): if not name.startswith("RESERVED"): available.append(name) if name == val: return member raise BadSpecValue("Unknown service type", want=val, available=sorted(available), meta=meta)
class LanTarget(Target): """ Knows how to talk to a device over the local network. It's one configuration option is default_broadcast which says what address to broadcast discovery if broadcast is given to sender calls as True. """ gaps = dictobj.Field( Gaps( gap_between_results=0.4, gap_between_ack_and_res=0.2, timeouts=[(0.2, 0.2), (0.1, 0.5), (0.2, 1), (1, 5)], )) default_broadcast = dictobj.Field( sb.defaulted(sb.string_spec(), "255.255.255.255")) discovery_options = dictobj.Field(discovery_options_spec) session_kls = NetworkSession
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)
def normalise_filled(self, meta, val): val = sb.string_spec().normalise(meta, val) if not val.startswith("d073d5"): raise BadSpecValue("serials must start with d073d5", got=val, meta=meta) if len(val) != 12: raise BadSpecValue( "serials must be 12 characters long, like d073d5001337", got=val, meta=meta) try: binascii.unhexlify(val) except binascii.Error as error: raise BadSpecValue("serials must be valid hex", error=error, got=val, meta=meta) return val
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())
class DayDusk(dictobj.Spec): schedules = dictobj.Field( sb.dictof(sb.string_spec(), Schedule.FieldSpec(formatter=MergedOptionStringFormatter)))
def normalise_filled(self, meta, val): return ",".join(sb.listof(sb.string_spec()).normalise(meta, val))
("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, }
def normalise_filled(self, meta, val): return sb.string_spec().normalise(meta, val)
class Options(dictobj.Spec): one = dictobj.Field(sb.string_spec()) two = dictobj.Field(sb.string_spec())
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)
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")