Device node: a device's ports are wired in flows
- flow/nodes/device.py: `device` node with params `device` and `config`;
provides the device's `in` ports, requires its `out` ports without
waking anything, never run (Node.passive), down "Offline" from presence
- DeviceDef.io and DevicePort go; GET /devices/{id} computes `io` and
`bindings` from the device nodes of published flows, in a fixed order,
cached by FlowStore.revision; the credential's scope comes from them
- issues `unknown_device` and `device_port` (wired twice; against a report
the wrong direction or dtype, or unlisted as advisory), rechecked when a
device is created or deleted; ValidationIssue.advisory is a field
- a device publishing on a bound port is credited to its node by the
server; the panel endpoint kind is gone from `_endpoints` and the canvas
- a rebuild that moves a device's bindings sends `dashboard_changed` with
an empty name, which the firmware refetches on
- startup moves each device's `io` rows into device nodes in the flows
declaring their messages, draft and published, one commit; rows no
flow declares are logged and left out
- frontend: Device node settings (device, Publishes, Follows, a port
picker from a report), palette shortcuts per reported port, DeviceDetail
lists bindings with "Use in flow" in place of the I/O editor
- docs: devices, flow editor, node types, issues, API
This commit is contained in:
@@ -911,6 +911,36 @@ export const DeadLetterSchema = {
|
||||
title: 'DeadLetter'
|
||||
} as const;
|
||||
|
||||
export const DeviceBindingSchema = {
|
||||
properties: {
|
||||
flow: {
|
||||
type: 'string',
|
||||
title: 'Flow'
|
||||
},
|
||||
node: {
|
||||
type: 'string',
|
||||
title: 'Node'
|
||||
},
|
||||
port: {
|
||||
type: 'string',
|
||||
title: 'Port'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
title: 'Message'
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
enum: ['in', 'out'],
|
||||
title: 'Direction'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['flow', 'node', 'port', 'message', 'direction'],
|
||||
title: 'DeviceBinding',
|
||||
description: 'Where a port is wired: the device node holding it.'
|
||||
} as const;
|
||||
|
||||
export const DeviceCreateSchema = {
|
||||
properties: {
|
||||
title: {
|
||||
@@ -946,13 +976,6 @@ export const DeviceDefSchema = {
|
||||
title: 'Touch',
|
||||
default: false
|
||||
},
|
||||
io: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DevicePort'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Io'
|
||||
},
|
||||
version: {
|
||||
type: 'integer',
|
||||
title: 'Version',
|
||||
@@ -965,7 +988,59 @@ export const DeviceDefSchema = {
|
||||
description: 'One device, and what it shows.'
|
||||
} as const;
|
||||
|
||||
export const DevicePortSchema = {
|
||||
export const DeviceDocumentSchema = {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
title: 'Id'
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
default: ''
|
||||
},
|
||||
dashboards: {
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Dashboards'
|
||||
},
|
||||
touch: {
|
||||
type: 'boolean',
|
||||
title: 'Touch',
|
||||
default: false
|
||||
},
|
||||
version: {
|
||||
type: 'integer',
|
||||
title: 'Version',
|
||||
default: 0
|
||||
},
|
||||
io: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DeviceIo'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Io'
|
||||
},
|
||||
bindings: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DeviceBinding'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Bindings'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
title: 'DeviceDocument',
|
||||
description: `A device as it reads itself: what it shows, and what its ports carry.
|
||||
|
||||
\`\`io\`\` is worked out from the device nodes in published flows, in a fixed
|
||||
order, so an unchanged state reads back as the same bytes.`
|
||||
} as const;
|
||||
|
||||
export const DeviceIoSchema = {
|
||||
properties: {
|
||||
port: {
|
||||
type: 'string',
|
||||
@@ -988,12 +1063,8 @@ export const DevicePortSchema = {
|
||||
},
|
||||
type: 'object',
|
||||
required: ['port', 'message', 'direction'],
|
||||
title: 'DevicePort',
|
||||
description: `One piece of the device's own hardware, wired to a message.
|
||||
|
||||
A wall panel is a node as well as a screen: a button on its GPIO, a
|
||||
microphone, a speaker. Which ports exist is the device's business, so the
|
||||
name is free text and \`\`config\`\` is passed through to it unread.`
|
||||
title: 'DeviceIo',
|
||||
description: 'One port as the device reads it: the message it publishes or follows.'
|
||||
} as const;
|
||||
|
||||
export const DevicePublicSchema = {
|
||||
@@ -1019,18 +1090,25 @@ export const DevicePublicSchema = {
|
||||
title: 'Touch',
|
||||
default: false
|
||||
},
|
||||
io: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DevicePort'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Io'
|
||||
},
|
||||
version: {
|
||||
type: 'integer',
|
||||
title: 'Version',
|
||||
default: 0
|
||||
},
|
||||
io: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DeviceIo'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Io'
|
||||
},
|
||||
bindings: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DeviceBinding'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Bindings'
|
||||
},
|
||||
online: {
|
||||
type: 'boolean',
|
||||
title: 'Online',
|
||||
@@ -1047,6 +1125,18 @@ export const DevicePublicSchema = {
|
||||
}
|
||||
],
|
||||
title: 'Last Seen'
|
||||
},
|
||||
report: {
|
||||
anyOf: [
|
||||
{
|
||||
additionalProperties: true,
|
||||
type: 'object'
|
||||
},
|
||||
{
|
||||
type: 'null'
|
||||
}
|
||||
],
|
||||
title: 'Report'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
@@ -1125,9 +1215,8 @@ export const EndpointSchema = {
|
||||
title: 'Endpoint',
|
||||
description: `Something wired into this flow that is not a node in it.
|
||||
|
||||
A dashboard control setting one of its messages, a tile showing one, a
|
||||
port on a panel's own hardware, or a node in another flow on the far side
|
||||
of a dotted name. The canvas draws these so a value never appears to come
|
||||
A dashboard control setting one of its messages, a tile showing one, or a
|
||||
node in another flow on the far side of a dotted name. The canvas draws these so a value never appears to come
|
||||
from nowhere — or worse, appears to come from whichever node happens to be
|
||||
drawn as a producer.`
|
||||
} as const;
|
||||
@@ -4638,7 +4727,7 @@ export const ValidationIssueSchema = {
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
enum: ['cycle', 'unconnected_input', 'missing_initial_value', 'node_error', 'unauthenticated_hook', 'self_loop_needs_initial', 'missing_source', 'node_unhealthy', 'ignored_device'],
|
||||
enum: ['cycle', 'unconnected_input', 'missing_initial_value', 'node_error', 'unauthenticated_hook', 'self_loop_needs_initial', 'missing_source', 'node_unhealthy', 'ignored_device', 'unknown_device', 'device_port'],
|
||||
title: 'Code'
|
||||
},
|
||||
message: {
|
||||
@@ -4694,12 +4783,11 @@ export const ValidationIssueSchema = {
|
||||
advisory: {
|
||||
type: 'boolean',
|
||||
title: 'Advisory',
|
||||
description: 'Worth saying, but not a fault — the UI says so in a softer tone.',
|
||||
readOnly: true
|
||||
default: false
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['code', 'message', 'advisory'],
|
||||
required: ['code', 'message'],
|
||||
title: 'ValidationIssue',
|
||||
description: 'Something wrong with a flow — a fault, or merely advisory.'
|
||||
} as const;
|
||||
|
||||
@@ -935,13 +935,14 @@ export class DevicesService {
|
||||
* Read Device
|
||||
* One device, read by a person or by the device holding its credential.
|
||||
*
|
||||
* The definition alone: a device compares what it reads with what it is
|
||||
* showing, and a field that changes on every request would rebuild its
|
||||
* screen each time. Declared after the pairing routes and ``/pending``, so
|
||||
* neither is mistaken for a device of that name.
|
||||
* The definition and what the flows wire its ports to, nothing about this
|
||||
* moment: a device compares what it reads with what it is showing, and a
|
||||
* field that changes on every request would rebuild its screen each time.
|
||||
* Declared after the pairing routes and ``/pending``, so neither is mistaken
|
||||
* for a device of that name.
|
||||
* @param data The data for the request.
|
||||
* @param data.deviceId
|
||||
* @returns DeviceDef Successful Response
|
||||
* @returns DeviceDocument Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static readDevice(data: DevicesReadDeviceData): CancelablePromise<DevicesReadDeviceResponse> {
|
||||
|
||||
@@ -229,6 +229,19 @@ export type DeadLetter = {
|
||||
reason: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Where a port is wired: the device node holding it.
|
||||
*/
|
||||
export type DeviceBinding = {
|
||||
flow: string;
|
||||
node: string;
|
||||
port: string;
|
||||
message: string;
|
||||
direction: 'in' | 'out';
|
||||
};
|
||||
|
||||
export type direction = 'in' | 'out';
|
||||
|
||||
export type DeviceCreate = {
|
||||
title?: string;
|
||||
};
|
||||
@@ -241,18 +254,29 @@ export type DeviceDef = {
|
||||
title?: string;
|
||||
dashboards?: Array<(string)>;
|
||||
touch?: boolean;
|
||||
io?: Array<DevicePort>;
|
||||
version?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* One piece of the device's own hardware, wired to a message.
|
||||
* A device as it reads itself: what it shows, and what its ports carry.
|
||||
*
|
||||
* A wall panel is a node as well as a screen: a button on its GPIO, a
|
||||
* microphone, a speaker. Which ports exist is the device's business, so the
|
||||
* name is free text and ``config`` is passed through to it unread.
|
||||
* ``io`` is worked out from the device nodes in published flows, in a fixed
|
||||
* order, so an unchanged state reads back as the same bytes.
|
||||
*/
|
||||
export type DevicePort = {
|
||||
export type DeviceDocument = {
|
||||
id: string;
|
||||
title?: string;
|
||||
dashboards?: Array<(string)>;
|
||||
touch?: boolean;
|
||||
version?: number;
|
||||
io?: Array<DeviceIo>;
|
||||
bindings?: Array<DeviceBinding>;
|
||||
};
|
||||
|
||||
/**
|
||||
* One port as the device reads it: the message it publishes or follows.
|
||||
*/
|
||||
export type DeviceIo = {
|
||||
port: string;
|
||||
message: string;
|
||||
direction: 'in' | 'out';
|
||||
@@ -261,8 +285,6 @@ export type DevicePort = {
|
||||
};
|
||||
};
|
||||
|
||||
export type direction = 'in' | 'out';
|
||||
|
||||
/**
|
||||
* A device as the Devices tab lists it, with whether it is there.
|
||||
*/
|
||||
@@ -271,10 +293,14 @@ export type DevicePublic = {
|
||||
title?: string;
|
||||
dashboards?: Array<(string)>;
|
||||
touch?: boolean;
|
||||
io?: Array<DevicePort>;
|
||||
version?: number;
|
||||
io?: Array<DeviceIo>;
|
||||
bindings?: Array<DeviceBinding>;
|
||||
online?: boolean;
|
||||
last_seen?: (string | null);
|
||||
report?: ({
|
||||
[key: string]: unknown;
|
||||
} | null);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -318,9 +344,8 @@ export type DType = 'float' | 'int' | 'str' | 'bool' | 'json' | 'series' | 'reco
|
||||
/**
|
||||
* Something wired into this flow that is not a node in it.
|
||||
*
|
||||
* A dashboard control setting one of its messages, a tile showing one, a
|
||||
* port on a panel's own hardware, or a node in another flow on the far side
|
||||
* of a dotted name. The canvas draws these so a value never appears to come
|
||||
* A dashboard control setting one of its messages, a tile showing one, or a
|
||||
* node in another flow on the far side of a dotted name. The canvas draws these so a value never appears to come
|
||||
* from nowhere — or worse, appears to come from whichever node happens to be
|
||||
* drawn as a producer.
|
||||
*/
|
||||
@@ -1489,20 +1514,17 @@ export type ValidationError = {
|
||||
* Something wrong with a flow — a fault, or merely advisory.
|
||||
*/
|
||||
export type ValidationIssue = {
|
||||
code: 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial' | 'missing_source' | 'node_unhealthy' | 'ignored_device';
|
||||
code: 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial' | 'missing_source' | 'node_unhealthy' | 'ignored_device' | 'unknown_device' | 'device_port';
|
||||
message: string;
|
||||
flow?: string;
|
||||
nodes?: Array<(string)>;
|
||||
node?: (string | null);
|
||||
port?: (string | null);
|
||||
message_name?: (string | null);
|
||||
/**
|
||||
* Worth saying, but not a fault — the UI says so in a softer tone.
|
||||
*/
|
||||
readonly advisory: boolean;
|
||||
advisory?: boolean;
|
||||
};
|
||||
|
||||
export type code = 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial' | 'missing_source' | 'node_unhealthy' | 'ignored_device';
|
||||
export type code = 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial' | 'missing_source' | 'node_unhealthy' | 'ignored_device' | 'unknown_device' | 'device_port';
|
||||
|
||||
export type ValidationResult = {
|
||||
issues?: Array<ValidationIssue>;
|
||||
@@ -1838,7 +1860,7 @@ export type DevicesReadDeviceData = {
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export type DevicesReadDeviceResponse = (DeviceDef);
|
||||
export type DevicesReadDeviceResponse = (DeviceDocument);
|
||||
|
||||
export type DevicesSaveDeviceData = {
|
||||
deviceId: string;
|
||||
|
||||
Reference in New Issue
Block a user