async def execute_task(self, **kwargs): extra = self.photons_app.extra_as_json positions = sb.listof(sb.listof(sb.float_spec())).normalise( Meta.empty(), extra) if any(len(position) != 2 for position in positions): raise PhotonsAppError( "Please enter positions as a list of two item lists of user_x, user_y" ) async def gen(reference, sender, **kwargs): ps = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather( ps, reference, **kwargs): if info["cap"].has_matrix: for i, (user_x, user_y) in enumerate(positions): yield TileMessages.SetUserPosition( tile_index=i, user_x=user_x, user_y=user_y, res_required=False, target=serial, ) await self.target.send(FromGenerator(gen), self.reference)
async def set_tile_positions(collector, target, reference, **kwargs): """ Set the positions of the tiles in your chain. ``lan:set_tile_positions d073d5f09124 -- '[[0, 0], [-1, 0], [-1, 1]]'`` """ extra = collector.photons_app.extra_as_json positions = sb.listof(sb.listof(sb.float_spec())).normalise( Meta.empty(), extra) if any(len(position) != 2 for position in positions): raise PhotonsAppError( "Please enter positions as a list of two item lists of user_x, user_y" ) async def gen(reference, sender, **kwargs): ps = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather( ps, reference, **kwargs): if info["cap"].has_matrix: for i, (user_x, user_y) in enumerate(positions): yield TileMessages.SetUserPosition( tile_index=i, user_x=user_x, user_y=user_y, res_required=False, target=serial, ) await target.send(FromGenerator(gen), reference)
def PowerToggle(duration=1, **kwargs): """ Returns a valid message that will toggle the power of devices used against it. For example: .. code-block:: python await target.send(PowerToggle(), ["d073d5000001", "d073d5000001"]) """ async def gen(reference, sender, **kwargs): get_power = DeviceMessages.GetPower() async for pkt in sender(get_power, reference, **kwargs): if pkt | DeviceMessages.StatePower: if pkt.level == 0: yield LightMessages.SetLightPower(level=65535, res_required=False, duration=duration, target=pkt.serial) else: yield LightMessages.SetLightPower(level=0, res_required=False, duration=duration, target=pkt.serial) return FromGenerator(gen)
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)
def make_secondary_msg(i, m): async def gen(reference, sender, **kwargs): with alter_called(("secondary", i)): async with hp.tick(0.1) as ticks: async for _ in ticks: called.append(("secondary", i)) yield DeviceMessages.SetPower(level=0) return FromGenerator(gen, reference_override=True)
def make_primary_msg(m): async def gen(reference, sender, **kwargs): with alter_called("primary"): async with hp.tick(0.3) as ticks: async for i, _ in ticks: called.append(("primary", i)) yield make_secondary_msg(i, m) return FromGenerator(gen, reference_override=True)
def power_on_and_color(self, state, keep_brightness=False, transition_color=False): power_message = self.power_message(state) color_message = self.color_message(state, keep_brightness) def receiver(serial, current_state): want_brightness = color_message.brightness if color_message.set_brightness else None pipeline = [] currently_off = current_state.power == 0 if currently_off: clone = color_message.clone() clone.period = 0 clone.brightness = 0 clone.set_brightness = True clone.set_hue = 0 if transition_color else clone.set_hue clone.set_saturation = 0 if transition_color else clone.set_saturation clone.set_kelvin = 0 if transition_color else clone.set_kelvin clone.target = serial pipeline.append(clone) clone = power_message.clone() clone.target = serial pipeline.append(clone) set_color = color_message.clone() set_color.target = serial if currently_off: set_color.brightness = (current_state.brightness if want_brightness is None else want_brightness) set_color.set_brightness = True elif want_brightness is not None: set_color.brightness = want_brightness set_color.set_brightness = True pipeline.append(set_color) return Pipeline(*pipeline, synchronized=True) async def gen(reference, sender, **kwargs): get_color = LightMessages.GetColor(ack_required=False, res_required=True) async for pkt in sender(get_color, reference, **kwargs): if pkt | LightMessages.LightState: yield receiver(pkt.serial, pkt.payload) return FromGenerator(gen)
async def assertScript(self, runner, gen, *, generator_kwargs=None, expected, **kwargs): msg = FromGenerator(gen, **(generator_kwargs or {})) await runner.sender(msg, runner.serials, **kwargs) assert len(runner.devices) > 0 for device in runner.devices: if device not in expected: assert False, f"No expectation for {device.serial}" device.compare_received(expected[device])
async def execute_task(self, **kwargs): async def gen(reference, sender, **kwargs): ps = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather( ps, reference, **kwargs): if info["cap"].has_matrix: yield TileMessages.GetDeviceChain(target=serial) async for pkt in self.target.send(FromGenerator(gen), self.reference): print(pkt.serial) for tile in tiles_from(pkt): print(" ", repr(tile))
def apply_scene(scene): transformer = Transformer() async def gen(reference, sender, **kwargs): for state in scene.values(): if "power" in state: yield transformer.power_message(state) if "color" in state: yield transformer.color_message(state, keep_brightness=False) return FromGenerator(gen)
def SetZones(colors, power_on=True, reference=None, **options): """ Set colors on all found multizone devices. This will use the extended multizone messages if they are supported by the device to increase reliability and speed of application. Usage looks like: .. code-block:: python msg = SetZones([["red", 10], ["blue", 10]], zone_index=1, duration=1) await target.send(msg, reference) By default the devices will be powered on. If you don't want this to happen then pass in power_on=False If you want to target a particular device or devices, pass in a reference. The options to this helper include: colors - [[color_specifier, length], …] For example, ``[[“red”, 1], [“blue”, 3], [“hue:100 saturation:0.5”, 5]]`` zone_index - default 0 An integer representing where on the device to start the colors duration - default 1 Application duration overrides - default None A dictionary containing hue, saturation, brightness and kelvin for overriding colors with """ async def gen(ref, sender, **kwargs): r = ref if reference is None else reference plans = {"set_zones": SetZonesPlan(colors, **options)} async for serial, _, messages in sender.gatherer.gather( plans, r, **kwargs): if messages is not Skip: if power_on: yield LightMessages.SetLightPower( level=65535, target=serial, duration=options.get("duration", 1), ack_required=True, res_required=False, ) yield messages return FromGenerator(gen)
async def execute_task(self, **kwargs): async def gen(reference, sender, **kwargs): plans = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather(plans, reference): print(f"Turning off effects for {serial}") yield LightMessages.SetWaveformOptional(res_required=False, target=serial) if info["cap"].has_multizone: yield SetZonesEffect("OFF", power_on=False) elif info["cap"].has_matrix: yield SetTileEffect("OFF", power_on=False) await self.target.send(FromGenerator(gen), self.reference)
async def assertScript(self, sender, gen, *, generator_kwargs=None, expected, **kwargs): msg = FromGenerator(gen, **(generator_kwargs or {})) await sender(msg, devices.serials, **kwargs) assert len(devices) > 0 for device in devices: if device not in expected: assert False, f"No expectation for {device.serial}" for device, msgs in expected.items(): assert device in devices devices.store(device).assertIncoming(*msgs, ignore=[DiscoveryMessages.GetService]) devices.store(device).clear()
async def get_device_chain(collector, target, reference, **kwargs): """ Get the devices in your chain """ async def gen(reference, sender, **kwargs): ps = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather( ps, reference, **kwargs): if info["cap"].has_matrix: yield TileMessages.GetDeviceChain(target=serial) async for pkt in target.send(FromGenerator(gen), reference): print(pkt.serial) for tile in tiles_from(pkt): print(" ", repr(tile))
def script(self, raw): """Return us a ScriptRunner for the given `raw` against this `target`""" items = list(self.simplify(raw)) if not items: items = None elif len(items) > 1: original = items async def gen(*args, **kwargs): for item in original: yield item items = list(self.simplify(FromGenerator(gen, reference_override=True)))[0] else: items = items[0] return self.script_runner_kls(items, target=self)
def msg(kls, options): if not isinstance(options, Options): options = Options.FieldSpec().normalise(Meta(options, []), options) async def gen(reference, sender, **kwargs): serials = [] canvases = [] combined_canvas = Canvas() plans = sender.make_plans("parts") async for serial, _, info in sender.gatherer.gather(plans, reference, **kwargs): serials.append(serial) for part in info: if part.device.cap.has_chain: combined_canvas.add_parts(part) else: nxt = Canvas() nxt.add_parts(part) canvases.append(nxt) if combined_canvas: canvases.append(combined_canvas) msgs = [] if options.power_on: for serial in serials: msgs.append( LightMessages.SetLightPower( level=65535, duration=options.duration, target=serial, res_required=False, ) ) for canvas in canvases: Applier(canvas, options.colors).apply() for msg in canvas.msgs( options.override_layer, duration=options.duration, acks=True ): msgs.append(msg) yield msgs return FromGenerator(gen)
async def get_tile_positions(collector, target, reference, **kwargs): """ Get the positions of the tiles in your chain. ``lan:get_tile_positions d073d5f09124`` """ async def gen(reference, sender, **kwargs): ps = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather( ps, reference, **kwargs): if info["cap"].has_matrix: yield TileMessages.GetDeviceChain(target=serial) async for pkt in target.send(FromGenerator(gen), reference): print(pkt.serial) for tile in tiles_from(pkt): print(f"\tuser_x: {tile.user_x}, user_y: {tile.user_y}") print("")
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)
async def doit(collector): lan_target = collector.resolve_target("lan") def e(error): log.error(error) def apply_scene(scene): transformer = Transformer() async def gen(reference, sender, **kwargs): for state in scene.values(): if "power" in state: yield transformer.power_message(state) if "color" in state: yield transformer.color_message(state, keep_brightness=False) return FromGenerator(gen) scripts = [] for scene in scenes: for serial, options in scene.items(): options["target"] = serial max_duration = max( [options.get("duration", 1) for options in scene.values()]) scripts.append((max_duration, apply_scene(scene))) async def gen(reference, sender, **kwargs): while True: for max_duration, script in scripts: start = time.time() r = yield script await r diff = max_duration - (time.time() - start) await asyncio.sleep(diff) apply_scenes = FromGenerator(gen) await lan_target.send(apply_scenes, message_timeout=1, error_catcher=e, find_timeout=10)
async def gen(reference, sender, **kwargs): async def inner_gen(level, reference, sender2, **kwargs2): assert sender is sender2 del kwargs2["error_catcher"] kwargs1 = dict(kwargs) del kwargs1["error_catcher"] assert kwargs1 == kwargs2 assert reference in devices.serials yield DeviceMessages.SetPower(level=level) get_power = DeviceMessages.GetPower() async for pkt in sender(get_power, reference, **kwargs): if pkt.serial == light1.serial: level = 1 elif pkt.serial == light2.serial: level = 2 elif pkt.serial == light3.serial: level = 3 else: assert False, f"Unknown serial: {pkt.serial}" yield FromGenerator(partial(inner_gen, level), reference_override=pkt.serial)
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 _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())
def SetZonesEffect(effect, power_on=True, power_on_duration=1, reference=None, **options): """ Set an effect on your multizone devices Where effect is one of the available effect types: OFF Turn the animation off MOVE A moving animation Options include: * offset * speed * duration Usage looks like: .. code-block:: python msg = SetZonesEffect("MOVE", speed=1) await target.send(msg, reference) By default the devices will be powered on. If you don't want this to happen then pass in ``power_on=False`` If you want to target a particular device or devices, pass in reference. """ typ = effect if type(effect) is str: for e in MultiZoneEffectType: if e.name.lower() == effect.lower(): typ = e break if typ is None: available = [e.name for e in MultiZoneEffectType] raise PhotonsAppError("Please specify a valid type", wanted=effect, available=available) options["type"] = typ options["res_required"] = False set_effect = MultiZoneMessages.SetMultiZoneEffect.empty_normalise( **options) async def gen(ref, sender, **kwargs): r = ref if reference is None else reference plans = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather( plans, r, **kwargs): if info["cap"].has_multizone: if power_on: yield LightMessages.SetLightPower( level=65535, target=serial, duration=power_on_duration, ack_required=True, res_required=False, ) msg = set_effect.clone() msg.target = serial yield msg return FromGenerator(gen)
async it "Can get results", sender: async def gen(reference, sender, **kwargs): yield DeviceMessages.GetPower(target=light1.serial) yield DeviceMessages.GetPower(target=light2.serial) yield DeviceMessages.GetPower(target=light3.serial) expected = { light1: [DeviceMessages.GetPower()], light2: [DeviceMessages.GetPower()], light3: [DeviceMessages.GetPower()], } got = defaultdict(list) async for pkt in sender.transport_target.send(FromGenerator(gen), devices.serials): got[pkt.serial].append(pkt) assert len(devices) > 0 for device in devices: if device not in expected: assert False, f"No expectation for {device.serial}" for device, msgs in expected.items(): assert device in devices devices.store(device).assertIncoming(*msgs, ignore=[DiscoveryMessages.GetService]) devices.store(device).clear() if expected[device]: assert len(got[device.serial]) == 1
def SetZonesEffect(effect, power_on=True, power_on_duration=1, reference=None, **options): """ Set an effect on your multizone devices Where effect is one of the available effect types: OFF Turn the animation off MOVE A moving animation Options include: * speed: duration in seconds to complete one cycle * duration: in seconds or specify 0 (the default) to run until manually stopped * direction: either "left" or "right" (default: "right") Usage looks like: .. code-block:: python msg = SetZonesEffect("MOVE", speed=1, duration=10, direction="left") await target.send(msg, reference) By default the devices will be powered on. If you don't want this to happen then pass in ``power_on=False`` If you want to target a particular device or devices, pass in reference. """ typ = effect if type(effect) is str: for e in MultiZoneEffectType: if e.name.lower() == effect.lower(): typ = e break if typ is None: available = [e.name for e in MultiZoneEffectType] raise PhotonsAppError("Please specify a valid type", wanted=effect, available=available) options["type"] = typ options["res_required"] = False direction = options.pop("direction", None) if isinstance(direction, str): direction = Direction.__members__.get(direction.upper()) if isinstance(direction, Direction): options["parameters"] = {"speed_direction": direction} set_effect = MultiZoneMessages.SetMultiZoneEffect.create(**options) async def gen(ref, sender, **kwargs): r = ref if reference is None else reference plans = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather( plans, r, **kwargs): if info["cap"].has_multizone: if power_on: yield LightMessages.SetLightPower( level=65535, target=serial, duration=power_on_duration, ack_required=True, res_required=False, ) msg = set_effect.clone() msg.target = serial yield msg return FromGenerator(gen)
def SetTileEffect(effect, power_on=True, power_on_duration=1, reference=None, **options): """ Set an effect on your tiles Where effect is one of the available effect types: OFF Turn the animation off FLAME A flame effect MORPH A Morph effect Options include: * speed * duration * palette Usage looks like: .. code-block:: python msg = SetTileEffect("MORPH", palette=["red", "blue", "green"]) await target.send(msg, reference) By default the devices will be powered on. If you don't want this to happen then pass in ``power_on=False``. If you want to target a particular device or devices, pass in reference. """ typ = effect if type(effect) is str: for e in TileEffectType: if e.name.lower() == effect.lower(): typ = e break if typ is None: available = [e.name for e in TileEffectType] raise PhotonsAppError("Please specify a valid type", wanted=effect, available=available) options["type"] = typ options["res_required"] = False if "palette" not in options: options["palette"] = default_tile_palette if len(options["palette"]) > 16: raise PhotonsAppError("Palette can only be up to 16 colors", got=len(options["palette"])) options["palette"] = list(make_hsbks([c, 1] for c in options["palette"])) options["palette_count"] = len(options["palette"]) set_effect = TileMessages.SetTileEffect.create(**options) async def gen(ref, sender, **kwargs): r = ref if reference is None else reference ps = sender.make_plans("capability") async for serial, _, info in sender.gatherer.gather(ps, r, **kwargs): if info["cap"].has_matrix: if power_on: yield LightMessages.SetLightPower( level=65535, target=serial, duration=power_on_duration, ack_required=True, res_required=False, ) msg = set_effect.clone() msg.target = serial yield msg return FromGenerator(gen)
async it "Can get results", runner: async def gen(reference, sender, **kwargs): yield DeviceMessages.GetPower(target=light1.serial) yield DeviceMessages.GetPower(target=light2.serial) yield DeviceMessages.GetPower(target=light3.serial) expected = { light1: [DeviceMessages.GetPower()], light2: [DeviceMessages.GetPower()], light3: [DeviceMessages.GetPower()], } got = defaultdict(list) async for pkt in runner.target.send(FromGenerator(gen), runner.serials): got[pkt.serial].append(pkt) assert len(runner.devices) > 0 for device in runner.devices: if device not in expected: assert False, f"No expectation for {device.serial}" device.compare_received(expected[device]) assert len(got[device.serial]) == 1 assert got[device.serial][0] | DeviceMessages.StatePower async it "Sends all the messages that are yielded", runner:
Events.ATTRIBUTE_CHANGE(device2, [ChangeAttr.test("power", 0)], True), Events.OUTGOING( device2, io2, pkt=CoreMessages.Acknowledgement(), replying_to=msg ), Events.OUTGOING(device2, io2, pkt=reply, replying_to=msg), ] async it "is possible to cleanly stop", V, sender, FakeTime, MockedCallLater: original = DeviceMessages.EchoRequest(echoing=b"hi", ack_required=False) async def gen(sd, reference, **kwargs): async with hp.tick(0, min_wait=0) as ticks: async for _ in ticks: await (yield original) msg = FromGenerator(gen, reference_override=True) got = [] with FakeTime() as t: async with MockedCallLater(t, precision=0.01): async with sender(msg, V.device.serial) as pkts: async for pkt in pkts: got.append(pkt) if len(got) == 5: raise pkts.StopPacketStream() assert len(got) == 5 expected = (DeviceMessages.EchoResponse, {"echoing": b"hi"}) reply = expected[0].create(**expected[1]) pytest.helpers.assertSamePackets(got, *[expected] * 5)