예제 #1
0
    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
예제 #2
0
    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
예제 #3
0
파일: store.py 프로젝트: thommo99/whirlwind
    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
예제 #4
0
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}")
예제 #5
0
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]
예제 #6
0
파일: types.py 프로젝트: delfick/photons
    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
예제 #7
0
        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, [])
예제 #8
0
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)
예제 #9
0
    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)
예제 #10
0
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
예제 #11
0
파일: database.py 프로젝트: delfick/photons
    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}"
예제 #12
0
    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
예제 #13
0
    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)
예제 #14
0
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)
예제 #15
0
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)
예제 #16
0
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)
예제 #17
0
    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
예제 #18
0
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)
예제 #19
0
    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)
예제 #20
0
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
예제 #21
0
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)
예제 #22
0
    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
예제 #23
0
 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())
예제 #24
0
class DayDusk(dictobj.Spec):
    schedules = dictobj.Field(
        sb.dictof(sb.string_spec(),
                  Schedule.FieldSpec(formatter=MergedOptionStringFormatter)))
예제 #25
0
 def normalise_filled(self, meta, val):
     return ",".join(sb.listof(sb.string_spec()).normalise(meta, val))
예제 #26
0
파일: types.py 프로젝트: delfick/photons
    ("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,
}
예제 #27
0
 def normalise_filled(self, meta, val):
     return sb.string_spec().normalise(meta, val)
예제 #28
0
 class Options(dictobj.Spec):
     one = dictobj.Field(sb.string_spec())
     two = dictobj.Field(sb.string_spec())
예제 #29
0
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)
예제 #30
0
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")