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
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)
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]
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
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)
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()), ]
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)
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
class Options(Operator.Options): an_int = dictobj.Field(sb.integer_spec()) a_boolean = dictobj.Field(sb.boolean, wrapper=sb.required)
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())), )
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)
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
def normalise_filled(self, meta, val): return sb.integer_spec().normalise(meta, val)
class Options(dictobj.Spec): relays = dictobj.NullableField(sb.listof(Relay.FieldSpec())) relays_count = dictobj.NullableField(sb.integer_spec())
class Relay(dictobj.Spec): power = dictobj.Field(sb.integer_spec(), default=0) @classmethod def create(kls, **kwargs): return kls.FieldSpec().empty_normalise(**kwargs)
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)
class Options(Operator.Options): an_int = dictobj.NullableField(sb.integer_spec()) a_boolean = dictobj.Field(sb.boolean, default=True)
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.
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))
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())
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")
# 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)")
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,
class Not(dictobj.Spec): there = dictobj.Field(sb.listof(sb.integer_spec()))
class Options(Operator.Options): will_be_wrong = dictobj.Field(sb.integer_spec()) will_be_missing = dictobj.Field(sb.boolean, wrapper=sb.required)