示例#1
0
 def __new__(*args, **kwargs):
     kls = type.__new__(*args, **kwargs)
     if kls.min_extended_fw is not None:
         spec = sb.tuple_spec(sb.integer_spec(), sb.integer_spec())
         kls.min_extended_fw = spec.normalise(Meta.empty(),
                                              kls.min_extended_fw)
     return kls
示例#2
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)
示例#3
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]
示例#4
0
文件: types.py 项目: delfick/photons
    def normalise_filled(self, meta, val):
        """
        Convert to and from a string into an integer
        """
        if self.unpacking:
            if type(val) is str:
                if not regexes["version_number"].match(val):
                    raise BadSpecValue(r"Expected string to match \d+.\d+",
                                       got=val,
                                       meta=meta)
                return val

            val = sb.integer_spec().normalise(meta, val)
            major = val >> 0x10
            minor = val & 0xFFFF
            return f"{major}.{minor}"
        else:
            if type(val) is int:
                return val

            val = sb.string_spec().normalise(meta, val)
            m = regexes["version_number"].match(val)
            if not m:
                raise BadSpecValue(
                    r"Expected version string to match (\d+.\d+)",
                    wanted=val,
                    meta=meta)

            groups = m.groupdict()
            major = int(groups["major"])
            minor = int(groups["minor"])
            return (major << 0x10) + minor
示例#5
0
文件: types.py 项目: delfick/photons
    def normalise_filled(self, meta, val):
        """
        If we don't have an enum or bitmask

        * Return the value as is if it's a float and we allow floats
        * Return the value as is if it's an integer
        * Complain otherwise

        If we have an enum option then convert the value into that enum

        If we have a bitmask option then convert the value into a list of the
        applicable bitmasks.
        """
        if self.enum is None and self.bitmask is None:
            if self.allow_float and type(val) is float:
                return val
            return sb.integer_spec().normalise(meta, val)

        if self.enum:
            kwargs = dict(unpacking=self.unpacking,
                          allow_unknown=self.unknown_enum_values)
            return enum_spec(self.pkt, self.enum,
                             **kwargs).normalise(meta, val)
        else:
            return bitmask_spec(self.pkt,
                                self.bitmask,
                                unpacking=self.unpacking).normalise(meta, val)
示例#6
0
 def __init__(self):
     self.specs = [
         range_spec(0, 360),
         range_spec(0, 1),
         range_spec(0, 1),
         range_spec(1500, 9000, spec=sb.integer_spec()),
     ]
示例#7
0
    async def execute_task(self, **kwargs):
        options = sb.set_options(
            indication=sb.required(sb.boolean()),
            duration_s=sb.required(sb.integer_spec()),
        ).normalise(Meta.empty(), self.photons_app.extra_as_json)

        await self.target.send(SetCleanConfig(**options), self.reference,
                               **kwargs)
示例#8
0
    def normalise_empty(self, meta):
        env = os.environ.get("NOISY_NETWORK_LIMIT")
        if env:
            if env == "true":
                env = 2
            elif env == "null":
                env = 0
            return sb.integer_spec().normalise(meta, env)

        animation_options = sb.set_options(
            noisy_network_limit=sb.defaulted(sb.integer_spec(), 0)).normalise(
                meta,
                meta.everything.get("animation_options") or {})

        if animation_options["noisy_network_limit"]:
            return animation_options["noisy_network_limit"]

        return 0
示例#9
0
 class Options(Operator.Options):
     an_int = dictobj.Field(sb.integer_spec())
     a_boolean = dictobj.Field(sb.boolean, wrapper=sb.required)
示例#10
0
 def setup(self):
     self.spec = sb.dictof(
         service_type_spec(),
         sb.set_options(host=sb.required(sb.string_spec()),
                        port=sb.required(sb.integer_spec())),
     )
示例#11
0
 def normalise(self, meta, val):
     if "ANIMATION_INFLIGHT_MESSAGE_LIMIT" in os.environ:
         val = os.environ["ANIMATION_INFLIGHT_MESSAGE_LIMIT"]
     if val is sb.NotSpecified:
         val = 2
     return sb.integer_spec().normalise(meta, val)
示例#12
0
class RunOptions(dictobj.Spec):
    pauser = dictobj.Field(
        semaphore_spec,
        help="A semaphore that when set will pause the animation",
    )

    combined = dictobj.Field(
        sb.boolean,
        default=True,
        help="Whether to join all found tiles into one animation")

    reinstate_on_end = dictobj.Field(
        sb.boolean,
        default=False,
        help=
        "Whether to return the tiles to how they were before the animation",
    )

    reinstate_duration = dictobj.Field(
        sb.float_spec,
        default=1,
        help="The duration used when reinstating state")

    noisy_network = dictobj.Field(
        noisy_network_spec(),
        help="""
        If this value is non 0, then we assume the network is a noisy network
        and we'll make some effort to make sure the tiles can keep up.
        The number provided is the max number of messages per device that is
        "inflight" before we drop messages before sending them.

        if this value is 0 (default) then we just send all the messgaes.
    """,
    )

    rediscover_every = dictobj.Field(
        sb.integer_spec,
        default=20,
        help="""
        This value is the number of seconds it should take before we try and
        rediscover devices on the network to add to the animation.
    """,
    )

    animation_limit = dictobj.Field(
        sb.integer_spec(),
        default=0,
        help="""
        This is the number of animations to run before stop running new
        animations.

        It defaults to no limit
    """,
    )

    animation_chooser = dictobj.Field(
        sb.string_choice_spec(["random", "cycle"]),
        default="cycle",
        help="""
        This decides how we determine which feature animation to run next.

        If the option is random (default) then the next feature will be the
        next feature in the animations list. Otherwise if it's set to
        random, then the next one will be chosen at random.
    """,
    )

    transition_chooser = dictobj.Field(
        sb.string_choice_spec(["random", "cycle"]),
        default="cycle",
        help="""
        This decides how we determine which transition animation to use next.

        If the option is random (default) then the next transition will be the
        next transition in the animations list. Otherwise if it's set to
        random, then the next one will be chosen at random.
    """,
    )

    transitions = dictobj.Field(
        TransitionOptions.FieldSpec,
        help="""
        The options for transitions

        run_first
            Run a transition before the first feature animation

        run_last
            Run a transition after the last feature animation (unless animations
            are cancelled)

        run_between
            Run transitions between feature animations

        animations
            Same option as in the ``animations`` option of the root options.
    """,
    )

    animations = dictobj.Field(
        sb.listof(animation_spec()),
        help="""
        The different animations you wish to run.

        These are a list of (name, ), (name, options); or (name, background, options)

        ``name``
            This is the name of the registered animation.

            If it's a tuple of (Animation, Options) where those are the classes
            that represent the animation, then a new animation is created from
            those options.

        ``background``
            If this value is not not specified, or null or true, then the current
            colors on the tiles are used as the starting canvas for the animation.

            If this value is False, then the starting canvas for the animation will
            be empty.

        options
            A dictionary of options relevant to the animation.
    """,
    )

    @property
    def animations_iter(self, register=None):
        features = []
        transitions = []

        for a in self.animations:
            make_animation, background = a.resolve()
            features.append((make_animation, background))

        for t in self.transitions.animations:
            make_animation, background = a.resolve()
            transitions.append((make_animation, background))

        features = iter(Chooser(self.animation_chooser, features))
        transitions = iter(Chooser(self.transition_chooser, transitions))

        def itr():
            if not features and transitions:
                if self.transitions.run_first or self.transitions.run_last:
                    yield next(transitions)
                return

            if transitions and self.transitions.run_first:
                yield next(transitions)

            if features:
                count = 0
                while True:
                    if self.animation_limit and count >= self.animation_limit:
                        break
                    count += 1

                    animation = yield next(features)

                    if animation.skip_next_transition:
                        continue

                    if transitions and self.transitions.run_between:
                        yield next(transitions)

            if transitions and self.transitions.run_last:
                yield next(transitions)

        return itr
示例#13
0
 def normalise_filled(self, meta, val):
     return sb.integer_spec().normalise(meta, val)
示例#14
0
 class Options(dictobj.Spec):
     relays = dictobj.NullableField(sb.listof(Relay.FieldSpec()))
     relays_count = dictobj.NullableField(sb.integer_spec())
示例#15
0
class Relay(dictobj.Spec):
    power = dictobj.Field(sb.integer_spec(), default=0)

    @classmethod
    def create(kls, **kwargs):
        return kls.FieldSpec().empty_normalise(**kwargs)
示例#16
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)
示例#17
0
 class Options(Operator.Options):
     an_int = dictobj.NullableField(sb.integer_spec())
     a_boolean = dictobj.Field(sb.boolean, default=True)
示例#18
0
        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
        for more information on what filters are available.
      """,
)

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://delfick.github.io/photons-core/binary_protocol.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.
示例#19
0
文件: io.py 项目: delfick/photons
 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))
示例#20
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())
示例#21
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")
示例#22
0
# coding: spec

from interactor.commander.spec_description import signature

from delfick_project.norms import sb

describe "signature":

    def assertSignature(self, spec, want):
        assert " ".join(signature(spec)) == want

    it "knows about integer_spec":
        self.assertSignature(sb.integer_spec(), "integer")

    it "knows about float_spec":
        self.assertSignature(sb.float_spec(), "float")

    it "knows about string_spec":
        self.assertSignature(sb.string_spec(), "string")

    it "knows about boolean":
        self.assertSignature(sb.boolean(), "boolean")

    it "knows about dictionary_spec":
        self.assertSignature(sb.dictionary_spec(), "dictionary")

    it "knows about string_choice_spec":
        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)")
示例#23
0
    return Meta.empty()


@pytest.fixture()
def overrides():
    return mock.Mock(name="overrides")


describe "range_spec":
    it "complains if the value isn't a float", meta:
        for val in (True, False, {}, [], None, lambda: 1):
            with assertRaises(BadSpecValue):
                scene_spec.range_spec(0, 1).normalise(meta, val)

    it "can use the spec that is provided", meta:
        got = scene_spec.range_spec(0, 1, sb.integer_spec()).normalise(meta, 0)
        assert got == 0
        assert type(got) == int

        # Prove it's not an integer without specifying integer_spec
        got = scene_spec.range_spec(0, 1).normalise(meta, 0)
        assert got == 0.0
        assert type(got) == float

    it "complains if less than minimum", meta:
        for val in (-1.0, -2.0, -3.0):
            with assertRaises(
                BadSpecValue,
                "Number must be between min and max",
                minimum=0,
                maximum=1,
示例#24
0
 class Not(dictobj.Spec):
     there = dictobj.Field(sb.listof(sb.integer_spec()))
示例#25
0
 class Options(Operator.Options):
     will_be_wrong = dictobj.Field(sb.integer_spec())
     will_be_missing = dictobj.Field(sb.boolean, wrapper=sb.required)