class TileDiceCommand(store.Command): """ Show dice on provided tiles and return the hsbk values that were sent """ finder = store.injected("finder") target = store.injected("targets.lan") matcher = df.matcher_field refresh = df.refresh_field async def execute(self): fltr = chp.filter_from_matcher(self.matcher, self.refresh) result = chp.ResultBuilder() afr = await self.finder.args_for_run() reference = self.finder.find(filtr=fltr) serials = await tile_serials_from_reference(self.target, reference, afr) if not serials: raise FoundNoDevices("Didn't find any tiles") await self.target.script(DeviceMessages.SetPower(level=65535) ).run_with_all(serials, afr, error_catcher=result.error) result.result["results"]["tiles"] = await tile_dice( self.target, serials, afr, error_catcher=result.error) return result
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 EffectCommand(store.Command): finder = store.injected("finder") target = store.injected("targets.lan") timeout = df.timeout_field matcher = df.matcher_field refresh = df.refresh_field apply_theme = dictobj.Field( sb.boolean, default=False, help= "Whether to apply a theme to the devices before running an animation", ) theme_options = dictobj.Field( sb.dictionary_spec, help="Any options to give to applying a theme") def theme_msg(self, gatherer): everything = {} theme_options = dict(self.theme_options) if "overrides" in theme_options: everything["overrides"] = theme_options["overrides"] if "colors" not in theme_options: theme_options["colors"] = default_colors options = ThemeOptions.FieldSpec().normalise(Meta(everything, []), theme_options) return ApplyTheme.script(options, gatherer=gatherer)
class HighlightArrangeCommand(store.Command): finder = store.injected("finder") target = store.injected("targets.lan") arranger = store.injected("arranger") serial = dictobj.Field(sb.string_spec, wrapper=sb.required) tile_index = dictobj.Field(sb.integer_spec, wrapper=sb.required) async def execute(self): afr = await self.finder.args_for_run() await self.arranger.highlight(self.serial, self.tile_index, self.target, afr) return {"ok": True}
class LeaveArrangeCommand(store.Command): finder = store.injected("finder") target = store.injected("targets.lan") arranger = store.injected("arranger") request_handler = store.injected("request_handler") async def execute(self): if not isinstance(self.request_handler, websocket.WebSocketHandler): raise NotAWebSocket() afr = await self.finder.args_for_run() await self.arranger.leave_arrange(self.request_handler.key, self.target, afr) return {"ok": True}
class TransformCommand(store.Command): """ Apply a http api like transformation to the lights """ finder = store.injected("finder") target = store.injected("targets.lan") matcher = df.matcher_field timeout = df.timeout_field refresh = df.refresh_field transform = dictobj.Field( sb.dictionary_spec(), wrapper=sb.required, help=""" A dictionary of what options to use to transform the lights with. For example, ``{"power": "on", "color": "red"}`` Or, ``{"color": "blue", "effect": "breathe", "cycles": 5}`` """, ) transform_options = dictobj.Field( sb.dictionary_spec(), help=""" A dictionay of options that modify the way the tranform is performed: keep_brightness Ignore brightness options in the request transition_color If the light is off and we power on, setting this to True will mean the color of the light is not set to the new color before we make it appear to be on. This defaults to False, which means it will appear to turn on with the new color """, ) async def execute(self): fltr = chp.filter_from_matcher(self.matcher, self.refresh) msg = Transformer.using(self.transform, **self.transform_options) script = self.target.script(msg) return await chp.run( script, fltr, self.finder, add_replies=False, message_timeout=self.timeout )
class ChangeArrangeCommand(store.Command): finder = store.injected("finder") target = store.injected("targets.lan") arranger = store.injected("arranger") serial = dictobj.Field(sb.string_spec, wrapper=sb.required) tile_index = dictobj.Field(sb.integer_spec, wrapper=sb.required) left_x = dictobj.Field(sb.integer_spec, wrapper=sb.required) top_y = dictobj.Field(sb.integer_spec, wrapper=sb.required) async def execute(self): afr = await self.finder.args_for_run() return await self.arranger.change(self.serial, self.tile_index, self.left_x, self.top_y, self.target, afr)
class StartArrangeCommand(store.Command): finder = store.injected("finder") target = store.injected("targets.lan") arranger = store.injected("arranger") request_handler = store.injected("request_handler") async def execute(self): if not isinstance(self.request_handler, websocket.WebSocketHandler): raise NotAWebSocket() afr = await self.finder.args_for_run() serials = await tile_serials_from_reference(self.target, self.finder.find(), afr) return await self.arranger.start_arrange(serials, self.request_handler.key, self.target, afr)
class DiscoverCommand(store.Command): """ Display information about all the devices that can be found on the network """ finder = store.injected("finder") matcher = df.matcher_field refresh = df.refresh_field just_serials = dictobj.Field( sb.boolean, default=False, help="Just return a list of serials instead of all the information per device", ) async def execute(self): fltr = chp.filter_from_matcher(self.matcher) if self.refresh is not None: fltr.force_refresh = self.refresh if self.just_serials: return await self.finder.serials(filtr=fltr) else: return await self.finder.info_for(filtr=fltr)
class StartAnimateCommand(store.Command): """ Start a tile animation """ finder = store.injected("finder") target = store.injected("targets.lan") animations = store.injected("animations") matcher = df.matcher_field refresh = df.refresh_field animation = dictobj.Field(valid_animation_name, wrapper=sb.required) options = dictobj.Field(sb.dictionary_spec) stop_conflicting = dictobj.Field(sb.boolean, default=False) async def execute(self): afr = await self.finder.args_for_run() fltr = chp.filter_from_matcher(self.matcher) if self.refresh is not None: fltr.force_refresh = self.refresh found_serials = await self.finder.serials(filtr=fltr) fltr = chp.filter_from_matcher({ "serial": found_serials, "cap": "chain" }) try: serials = await self.finder.serials(filtr=fltr) except FoundNoDevices: raise FoundNoTiles(matcher=self.matcher) find_fltr = chp.clone_filter(fltr, force_refresh=False) reference = self.finder.find(filtr=find_fltr) animation_id = self.animations.start( self.animation, self.target, serials, reference, afr, self.options, stop_conflicting=self.stop_conflicting, ) return {"animation_id": animation_id}
class HelpCommand(store.Command): """ Display the documentation for the specified command """ path = store.injected("path") store = store.injected("store") command = dictobj.Field(sb.string_spec, default="help", help="The command to show help for") @property def command_kls(self): available = self.store.paths[self.path] if self.command not in available: raise NoSuchCommand(wanted=self.command, available=sorted(available)) return available[self.command]["kls"] async def execute(self): header = f"Command {self.command}" kls = self.command_kls doc = dedent(getattr(kls, "__help__", kls.__doc__)) fields = chp.fields_description(kls) fields_string = "" if fields: fields_string = ["", "Arguments\n---------", ""] for name, type_info, desc in fields: fields_string.append(f"{name}: {type_info}") for line in desc.split("\n"): if not line.strip(): fields_string.append("") else: fields_string.append(f"\t{line}") fields_string.append("") fields_string = "\n".join(fields_string) extra = "" if self.command == "help": extra = "\nAvailable commands:\n{}".format("\n".join( f" * {name}" for name in sorted(self.store.paths[self.path]))) return f"{header}\n{'=' * len(header)}\n{doc}{fields_string}{extra}"
class RemoveAllAnimateCommand(store.Command): """ Stop and remove all animations """ animations = store.injected("animations") async def execute(self): self.animations.remove_all() return {"success": True}
class StatusAnimateCommand(store.Command): """ Return status of an animation """ animations = store.injected("animations") animation_id = dictobj.Field(sb.string_spec, wrapper=sb.optional_spec) async def execute(self): return self.animations.status(self.animation_id)
class RemoveAnimateCommand(store.Command): """ Stop and remove a tile animation """ animations = store.injected("animations") animation_id = dictobj.Field(sb.string_spec, wrapper=sb.required) async def execute(self): self.animations.remove(self.animation_id) return {"success": True}
class PauseAnimateCommand(store.Command): """ Pause a tile animation """ animations = store.injected("animations") animation_id = dictobj.Field(sb.string_spec, wrapper=sb.required) async def execute(self): await self.animations.pause(self.animation_id) return {"success": True}
class QueryCommand(store.Command): """ Send a pkt to devices and return the result """ finder = store.injected("finder") target = store.injected("targets.lan") protocol_register = store.injected("protocol_register") matcher = df.matcher_field timeout = df.timeout_field refresh = df.refresh_field pkt_type = df.pkt_type_field pkt_args = df.pkt_args_field async def execute(self): fltr = chp.filter_from_matcher(self.matcher, self.refresh) msg = chp.make_message(self.protocol_register, self.pkt_type, self.pkt_args) script = self.target.script(msg) return await chp.run(script, fltr, self.finder, message_timeout=self.timeout)
class AvailableAnimateCommand(store.Command): """ Return available animations """ animations = store.injected("animations") async def execute(self): response = [] for name in sorted(self.animations.animators): response.append({"name": name}) return {"animations": response}
class SetCommand(store.Command): """ Send a pkt to devices. This is the same as query except res_required is False and results aren't returned """ finder = store.injected("finder") target = store.injected("targets.lan") protocol_register = store.injected("protocol_register") matcher = df.matcher_field timeout = df.timeout_field refresh = df.refresh_field pkt_type = df.pkt_type_field pkt_args = df.pkt_args_field async def execute(self): fltr = chp.filter_from_matcher(self.matcher, self.refresh) msg = chp.make_message(self.protocol_register, self.pkt_type, self.pkt_args) msg.res_required = False script = self.target.script(msg) return await chp.run(script, fltr, self.finder, message_timeout=self.timeout)
class SceneDeleteCommand(store.Command): """ Delete a scene """ db_queue = store.injected("db_queue") uuid = dictobj.Field( sb.string_spec, wrapper=sb.required, help="The uuid of the scene to delete" ) async def execute(self): def delete(db): for thing in db.queries.get_scenes(uuid=self.uuid).all(): db.delete(thing) return {"deleted": True} return await self.db_queue.request(delete)
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 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 StatusStreamAnimateCommand(store.Command): """ An endless stream of updates to animation status """ finder = store.injected("finder") target = store.injected("targets.lan") animations = store.injected("animations") progress_cb = store.injected("progress_cb") final_future = store.injected("final_future") request_handler = store.injected("request_handler") async def execute(self): if not isinstance(self.request_handler, websocket.WebSocketHandler): raise NotAWebSocket( "status stream can only be called from a websocket") u, fut = self.animations.add_listener() self.progress_cb({"available": self.animations.available()}) while True: futs = [ self.request_handler.connection_future, self.final_future, fut ] await asyncio.wait(futs, return_when=asyncio.FIRST_COMPLETED) if self.request_handler.connection_future.done( ) or self.final_future.done(): log.info(hp.lc("Connection to status stream went away")) afr = await self.finder.args_for_run() await self.animations.remove_listener(u, self.request_handler.key, self.target, afr) break self.progress_cb( {"status": self.animations.status(sb.NotSpecified)}) fut.reset()
class StatusEffectCommand(store.Command): """ Returns the current status of effects on devices that support them """ finder = store.injected("finder") target = store.injected("targets.lan") timeout = df.timeout_field matcher = df.matcher_field refresh = df.refresh_field def convert_enums(self, info): if isinstance(info, Product): info = { "pid": info.pid, "vid": info.vendor.vid, "cap": info.cap.as_dict(), "name": info.name, } if info is Skip: return {"type": "SKIP"} elif isinstance(info, dict): result = {} for k, v in info.items(): result[k] = self.convert_enums(v) return result elif isinstance(info, list): return [self.convert_enums(i) for i in info] elif isinstance(info, Enum): return info.name else: return info async def execute(self): fltr = chp.filter_from_matcher(self.matcher) if self.refresh is not None: fltr.force_refresh = self.refresh gatherer = Gatherer(self.target) plans = make_plans("firmware_effects", "capability") afr = await self.finder.args_for_run() serials = await self.finder.serials(filtr=fltr) result = chp.ResultBuilder() result.add_serials(serials) async for serial, _, info in gatherer.gather_per_serial( plans, serials, afr, error_catcher=result.error, message_timeout=self.timeout, ): r = {} if "capability" in info: r["product"] = info["capability"]["product"] if "firmware_effects" in info: r["effect"] = info["firmware_effects"] result.result["results"][serial] = self.convert_enums(r) return result
class SceneApplyCommand(store.Command): """ Apply a scene """ finder = store.injected("finder") target = store.injected("targets.lan") db_queue = store.injected("db_queue") matcher = df.matcher_field timeout = df.timeout_field uuid = dictobj.Field(sb.string_spec, wrapper=sb.required, help="The uuid of the scene to apply") overrides = dictobj.Field(sb.dictionary_spec, help="Overrides to the scene") async def execute(self): ts = [] result = chp.ResultBuilder() def get(db): info = [] for scene in db.queries.get_scenes(uuid=self.uuid).all(): info.append(scene.as_object()) if not info: raise NoSuchScene(uuid=self.uuid) return info for scene in await self.db_queue.request(get): fltr = chp.filter_from_matcher(scene.matcher, False) if scene.zones: multizonefltr = self.clone_fltr_with_cap(fltr, "multizone") ts.append(hp.async_as_background(self.apply_zones(multizonefltr, scene, result))) notmultizonefltr = self.clone_fltr_with_no_cap(fltr, "multizone") ts.append(hp.async_as_background(self.transform(notmultizonefltr, scene, result))) elif scene.chain: chainfltr = self.clone_fltr_with_cap(fltr, "chain") ts.append(hp.async_as_background(self.apply_chain(chainfltr, scene, result))) notchainfltr = self.clone_fltr_with_no_cap(fltr, "chain") ts.append(hp.async_as_background(self.transform(notchainfltr, scene, result))) else: ts.append(hp.async_as_background(self.transform(fltr, scene, result))) for t in ts: try: await t except Exception as error: result.error(error) return result async def transform(self, fltr, scene, result): options = scene.transform_options options.update(self.overrides) msg = Transformer.using(options) script = self.target.script(msg) try: await chp.run( script, fltr, self.finder, add_replies=False, message_timeout=self.timeout, result=result, ) except FoundNoDevices: pass async def apply_zones(self, fltr, scene, result): script = self.target.script(list(scene.zone_msgs(self.overrides))) try: await chp.run( script, fltr, self.finder, add_replies=False, message_timeout=self.timeout, result=result, ) except FoundNoDevices: pass async def apply_chain(self, fltr, scene, result): script = self.target.script(list(scene.chain_msgs(self.overrides))) try: await chp.run( script, fltr, self.finder, add_replies=False, message_timeout=self.timeout, result=result, ) except FoundNoDevices: pass def clone_fltr_with_no_cap(self, fltr, cap): clone = fltr.clone() if clone.cap is sb.NotSpecified: clone.cap = [f"not_{cap}"] else: clone.cap.append(cap) clone.cap = [c for c in clone.cap if c != cap] return clone def clone_fltr_with_cap(self, fltr, cap): clone = fltr.clone() if clone.cap is sb.NotSpecified: clone.cap = [cap] else: clone.cap.append(cap) clone.cap = [c for c in clone.cap if c != f"not_{cap}"] return clone
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})