diff --git a/.changeset/famous-actors-mix.md b/.changeset/famous-actors-mix.md new file mode 100644 index 000000000..791832d41 --- /dev/null +++ b/.changeset/famous-actors-mix.md @@ -0,0 +1,5 @@ +--- +"@smartthings/cli": patch +--- + +feat: Refactored devices command and added health and status flags. diff --git a/packages/cli/src/__tests__/commands/devices.test.ts b/packages/cli/src/__tests__/commands/devices.test.ts index e7c51bd40..098e0f5d8 100644 --- a/packages/cli/src/__tests__/commands/devices.test.ts +++ b/packages/cli/src/__tests__/commands/devices.test.ts @@ -84,6 +84,22 @@ describe('DevicesCommand', () => { expect(withLocationsAndRoomsMock).toHaveBeenCalledTimes(0) }) + it('adds health status with health flag', async () => { + await expect(DevicesCommand.run(['--health'])).resolves.not.toThrow() + + expect(outputListingMock).toHaveBeenCalledTimes(1) + expect(outputListingMock.mock.calls[0][1].listTableFieldDefinitions) + .toEqual(['label', 'name', 'type', { label: 'Health', prop: 'healthState.state' }, 'deviceId']) + + const listDevices = outputListingMock.mock.calls[0][3] + + expect(await listDevices()).toBe(devices) + + expect(listSpy).toHaveBeenCalledTimes(1) + expect(listSpy).toHaveBeenCalledWith(expect.objectContaining({ capability: undefined })) + expect(withLocationsAndRoomsMock).toHaveBeenCalledTimes(0) + }) + it('adds locations with verbose flag', async () => { await expect(DevicesCommand.run(['--verbose'])).resolves.not.toThrow() @@ -185,7 +201,7 @@ describe('DevicesCommand', () => { }) it('uses devices.get to get device', async () => { - await expect(DevicesCommand.run(['--capability', 'cmd-line-capability'])).resolves.not.toThrow() + await expect(DevicesCommand.run([])).resolves.not.toThrow() expect(outputListingMock).toHaveBeenCalledTimes(1) expect(outputListingMock.mock.calls[0][1].listTableFieldDefinitions) @@ -198,6 +214,65 @@ describe('DevicesCommand', () => { expect(await getDevice('chosen-device-id')).toBe(device) expect(getSpy).toHaveBeenCalledTimes(1) - expect(getSpy).toHaveBeenCalledWith('chosen-device-id') + expect(getSpy).toHaveBeenCalledWith('chosen-device-id', { includeStatus: undefined }) + }) + + it('uses UUID from the command line', async () => { + const deviceId = 'device-id' + const getSpy = jest.spyOn(DevicesEndpoint.prototype, 'get').mockImplementation() + + outputListingMock.mockImplementationOnce(async (_command, _config, _id, _listFunction, getFunction) => { + await getFunction(deviceId) + }) + + await expect(DevicesCommand.run([deviceId])).resolves.not.toThrow() + expect(outputListing).toBeCalledWith( + expect.anything(), + expect.anything(), + deviceId, + expect.anything(), + expect.anything(), + ) + expect(getSpy).toBeCalledWith(deviceId, { includeStatus: undefined }) + }) + + it('includes attribute values when status flag is set', async () => { + const deviceId = 'device-id' + const getSpy = jest.spyOn(DevicesEndpoint.prototype, 'get').mockImplementation() + + outputListingMock.mockImplementationOnce(async (_command, _config, _id, _listFunction, getFunction) => { + await getFunction(deviceId) + }) + + await expect(DevicesCommand.run([deviceId, '--status'])).resolves.not.toThrow() + expect(outputListing).toBeCalledWith( + expect.anything(), + expect.anything(), + deviceId, + expect.anything(), + expect.anything(), + ) + expect(getSpy).toBeCalledWith(deviceId, { includeStatus: true }) + }) + + it('includes health status when health flag is set', async () => { + const deviceId = 'device-id' + const getSpy = jest.spyOn(DevicesEndpoint.prototype, 'get').mockImplementation() + const getHealthSpy = jest.spyOn(DevicesEndpoint.prototype, 'getHealth').mockImplementation() + + outputListingMock.mockImplementationOnce(async (_command, _config, _id, _listFunction, getFunction) => { + await getFunction(deviceId) + }) + + await expect(DevicesCommand.run([deviceId, '--health'])).resolves.not.toThrow() + expect(outputListing).toBeCalledWith( + expect.anything(), + expect.anything(), + deviceId, + expect.anything(), + expect.anything(), + ) + expect(getSpy).toBeCalledWith(deviceId, { includeStatus: undefined }) + expect(getHealthSpy).toBeCalledWith(deviceId) }) }) diff --git a/packages/cli/src/__tests__/lib/commands/devices-util.test.ts b/packages/cli/src/__tests__/lib/commands/devices-util.test.ts index 69af7e1ac..f316a4532 100644 --- a/packages/cli/src/__tests__/lib/commands/devices-util.test.ts +++ b/packages/cli/src/__tests__/lib/commands/devices-util.test.ts @@ -1,11 +1,203 @@ -import { Device } from '@smartthings/core-sdk' +import { Device, DeviceStatus } from '@smartthings/core-sdk' import { summarizedText, Table, TableGenerator } from '@smartthings/cli-lib' -import { buildTableOutput } from '../../../lib/commands/devices-util' +import { + buildEmbeddedStatusTableOutput, + buildStatusTableOutput, + buildTableOutput, + prettyPrintAttribute, +} from '../../../lib/commands/devices-util' describe('devices-util', () => { + + describe('prettyPrintAttribute', () => { + it ('handles integer value', () => { + expect(prettyPrintAttribute(100)).toEqual('100') + }) + + it ('handles decimal value', () => { + expect(prettyPrintAttribute(21.5)).toEqual('21.5') + }) + + it ('handles string value', () => { + expect(prettyPrintAttribute('active')).toEqual('"active"') + }) + + it ('handles object value', () => { + expect(prettyPrintAttribute({ x: 1, y: 2 })).toEqual('{"x":1,"y":2}') + }) + + it ('handles large object value', () => { + const value = { + name: 'Entity name', + id: 'entity-id', + description: 'This is a test entity. It serves no other purpose other than to be used in this test.', + version: 1, + precision: 120.375, + } + const expectedResult = JSON.stringify(value, null, 2) + expect(prettyPrintAttribute(value)).toEqual(expectedResult) + }) + }) + + describe('buildStatusTableOutput', () => { + const tablePushMock: jest.Mock = jest.fn() + const tableToStringMock = jest.fn() + const tableMock = { + push: tablePushMock, + toString: tableToStringMock, + } as unknown as Table + const newOutputTableMock = jest.fn().mockReturnValue(tableMock) + + const tableGeneratorMock: TableGenerator = { + newOutputTable: newOutputTableMock, + buildTableFromItem: jest.fn(), + buildTableFromList: jest.fn(), + } as TableGenerator + + it('handles a single component', () => { + const deviceStatus: DeviceStatus = { + components: { + main: { + switch: { + switch: { + value: 'off', + }, + }, + }, + }, + } + + tableToStringMock.mockReturnValueOnce('main component status') + + expect(buildStatusTableOutput(tableGeneratorMock, deviceStatus)) + .toEqual('main component status\n') + + expect(tablePushMock).toHaveBeenCalledTimes(1) + expect(tablePushMock).toHaveBeenCalledWith(['switch', 'switch', '"off"']) + }) + + it('handles a multiple components', () => { + const deviceStatus: DeviceStatus = { + components: { + main: { + switch: { + switch: { + value: 'off', + }, + }, + }, + light: { + switchLevel: { + level: { + value: 80, + }, + }, + }, + }, + } + + tableToStringMock.mockReturnValueOnce('main component status') + tableToStringMock.mockReturnValueOnce('light component status') + + expect(buildStatusTableOutput(tableGeneratorMock, deviceStatus)) + .toEqual('\nmain component\nmain component status\n\nlight component\nlight component status\n') + + expect(tablePushMock).toHaveBeenCalledTimes(2) + expect(tablePushMock).toHaveBeenNthCalledWith(1, ['switch', 'switch', '"off"']) + expect(tablePushMock).toHaveBeenNthCalledWith(2, ['switchLevel', 'level', '80']) + }) + }) + + describe('buildEmbeddedStatusTableOutput', () => { + const tablePushMock: jest.Mock = jest.fn() + const tableToStringMock = jest.fn() + const tableMock = { + push: tablePushMock, + toString: tableToStringMock, + } as unknown as Table + const newOutputTableMock = jest.fn().mockReturnValue(tableMock) + + const tableGeneratorMock: TableGenerator = { + newOutputTable: newOutputTableMock, + buildTableFromItem: jest.fn(), + buildTableFromList: jest.fn(), + } as TableGenerator + + it('handles a single component', () => { + const device = { + components: [ + { + id: 'main', + capabilities: [ + { + id: 'switch', + status: { + switch: { + value: 'off', + }, + }, + }, + ], + }, + ], + } as unknown as Device + + tableToStringMock.mockReturnValueOnce('main component status') + + expect(buildEmbeddedStatusTableOutput(tableGeneratorMock, device)) + .toEqual('main component status\n') + + expect(tablePushMock).toHaveBeenCalledTimes(1) + expect(tablePushMock).toHaveBeenCalledWith(['switch', 'switch', '"off"']) + }) + + it('handles a multiple components', () => { + const device = { + components: [ + { + id: 'main', + capabilities: [ + { + id: 'switch', + status: { + switch: { + value: 'off', + }, + }, + }, + ], + }, + { + id: 'light', + capabilities: [ + { + id: 'switchLevel', + status: { + level: { + value: 80, + }, + }, + }, + ], + }, + ], + } as unknown as Device + + tableToStringMock.mockReturnValueOnce('main component status') + tableToStringMock.mockReturnValueOnce('light component status') + + expect(buildEmbeddedStatusTableOutput(tableGeneratorMock, device)) + .toEqual('\nmain component\nmain component status\n\nlight component\nlight component status\n') + + expect(tablePushMock).toHaveBeenCalledTimes(2) + expect(tablePushMock).toHaveBeenNthCalledWith(1, ['switch', 'switch', '"off"']) + expect(tablePushMock).toHaveBeenNthCalledWith(2, ['switchLevel', 'level', '80']) + }) + }) + describe('buildTableOutput', () => { const tablePushMock: jest.Mock = jest.fn() const tableToStringMock = jest.fn() @@ -33,7 +225,7 @@ describe('devices-util', () => { manufacturerName: 'manufacturer-name', locationId: 'location-id', roomId: 'room-id', - components: [{ id: 'component-id', capabilities: [{ id: 'capability-id' }] }], + components: [{ id: 'main', capabilities: [{ id: 'capability-id' }] }], childDevices: [{ id: 'child-id' }], profile: { id: 'device-profile-id' }, } as unknown as Device @@ -43,17 +235,17 @@ describe('devices-util', () => { .toEqual('Main Info\nmain table\n\n' + summarizedText) expect(tablePushMock).toHaveBeenCalledTimes(10) + expect(tablePushMock).toHaveBeenCalledWith(['Label', 'device label']) expect(tablePushMock).toHaveBeenCalledWith(['Name', 'device name']) - expect(tablePushMock).toHaveBeenCalledWith(['Type', 'device type']) expect(tablePushMock).toHaveBeenCalledWith(['Id', 'device-id']) - expect(tablePushMock).toHaveBeenCalledWith(['Label', 'device label']) + expect(tablePushMock).toHaveBeenCalledWith(['Type', 'device type']) expect(tablePushMock).toHaveBeenCalledWith(['Manufacturer Code', 'device-manufacturer-code']) expect(tablePushMock).toHaveBeenCalledWith(['Location Id', 'location-id']) expect(tablePushMock).toHaveBeenCalledWith(['Room Id', 'room-id']) - expect(tablePushMock).toHaveBeenCalledWith(['Child Devices', 'child-id']) - expect(tablePushMock).toHaveBeenCalledWith(['component-id component', 'capability-id']) expect(tablePushMock).toHaveBeenCalledWith(['Profile Id', 'device-profile-id']) - expect(tableToStringMock).toHaveBeenCalledTimes(1) + expect(tablePushMock).toHaveBeenCalledWith(['Capabilities', 'capability-id']) + expect(tablePushMock).toHaveBeenCalledWith(['Child Devices', 'child-id']) + expect(tableToStringMock).toHaveBeenCalledTimes(2) }) it('handles old style profile id', () => { @@ -65,7 +257,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(tablePushMock).toHaveBeenCalledWith(['Profile Id', 'device-profile-id']) }) @@ -76,14 +268,58 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(tablePushMock).toHaveBeenCalledWith(['Manufacturer Code', '']) expect(tablePushMock).toHaveBeenCalledWith(['Location Id', '']) expect(tablePushMock).toHaveBeenCalledWith(['Room Id', '']) - expect(tablePushMock).toHaveBeenCalledWith(['Child Devices', '']) expect(tablePushMock).toHaveBeenCalledWith(['Profile Id', '']) }) + it('displays health state', () => { + const device = { + healthState: { + state: 'ONLINE', + lastUpdatedDate: '2022-07-28T14:50:42.899Z', + }, + } as unknown as Device + tableToStringMock.mockReturnValueOnce('main table') + + expect(buildTableOutput(tableGeneratorMock, device)) + .toEqual('Main Info\nmain table\n\n' + summarizedText) + + expect(tablePushMock).toHaveBeenCalledTimes(10) + expect(tablePushMock).toHaveBeenCalledWith(['Device Health', 'ONLINE']) + expect(tablePushMock).toHaveBeenCalledWith(['Last Updated', '2022-07-28T14:50:42.899Z']) + }) + + it('displays device status', () => { + const device = { + components: [ + { + id: 'main', + capabilities: [ + { + id: 'capability-id', + status: { + switch: { + value: 'off', + }, + }, + }, + ], + }, + ], + } as unknown as Device + tableToStringMock.mockReturnValueOnce('main table') + tableToStringMock.mockReturnValueOnce('device status') + + expect(buildTableOutput(tableGeneratorMock, device)) + .toEqual('Main Info\nmain table\n\nDevice Status\ndevice status\n\n' + summarizedText) + + expect(tablePushMock).toHaveBeenCalledTimes(10) + expect(tableToStringMock).toHaveBeenCalledTimes(2) + }) + it('includes app info', () => { const app = { installedAppId: 'installed-app-id' } const device = { app } as unknown as Device @@ -93,7 +329,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from app)\napp info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(app, ['installedAppId', 'externalId', { prop: 'profile.id', label: 'Profile Id' }]) @@ -107,7 +343,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from ble)\nNo Device Integration Info for BLE devices\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(0) }) @@ -120,7 +356,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from bleD2D)\nbleD2D info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(bleD2D, ['advertisingId', 'identifier', 'configurationVersion', 'configurationUrl']) @@ -135,7 +371,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from dth)\ndth info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(dth, ['deviceTypeId', 'deviceTypeName', 'completedSetup', 'deviceNetworkType', @@ -151,7 +387,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from lan)\nlan info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(lan, ['networkId', 'driverId', 'executingLocally', 'hubId', 'provisioningState']) @@ -166,7 +402,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from zigbee)\nzigbee info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(zigbee, ['eui', 'networkId', 'driverId', 'executingLocally', 'hubId', 'provisioningState']) @@ -181,7 +417,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from zwave)\nzwave info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(zwave, ['networkId', 'driverId', 'executingLocally', 'hubId', 'networkSecurityLevel', 'provisioningState']) @@ -196,7 +432,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from ir)\nir info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(ir, ['parentDeviceId', 'profileId', 'ocfDeviceType', 'irCode']) @@ -211,7 +447,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from irOcf)\nir ocf info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(irOcf, ['parentDeviceId', 'profileId', 'ocfDeviceType', 'irCode']) @@ -226,7 +462,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from ocf)\nocf info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(ocf, ['deviceId', 'ocfDeviceType', 'name', 'specVersion', 'verticalDomainSpecVersion', @@ -243,7 +479,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from viper)\nviper info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(viper, ['uniqueIdentifier', 'manufacturerName', 'modelName', 'swVersion', 'hwVersion']) diff --git a/packages/cli/src/commands/devices.ts b/packages/cli/src/commands/devices.ts index 71725cb32..1cea103bf 100644 --- a/packages/cli/src/commands/devices.ts +++ b/packages/cli/src/commands/devices.ts @@ -1,8 +1,8 @@ import { Flags } from '@oclif/core' -import { Device, DeviceIntegrationType, DeviceListOptions } from '@smartthings/core-sdk' +import { Device, DeviceIntegrationType, DeviceGetOptions, DeviceListOptions } from '@smartthings/core-sdk' -import { APICommand, outputListing, withLocationsAndRooms } from '@smartthings/cli-lib' +import { APICommand, outputListing, TableFieldDefinition, withLocationsAndRooms } from '@smartthings/cli-lib' import { buildTableOutput } from '../lib/commands/devices-util' @@ -39,6 +39,14 @@ export default class DevicesCommand extends APICommand { + const listTableFieldDefinitions: TableFieldDefinition[] = ['label', 'name', 'type', 'deviceId'] + + if (this.flags.verbose) { + listTableFieldDefinitions.splice(3, 0, 'location', 'room') + } + + if (this.flags.health) { + listTableFieldDefinitions.splice(3, 0, { + prop: 'healthState.state', + label: 'Health', + }) + } + const config = { primaryKeyName: 'deviceId', sortKeyName: 'label', - listTableFieldDefinitions: ['label', 'name', 'type', 'deviceId'], + listTableFieldDefinitions, buildTableOutput: (data: Device) => buildTableOutput(this.tableGenerator, data), } - if (this.flags.verbose) { - config.listTableFieldDefinitions.splice(3, 0, 'location', 'room') + + const deviceGetOptions: DeviceGetOptions = { + includeStatus: this.flags.status, } const deviceListOptions: DeviceListOptions = { @@ -74,6 +96,8 @@ export default class DevicesCommand extends APICommand this.client.devices.get(id), + async (id) => { + // Note -- we have to do this explicitly because the API does not honor the includeHealth parameter + // for individual devices + if (this.flags.health) { + const [device, healthState] = await Promise.all([ + this.client.devices.get(id, deviceGetOptions), + this.client.devices.getHealth(id), + ]) + return { ...device, healthState } + } + return this.client.devices.get(id, deviceGetOptions) + }, ) } } diff --git a/packages/cli/src/commands/devices/capability-status.ts b/packages/cli/src/commands/devices/capability-status.ts index 414309d34..6b4386003 100644 --- a/packages/cli/src/commands/devices/capability-status.ts +++ b/packages/cli/src/commands/devices/capability-status.ts @@ -3,7 +3,7 @@ import { CapabilityReference, CapabilityStatus } from '@smartthings/core-sdk' import { APICommand, chooseComponent, chooseDevice, formatAndWriteItem, selectFromList, stringTranslateToId, TableGenerator } from '@smartthings/cli-lib' -import { prettyPrintAttribute } from './status' +import { prettyPrintAttribute } from '../../lib/commands/devices-util' function buildTableOutput(tableGenerator: TableGenerator, capability: CapabilityStatus): string { diff --git a/packages/cli/src/commands/devices/component-status.ts b/packages/cli/src/commands/devices/component-status.ts index 9414ab016..55dbdd06b 100644 --- a/packages/cli/src/commands/devices/component-status.ts +++ b/packages/cli/src/commands/devices/component-status.ts @@ -2,7 +2,7 @@ import { ComponentStatus } from '@smartthings/core-sdk' import { APICommand, chooseComponent, chooseDevice, formatAndWriteItem, TableGenerator } from '@smartthings/cli-lib' -import { prettyPrintAttribute } from './status' +import { prettyPrintAttribute } from '../../lib/commands/devices-util' function buildTableOutput(tableGenerator: TableGenerator, component: ComponentStatus): string { diff --git a/packages/cli/src/commands/devices/status.ts b/packages/cli/src/commands/devices/status.ts index 9e0385817..fc378bb39 100644 --- a/packages/cli/src/commands/devices/status.ts +++ b/packages/cli/src/commands/devices/status.ts @@ -1,44 +1,8 @@ -import { DeviceStatus } from '@smartthings/core-sdk' +import { APICommand, chooseDevice, formatAndWriteItem } from '@smartthings/cli-lib' -import { APICommand, chooseDevice, formatAndWriteItem, TableGenerator } from '@smartthings/cli-lib' +import { buildStatusTableOutput } from '../../lib/commands/devices-util' -export function prettyPrintAttribute(value: unknown): string { - let result = JSON.stringify(value) - if (result.length > 50) { - result = JSON.stringify(value, null, 2) - } - return result -} - -export function buildTableOutput(tableGenerator: TableGenerator, data: DeviceStatus): string { - let output = '' - if (data.components) { - const componentIds = Object.keys(data.components) - for (const componentId of componentIds) { - const table = tableGenerator.newOutputTable({ head: ['Capability', 'Attribute', 'Value'] }) - if (componentIds.length > 1) { - output += `\n${componentId} component\n` - } - const component = data.components[componentId] - for (const capabilityName of Object.keys(component)) { - const capability = component[capabilityName] - for (const attributeName of Object.keys(capability)) { - const attribute = capability[attributeName] - table.push([ - capabilityName, - attributeName, - attribute.value !== null ? - `${prettyPrintAttribute(attribute.value)}${attribute.unit ? ' ' + attribute.unit : ''}` : '']) - } - } - output += table.toString() - output += '\n' - } - } - return output -} - export default class DeviceStatusCommand extends APICommand { static description = "get the current status of all of a device's component's attributes" @@ -55,6 +19,7 @@ export default class DeviceStatusCommand extends APICommand { const deviceId = await chooseDevice(this, this.args.id, { allowIndex: true }) const presentation = await this.client.devices.getStatus(deviceId) - await formatAndWriteItem(this, { buildTableOutput: data => buildTableOutput(this.tableGenerator, data) }, presentation) + await formatAndWriteItem(this, { buildTableOutput: data => + buildStatusTableOutput(this.tableGenerator, data) }, presentation) } } diff --git a/packages/cli/src/lib/commands/devices-util.ts b/packages/cli/src/lib/commands/devices-util.ts index fba585301..7674079e6 100644 --- a/packages/cli/src/lib/commands/devices-util.ts +++ b/packages/cli/src/lib/commands/devices-util.ts @@ -1,27 +1,102 @@ -import { Device } from '@smartthings/core-sdk' +import { Device, DeviceHealth, DeviceStatus } from '@smartthings/core-sdk' import { summarizedText, TableGenerator } from '@smartthings/cli-lib' export type DeviceWithLocation = Device & { location?: string } -export const buildTableOutput = (tableGenerator: TableGenerator, device: Device & { profileId?: string }): string => { +export const prettyPrintAttribute = (value: unknown): string => { + let result = JSON.stringify(value) + if (result.length > 50) { + result = JSON.stringify(value, null, 2) + } + return result +} + +export const buildStatusTableOutput = (tableGenerator: TableGenerator, data: DeviceStatus): string => { + let output = '' + if (data.components) { + const componentIds = Object.keys(data.components) + for (const componentId of componentIds) { + const table = tableGenerator.newOutputTable({ head: ['Capability', 'Attribute', 'Value'] }) + if (componentIds.length > 1) { + output += `\n${componentId} component\n` + } + const component = data.components[componentId] + for (const capabilityName of Object.keys(component)) { + const capability = component[capabilityName] + for (const attributeName of Object.keys(capability)) { + const attribute = capability[attributeName] + table.push([ + capabilityName, + attributeName, + attribute.value !== null ? + `${prettyPrintAttribute(attribute.value)}${attribute.unit ? ' ' + attribute.unit : ''}` : '']) + } + } + output += table.toString() + output += '\n' + } + } + return output +} + +export const buildEmbeddedStatusTableOutput = (tableGenerator: TableGenerator, data: Device): string => { + let output = '' + let hasStatus = false + if (data.components) { + for (const component of data.components) { + const table = tableGenerator.newOutputTable({ head: ['Capability', 'Attribute', 'Value'] }) + if (data.components.length > 1) { + output += `\n${component.id} component\n` + } + + for (const capability of component.capabilities) { + if (capability.status) { + hasStatus = true + for (const attributeName of Object.keys(capability.status)) { + const attribute = capability.status[attributeName] + table.push([ + capability.id, + attributeName, + attribute.value !== null ? + `${prettyPrintAttribute(attribute.value)}${attribute.unit ? ' ' + attribute.unit : ''}` : '']) + } + } + } + output += table.toString() + output += '\n' + } + } + return hasStatus ? output : '' +} + +export const buildTableOutput = (tableGenerator: TableGenerator, device: Device & { profileId?: string; healthState?: DeviceHealth }): string => { const table = tableGenerator.newOutputTable() + table.push(['Label', device.label]) table.push(['Name', device.name]) - table.push(['Type', device.type]) table.push(['Id', device.deviceId]) - table.push(['Label', device.label]) + table.push(['Type', device.type]) table.push(['Manufacturer Code', device.deviceManufacturerCode ?? '']) table.push(['Location Id', device.locationId ?? '']) table.push(['Room Id', device.roomId ?? '']) + table.push(['Profile Id', device.profile?.id ?? (device.profileId ?? '')]) for (const comp of device.components ?? []) { - table.push([`${comp.id} component`, comp.capabilities.map(capability => capability.id).join('\n')]) + const label = comp.id === 'main' ? 'Capabilities' : `Capabilities (${comp.id})` + table.push([label, comp.capabilities.map(capability => capability.id).join('\n')]) + } + if (device.childDevices) { + table.push(['Child Devices', device.childDevices?.map(child => child.id).join('\n') ?? '']) + } + if (device.healthState) { + table.push(['Device Health', device.healthState.state]) + table.push(['Last Updated', device.healthState.lastUpdatedDate]) } - table.push(['Child Devices', device.childDevices?.map(child => child.id).join('\n') ?? '']) - table.push(['Profile Id', device.profile?.id ?? (device.profileId ?? '')]) const mainInfo = table.toString() + const statusInfo = buildEmbeddedStatusTableOutput(tableGenerator, device) + let deviceIntegrationInfo = 'None' let infoFrom if ('app' in device) { @@ -73,6 +148,7 @@ export const buildTableOutput = (tableGenerator: TableGenerator, device: Device } return `Main Info\n${mainInfo}\n\n` + + (statusInfo ? `Device Status\n${statusInfo}\n` : '') + (infoFrom ? `Device Integration Info (from ${infoFrom})\n${deviceIntegrationInfo}\n\n` : '') + summarizedText }