Bread’N’Butter Features

The following section describes the Bread’N’Butter features of the middlelayer api.

Configurables

Devices can have Nodes with properties. Node structures are, as devices, created with Configurable classes.

It might be necessary to access the top-level device within a Node and this can be straightforwardly done with Configurable.get_root() as shown below.

from karabo.middlelayer import (
    Configurable, Device, Double, MetricPrefix, Node, Slot, State, Unit,
    background)


class FilterNode(Configurable):
    filterPosition = Double(
        displayedName="Filter Position",
        unitSymbol=Unit.METER,
        metricPrefixSymbol=MetricPrefix.MILLI,
        absoluteError=0.01)

    @Slot(allowedStates=[State.ON])
    async def move(self):
        # access main device via get_root to notify a state change
        root = self.get_root()
        root.state = State.MOVING
        # Move the filter position in a background task
        background(self._move_filter())

    async def _move_filter(self):
        try:
            pass
            # do something
        finally:
            root = self.get_root()
            root.state = State.ON


class GlobalDevice(Device):
    node = Node(FilterNode, displayedName="Filter")

Furthermore, it is possible to do a bulkset of a Hash container on a Configurable and only consider the changes. Note that, if a single value cannot be validated, the whole Hash is not set. In all cases the timestamp information is preserved, either from the value of the Hash element or an eventual KaraboValue that is coming from a Proxy, e.g. via connectDevice.

from karabo.middlelayer import (
    Configurable, Device, Double, Hash, Int32, MetricPrefix, Node,
    QuantityValue, Slot, State, Unit, minutesAgo)


class FilterNode(Configurable):
    filterPosition = Double(
        displayedName="Filter Position",
        unitSymbol=Unit.METER,
        metricPrefixSymbol=MetricPrefix.MILLI,
        absoluteError=0.01)


class MotorDevice(Device):

    channel = Int32(
        defaultValue=0)

    targetPosition = Double(
        defaultValue=0.0)

    velocity = Double(
        defaultValue=0.0,
        minInc=0.0,
        maxInc=10)

    node = Node(FilterNode, displayedName="Filter")


    def updateFromExternal(self):
        # get external data, everytime the timestamp is considered, either
        # via Hash attributes or `KaraboValue`
        h = Hash("targetPosition", 4.2,
                 "velocity", QuantityValue(4.2, timestamp=minutesAgo(2)),
                 "node.filterPosition", 2)
        # 1. All values will be applied only if they changed
        self.set(h, only_changes=True)

        # 2. All values will be applied
        self.set(h)

        h = Hash("targetPosition", 4.2,
                 "velocity", QuantityValue(100.2, timestamp=minutesAgo(2)),
                 "node.filterPosition", 12)
        # NO VALUE will be applied as velocity is outside limits!
        self.set(h, only_changes=True)

Util functions

Remove Quantity Values from core functions

A good practice is to work with fixed expectations about units and timestamps and strip them away in critical math operations, especially when relying on the external libray numpy. Decorate a function to remove KaraboValue input with removeQuantity.

This function works as well with async declarations and can be used as

@removeQuantity
def calculate(x, y):
    assert not isinstance(x, KaraboValue)
    assert not isinstance(y, KaraboValue)
    return x, y


@removeQuantity
def calculate(boolean_x, boolean_x):
    assert not isinstance(x, KaraboValue)
    assert not isinstance(y, KaraboValue)
    # Identity comparison possible!
    return x is y

Note

This decorator does not cast to base units! Retrieving the magnitude of the KaraboValue allows for identity comparison.

Maximum and minimum

Typically, values in devices and proxies are a KaraboValue which comes with a timestamp and a unit. However, not every simple mathmatical operation with KaraboValues is supported by common packages. In order to take the maximum and minimum of an iterable please have a look:

from karabo.middlelayer import maximum, minimum, QuantityValue, minutesAgo

t1 = minutesAgo(1)
t2 = minutesAgo(10)

a = QuantityValue(3, "m", timestamp=t1)
b = QuantityValue(1000, "mm", timestamp=t2)
m = maximum([a, b])
assert m == 3 * unit.meter
m = minimum([a, b])
assert m == 1000 * millimeter
# Timestamp is the newest one -> 1
assert m.timestamp == t1

Get and Set Property

Sometimes in scripting it is very convenient to get properties from devices or proxies in a getattr fashion, especially with noded structures. Use get_property as equivalent to python’s builtin getattr. Similarly, the set_property function is the equivalent to pythons setattr.

from karabo.middlelayer import get_property, set_property

    prop = get_property(proxy, "node.subnode.property")
    # This is equivalent to accessing
    prop = proxy.node.subnode.property

    # Set a value
    set_property(proxy, "node.subnode.property", 5)
    proxy.node.subnode.property = 5

Alarms in Devices

Each device is equipped with a globalAlarmCondition property which can set if an alarm should be highlighted. All alarm levels are provided by an enum of AlarmCondition according to the severity of the alarm level. In the middlelayer API the globalAlarmCondition does NOT require an acknowledging of the alarm setting.

There are different ways of alarm monitoring depending on the environment. The device developer can write an own alarm monitor as shown below observing the difference of temperatures:

from karabo.middlelayer import (
    AlarmCondition, background, connectDevice, Device, waitUntilNew)

class AlarmDevice(Device):

    async def onInitialization(self):
        self.temp_upstream = await connectDevice("REMOTE_UPSTREAM")
        self.temp_downstream = await connectDevice("REMOTE_DOWNSTREAM")
        background(self.monitor())

    async def monitor(self):
        while True:
            await waitUntilNew(self.temp_upstream.value,
                               self.temp_downstream.value)
            diff = abs(self.temp_upstream.value - self.temp_downstream.value)
            if diff > 5:
                level = AlarmCondition.WARN
            elif diff > 10:
                level = AlarmCondition.ALARM
            else:
                level = AlarmCondition.NONE

            # Only set the new value if there is a difference!
            if level != self.globalAlarmCondition:
                self.globalAlarmCondition = level

Note

The default value of the globalAlarmCondition property is AlarmCondition.NONE. Other simple settings are AlarmCondition.WARN, AlarmCondition.ALARM and AlarmCondition.INTERLOCK.

The alarm monitoring can also be automatically configured within a property with different steps and information.

from karabo.middlelayer import (
    AlarmCondition, background, connectDevice, Device, Double, waitUntilNew)

class AlarmDevice(Device):

    temperatureDiff = Double(
        displayedName="Temperature Difference",
        accessMode=AccessMode.READONLY,
        defaultValue=0.0,
        warnLow=-5.0,
        alarmInfo_warnLow="A temperature warnLow",
        alarmNeedsAck_warnLow=True,
        warnHigh=5.0,
        alarmInfo_warnHigh="A temperature warnHigh",
        alarmNeedsAck_warnHigh=True,
        alarmLow=-10.0,
        alarmInfo_alarmLow="A temperature alarmLow",
        alarmNeedsAck_alarmLow=True,
        alarmHigh=10.0,
        alarmInfo_alarmHigh="Alarm: The temperature is critical",
        alarmNeedsAck_alarmHigh=True)


    async def onInitialization(self):
        self.temp_upstream = await connectDevice("REMOTE_UPSTREAM")
        self.temp_downstream = await connectDevice("REMOTE_DOWNSTREAM")
        background(self.monitor())

    async def monitor(self):
        while True:
            await waitUntilNew(self.temp_upstream.value,
                               self.temp_downstream.value)
            diff = self.temp_upstream.value - self.temp_downstream.value
            self.temperatureDiff = diff

Table Element (VectorHash)

Known as TABLE_ELEMENT in the bound API, VectorHash allows users to specify custom entries, in the form of a table, that are, programmatically, available later in the form of an iterable.

Like other karabo properties, VectorHash is initialized by displayedName, description, defaultValue, and accessMode. As well, it has a rows field that describes what each row in the table contains.

This rows field expects a class that inherits from Configurable.

class RowSchema(Configurable):
    deviceId = String(
            displayedName="DeviceId",
            defaultValue="")
    instanceCount = Int(
            displayedName="Count")

This class can have as many parameters as desired, and these will be represented as columns in the table.

With RowSchema, the definition of the VectorHash is as follows:

class MyMLDevice(Device):
    userConfig = VectorHash(
                   rows=RowSchema,
                   displayedName="Hot Initialisation",
                   defaultValue=[],
                   minSize=1, maxSize=4)

The user will now be presented with an editable table:

../_images/VectorHash.png

Note that it is possible to provide the user with predefined entries, such as default values or reading a configuration file, by providing a populated array in the defaultValue option. The minSize and maxSize arguments can limit the table’s size if needed.

class MyMLDevice(Device):
    userConfig = VectorHash(
                   rows=RowSchema,
                   displayedName="Default Value Example",
                   defaultValue=[Hash("deviceId", "XHQ_EG_DG/MOTOR/1",
                                      "instanceCount", 1),
                                 Hash("deviceId", "XHQ_EG_DG/MOTOR/2",
                                      "instanceCount", 2)],
                   minSize=1, maxSize=4)

The Table Element can be a bit customized with the displayType attribute. Specifying for example

class RowSchema(Configurable):
    deviceId = String(
            displayedName="DeviceId",
            defaultValue="")
    state = String(
            defaultValue="UNKNOWN",
            displayType="State",
            displayedName="State")

will have a table schema that describes the state column with a displayType State. The graphical user interface can then color the column according to state color.

With Karabo 2.14.X, the table offers more customization, e.g.

class RowSchema(Configurable):
    progress = Double(
            displayedName="ProgressBar",
            displayType="TableProgressBar"
            defaultValue=0.0,
            minInc=0.0,
            maxInc=100.0)

    stringColor = String(
            defaultValue="anystring",
            displayType="TableColor|default=white&xfel=orange&desy=blue",
            displayedName="stringColor")

    numberColor = Int32(
            displayedName="Number Color",
            displayType="TableColor|0=red&1=orange&2=blue"
            defaultValue=0)

Hence, for different displayTypes more options are available.

  • A progressbar can be declared with TableProgressBar on a number descriptor.

  • Background coloring can be provided for strings and numbers with the TableColor displayType. The coloring is then appended in an URI scheme (separator &) which is append to the displayType after |. Declaration of a default background brush can be set with the default setting.

class RowSchema(Configurable):
    progress = Bool(
            displayedName="Bool Button",
            displayType="TableBoolButton",
            defaultValue=True)

For read only table element a button can be declared via TableBoolButton. The button is enabled depending on the boolean setting.

Clicking the button will send a Hash to the device via the slot requestAction.

The hash contains keys with data:

action: TableButton
path: the property key
table: the table data

The table data itself is a Hash with:

data = Hash(
    "rowData", h,
    "row", row,
    "column", column,
    "header", header)

The rowData is a Hash of the row of the table button. The elements row and column provide the indexes of the button and the header the column string.

Another option (since Karabo 2.15.X) for a button can be the TableStringButton. Besides access level considerations this button is always enabled to provide an action on a deviceScene or url.

class RowSchema(Configurable):

    description = String(
            displayedName="Description",
            defaultValue="")

    view = String(
            displayedName="View",
            displayType="TableStringButton",
            defaultValue="")

The value for both protocols are strings and an example to set a table

device_scene = "deviceScene|deviceId=YOURDEVICE&name=YOURSCENENAME"
open_url = "url|www.xfel.eu"

self.table = [Hash("description", "Important device", "view", device_scene),
              Hash("description", "Important device doc", "view", open_url)]

Vector handling in tables is significantly increased in Karabo 2.16.X. Specifiy a button with TableVectorButton to launch a list edit dialog via a button in the table element.

class RowSchema(Configurable):

    devices = VectorString(
                displayedName="View",
                displayType="TableVectorButton",
                defaultValue=[])

Once the VectorHash has been populated, it is possible to iterate through its rows, which are themselves internally stored as a TableValue, which itself encapsulates a numpy array. From Karabo 2.14.0 onwards it is possible to convert the np.array value to a list of Hashes with

table = self.userConfig.to_hashlist()

Moreover, iterating over the encapsulated numpy array can be done like

@Slot(displayedName="Do something with table")
async def doSomethingTable(self):
    # This loops over the array (.value)
    for row in self.userConfig.value:
        # do something ..., e.g. check the first column
        first_column = row[0]

If an action is required on VectorHash update, e.g. a row is added or removed, then the VectorHash should be defined within a decorator:

@VectorHash(rows=RowSchema,
            displayedName="Hot Initialisation",
            defaultValue=[])
async def tableUpdate(self, updatedTable):
    self.userConfig = updatedTable
    # This loops over the array (.value)
    for row in updatedTable.value:
        # do something ...

Overwrite Properties

Classes in Karabo may have default properties. A type of motors may have a default speed, a camera may have a default frame rate, and so forth.

Let’s say a base class for a motor has a default max speed of 60 rpms:

class MotorTemplate(Configurable):
    maxrpm = Int32(
                displayedName="Max Rotation Per Minutes",
                accessMode=AccessMode.READONLY,
                allowedStates={State.ON},
                unitSymbol=Unit.NUMBER)
    maxrpm = 60

    ...[much more business logic]...

All instances of that MotorTemplate will have a fixed maximum rpm 60, that can be seen when the state is State.ON, and is read only.

We now would like to create a custom motor, for a slightly different usage, where users can set this maximum, according to their needs, but all other parameters and functions remain the same.

It is possible to do so by inheriting from MotorTemplate, and using the karabo.middlelayer.Overwrite element:

class CustomMotor(MotorTemplate):
    maxrpm = Overwrite(
                minExc=1,
                accessMode=AccessMode.RECONFIGURABLE,
                allowedStates={State.OFF, State.INIT})

Note that only the required fields are modified. Others, such as displayedName will retain their original values.

Using Overwrite allows inheritance whilst replacing pre-existing parameters, keeping the namespace clean, and avoiding confusion between the local and inherited scope, thus providing a simpler codebase.

Schema injection

A parameter injection is a modification of the class of an object. It is used to add new parameters to an instantiated device or to update the attributes of already existing parameters. An example for the latter is the change of the size of an array depending on a user specified Karabo parameter.

The following code shows an example for the injection of a string and a node into the class of a device:

from karabo.middlelayer import Device

class MyDevice(Device):

    async def onInitialization(self):
        # should it be needed to test that the node is there
        self.my_node = None

    async def inject_something(self):
        # inject a new property into our personal class:
        self.__class__.injected_string = String()
        self.__class__.my_node = Node(MyNode, displayedName="position")
        await self.publishInjectedParameters()

        # use the property as any other property:
        self.injected_string = "whatever"
        # the test that the node is there is superfluous here
        if self.my_node is not None:
            self.my_node.reached = False

class MyNode(Configurable):
    reached = Bool(
        displayedName="On position",
        description="On position flag",
        defaultValue=True,
        accessMode=AccessMode.RECONFIGURABLE
    )

Note that calling :Python:`inject_something` again resets the values of properties to their defaults.

Middlelayer class based injection differs strongly from C++ and bound api parameter injection, and the following points should be remembered:

  • classes can only be injected into the top layer of the empty class and, consequently, of the schema rendition

  • the order of injection defines the order in schema rendition

  • classes injected can be simple (Double, Bool, etc.) or complex (Node, an entire class hierarchies, etc.)

  • later modification of injected class structure is not seen in the schema. Modification can only be achieved by overwriting the top level assignment of the class and calling publishInjectedParameters()

  • injected classes are not affected by later calls to publishInjectedParameters() used to inject other classes

  • deleted (del) injected classes are removed from the schema by calling publishInjectedParameters()

In the above example, new properties are added to the top level class of the device. Next, we consider the case where we want to update a property inside a child of the top level class.

:Python:`Overwrite` is used to update the attributes of existing Karabo property descriptors. Similar to the description in Chapter overwrite for the creation of a class, the mechanism can be used in schema injection.

class Configurator(Device):

    beckhoffComs = String(
        displayedName="BeckhoffComs in Topic",
        options=[])

    @Slot("Find BeckhoffComs")
    async def findBeckhoffComs(self):
        # Get a list of beckhoffCom deviceIds
        options = await self.get_beckhoff_coms()
        self.__class__.beckhoffComs = Overwrite(options=options)
        await self.publishInjectedParameters()

Karabo Devices are instances of a device class (classId). Hence, all instances of the same class on the same device server share the same static schema and a modification to any class object of the schema is propagated to all device children. However, as described above, it is possible to modify the top-layer device class. This leads to, that a change in a noded structure requires a full reconstruction (using a new class) and injection into the device’s top level class under the same property key.

Below is a MotorDevice with two different motor axes, rotary and linear represented in a Node. In the following, we would like to change on runtime the minInc and maxInc attribute of the targetPosition property. However, Schema injection for runtime attributes in mostly not worth it and should be avoided. Nevertheless, this example shows the technical possiblity.

from karabo.middlelayer import (
    Configurable, Device, Double, isSet, Node, String, Slot, VectorDouble)


def get_axis_schema(key, limits=None):

    class AxisSchema(Configurable):

        node_key = key

        @Slot()
        async def updateLimits(self):
            await self.get_root().updateAxisLimits(
                self.node_key, self.targetLimits.value)

        @VectorDouble(
            defaultValue=None,
            minSize=2, maxSize=2,
            displayedName="Target Limits")
        async def targetLimits(self, value):
            if not isSet(value):
                self.targetLimits = value
                return
            # Setter function always called in initialization
            self.targetLimits = value

        targetPosition = Double(
            displayedName="Value",
            minInc=limits[0] if limits is not None else None,
            maxInc=limits[1] if limits is not None else None)

    return AxisSchema


class MotorDevice(Device):

    # Node's take classes to build up
    rotary = Node(get_axis_schema("rotary", None))
    linear = Node(get_axis_schema("linear", [0, 90]))

    async def updateAxisLimits(self, key, limits):
        # 1. Get the previous configuration from the node under `key`
        h = self.configurationAsHash()[key]
        # 2. Create a new configurable schema for `key` with `limits`
        conf_schema = get_axis_schema(key, limits)
        # 3. Set the new node on `key` and inject with previous
        # configuration `h`
        setattr(self.__class__, key, Node(conf_schema))
        await self.publishInjectedParameters(key, h)

The factory function :Python:`get_limit_schema` provides each Node with a Configurable class. During the creation, the minInc and maxInc attributes can be assigned to the targetPosition property. Here, the class itself has a Slot that propagates to the MotorDevice to inject a new Axis under the same key with different limits. During a runtime schema injection via the Slot updateLimits, we again create a new updated Configurable class for the Node - and - to make sure that the configuration of the MotorDevice is preserved, the existing configuration is passed to the :Python:`publishInjectedParameters` method. During the initialization, all eventual setters are called as usual.

Slots are decorating functions. If you want to add a Slot, or change the function it is bound to (decorating), the following will do the trick:

async def very_private(self):
    self.log.INFO("This very private function is now exposed!!")

@Slot("Inject a slot")
async def inject_slot(self):
    # Inject a new slot in our schema
    self.__class__.injectedSlot = Slot(displayedName="Injected Slot")
    self.__class__.injectedSlot.__call__(type(self).very_private)
    await self.publishInjectedParameters()

Note

The key to that slot will not be very_private but instead injectedSlot So yes, cool that we can change the behaviour of a slot on the fly by changing the function the slot calls, but the key won’t reflect that.

If you do change the functions that are called, do put in a log message.

Warning

Consider instead injecting a node with a proper Slot definition.

Injected Properties and the DAQ need some ground rules in order to record these properties correctly.

In order for the DAQ to record injected properties, the DAQ needs to request the updated schema again, using the Run Controller’s applyConfiguration() slot.

This can be prone to operator errors, and therefore it is recommended that only properties injected at instantiation to be recorded.

Device Scenes

Karabo provides a protocol for devices to share predefined scenes. These allows the author of a device to provide what they think are a good starting point. Moreover, these are easily accessible by from the topology panel in the GUI:

../_images/default_scenes.png

A default scene can also be accessed by double-clicking on a device.

This section shows how to enable your device to have builtin scenes.

Implementing this functionality requires the creation of a scene, in the scene editor, conversion to Python, and adding the requestScene framework slot.

Begin by drawing an adequate scene in the GUI’s scene editor, and save it locally on your computer as SVG (right-click on scene -> Save to File).

Use the karabo-scene2py utility to convert the SVG file to Python code:

$ karabo-scene2py scene.svg SA1_XTD2_UND/MDL/GAINCURVE_SCAN > scenes.py

The first argument is the scene file, the second is an optional deviceId to be substituted.

As it is generated code, make sure the file is PEP8 compliant. The final result should look more or less like the following:

from karabo.common.scenemodel.api import (
    IntLineEditModel, LabelModel, SceneModel, write_scene
 )

def get_scene(deviceId):
    input = IntLineEditModel(height=31.0,
                             keys=['{}.config.movingAverageCount'.format(deviceId)],
                             parent_component='EditableApplyLaterComponent',
                             width=67.0, x=227.0, y=18.0)
    label = LabelModel(font='Ubuntu,11,-1,5,50,0,0,0,0,0', foreground='#000000',
                       height=27.0, parent_component='DisplayComponent',
                       text='Running Average Shot Count',
                       width=206.0, x=16.0, y=15.0)
    scene = SceneModel(height=1017.0, width=1867.0, children=[input, label])

    return write_scene(scene)

Add this file to your project.

In your device, add a read-only VectorString property called availableScenes, and implement the requestScene framework slot. This is a predefined slot, which allows various actors to understand the scene protocol.

The slot takes a Hash params and returns a Hash with the origin, its datatype (deviceScene), and the scene itself:

from karabo.middlelayer import AccessMode, DaqPolicy, VectoString, slot
from .scenes import get_scene


availableScenes = VectorString(
    displayedName="Available Scenes",
    displayType="Scenes",
    accessMode=AccessMode.READONLY,
    defaultValue=["overview"],
    daqPolicy=DaqPolicy.OMIT)

@slot
def requestScene(self, params):
    name = params.get('name', default='overview')
    payload = Hash('success', True, 'name', name,
                   'data', get_scene(self.deviceId))

    return Hash('type', 'deviceScene',
                'origin', self.deviceId,
                'payload', payload)

Note

Note that we use here slot, and not Slot(). These are two different functions. slot provides framework-level slots, whereas Slot are device-level.

Would you want to provide several scenes (e.g., simple overview and control scene), you can define several functions in scenes.py, and modify requestScene to check params[‘name’]:

from karabo.middlelayer import AccessMode, DaqPolicy, VectoString, slot
import .scenes

availableScenes = VectorString(
    displayedName="Available Scenes",
    displayType="Scenes",
    accessMode=AccessMode.READONLY,
    defaultValue=["overview", "controls"],
    daqPolicy=DaqPolicy.OMIT)

@slot
def requestScene(self, params):
    payload = Hash('success', False)
    name = params.get('name', default='overview')

    if name == 'overview':
        payload.set('success', True)
        payload.set('name', name)
        payload.set('data', scenes.overview(self.deviceId))

    elif name == 'controls':
        payload.set('success', True)
        payload.set('name', name)
        payload.set('data', scenes.controls(self.deviceId))

    return Hash('type', 'deviceScene',
                'origin', self.deviceId,
                'payload', payload)

Note

There is the convention that the default scene (of your choice) should be first in the availableScenes list.

As described in Table Element (VectorHash), table elements are vectors of hash, the schema is specified as Hash serialized to XML, (which karabo-scene2py takes care of).

In this case, it’s fine to break the PEP8 80 characters limit. A table element looks like:

 table = TableElementModel(
     column_schema='TriggerRow:<root KRB_Artificial="">CONTENT</root>',
     height=196.0, keys=['{}.triggerEnv'.format(deviceId)],
     klass='DisplayTableElement',
     parent_component='DisplayComponent',
     width=436.0, x=19.0, y=484.0
)

The following applies whether you want to link to another of your scenes or to another device’s scene.

Let’s say that you want to add links in your overview scene to your controls scene.

The DeviceSceneLinkModel allows you to specify links to other dynamically provided scenes.

In your scenes.py, import DeviceSceneLinkModel and SceneTargetWindow from karabo.common.scenemodel.api and extend overview(deviceId)():

from karabo.common.scenemodel.api import DeviceSceneLinkModel, SceneTargetWindow

 def overview(deviceId):
    # remaining scene stays the same

     link_to_controls = DeviceSceneLinkModel(
         height=40.0, width=314.0, x=114.0, y=227.0,
         parent_component='DisplayComponent',
         keys=['{}.availableScenes'.format(deviceId)], target='controls',
         text='Controls scene',
         target_window=SceneTargetWindow.Dialog)

     children = [label, input, link_to_controls]
     scene = SceneModel(height=1017.0, width=1867.0, children=children)

    return write_scene(scene)

If you want to link to another device, make overview() accept another remoteDeviceId parameter, and point the link to that device:

def overview(deviceId, remoteDeviceId):
    # remaining scene stays the same

    link_to_remote = DeviceSceneLinkModel(
        height=40.0, width=314.0, x=114.0, y=267.0,
        parent_component='DisplayComponent',
        text='Link to other device',
        keys=['{}.availableScenes'.format(remoteDeviceId)], target='scene',
        target_window=SceneTargetWindow.Dialog
     )

     children = [label, input, link_to_controls, link_to_remote]
     scene = SceneModel(height=1017.0, width=1867.0, children=children)

    return write_scene(scene)

Note

remoteDeviceId is merely the deviceId, here. If you have a proxy, you may want to rethink the arguments to overview and pass it self or the proxy object. Then you can find out exactly what scenes are available there, e.g.:

target = ‘controls’ if ‘controls’ in px.availableScenes else ‘scene’ keys=[‘{}.availableScenes’.format(px.deviceId)], target=target,

GainCurveScan: provides a single default scene

Karabacon: provides several scenes

KEP21: definition of the scene protocol