The Device Finder

It can be really helpful to target devices based on certain attributes without having to first ask the device all the necessary questions. To make this task easier, photons provides a DeviceFinder special reference:

from photons_control.device_finder import DeviceFinder
from photons_messages import DeviceMessages


async def my_action(target):
    async with target.session() as sender:
        reference = DeviceFinder.from_kwargs(label="kitchen")
        msg = DeviceFinder.SetPower(level=0)

        # Turn off the device with the label kitchen
        await sender(msg, reference)

To create a special reference, you use the same methods as the Filter class mentioned below. Or you can create a DeviceFinder by instantiating it with an instance of that Filter class.

Once you have a DeviceFinder object you then use it like any other reference object in photons:

Valid Filters

You can create a Filter using a number of different formats:

from photons_control.device_finder import Filter


fltr = Filter.from_json_str('{"refresh_info": true, "firmware_version": "1.22"}')

# or
fltr = Filter.from_options({"refresh_info": True, "firmware_version": "1.22"})

# or
fltr = Filter.from_kwargs(refresh_info=True, firmware_version="1.22")

# or
fltr = Filter.from_key_value_str("refresh_info=true firmware_version=1.22")

# or
fltr = Filter.from_url_str("refresh_info=true&firmware_version=1.22")

The filter takes in:

refresh_info:

Refresh the information on the device

refresh_discovery:

Refresh discovery information

serial

The serial of the device

label

The label set on the device

power

Either “on” or “off” depending on whether the device is on or not.

group_id

The uuid of the group set on this device

group_name

The name of this group. Note that if you have several devices that have the same group, then this will be set to the label of the group with the newest updated_at option.

location_id

The uuid of the location set on this device

location_name

The name of this location. Note that if you have several devices that have the same location_id, then this will be set to the label of the location with the newest updated_at option.

hue, saturation, brightness, kelvin

The hsbk values of the device. You can specify a range by saying something like 10-30, which would match any device with a hsbk value between 10 and 30 (inclusive).

firmware_version

The version of the HostFirmware as a string of “{major}.{minor}”.

product_id

The product id of the device as an integer. You can see the hex product id of each device type in the photons_products module.

product_identifier

A string identifying the product type of the device. You can find these in the photons_products module.

cap

A list of strings of capabilities this device has.

Capabilities include:

  • ir and not_ir

  • color and not_color

  • chain and not_chain

  • matrix and not_matrix

  • multizone and not_multizone

  • variable_color_temp and not_variable_color_temp

When a property in the filter is an array, it will match any device that matches against any of the items in the array.

And a filter with multiple properties will only match devices that match against all those properties.

Label properties product_identifier, label, location_name, group_name are matched with globs. So if you have device1 with product_identifier of lifx_a19_plus and device2 with a product_identifier of lifx_br30_plus you can choose both of them by using Filter.from_kwargs(product_identifier="*_plus")

Sharing gathered data

If you want to share data retrieved from the devices between multiple DeviceFinder objects then you can create a Finder object and pass that in when you create the DeviceFinder:

from photons_control.device_finder import DeviceFinder, Finder
from photons_messages import DeviceMessages


async def my_action(target):
    async with target.session() as sender:
        finder = Finder(sender)

        # Turn off the lights with label kitchen
        reference = DeviceFinder.from_kwargs(label="kitcehn", finder=finder)
        await sender(DeviceMessages.SetPower(level=0), reference)

        # Turn on the lights with label attic
        # Note that without passing in finder, it would ask the devices for
        # their label again
        reference = DeviceFinder.from_options({"label": "attic"}, finder=finder)
        await sender(DeviceMessages.SetPower(level=65535), reference)

Streaming serials and info from the finder

It’s possible to stream devices from the DeviceFinder. The advantage here is the SpecialReference waits for all devices to respond before returning what it found, but we can use the finder to instead stream devices as they answer enough questions:

from photons_control.device_finder import DeviceFinder, Finder


async def my_action(target):
    async with target.session() as sender:
        # The finder is optional, but does mean subsequent calls to
        # serials or info will not have to ask the devices for information
        # that it already asked for
        finder = Finder(sender)

        reference = DeviceFinder.from_kwargs(cap=["matrix"], group_name=["attic"], finder=finder)

        async for device in reference.serials(sender):
            print(device.serial)

        async for device in reference.info(sender):
            # This returns the same device objects as .serials
            # But asks all the questions to the device so that
            # ``device.info`` is fully populated
            print(device.serial, device.info)

Note

the info property is a dictionary of values on the device where the available properties are those you can control on the Filter class.

Daemon

You can start a daemon that you can use to query the network continuously.

from photons_control.device_finder import DeviceFinderDaemon, Filter


# These points of information have their own default refresh numbers
# But you can override them like this.
time_between_queries = {"LIGHT_STATE": 10, "FIRMWARE": 300, "GROUP": 60, "LOCATION": 60}

async with target.session() as sender:
    daemon = DeviceFinderDaemon(
        sender,
        photons_app.final_future,
        search_interval=20,
        time_between_queries=time_between_queries,
    )

    # Optionally start searching for information straight away
    daemon.start()

    try:
        # Create an instance of the Filter
        fltr = Filter.from_kwargs(label="den")

        # This returns the devices with whatever information they currently have
        async for device in daemon.serials(fltr):
            print(device.serial)

        # This returns devices after first getting all the information
        async for device in daemon.info(fltr):
            print(device.serial)
            print(device.info)
    finally:
        await daemon.finish()

The daemon takes in the following arguments:

limit - default 30

This is the limit of inflight messages sent by the daemon. You can pass in an asyncio.Semaphore or a number and a Semaphore will be made for you.

finder - optional

The finder object that does all the hard work. If one is not supplied then one is made for you

forget_after - default 30

The number of seconds since a device is last discovered before we forget it ever existed

final_future - defaults to the final_future on the sender

A future that when cancelled will shut down the daemon.

search_interval - default 20

The number of seconds between each discovery

time_between_queries - optional

A dictionary of refresh times for the different points of information the device finder looks for.

By default it is:

{"LIGHT_STATE": 10, "VERSION": None, "FIRMWARE": 300, "GROUP": 60, "LOCATION": 60}

The None value for VERSION means the version information is never asked for again. The numbers in the rest of them is the minimum number of seconds since getting a result before it asks for an updated value.

The daemon will then sit there and keep discovering devices and asking those devices questions to update their state. It tries it’s best to send the least amount of packets on the network as possible.