1 changed files with 267 additions and 0 deletions
@ -0,0 +1,267 @@ |
|||||
|
# Gateway Support in Device Install Framework — Design Spec |
||||
|
|
||||
|
## Goal |
||||
|
|
||||
|
Extend the device install wizard to support gateway provisioning: creating a gateway device, configuring one or more connectors via shared attributes, and providing the gateway launch command (docker-compose download URL) in post-install instructions. |
||||
|
|
||||
|
## New Step Types |
||||
|
|
||||
|
### GATEWAY |
||||
|
|
||||
|
Creates a gateway device — a device with `additionalInfo: { gateway: true }`. |
||||
|
|
||||
|
- **Template:** device JSON (same format as DEVICE step), must include `"additionalInfo": {"gateway": true}` |
||||
|
- **After creation:** fetches device credentials (same as DEVICE) |
||||
|
- **Find-or-create:** no — always creates new (same as DEVICE) |
||||
|
- **Output variables:** |
||||
|
- `${gateway.id}` — device UUID |
||||
|
- `${gateway.name}` — device name |
||||
|
- `${gateway.token}` — access token |
||||
|
- `${gateway.dockerComposeUrl}` — `/api/device-connectivity/gateway-launch/${gateway.id}/docker-compose/download` |
||||
|
|
||||
|
### GATEWAY_CONNECTOR |
||||
|
|
||||
|
Configures a connector on a previously created gateway by saving connector config as shared attributes. |
||||
|
|
||||
|
- **Template:** connector config JSON — the object with `name`, `type`, `configurationJson`, `logLevel`, etc. |
||||
|
- **Behavior:** |
||||
|
1. Fetch current `active_connectors` shared attribute from the gateway (may be empty/missing — default to `[]`) |
||||
|
2. Append connector name to the array |
||||
|
3. Save updated `active_connectors` as shared attribute on the gateway |
||||
|
4. Save `{connectorName}: connectorConfig` as shared attribute on the gateway |
||||
|
- **Target entity:** uses `${gateway.id}` from the preceding GATEWAY step |
||||
|
- **Output variables:** `${gatewayConnector.name}` — the connector name from the template |
||||
|
- **Multiple steps:** each GATEWAY_CONNECTOR step appends to `active_connectors`. Two connectors → two steps → `active_connectors = ["Modbus Connector", "MQTT Connector"]` |
||||
|
|
||||
|
## Extension: Optional Attributes on Entity Steps |
||||
|
|
||||
|
All entity creation steps (DEVICE, GATEWAY, DEVICE_PROFILE, DASHBOARD, RULE_CHAIN) gain two optional fields in the step definition: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"type": "GATEWAY", |
||||
|
"name": "${deviceName}", |
||||
|
"template": "gateway.json", |
||||
|
"serverAttributes": "server-attributes.json", |
||||
|
"sharedAttributes": "shared-attributes.json" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- `serverAttributes` — optional, path to JSON file in ZIP. After entity creation, file is read, variables resolved, and saved as `SERVER_SCOPE` attributes on the created entity. |
||||
|
- `sharedAttributes` — optional, path to JSON file in ZIP. Same, saved as `SHARED_SCOPE`. |
||||
|
|
||||
|
Both files contain a flat JSON object of key-value pairs: |
||||
|
```json |
||||
|
{ |
||||
|
"firmwareVersion": "1.2.3", |
||||
|
"configUrl": "${http.host}:${http.port}/config" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Changes to Existing Models |
||||
|
|
||||
|
### DeviceInstallStep interface (device-package.models.ts) |
||||
|
|
||||
|
Add fields: |
||||
|
```typescript |
||||
|
export interface DeviceInstallStep { |
||||
|
type: InstallStepType; |
||||
|
name: string; |
||||
|
file?: string; |
||||
|
template?: string; |
||||
|
serverAttributes?: string; // NEW |
||||
|
sharedAttributes?: string; // NEW |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### InstallStepType enum |
||||
|
|
||||
|
Add: |
||||
|
```typescript |
||||
|
GATEWAY = 'GATEWAY', |
||||
|
GATEWAY_CONNECTOR = 'GATEWAY_CONNECTOR' |
||||
|
``` |
||||
|
|
||||
|
### ENTITY_STEP_TYPES set |
||||
|
|
||||
|
Add `GATEWAY` and `GATEWAY_CONNECTOR`. |
||||
|
|
||||
|
### stepTypeAliasMap |
||||
|
|
||||
|
Add: |
||||
|
```typescript |
||||
|
GATEWAY: 'gateway', |
||||
|
GATEWAY_CONNECTOR: 'gatewayConnector' |
||||
|
``` |
||||
|
|
||||
|
## Variable Resolution Updates |
||||
|
|
||||
|
### New named entity outputs |
||||
|
|
||||
|
| Step type | Variables | |
||||
|
|-----------|-----------| |
||||
|
| GATEWAY | `${gateway.id}`, `${gateway.name}`, `${gateway.token}`, `${gateway.dockerComposeUrl}` | |
||||
|
| GATEWAY_CONNECTOR | `${gatewayConnector.name}` | |
||||
|
|
||||
|
### EntityStepOutput interface |
||||
|
|
||||
|
Add optional `dockerComposeUrl` field: |
||||
|
```typescript |
||||
|
export interface EntityStepOutput { |
||||
|
id: string; |
||||
|
name: string; |
||||
|
token?: string; |
||||
|
dockerComposeUrl?: string; // NEW |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Frontend Implementation (createEntity) |
||||
|
|
||||
|
### GATEWAY case |
||||
|
|
||||
|
Same as DEVICE: |
||||
|
1. Save device via `deviceService.saveDevice(template, {ignoreErrors: true})` |
||||
|
2. Fetch credentials via `deviceService.getDeviceCredentials(id)` |
||||
|
3. Return output with `dockerComposeUrl` computed from the device ID |
||||
|
|
||||
|
```typescript |
||||
|
case InstallStepType.GATEWAY: { |
||||
|
const result = await firstValueFrom(this.deviceService.saveDevice(template, {ignoreErrors: true})); |
||||
|
const creds = await firstValueFrom(this.deviceService.getDeviceCredentials(result.id.id, false, {ignoreErrors: true})); |
||||
|
return { |
||||
|
id: result.id.id, |
||||
|
name: result.name, |
||||
|
token: creds.credentialsId, |
||||
|
dockerComposeUrl: `/api/device-connectivity/gateway-launch/${result.id.id}/docker-compose/download` |
||||
|
}; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### GATEWAY_CONNECTOR case |
||||
|
|
||||
|
1. Read and resolve the connector template from ZIP |
||||
|
2. Extract `name` from the connector config |
||||
|
3. Fetch current `active_connectors` from gateway's shared attributes (or default to `[]`) |
||||
|
4. Append connector name |
||||
|
5. Save both attributes to gateway via `attributeService.saveEntityAttributes()` |
||||
|
|
||||
|
```typescript |
||||
|
case InstallStepType.GATEWAY_CONNECTOR: { |
||||
|
const gatewayOutput = this.entityOutputs.get('gateway'); |
||||
|
if (!gatewayOutput) throw new Error('GATEWAY step must precede GATEWAY_CONNECTOR'); |
||||
|
const gatewayEntityId = { entityType: 'DEVICE', id: gatewayOutput.id }; |
||||
|
|
||||
|
// Fetch current active_connectors |
||||
|
const attrs = await firstValueFrom(this.attributeService.getEntityAttributes( |
||||
|
gatewayEntityId, AttributeScope.SHARED_SCOPE, ['active_connectors'], {ignoreErrors: true} |
||||
|
)); |
||||
|
const activeConnectors: string[] = attrs.find(a => a.key === 'active_connectors')?.value || []; |
||||
|
|
||||
|
// Add this connector |
||||
|
const connectorName = template.name; |
||||
|
if (!activeConnectors.includes(connectorName)) { |
||||
|
activeConnectors.push(connectorName); |
||||
|
} |
||||
|
|
||||
|
// Save attributes |
||||
|
await firstValueFrom(this.attributeService.saveEntityAttributes( |
||||
|
gatewayEntityId, AttributeScope.SHARED_SCOPE, |
||||
|
[ |
||||
|
{ key: 'active_connectors', value: activeConnectors }, |
||||
|
{ key: connectorName, value: template } |
||||
|
], |
||||
|
{ignoreErrors: true} |
||||
|
)); |
||||
|
|
||||
|
return { id: gatewayOutput.id, name: connectorName }; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Attribute saving after any entity step |
||||
|
|
||||
|
After `createEntity()` returns, check if the step has `serverAttributes` or `sharedAttributes`. If so, read the file, resolve variables, and save: |
||||
|
|
||||
|
```typescript |
||||
|
if (step.serverAttributes) { |
||||
|
const attrsJson = JSON.parse(this.resolveVariables(this.zipFiles.get(step.serverAttributes))); |
||||
|
const attrs = Object.entries(attrsJson).map(([key, value]) => ({ key, value })); |
||||
|
await firstValueFrom(this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, attrs, {ignoreErrors: true})); |
||||
|
} |
||||
|
if (step.sharedAttributes) { |
||||
|
const attrsJson = JSON.parse(this.resolveVariables(this.zipFiles.get(step.sharedAttributes))); |
||||
|
const attrs = Object.entries(attrsJson).map(([key, value]) => ({ key, value })); |
||||
|
await firstValueFrom(this.attributeService.saveEntityAttributes(entityId, AttributeScope.SHARED_SCOPE, attrs, {ignoreErrors: true})); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Translation Keys |
||||
|
|
||||
|
Add: |
||||
|
``` |
||||
|
"iot-hub.device-install-step-type-GATEWAY": "Gateway", |
||||
|
"iot-hub.device-install-step-type-GATEWAY_CONNECTOR": "Gateway Connector" |
||||
|
``` |
||||
|
|
||||
|
## Example Gateway Package |
||||
|
|
||||
|
``` |
||||
|
modbus-sensor.zip/ |
||||
|
├── device-info.json |
||||
|
├── prerequisites.md |
||||
|
├── form.json |
||||
|
├── gateway.json |
||||
|
├── modbus-connector.json |
||||
|
├── dashboard.json |
||||
|
└── post-install.md |
||||
|
``` |
||||
|
|
||||
|
`device-info.json`: |
||||
|
```json |
||||
|
{ |
||||
|
"name": "Modbus Sensor", |
||||
|
"vendor": "Example", |
||||
|
"hardwareType": "SENSOR", |
||||
|
"connectivityTypes": ["GATEWAY_MODBUS"], |
||||
|
"installSteps": { |
||||
|
"GATEWAY_MODBUS": [ |
||||
|
{"type": "SHOW_INSTRUCTION", "name": "Prerequisites", "file": "prerequisites.md"}, |
||||
|
{"type": "SHOW_FORM", "name": "Configuration", "file": "form.json"}, |
||||
|
{"type": "GATEWAY", "name": "${deviceName} Gateway", "template": "gateway.json"}, |
||||
|
{"type": "GATEWAY_CONNECTOR", "name": "Modbus Connector", "template": "modbus-connector.json"}, |
||||
|
{"type": "DASHBOARD", "name": "Modbus Monitor", "template": "dashboard.json"}, |
||||
|
{"type": "SHOW_INSTRUCTION", "name": "Launch Gateway", "file": "post-install.md"} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`gateway.json`: |
||||
|
```json |
||||
|
{ |
||||
|
"name": "${deviceName} Gateway", |
||||
|
"type": "Gateway", |
||||
|
"additionalInfo": {"gateway": true} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`post-install.md`: |
||||
|
```markdown |
||||
|
## Launch Gateway |
||||
|
|
||||
|
1. [Download docker-compose.yml](${gateway.dockerComposeUrl}) |
||||
|
2. Place the file in a directory and run: |
||||
|
|
||||
|
\`\`\`bash |
||||
|
docker compose up |
||||
|
\`\`\` |
||||
|
|
||||
|
The gateway will connect to ThingsBoard at `${mqtt.host}:${mqtt.port}` using access token `${gateway.token}`. |
||||
|
``` |
||||
|
|
||||
|
## No Backend Changes |
||||
|
|
||||
|
All new logic is frontend-only: |
||||
|
- GATEWAY uses existing `saveDevice` API (same as DEVICE) |
||||
|
- GATEWAY_CONNECTOR uses existing `saveEntityAttributes` API |
||||
|
- Attribute saving uses existing `saveEntityAttributes` API |
||||
|
- `dockerComposeUrl` is a constructed URL string, not a new endpoint |
||||
Loading…
Reference in new issue