Example #1
0
class TileDiceCommand(store.Command):
    """
    Show dice on provided tiles and return the hsbk values that were sent
    """

    finder = store.injected("finder")
    target = store.injected("targets.lan")

    matcher = df.matcher_field
    refresh = df.refresh_field

    async def execute(self):
        fltr = chp.filter_from_matcher(self.matcher, self.refresh)

        result = chp.ResultBuilder()
        afr = await self.finder.args_for_run()
        reference = self.finder.find(filtr=fltr)

        serials = await tile_serials_from_reference(self.target, reference,
                                                    afr)
        if not serials:
            raise FoundNoDevices("Didn't find any tiles")

        await self.target.script(DeviceMessages.SetPower(level=65535)
                                 ).run_with_all(serials,
                                                afr,
                                                error_catcher=result.error)

        result.result["results"]["tiles"] = await tile_dice(
            self.target, serials, afr, error_catcher=result.error)

        return result
Example #2
0
class PowerToggleCommand(store.Command):
    """
    Toggle the power of the lights you specify
    """

    finder = store.injected("finder")
    target = store.injected("targets.lan")

    matcher = df.matcher_field
    timeout = df.timeout_field
    refresh = df.refresh_field

    duration = dictobj.NullableField(sb.float_spec)

    async def execute(self):
        fltr = chp.filter_from_matcher(self.matcher, self.refresh)

        kwargs = {}
        if self.duration:
            kwargs["duration"] = self.duration
        msg = PowerToggle(**kwargs)

        script = self.target.script(msg)
        return await chp.run(
            script, fltr, self.finder, add_replies=False, message_timeout=self.timeout
        )
Example #3
0
class EffectCommand(store.Command):
    finder = store.injected("finder")
    target = store.injected("targets.lan")

    timeout = df.timeout_field
    matcher = df.matcher_field
    refresh = df.refresh_field

    apply_theme = dictobj.Field(
        sb.boolean,
        default=False,
        help=
        "Whether to apply a theme to the devices before running an animation",
    )
    theme_options = dictobj.Field(
        sb.dictionary_spec, help="Any options to give to applying a theme")

    def theme_msg(self, gatherer):
        everything = {}
        theme_options = dict(self.theme_options)

        if "overrides" in theme_options:
            everything["overrides"] = theme_options["overrides"]

        if "colors" not in theme_options:
            theme_options["colors"] = default_colors

        options = ThemeOptions.FieldSpec().normalise(Meta(everything, []),
                                                     theme_options)
        return ApplyTheme.script(options, gatherer=gatherer)
Example #4
0
class HighlightArrangeCommand(store.Command):
    finder = store.injected("finder")
    target = store.injected("targets.lan")
    arranger = store.injected("arranger")

    serial = dictobj.Field(sb.string_spec, wrapper=sb.required)
    tile_index = dictobj.Field(sb.integer_spec, wrapper=sb.required)

    async def execute(self):
        afr = await self.finder.args_for_run()
        await self.arranger.highlight(self.serial, self.tile_index,
                                      self.target, afr)
        return {"ok": True}
Example #5
0
class LeaveArrangeCommand(store.Command):
    finder = store.injected("finder")
    target = store.injected("targets.lan")
    arranger = store.injected("arranger")
    request_handler = store.injected("request_handler")

    async def execute(self):
        if not isinstance(self.request_handler, websocket.WebSocketHandler):
            raise NotAWebSocket()

        afr = await self.finder.args_for_run()
        await self.arranger.leave_arrange(self.request_handler.key,
                                          self.target, afr)
        return {"ok": True}
Example #6
0
class TransformCommand(store.Command):
    """
    Apply a http api like transformation to the lights
    """

    finder = store.injected("finder")
    target = store.injected("targets.lan")

    matcher = df.matcher_field
    timeout = df.timeout_field
    refresh = df.refresh_field

    transform = dictobj.Field(
        sb.dictionary_spec(),
        wrapper=sb.required,
        help="""
            A dictionary of what options to use to transform the lights with.

            For example,
            ``{"power": "on", "color": "red"}``

            Or,
            ``{"color": "blue", "effect": "breathe", "cycles": 5}``
          """,
    )

    transform_options = dictobj.Field(
        sb.dictionary_spec(),
        help="""
            A dictionay of options that modify the way the tranform
            is performed:

            keep_brightness
                Ignore brightness options in the request

            transition_color
                If the light is off and we power on, setting this to True will mean the
                color of the light is not set to the new color before we make it appear
                to be on. This defaults to False, which means it will appear to turn on
                with the new color
            """,
    )

    async def execute(self):
        fltr = chp.filter_from_matcher(self.matcher, self.refresh)
        msg = Transformer.using(self.transform, **self.transform_options)
        script = self.target.script(msg)
        return await chp.run(
            script, fltr, self.finder, add_replies=False, message_timeout=self.timeout
        )
Example #7
0
class ChangeArrangeCommand(store.Command):
    finder = store.injected("finder")
    target = store.injected("targets.lan")
    arranger = store.injected("arranger")

    serial = dictobj.Field(sb.string_spec, wrapper=sb.required)
    tile_index = dictobj.Field(sb.integer_spec, wrapper=sb.required)
    left_x = dictobj.Field(sb.integer_spec, wrapper=sb.required)
    top_y = dictobj.Field(sb.integer_spec, wrapper=sb.required)

    async def execute(self):
        afr = await self.finder.args_for_run()
        return await self.arranger.change(self.serial, self.tile_index,
                                          self.left_x, self.top_y, self.target,
                                          afr)
Example #8
0
class StartArrangeCommand(store.Command):
    finder = store.injected("finder")
    target = store.injected("targets.lan")
    arranger = store.injected("arranger")
    request_handler = store.injected("request_handler")

    async def execute(self):
        if not isinstance(self.request_handler, websocket.WebSocketHandler):
            raise NotAWebSocket()

        afr = await self.finder.args_for_run()
        serials = await tile_serials_from_reference(self.target,
                                                    self.finder.find(), afr)
        return await self.arranger.start_arrange(serials,
                                                 self.request_handler.key,
                                                 self.target, afr)
Example #9
0
class DiscoverCommand(store.Command):
    """
    Display information about all the devices that can be found on the network
    """

    finder = store.injected("finder")
    matcher = df.matcher_field
    refresh = df.refresh_field

    just_serials = dictobj.Field(
        sb.boolean,
        default=False,
        help="Just return a list of serials instead of all the information per device",
    )

    async def execute(self):
        fltr = chp.filter_from_matcher(self.matcher)

        if self.refresh is not None:
            fltr.force_refresh = self.refresh

        if self.just_serials:
            return await self.finder.serials(filtr=fltr)
        else:
            return await self.finder.info_for(filtr=fltr)
Example #10
0
class StartAnimateCommand(store.Command):
    """
    Start a tile animation
    """

    finder = store.injected("finder")
    target = store.injected("targets.lan")
    animations = store.injected("animations")

    matcher = df.matcher_field
    refresh = df.refresh_field

    animation = dictobj.Field(valid_animation_name, wrapper=sb.required)
    options = dictobj.Field(sb.dictionary_spec)
    stop_conflicting = dictobj.Field(sb.boolean, default=False)

    async def execute(self):
        afr = await self.finder.args_for_run()
        fltr = chp.filter_from_matcher(self.matcher)

        if self.refresh is not None:
            fltr.force_refresh = self.refresh

        found_serials = await self.finder.serials(filtr=fltr)

        fltr = chp.filter_from_matcher({
            "serial": found_serials,
            "cap": "chain"
        })
        try:
            serials = await self.finder.serials(filtr=fltr)
        except FoundNoDevices:
            raise FoundNoTiles(matcher=self.matcher)

        find_fltr = chp.clone_filter(fltr, force_refresh=False)
        reference = self.finder.find(filtr=find_fltr)

        animation_id = self.animations.start(
            self.animation,
            self.target,
            serials,
            reference,
            afr,
            self.options,
            stop_conflicting=self.stop_conflicting,
        )
        return {"animation_id": animation_id}
Example #11
0
class HelpCommand(store.Command):
    """
    Display the documentation for the specified command
    """

    path = store.injected("path")
    store = store.injected("store")

    command = dictobj.Field(sb.string_spec,
                            default="help",
                            help="The command to show help for")

    @property
    def command_kls(self):
        available = self.store.paths[self.path]
        if self.command not in available:
            raise NoSuchCommand(wanted=self.command,
                                available=sorted(available))
        return available[self.command]["kls"]

    async def execute(self):
        header = f"Command {self.command}"
        kls = self.command_kls
        doc = dedent(getattr(kls, "__help__", kls.__doc__))

        fields = chp.fields_description(kls)
        fields_string = ""
        if fields:
            fields_string = ["", "Arguments\n---------", ""]
            for name, type_info, desc in fields:
                fields_string.append(f"{name}: {type_info}")
                for line in desc.split("\n"):
                    if not line.strip():
                        fields_string.append("")
                    else:
                        fields_string.append(f"\t{line}")
                fields_string.append("")
            fields_string = "\n".join(fields_string)

        extra = ""
        if self.command == "help":
            extra = "\nAvailable commands:\n{}".format("\n".join(
                f" * {name}" for name in sorted(self.store.paths[self.path])))

        return f"{header}\n{'=' * len(header)}\n{doc}{fields_string}{extra}"
Example #12
0
class RemoveAllAnimateCommand(store.Command):
    """
    Stop and remove all animations
    """

    animations = store.injected("animations")

    async def execute(self):
        self.animations.remove_all()
        return {"success": True}
Example #13
0
class StatusAnimateCommand(store.Command):
    """
    Return status of an animation
    """

    animations = store.injected("animations")

    animation_id = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec)

    async def execute(self):
        return self.animations.status(self.animation_id)
Example #14
0
class RemoveAnimateCommand(store.Command):
    """
    Stop and remove a tile animation
    """

    animations = store.injected("animations")

    animation_id = dictobj.Field(sb.string_spec, wrapper=sb.required)

    async def execute(self):
        self.animations.remove(self.animation_id)
        return {"success": True}
Example #15
0
class PauseAnimateCommand(store.Command):
    """
    Pause a tile animation
    """

    animations = store.injected("animations")

    animation_id = dictobj.Field(sb.string_spec, wrapper=sb.required)

    async def execute(self):
        await self.animations.pause(self.animation_id)
        return {"success": True}
Example #16
0
class QueryCommand(store.Command):
    """
    Send a pkt to devices and return the result
    """

    finder = store.injected("finder")
    target = store.injected("targets.lan")
    protocol_register = store.injected("protocol_register")

    matcher = df.matcher_field
    timeout = df.timeout_field
    refresh = df.refresh_field

    pkt_type = df.pkt_type_field
    pkt_args = df.pkt_args_field

    async def execute(self):
        fltr = chp.filter_from_matcher(self.matcher, self.refresh)
        msg = chp.make_message(self.protocol_register, self.pkt_type, self.pkt_args)
        script = self.target.script(msg)
        return await chp.run(script, fltr, self.finder, message_timeout=self.timeout)
Example #17
0
class AvailableAnimateCommand(store.Command):
    """
    Return available animations
    """

    animations = store.injected("animations")

    async def execute(self):
        response = []
        for name in sorted(self.animations.animators):
            response.append({"name": name})

        return {"animations": response}
Example #18
0
class SetCommand(store.Command):
    """
    Send a pkt to devices. This is the same as query except res_required is False
    and results aren't returned
    """

    finder = store.injected("finder")
    target = store.injected("targets.lan")
    protocol_register = store.injected("protocol_register")

    matcher = df.matcher_field
    timeout = df.timeout_field
    refresh = df.refresh_field

    pkt_type = df.pkt_type_field
    pkt_args = df.pkt_args_field

    async def execute(self):
        fltr = chp.filter_from_matcher(self.matcher, self.refresh)
        msg = chp.make_message(self.protocol_register, self.pkt_type, self.pkt_args)
        msg.res_required = False
        script = self.target.script(msg)
        return await chp.run(script, fltr, self.finder, message_timeout=self.timeout)
Example #19
0
class SceneDeleteCommand(store.Command):
    """
    Delete a scene
    """

    db_queue = store.injected("db_queue")

    uuid = dictobj.Field(
        sb.string_spec, wrapper=sb.required, help="The uuid of the scene to delete"
    )

    async def execute(self):
        def delete(db):
            for thing in db.queries.get_scenes(uuid=self.uuid).all():
                db.delete(thing)

            return {"deleted": True}

        return await self.db_queue.request(delete)
Example #20
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)
Example #21
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)
Example #22
0
class StatusStreamAnimateCommand(store.Command):
    """
    An endless stream of updates to animation status
    """

    finder = store.injected("finder")
    target = store.injected("targets.lan")
    animations = store.injected("animations")
    progress_cb = store.injected("progress_cb")
    final_future = store.injected("final_future")
    request_handler = store.injected("request_handler")

    async def execute(self):
        if not isinstance(self.request_handler, websocket.WebSocketHandler):
            raise NotAWebSocket(
                "status stream can only be called from a websocket")

        u, fut = self.animations.add_listener()
        self.progress_cb({"available": self.animations.available()})

        while True:
            futs = [
                self.request_handler.connection_future, self.final_future, fut
            ]
            await asyncio.wait(futs, return_when=asyncio.FIRST_COMPLETED)

            if self.request_handler.connection_future.done(
            ) or self.final_future.done():
                log.info(hp.lc("Connection to status stream went away"))
                afr = await self.finder.args_for_run()
                await self.animations.remove_listener(u,
                                                      self.request_handler.key,
                                                      self.target, afr)
                break

            self.progress_cb(
                {"status": self.animations.status(sb.NotSpecified)})
            fut.reset()
Example #23
0
class StatusEffectCommand(store.Command):
    """
    Returns the current status of effects on devices that support them
    """
    finder = store.injected("finder")
    target = store.injected("targets.lan")

    timeout = df.timeout_field
    matcher = df.matcher_field
    refresh = df.refresh_field

    def convert_enums(self, info):
        if isinstance(info, Product):
            info = {
                "pid": info.pid,
                "vid": info.vendor.vid,
                "cap": info.cap.as_dict(),
                "name": info.name,
            }

        if info is Skip:
            return {"type": "SKIP"}
        elif isinstance(info, dict):
            result = {}
            for k, v in info.items():
                result[k] = self.convert_enums(v)
            return result
        elif isinstance(info, list):
            return [self.convert_enums(i) for i in info]
        elif isinstance(info, Enum):
            return info.name
        else:
            return info

    async def execute(self):
        fltr = chp.filter_from_matcher(self.matcher)

        if self.refresh is not None:
            fltr.force_refresh = self.refresh

        gatherer = Gatherer(self.target)
        plans = make_plans("firmware_effects", "capability")

        afr = await self.finder.args_for_run()
        serials = await self.finder.serials(filtr=fltr)

        result = chp.ResultBuilder()
        result.add_serials(serials)

        async for serial, _, info in gatherer.gather_per_serial(
                plans,
                serials,
                afr,
                error_catcher=result.error,
                message_timeout=self.timeout,
        ):
            r = {}
            if "capability" in info:
                r["product"] = info["capability"]["product"]
            if "firmware_effects" in info:
                r["effect"] = info["firmware_effects"]
            result.result["results"][serial] = self.convert_enums(r)

        return result
Example #24
0
class SceneApplyCommand(store.Command):
    """
    Apply a scene
    """

    finder = store.injected("finder")
    target = store.injected("targets.lan")
    db_queue = store.injected("db_queue")

    matcher = df.matcher_field
    timeout = df.timeout_field

    uuid = dictobj.Field(sb.string_spec, wrapper=sb.required, help="The uuid of the scene to apply")

    overrides = dictobj.Field(sb.dictionary_spec, help="Overrides to the scene")

    async def execute(self):
        ts = []
        result = chp.ResultBuilder()

        def get(db):
            info = []
            for scene in db.queries.get_scenes(uuid=self.uuid).all():
                info.append(scene.as_object())
            if not info:
                raise NoSuchScene(uuid=self.uuid)
            return info

        for scene in await self.db_queue.request(get):
            fltr = chp.filter_from_matcher(scene.matcher, False)

            if scene.zones:
                multizonefltr = self.clone_fltr_with_cap(fltr, "multizone")
                ts.append(hp.async_as_background(self.apply_zones(multizonefltr, scene, result)))

                notmultizonefltr = self.clone_fltr_with_no_cap(fltr, "multizone")
                ts.append(hp.async_as_background(self.transform(notmultizonefltr, scene, result)))

            elif scene.chain:
                chainfltr = self.clone_fltr_with_cap(fltr, "chain")
                ts.append(hp.async_as_background(self.apply_chain(chainfltr, scene, result)))

                notchainfltr = self.clone_fltr_with_no_cap(fltr, "chain")
                ts.append(hp.async_as_background(self.transform(notchainfltr, scene, result)))

            else:
                ts.append(hp.async_as_background(self.transform(fltr, scene, result)))

        for t in ts:
            try:
                await t
            except Exception as error:
                result.error(error)

        return result

    async def transform(self, fltr, scene, result):
        options = scene.transform_options
        options.update(self.overrides)

        msg = Transformer.using(options)
        script = self.target.script(msg)
        try:
            await chp.run(
                script,
                fltr,
                self.finder,
                add_replies=False,
                message_timeout=self.timeout,
                result=result,
            )
        except FoundNoDevices:
            pass

    async def apply_zones(self, fltr, scene, result):
        script = self.target.script(list(scene.zone_msgs(self.overrides)))
        try:
            await chp.run(
                script,
                fltr,
                self.finder,
                add_replies=False,
                message_timeout=self.timeout,
                result=result,
            )
        except FoundNoDevices:
            pass

    async def apply_chain(self, fltr, scene, result):
        script = self.target.script(list(scene.chain_msgs(self.overrides)))
        try:
            await chp.run(
                script,
                fltr,
                self.finder,
                add_replies=False,
                message_timeout=self.timeout,
                result=result,
            )
        except FoundNoDevices:
            pass

    def clone_fltr_with_no_cap(self, fltr, cap):
        clone = fltr.clone()
        if clone.cap is sb.NotSpecified:
            clone.cap = [f"not_{cap}"]
        else:
            clone.cap.append(cap)
            clone.cap = [c for c in clone.cap if c != cap]
        return clone

    def clone_fltr_with_cap(self, fltr, cap):
        clone = fltr.clone()
        if clone.cap is sb.NotSpecified:
            clone.cap = [cap]
        else:
            clone.cap.append(cap)
            clone.cap = [c for c in clone.cap if c != f"not_{cap}"]
        return clone
Example #25
0
class SceneCaptureCommand(store.Command):
    """
    Capture a scene
    """

    path = store.injected("path")
    finder = store.injected("finder")
    target = store.injected("targets.lan")
    db_queue = store.injected("db_queue")
    executor = store.injected("executor")

    matcher = df.matcher_field
    refresh = df.refresh_field

    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")

    just_return = dictobj.Field(
        sb.boolean,
        default=False,
        help="Just return the scene rather than storing it in the database",
    )

    async def execute(self):
        fltr = chp.filter_from_matcher(self.matcher, self.refresh)
        details = await self.finder.info_for(filtr=fltr)

        msgs = []
        for serial, info in details.items():
            msgs.append(DeviceMessages.GetPower(target=serial))

            if "multizone" in info["cap"]:
                msgs.append(
                    MultiZoneMessages.GetColorZones(start_index=0, end_index=255, target=serial)
                )
            elif "chain" in info["cap"]:
                msgs.append(
                    TileMessages.Get64(tile_index=0, length=5, x=0, y=0, width=8, target=serial)
                )
            else:
                msgs.append(LightMessages.GetColor(target=serial))

        state = defaultdict(dict)
        afr = await self.finder.args_for_run()
        async for pkt, _, _ in self.target.script(msgs).run_with(
            None, afr, multiple_replies=True, first_wait=0.5
        ):
            if pkt | DeviceMessages.StatePower:
                state[pkt.serial]["power"] = pkt.level != 0
            elif pkt | LightMessages.LightState:
                hsbk = f"kelvin:{pkt.kelvin} saturation:{pkt.saturation} brightness:{pkt.brightness} hue:{pkt.hue}"
                state[pkt.serial]["color"] = hsbk
            elif pkt | MultiZoneMessages.StateMultiZone:
                if "zones" not in state[pkt.serial]:
                    state[pkt.serial]["zones"] = {}
                for i, zi in enumerate(range(pkt.zone_index, pkt.zone_index + 8)):
                    c = pkt.colors[i]
                    state[pkt.serial]["zones"][zi] = [c.hue, c.saturation, c.brightness, c.kelvin]
            elif pkt | TileMessages.State64:
                if "chain" not in state[pkt.serial]:
                    state[pkt.serial]["chain"] = {}
                colors = [[c.hue, c.saturation, c.brightness, c.kelvin] for c in pkt.colors]
                state[pkt.serial]["chain"][pkt.tile_index] = colors

        scene = []
        for serial, info in sorted(state.items()):
            if "zones" in info:
                info["zones"] = [hsbk for _, hsbk in sorted(info["zones"].items())]
            if "chain" in info:
                info["chain"] = [hsbks for _, hsbks in sorted(info["chain"].items())]

            scene.append({"matcher": {"serial": serial}, **info})

        if self.just_return:
            return scene

        args = {
            "uuid": self.uuid,
            "scene": scene,
            "label": self.label,
            "description": self.description,
        }
        return await self.executor.execute(self.path, {"command": "scene_change", "args": args})