Ejemplo n.º 1
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)
Ejemplo n.º 2
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)
Ejemplo n.º 3
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
        )
Ejemplo n.º 4
0
class TileFallingOptions(AnimationOptions):
    num_iterations = dictobj.Field(sb.integer_spec, default=-1)
    random_orientations = dictobj.Field(sb.boolean, default=False)

    line_hues = dictobj.NullableField(split_by_comma(hue_range_spec()),
                                      default=Empty)
    fade_amount = dictobj.Field(sb.float_spec, default=0.1)
    line_tip_hues = dictobj.NullableField(split_by_comma(hue_range_spec()),
                                          default=Empty)

    min_speed = dictobj.Field(sb.float_spec, default=0.2)
    max_speed = dictobj.Field(sb.float_spec, default=0.4)

    def final_iteration(self, iteration):
        if self.num_iterations == -1:
            return False
        return self.num_iterations <= iteration
Ejemplo n.º 5
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)
Ejemplo n.º 6
0
class PowerToggleCommand(store.Command, DeviceChangeMixin):
    """
    Toggle the power of the lights you specify
    """

    duration = dictobj.NullableField(sb.float_spec,
                                     help="Duration of the toggle")

    group = dictobj.NullableField(
        sb.boolean, help="Whether to treat the lights as a group")

    async def execute(self):
        kwargs = {}
        if self.duration:
            kwargs["duration"] = self.duration
        if self.group is not None:
            kwargs["group"] = self.group
        msg = PowerToggle(**kwargs)
        return await self.send(msg, add_replies=False)
Ejemplo n.º 7
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)
Ejemplo n.º 8
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)
Ejemplo n.º 9
0
class AnimationInfoCommand(store.Command):
    """
    Return information about running animations
    """

    animations_runner = store.injected("animations")

    identity = dictobj.NullableField(
        sb.string_spec,
        help="optional identity of a specific animation you want info for",
    )

    async def execute(self):
        return self.animations_runner.info(expand=True, identity=self.identity)
Ejemplo n.º 10
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)

    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)

    duration = dictobj.NullableField(sb.float_spec)
    power = dictobj.NullableField(power_spec)
    transform_options = dictobj.NullableField(
        sb.dictof(sb.string_spec(), sb.boolean()))

    reference = dictobj.Field(reference_spec)

    @property
    def extra(self):
        keys_except = ["days", "hour", "minute", "reference"]
        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]
Ejemplo n.º 11
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)
Ejemplo n.º 12
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 = dictobj.Field(sb.typed(hp.Firmware), wrapper=sb.optional_spec)

    product_id = dictobj.Field(sb.integer_spec, wrapper=sb.optional_spec)

    def setup(self, *args, **kwargs):
        super().setup(*args, **kwargs)
        self.point_futures = {
            e: hp.ResettableFuture(name=f"Device({self.serial})::setup[point_futures.{e.name}]")
            for e in InfoPoints
        }
        self.point_futures[None] = hp.ResettableFuture(
            name=f"Device::setup({self.serial})[point_futures.None]"
        )
        self.refreshing = hp.ResettableFuture(name=f"Device({self.serial})::[refreshing]")

    @hp.memoized_property
    def final_future(self):
        return hp.create_future(name=f"Device({self.serial})::final_future")

    @property
    def property_fields(self):
        return [
            "group_id",
            "group_name",
            "location_name",
            "location_id",
            "firmware_version",
            "abilities",
            "product_name",
            "product_type",
        ]

    @property
    def firmware_version(self):
        if self.firmware is sb.NotSpecified:
            return self.firmware
        return str(self.firmware)

    @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"]
        del actual["firmware"]
        for key in self.property_fields:
            actual[key] = self[key]

        actual["cap"] = actual.pop("abilities")
        actual["product_type"] = actual["product_type"].name.lower()
        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 not in ("firmware", "limit")] + self.property_fields
        has_atleast_one_field = False

        for field in fields:
            val = self[field]
            if field == "abilities":
                field = "cap"
            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.StateLabel:
            self.label = pkt.label
            return InfoPoints.LABEL

        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 = hp.Firmware(pkt.version_major, pkt.version_minor, pkt.build)
            return InfoPoints.FIRMWARE

        elif pkt | DeviceMessages.StateVersion:
            self.product_id = pkt.product
            return InfoPoints.VERSION

    @property
    def cap(self):
        if self.product_id is sb.NotSpecified:
            return sb.NotSpecified

        product = Products[1, self.product_id]

        if self.firmware is sb.NotSpecified:
            return product.cap

        return product.cap(self.firmware.major, self.firmware.minor)

    @property
    def product_type(self):
        if self.cap is sb.NotSpecified:
            return DeviceType.UNKNOWN
        elif self.cap.is_light:
            return DeviceType.LIGHT
        else:
            return DeviceType.NON_LIGHT

    @property
    def abilities(self):
        if self.cap is sb.NotSpecified:
            return sb.NotSpecified
        return self.cap.abilities

    @property
    def product_name(self):
        if self.cap is sb.NotSpecified:
            return sb.NotSpecified
        return self.cap.product.friendly

    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, exc_typ=None, exc=None, tb=None):
        self.final_future.cancel()
        del self.final_future

    async def refresh_information_loop(self, sender, time_between_queries, collections):
        if self.refreshing.done():
            return

        self.refreshing.reset()
        self.refreshing.set_result(True)
        try:
            await self._refresh_information_loop(sender, time_between_queries, collections)
        finally:
            self.refreshing.reset()

    async def _refresh_information_loop(self, sender, time_between_queries, collections):
        points = iter(itertools.cycle(list(InfoPoints)))
        nxt = next(points)

        def should_send_point(point):
            if point.value.condition and not point.value.condition(self):
                return False

            fut = self.point_futures[point]

            if not fut.done():
                return True

            refresh = refreshes[point]
            if refresh is None:
                return False

            if time.time() - fut.result() < refreshes[point]:
                return False

            return True

        def find_point():
            nonlocal nxt
            started = nxt

            if should_send_point(nxt):
                e = nxt
                nxt = next(points)
                return e

            while True:
                nxt = next(points)
                if nxt is started:
                    return

                if should_send_point(nxt):
                    e = nxt
                    nxt = next(points)
                    return e

        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 with hp.tick(
                1,
                final_future=self.final_future,
                name=f"Device({self.serial})::refresh_information_loop[tick]",
            ) as ticks:
                async for info in ticks:
                    if self.final_future.done():
                        return

                    e = find_point()
                    if e is None:
                        continue

                    if self.serial not in sender.found:
                        break

                    t = yield e.value.msg
                    await t

        msg = FromGenerator(gen, reference_override=self.serial)
        async for pkt in sender(msg, self.serial, limit=self.limit, find_timeout=5):
            if pkt | CoreMessages.StateUnhandled:
                continue
            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, points=None):
        if fltr is None:
            return True

        pts = points
        if pts is None:
            pts = list(self.points_from_fltr(fltr))

        async def gen(reference, sender, **kwargs):
            for e in pts:
                if e.value.condition and not e.value.condition(self):
                    continue
                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):
            if pkt | CoreMessages.StateUnhandled:
                continue
            point = self.set_from_pkt(pkt, collections)
            self.point_futures[point].reset()
            self.point_futures[point].set_result(time.time())

        # Without the information loop we ask for all the messages before getting replies
        # And so the switch doesn't respond to LIGHT_STATE
        # And it doesn't know to send LABEL yet
        # So we force that to happen!
        if points is None and InfoPoints.LABEL in pts and self.product_type is DeviceType.NON_LIGHT:
            if self.label is sb.NotSpecified:
                await self.matches(sender, fltr, collections, points=[InfoPoints.LABEL])

        return self.matches_fltr(fltr)
Ejemplo n.º 13
0
class RunEffectCommand(EffectCommand):
    """
    Start or stop a firmware animation on devices that support them
    """

    matrix_animation = dictobj.NullableField(
        enum_spec(None, TileEffectType, unpacking=True),
        help="""
            The animation to run for matrix devices.

            This can be FLAME, MORPH or OFF.

            If you don't supply this these devices will not run any animation"
        """,
    )
    matrix_options = dictobj.Field(
        sb.dictionary_spec,
        help="""
                Any options to give to the matrix animation. For example duration
            """,
    )

    linear_animation = dictobj.NullableField(
        enum_spec(None, MultiZoneEffectType, unpacking=True),
        help="""
            The animation to run for linear devices.

            Currently only MOVE or OFF are supported

            If you don't supply this these devices will not run any animation"
        """,
    )
    linear_options = dictobj.Field(
        sb.dictionary_spec,
        help="""
                Options for the linear firmware effect:

                - speed: duration in seconds to complete one cycle of the effect
                - duration: time in seconds the effect will run.
                - direction: either "left" or "right" (default: "right")

                If duration is not specified or set to 0, the effect will run
                until it is manually stopped.

            """,
    )

    async def execute(self):
        async def gen(reference, afr, **kwargs):
            if self.apply_theme:
                yield self.theme_msg()

            if self.matrix_animation:
                yield SetTileEffect(self.matrix_animation,
                                    **self.matrix_options)

            if self.linear_animation:
                yield SetZonesEffect(self.linear_animation,
                                     **self.linear_options)

        return await self.send(FromGeneratorPerSerial(gen), add_replies=False)
Ejemplo n.º 14
0
 class Options(dictobj.Spec):
     port = dictobj.NullableField(sb.integer_spec())
     service = dictobj.Field(sb.overridden(Services.UDP))
     state_service = dictobj.Field(sb.overridden(Services.UDP))
Ejemplo n.º 15
0
class RunEffectCommand(EffectCommand):
    """
    Start or stop a firmware animation on devices that support them
    """

    matrix_animation = dictobj.NullableField(
        enum_spec(None, TileEffectType, unpacking=True),
        help="""
            The animation to run for matrix devices.

            This can be FLAME, MORPH or OFF.

            If you don't supply this these devices will not run any animation"
        """,
    )
    matrix_options = dictobj.Field(
        sb.dictionary_spec,
        help="""
                Any options to give to the matrix animation. For example duration
            """,
    )

    linear_animation = dictobj.NullableField(
        enum_spec(None, MultiZoneEffectType, unpacking=True),
        help="""
            The animation to run for linear devices.

            Currently only MOVE or OFF are supported

            If you don't supply this these devices will not run any animation"
        """,
    )
    linear_options = dictobj.Field(
        sb.dictionary_spec,
        help="""
                Any options to give to the linear animation. For example duration
            """,
    )

    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)
        theme_msg = self.theme_msg(gatherer)

        async def gen(reference, afr, **kwargs):
            if self.apply_theme:
                yield theme_msg

            if self.matrix_animation:
                yield SetTileEffect(self.matrix_animation,
                                    gatherer=gatherer,
                                    **self.matrix_options)

            if self.linear_animation:
                yield SetZonesEffect(self.linear_animation,
                                     gatherer=gatherer,
                                     **self.linear_options)

        script = self.target.script(FromGeneratorPerSerial(gen))
        return await chp.run(
            script,
            fltr,
            self.finder,
            message_timeout=self.timeout,
            add_replies=False,
        )
Ejemplo n.º 16
0
 class Options(Operator.Options):
     an_int = dictobj.NullableField(sb.integer_spec())
     a_boolean = dictobj.Field(sb.boolean, default=True)
Ejemplo n.º 17
0
 class Options(dictobj.Spec):
     relays = dictobj.NullableField(sb.listof(Relay.FieldSpec()))
     relays_count = dictobj.NullableField(sb.integer_spec())
Ejemplo n.º 18
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})
Ejemplo n.º 19
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)
Ejemplo n.º 20
0
 class Options(dictobj.Spec):
     got_event_fut = dictobj.NullableField(sb.any_spec())
     record_annotations = dictobj.Field(sb.boolean, default=False)
     record_events_store = dictobj.Field(sb.any_spec(), wrapper=sb.required)
Ejemplo n.º 21
0
class PhotonsApp(dictobj.Spec):
    """
    The main photons_app object.

    .. dictobj_params::
    """

    config = dictobj.Field(
        sb.file_spec, wrapper=sb.optional_spec, help="The root configuration file"
    )
    extra = dictobj.Field(
        sb.string_spec, default="", help="The arguments after the ``--`` in the commandline"
    )
    debug = dictobj.Field(sb.boolean, default=False, help="Whether we are in debug mode or not")
    artifact = dictobj.Field(
        default="", format_into=sb.string_spec, help="The artifact string from the commandline"
    )
    reference = dictobj.Field(
        default="", format_into=sb.string_spec, help="The device(s) to send commands to"
    )
    cleaners = dictobj.Field(
        lambda: sb.overridden([]),
        help="A list of functions to call when cleaning up at the end of the program",
    )
    default_activate = dictobj.NullableField(
        sb.listof(sb.string_spec()), help="A list of photons modules to load by default"
    )
    task_specifier = dictobj.Field(
        sb.delayed(task_specifier_spec()), help="Used to determine chosen task and target"
    )

    @hp.memoized_property
    def final_future(self):
        return self.loop.create_future()

    @hp.memoized_property
    def graceful_final_future(self):
        fut = self.loop.create_future()
        fut.setup = False
        return fut

    @hp.memoized_property
    def loop(self):
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        if self.debug:
            loop.set_debug(True)
        return loop

    @hp.memoized_property
    def extra_as_json(self):
        options = "{}" if self.extra in (None, "", sb.NotSpecified) else self.extra

        location = None
        if options.startswith("file://"):
            parsed = urlparse(options)
            location = os.path.abspath(f"{parsed.netloc}{unquote(parsed.path)}")
            if not os.path.exists(location):
                raise BadOption(f"The path {location} does not exist")

            with open(location, "r") as fle:
                options = fle.read()

        try:
            return json.loads(options)
        except (TypeError, ValueError) as error:
            kwargs = {"error": error}
            if location:
                kwargs["read_from"] = location
            raise BadOption("The options after -- wasn't valid json", **kwargs)

    def separate_final_future(self, sleep=0):
        other_future = asyncio.Future()

        def stop():
            other_future.cancel()

        self.loop.remove_signal_handler(signal.SIGTERM)
        self.loop.add_signal_handler(signal.SIGTERM, stop)

        class CM:
            async def __aenter__(s):
                return other_future

            async def __aexit__(s, exc_typ, exc, tb):
                if sleep > 0:
                    await asyncio.sleep(sleep)
                self.final_future.cancel()

        return CM()

    @contextmanager
    def using_graceful_future(self):
        """
        This context manager is used so that a server may shut down before
        the real final_future is stopped.

        This is useful because many photons resources will stop themselves
        when the real final_future is stopped.

        But we may want to stop (say a server) before we run cleanup activities
        and mark final_future as done.

        Usage is like::

            with photons_app.graceful_final_future() as final_future:
                try:
                    await final_future
                except ApplicationStopped:
                    await asyncio.sleep(7)
        """
        final_future = self.final_future

        graceful_future = self.graceful_final_future
        graceful_future.setup = True

        reinstate_handler = False

        if platform.system() != "Windows":

            def stop():
                if not graceful_future.done():
                    graceful_future.set_exception(ApplicationStopped)

            reinstate_handler = self.loop.remove_signal_handler(signal.SIGTERM)
            self.loop.add_signal_handler(signal.SIGTERM, stop)

        yield graceful_future

        # graceful future is no longer in use
        graceful_future.setup = False

        if reinstate_handler:

            def stop():
                if not final_future.done():
                    final_future.set_exception(ApplicationStopped)

            self.loop.remove_signal_handler(signal.SIGTERM)
            self.loop.add_signal_handler(signal.SIGTERM, stop)

    async def cleanup(self, targets):
        for cleaner in self.cleaners:
            try:
                await cleaner()
            except asyncio.CancelledError:
                break
            except KeyboardInterrupt:
                break
            except (RuntimeWarning, Exception):
                exc_info = sys.exc_info()
                log.error(exc_info[1], exc_info=exc_info)

        for target in targets:
            try:
                if hasattr(target, "finish"):
                    await target.finish()
            except asyncio.CancelledError:
                break
            except KeyboardInterrupt:
                break
            except (RuntimeWarning, Exception):
                exc_info = sys.exc_info()
                log.error(exc_info[1], exc_info=exc_info)
Ejemplo n.º 22
0
from delfick_project.norms import dictobj, sb

refresh_field = dictobj.NullableField(
    sb.boolean,
    help="""
        Whether to refresh our idea of what is on the network

        If this is False then we will use the cached notion of what's on the network.
      """,
)

timeout_field = dictobj.Field(
    sb.float_spec,
    default=20,
    help="The max amount of time we wait for replies from the lights")

matcher_field = dictobj.NullableField(
    sb.or_spec(sb.string_spec(), sb.dictionary_spec()),
    help="""
        What lights to target. If this isn't specified then we interact with all
        the lights that can be found on the network.

        This can be specfied as either a space separated key=value string or as
        a dictionary.

        For example,
        "label=kitchen,bathroom location_name=home"
        or
        ``{"label": ["kitchen", "bathroom"], "location_name": "home"}``

        See https://delfick.github.io/photons-core/modules/photons_device_finder.html#valid-filters
Ejemplo n.º 23
0
class Overrides(dictobj.Spec):
    hue = dictobj.NullableField(sb.float_spec)
    saturation = dictobj.NullableField(sb.float_spec)
    brightness = dictobj.NullableField(sb.float_spec)
    kelvin = dictobj.NullableField(sb.integer_spec)
Ejemplo n.º 24
0
pkt_type_field = dictobj.Field(
    sb.or_spec(sb.integer_spec(), sb.string_spec()),
    wrapper=sb.required,
    help="""
        The type of packet to send to the lights. This can be a number or
        the name of the packet as known by the photons framework.

        A list of what's available can be found at
        https://photons.delfick.com/interacting/packets.html
      """,
)

pkt_args_field = dictobj.NullableField(
    sb.dictionary_spec(),
    help="""
        A dictionary of fields that make up the payload of the packet we
        are sending to the lights.
      """,
)


@store.command(name="status")
class StatusCommand(store.Command):
    async def execute(self):
        return {"on": True}


@store.command(name="discover")
class DiscoverCommand(store.Command, DeviceChangeMixin):
    """
    Display information about all the devices that can be found on the network