Exemplo n.º 1
0
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)
Exemplo n.º 2
0
    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)
Exemplo n.º 3
0
 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)
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
    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)
Exemplo n.º 6
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
Exemplo n.º 7
0
 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
Exemplo n.º 8
0
    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)
Exemplo n.º 9
0
 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)
Exemplo n.º 10
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)
Exemplo n.º 11
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]
Exemplo n.º 12
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)
Exemplo n.º 13
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)
Exemplo n.º 14
0
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()))
Exemplo n.º 15
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)
Exemplo n.º 16
0
    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)
Exemplo n.º 17
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)
Exemplo n.º 18
0
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)
Exemplo n.º 19
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)
Exemplo n.º 20
0
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",
    )
Exemplo n.º 21
0
 class Not(dictobj.Spec):
     there = dictobj.Field(sb.listof(sb.integer_spec()))
Exemplo n.º 22
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())
Exemplo n.º 23
0
        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)
Exemplo n.º 24
0
 def normalise_filled(self, meta, val):
     return ",".join(sb.listof(sb.string_spec()).normalise(meta, val))
Exemplo n.º 25
0
        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":
Exemplo n.º 26
0
 class Options(dictobj.Spec):
     relays = dictobj.NullableField(sb.listof(Relay.FieldSpec()))
     relays_count = dictobj.NullableField(sb.integer_spec())
Exemplo n.º 27
0
    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)
Exemplo n.º 28
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")
Exemplo n.º 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)
Exemplo n.º 30
0
    ("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,
}