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)
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)
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 )
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
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)
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)
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)
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)
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)
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]
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)
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)
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)
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 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, )
class Options(Operator.Options): an_int = dictobj.NullableField(sb.integer_spec()) a_boolean = dictobj.Field(sb.boolean, default=True)
class Options(dictobj.Spec): relays = dictobj.NullableField(sb.listof(Relay.FieldSpec())) relays_count = dictobj.NullableField(sb.integer_spec())
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})
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)
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)
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)
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
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)
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