diff --git a/.changeset/eight-apricots-lie.md b/.changeset/eight-apricots-lie.md new file mode 100644 index 000000000..291dac51d --- /dev/null +++ b/.changeset/eight-apricots-lie.md @@ -0,0 +1,6 @@ +--- +"@smartthings/cli-testlib": patch +"@smartthings/cli": patch +--- + +feat: added device and location history commands diff --git a/packages/cli/src/__tests__/commands/devices.test.ts b/packages/cli/src/__tests__/commands/devices.test.ts index 098e0f5d8..774db58c4 100644 --- a/packages/cli/src/__tests__/commands/devices.test.ts +++ b/packages/cli/src/__tests__/commands/devices.test.ts @@ -13,6 +13,8 @@ import { buildTableOutput } from '../../lib/commands/devices-util' jest.mock('../../lib/commands/devices-util') describe('DevicesCommand', () => { + const deviceId = 'device-id' + const getSpy = jest.spyOn(DevicesEndpoint.prototype, 'get').mockImplementation() const outputListingMock = jest.mocked(outputListing) it('passes undefined for location id when not specified', async () => { @@ -218,9 +220,6 @@ describe('DevicesCommand', () => { }) 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) }) @@ -237,9 +236,6 @@ describe('DevicesCommand', () => { }) 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) }) @@ -256,8 +252,6 @@ describe('DevicesCommand', () => { }) 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) => { diff --git a/packages/cli/src/__tests__/commands/devices/history.test.ts b/packages/cli/src/__tests__/commands/devices/history.test.ts new file mode 100644 index 000000000..b492f9cb0 --- /dev/null +++ b/packages/cli/src/__tests__/commands/devices/history.test.ts @@ -0,0 +1,82 @@ +import { + buildOutputFormatter, + calculateOutputFormat, + chooseDevice, + IOFormat, + jsonFormatter, + writeOutput, +} from '@smartthings/cli-lib' +import { Device, DeviceActivity, DevicesEndpoint, HistoryEndpoint, PaginatedList } from '@smartthings/core-sdk' +import DeviceHistoryCommand from '../../../commands/devices/history' +import { writeDeviceEventsTable } from '../../../lib/commands/history-util' + + +jest.mock('../../../lib/commands/history-util') + +describe('DeviceHistoryCommand', () => { + const getDeviceSpy = jest.spyOn(DevicesEndpoint.prototype, 'get').mockImplementation() + const historySpy = jest.spyOn(HistoryEndpoint.prototype, 'devices').mockImplementation() + const deviceSelectionMock = jest.mocked(chooseDevice).mockResolvedValue('deviceId') + const calculateOutputFormatMock = jest.mocked(calculateOutputFormat).mockReturnValue(IOFormat.COMMON) + const writeDeviceEventsTableMock = jest.mocked(writeDeviceEventsTable) + + it('prompts user to select device', async () => { + getDeviceSpy.mockResolvedValue({ locationId: 'locationId' } as Device) + historySpy.mockResolvedValueOnce({ + items: [], + hasNext: (): boolean => false, + } as unknown as PaginatedList) + await expect(DeviceHistoryCommand.run(['deviceId'])).resolves.not.toThrow() + + expect(deviceSelectionMock).toBeCalledWith( + expect.any(DeviceHistoryCommand), + 'deviceId', + { allowIndex: true }, + ) + }) + + it('queries history and writes event table interactively', async () => { + getDeviceSpy.mockResolvedValue({ locationId: 'locationId' } as Device) + historySpy.mockResolvedValueOnce({ + items: [], + hasNext: (): boolean => false, + } as unknown as PaginatedList) + + await expect(DeviceHistoryCommand.run(['deviceId'])).resolves.not.toThrow() + + expect(getDeviceSpy).toBeCalledTimes(1) + expect(getDeviceSpy).toBeCalledWith('deviceId') + expect(historySpy).toBeCalledTimes(1) + expect(historySpy).toBeCalledWith({ + deviceId: 'deviceId', + locationId: 'locationId', + }) + expect(writeDeviceEventsTableMock).toBeCalledTimes(1) + }) + + it('queries history and write event table directly', async () => { + const buildOutputFormatterMock = jest.mocked(buildOutputFormatter) + const writeOutputMock = jest.mocked(writeOutput) + + getDeviceSpy.mockResolvedValue({ locationId: 'locationId' } as Device) + historySpy.mockResolvedValueOnce({ + items: [], + hasNext: (): boolean => false, + } as unknown as PaginatedList) + calculateOutputFormatMock.mockReturnValue(IOFormat.JSON) + buildOutputFormatterMock.mockReturnValue(jsonFormatter(4)) + + await expect(DeviceHistoryCommand.run(['deviceId'])).resolves.not.toThrow() + + expect(getDeviceSpy).toBeCalledTimes(1) + expect(getDeviceSpy).toBeCalledWith('deviceId') + expect(historySpy).toBeCalledTimes(1) + expect(historySpy).toBeCalledWith({ + deviceId: 'deviceId', + locationId: 'locationId', + }) + expect(writeDeviceEventsTableMock).toBeCalledTimes(0) + expect(buildOutputFormatterMock).toBeCalledTimes(1) + expect(writeOutputMock).toBeCalledTimes(1) + }) +}) diff --git a/packages/cli/src/__tests__/commands/locations/history.test.ts b/packages/cli/src/__tests__/commands/locations/history.test.ts new file mode 100644 index 000000000..26bf1914f --- /dev/null +++ b/packages/cli/src/__tests__/commands/locations/history.test.ts @@ -0,0 +1,61 @@ +import { + buildOutputFormatter, + calculateOutputFormat, + IOFormat, + jsonFormatter, + writeOutput, +} from '@smartthings/cli-lib' +import { DeviceActivity, HistoryEndpoint, PaginatedList } from '@smartthings/core-sdk' +import LocationHistoryCommand from '../../../commands/locations/history' +import { writeDeviceEventsTable } from '../../../lib/commands/history-util' +import { chooseLocation } from '../../../commands/locations' + + +jest.mock('../../../lib/commands/history-util') +jest.mock('../../../commands/locations') + +describe('LocationHistoryCommand', () => { + const mockChooseLocation = jest.mocked(chooseLocation).mockResolvedValue('locationId') + const historySpy = jest.spyOn(HistoryEndpoint.prototype, 'devices').mockImplementation() + const calculateOutputFormatMock = jest.mocked(calculateOutputFormat).mockReturnValue(IOFormat.COMMON) + const writeDeviceEventsTableMock = jest.mocked(writeDeviceEventsTable) + + it('queries history and writes event table interactively', async () => { + historySpy.mockResolvedValueOnce({ + items: [], + hasNext: (): boolean => false, + } as unknown as PaginatedList) + + await expect(LocationHistoryCommand.run(['locationId'])).resolves.not.toThrow() + + expect(mockChooseLocation).toBeCalledTimes(1) + expect(historySpy).toBeCalledTimes(1) + expect(historySpy).toBeCalledWith({ + locationId: 'locationId', + }) + expect(writeDeviceEventsTableMock).toBeCalledTimes(1) + }) + + it('queries history and write event table directly', async () => { + const buildOutputFormatterMock = jest.mocked(buildOutputFormatter) + const writeOutputMock = jest.mocked(writeOutput) + + historySpy.mockResolvedValueOnce({ + items: [], + hasNext: (): boolean => false, + } as unknown as PaginatedList) + calculateOutputFormatMock.mockReturnValue(IOFormat.JSON) + buildOutputFormatterMock.mockReturnValue(jsonFormatter(4)) + + await expect(LocationHistoryCommand.run(['locationId'])).resolves.not.toThrow() + + expect(mockChooseLocation).toBeCalledTimes(1) + expect(historySpy).toBeCalledTimes(1) + expect(historySpy).toBeCalledWith({ + locationId: 'locationId', + }) + expect(writeDeviceEventsTableMock).toBeCalledTimes(0) + expect(buildOutputFormatterMock).toBeCalledTimes(1) + expect(writeOutputMock).toBeCalledTimes(1) + }) +}) 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 03ebcc395..189325d82 100644 --- a/packages/cli/src/__tests__/lib/commands/devices-util.test.ts +++ b/packages/cli/src/__tests__/lib/commands/devices-util.test.ts @@ -11,6 +11,21 @@ import { describe('devices-util', () => { + 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 buildTableFromItemMock = jest.fn() + const buildTableFromListMock = jest.fn() + + const tableGeneratorMock: TableGenerator = { + newOutputTable: newOutputTableMock, + buildTableFromItem: buildTableFromItemMock, + buildTableFromList: buildTableFromListMock, + } describe('prettyPrintAttribute', () => { it ('handles integer value', () => { @@ -43,19 +58,6 @@ describe('devices-util', () => { }) 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 = { @@ -112,19 +114,6 @@ describe('devices-util', () => { }) 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 = { @@ -199,21 +188,6 @@ describe('devices-util', () => { }) describe('buildTableOutput', () => { - 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 buildTableFromItemMock = jest.fn() - const buildTableFromListMock = jest.fn() - - const tableGeneratorMock: TableGenerator = { - newOutputTable: newOutputTableMock, - buildTableFromItem: buildTableFromItemMock, - buildTableFromList: buildTableFromListMock, - } it('includes all main fields', () => { const device = { @@ -494,7 +468,7 @@ describe('devices-util', () => { expect(buildTableOutput(tableGeneratorMock, device)) .toEqual('Main Info\nmain table\n\nDevice Integration Info (from virtual)\nvirtual device info\n\n' + summarizedText) - expect(tablePushMock).toHaveBeenCalledTimes(9) + expect(tablePushMock).toHaveBeenCalledTimes(8) expect(buildTableFromItemMock).toHaveBeenCalledTimes(1) expect(buildTableFromItemMock).toHaveBeenCalledWith(virtual, ['name', { prop: 'hubId', skipEmpty: true }, { prop: 'driverId', skipEmpty: true }]) diff --git a/packages/cli/src/__tests__/lib/commands/history-util.test.ts b/packages/cli/src/__tests__/lib/commands/history-util.test.ts new file mode 100644 index 000000000..379a19597 --- /dev/null +++ b/packages/cli/src/__tests__/lib/commands/history-util.test.ts @@ -0,0 +1,204 @@ +import inquirer from 'inquirer' +import { DeviceActivity, PaginatedList } from '@smartthings/core-sdk' +import { SmartThingsCommandInterface, Table, TableGenerator } from '@smartthings/cli-lib' +import { + toEpochTime, + sortEvents, + getNextDeviceEvents, + writeDeviceEventsTable, +} from '../../../lib/commands/history-util' + + +describe('devices-util', () => { + + describe('epochTime', () => { + it ('handles ISO input', () => { + expect(toEpochTime('2022-08-01T22:41:42.559Z')).toBe(1659393702559) + }) + + it ('handles locale time input', () => { + const expected = new Date('8/1/2022, 6:41:42 PM').getTime() + expect(toEpochTime('8/1/2022, 6:41:42 PM')).toBe(expected) + }) + + it ('handles undefined input', () => { + expect(toEpochTime(undefined)).toBeUndefined() + }) + }) + + describe('sortEvents', () => { + it('sorts in reverse order', () => { + const events = [ + { epoch: 1659394186591 }, + { epoch: 1659394186592 }, + { epoch: 1659394186593 }, + { epoch: 1659394186590 }, + ] as DeviceActivity[] + + const result = sortEvents([...events]) + + expect(result).toBeDefined() + expect(result.length).toBe(4) + expect(result[0]).toBe(events[2]) + expect(result[1]).toBe(events[1]) + expect(result[2]).toBe(events[0]) + expect(result[3]).toBe(events[3]) + }) + }) + + describe('getNextDeviceEvents', () => { + const tablePushMock: jest.Mock = jest.fn() + const tableToStringMock = jest.fn() + const tableMock = { + push: tablePushMock, + toString: tableToStringMock, + } as unknown as Table + + it('outputs local time and string values', () => { + const time = new Date() + const items = [ + { time, component: 'main', capability: 'switch', attribute: 'switch', value: 'on' }, + ] as unknown as DeviceActivity[] + + getNextDeviceEvents(tableMock, items, {}) + + expect(tablePushMock).toHaveBeenCalledTimes(1) + expect(tablePushMock).toHaveBeenCalledWith([ + time.toLocaleString(), + 'main', + 'switch', + 'switch', + '"on"', + ]) + expect(tableToStringMock).toHaveBeenCalledTimes(0) + }) + + it('outputs UTC time and number value', () => { + const time = new Date() + const items = [ + { time, component: 'main', capability: 'switchLevel', attribute: 'level', value: 80 }, + ] as unknown as DeviceActivity[] + + getNextDeviceEvents(tableMock, items, { utcTimeFormat: true }) + + expect(tablePushMock).toHaveBeenCalledTimes(1) + expect(tablePushMock).toHaveBeenCalledWith([ + time.toISOString(), + 'main', + 'switchLevel', + 'level', + '80', + ]) + expect(tableToStringMock).toHaveBeenCalledTimes(0) + }) + + it('outputs device name and number with units value', () => { + const time = new Date() + const items = [ + { time, deviceName: 'Thermometer', component: 'main', capability: 'temperature', attribute: 'temp', value: 72, unit: 'F' }, + ] as unknown as DeviceActivity[] + + getNextDeviceEvents(tableMock, items, { includeName: true }) + + expect(tablePushMock).toHaveBeenCalledTimes(1) + expect(tablePushMock).toHaveBeenCalledWith([ + time.toLocaleString(), + 'Thermometer', + 'main', + 'temperature', + 'temp', + '72 F', + ]) + expect(tableToStringMock).toHaveBeenCalledTimes(0) + }) + }) + + describe('writeDeviceEventsTable', () => { + const promptSpy = jest.spyOn(inquirer, 'prompt') + const stdOutSpy = jest.spyOn(process.stdout, 'write') + const tablePushMock: jest.Mock = jest.fn() + const tableToStringMock = jest.fn().mockReturnValue('table') + 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 + + const commandMock = { + tableGenerator: tableGeneratorMock, + } as SmartThingsCommandInterface + + const items = [ + { time: new Date(), deviceName: 'Thermometer', component: 'main', capability: 'temperature', attribute: 'temp', value: 72, unit: 'F' }, + ] as unknown as DeviceActivity[] + + const hasNext = jest.fn() + const next = jest.fn() + const dataMock = { + items, + hasNext, + next, + } as unknown as PaginatedList + + it('omits the device name by default', async () => { + hasNext.mockReturnValue(false) + + await writeDeviceEventsTable(commandMock, dataMock) + + expect(newOutputTableMock).toHaveBeenCalledTimes(1) + expect(newOutputTableMock).toHaveBeenCalledWith({ + isList: true, + head: ['Time', 'Component', 'Capability', 'Attribute', 'Value'], + }) + expect(tablePushMock).toHaveBeenCalledTimes(1) + expect(stdOutSpy).toHaveBeenCalledTimes(1) + }) + + it('includes the device name when specified', async () => { + hasNext.mockReturnValue(false) + + await writeDeviceEventsTable(commandMock, dataMock, { includeName: true }) + + expect(newOutputTableMock).toHaveBeenCalledTimes(1) + expect(newOutputTableMock).toHaveBeenCalledWith({ + isList: true, + head: ['Time', 'Device Name', 'Component', 'Capability', 'Attribute', 'Value'], + }) + expect(tablePushMock).toHaveBeenCalledTimes(1) + expect(stdOutSpy).toHaveBeenCalledTimes(1) + }) + + it('returns next page when prompted until no more', async () => { + hasNext.mockReturnValueOnce(true) + hasNext.mockReturnValueOnce(true) + hasNext.mockReturnValueOnce(false) + promptSpy.mockResolvedValueOnce({ more: '' }) + promptSpy.mockResolvedValueOnce({ more: 'y' }) + + await writeDeviceEventsTable(commandMock, dataMock) + + expect(newOutputTableMock).toHaveBeenCalledTimes(3) + expect(tablePushMock).toHaveBeenCalledTimes(3) + expect(stdOutSpy).toHaveBeenCalledTimes(3) + }) + + it('returns next page until canceled', async () => { + hasNext.mockReturnValue(true) + promptSpy.mockResolvedValueOnce({ more: '' }) + promptSpy.mockResolvedValueOnce({ more: 'n' }) + + await writeDeviceEventsTable(commandMock, dataMock) + + expect(newOutputTableMock).toHaveBeenCalledTimes(2) + expect(tablePushMock).toHaveBeenCalledTimes(2) + expect(stdOutSpy).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/packages/cli/src/commands/devices/history.ts b/packages/cli/src/commands/devices/history.ts new file mode 100644 index 000000000..d8b5358f3 --- /dev/null +++ b/packages/cli/src/commands/devices/history.ts @@ -0,0 +1,47 @@ +import { + APICommand, + buildOutputFormatter, + calculateOutputFormat, + chooseDevice, + formatAndWriteItem, + IOFormat, + writeOutput, +} from '@smartthings/cli-lib' +import { historyFlags, toEpochTime, writeDeviceEventsTable } from '../../lib/commands/history-util' + + +export default class DeviceHistoryCommand extends APICommand { + static description = 'get the current preferences of a device' + + static flags = { + ...APICommand.flags, + ...formatAndWriteItem.flags, + ...historyFlags, + } + + static args = [{ + name: 'id', + description: 'the device id', + }] + + async run(): Promise { + const deviceId = await chooseDevice(this, this.args.id, { allowIndex: true }) + const device = await this.client.devices.get(deviceId) + const params = { + deviceId, + locationId: device.locationId, + limit: this.flags.limit, + before: toEpochTime(this.flags.before), + after: toEpochTime(this.flags.after), + } + + const history = await this.client.history.devices(params) + + if (calculateOutputFormat(this) === IOFormat.COMMON) { + await writeDeviceEventsTable(this, history, { utcTimeFormat: this.flags.utc }) + } else { + const outputFormatter = buildOutputFormatter(this) + await writeOutput(outputFormatter(history.items), this.flags.output) + } + } +} diff --git a/packages/cli/src/commands/locations.ts b/packages/cli/src/commands/locations.ts index 6a2be9321..328d3a6fe 100644 --- a/packages/cli/src/commands/locations.ts +++ b/packages/cli/src/commands/locations.ts @@ -1,6 +1,6 @@ import { Location, LocationItem } from '@smartthings/core-sdk' -import { APICommand, outputListing, selectFromList } from '@smartthings/cli-lib' +import { APICommand, outputListing, selectFromList, stringTranslateToId } from '@smartthings/cli-lib' export const tableFieldDefinitions = [ @@ -8,16 +8,28 @@ export const tableFieldDefinitions = [ 'latitude', 'longitude', 'regionRadius', 'temperatureScale', 'locale', ] -export async function chooseLocation(command: APICommand, preselectedId?: string, autoChoose?: boolean): Promise { +export async function chooseLocation( + command: APICommand, + locationFromArg?: string, + autoChoose?: boolean, + allowIndex?: boolean): Promise { + const config = { itemName: 'location', primaryKeyName: 'locationId', sortKeyName: 'name', } + + const listItems = (): Promise => command.client.locations.list() + + const preselectedId = allowIndex + ? await stringTranslateToId(config, locationFromArg, listItems) + : locationFromArg + return selectFromList(command, config, { preselectedId, autoChoose, - listItems: () => command.client.locations.list(), + listItems, }) } diff --git a/packages/cli/src/commands/locations/history.ts b/packages/cli/src/commands/locations/history.ts new file mode 100644 index 000000000..90994235a --- /dev/null +++ b/packages/cli/src/commands/locations/history.ts @@ -0,0 +1,46 @@ +import { + APICommand, + buildOutputFormatter, + calculateOutputFormat, + formatAndWriteItem, + IOFormat, + writeOutput, +} from '@smartthings/cli-lib' +import { historyFlags, toEpochTime, writeDeviceEventsTable } from '../../lib/commands/history-util' +import { chooseLocation } from '../locations' + + +export default class LocationDeviceHistoryCommand extends APICommand { + static description = 'get the current preferences of a device' + + static flags = { + ...APICommand.flags, + ...formatAndWriteItem.flags, + ...historyFlags, + } + + static args = [{ + name: 'id', + description: 'the location id', + }] + + async run(): Promise { + const id = await chooseLocation(this, this.args.id, true, true) + + const params = { + locationId: id, + limit: this.flags.limit, + before: toEpochTime(this.flags.before), + after: toEpochTime(this.flags.after), + } + + const history = await this.client.history.devices(params) + + if (calculateOutputFormat(this) === IOFormat.COMMON) { + await writeDeviceEventsTable(this, history, { includeName: true, utcTimeFormat: this.flags.utc }) + } else { + const outputFormatter = buildOutputFormatter(this) + await writeOutput(outputFormatter(history.items), this.flags.output) + } + } +} diff --git a/packages/cli/src/lib/commands/history-util.ts b/packages/cli/src/lib/commands/history-util.ts new file mode 100644 index 000000000..f26a3b35b --- /dev/null +++ b/packages/cli/src/lib/commands/history-util.ts @@ -0,0 +1,96 @@ +import inquirer from 'inquirer' +import { SmartThingsCommandInterface, Table } from '@smartthings/cli-lib' +import { DeviceActivity, PaginatedList } from '@smartthings/core-sdk' +import { Flags } from '@oclif/core' + + +export interface DeviceActivityOptions { + includeName?: boolean + utcTimeFormat?: boolean +} + +export const historyFlags = { + 'after': Flags.string({ + char: 'A', + description: 'return events newer than or equal to this timestamp, expressed as an epoch time in milliseconds or an ISO time string', + }), + 'before': Flags.string({ + char: 'B', + description: 'return events older than than this timestamp, expressed as an epoch time in milliseconds or an ISO time string', + }), + 'limit': Flags.integer({ + char: 'L', + description: 'maximum number of events to return, defaults to 20', + }), + 'utc': Flags.boolean({ + char: 'U', + description: 'display times in UTC time zone. Defaults to local time', + }), +} + +export function toEpochTime(date?: string): number | undefined { + if (date) { + return new Date(date).getTime() + } +} + +export function sortEvents(list: DeviceActivity[]): DeviceActivity[] { + return list.sort((a, b) => a.epoch === b.epoch ? 0 : (a.epoch < b.epoch ? 1 : -1)) +} + +export function getNextDeviceEvents(table: Table, items: DeviceActivity[], options: Partial): void { + for (const item of items) { + const date = new Date(item.time) + const value = JSON.stringify(item.value) + const row = options.includeName ? [ + options.utcTimeFormat ? date.toISOString() : date.toLocaleString(), + item.deviceName, + item.component, + item.capability, + item.attribute, + item.unit ? `${value} ${item.unit}` : value, + ] : [ + options.utcTimeFormat ? date.toISOString() : date.toLocaleString(), + item.component, + item.capability, + item.attribute, + item.unit ? `${value} ${item.unit}` : value, + ] + table.push(row) + } +} + +export async function writeDeviceEventsTable( + command: SmartThingsCommandInterface, + data: PaginatedList, + options?: Partial): Promise { + + const opts = { includeName: false, utcTimeFormat: false, ...options } + const head = options && options.includeName ? + ['Time', 'Device Name', 'Component', 'Capability', 'Attribute', 'Value'] : + ['Time', 'Component', 'Capability', 'Attribute', 'Value'] + + if (data.items) { + let table = command.tableGenerator.newOutputTable({ isList: true, head }) + getNextDeviceEvents(table, sortEvents(data.items), opts) + process.stdout.write(table.toString()) + + let more = 'y' + while (more.toLowerCase() !== 'n' && data.hasNext()) { + more = (await inquirer.prompt({ + type: 'input', + name: 'more', + message: 'Fetch more history records ([y]/n)?', + })).more + + if (more.toLowerCase() !== 'n') { + table = command.tableGenerator.newOutputTable({ isList: true, head }) + await data.next() + if (data.items.length) { + getNextDeviceEvents(table, sortEvents(data.items), opts) + process.stdout.write(table.toString()) + } + } + } + } +} diff --git a/packages/testlib/src/index.ts b/packages/testlib/src/index.ts index a49e81511..c4e9086f3 100644 --- a/packages/testlib/src/index.ts +++ b/packages/testlib/src/index.ts @@ -24,7 +24,10 @@ jest.mock('@smartthings/cli-lib', () => { yamlExists: jest.fn(), chooseDevice: jest.fn(), chooseComponent: jest.fn(), - summarizedText: 'summarized text', // TODO refactor test using this + summarizedText: 'summarized text', // TODO refactor test using this, + calculateOutputFormat: jest.fn(), + writeOutput: jest.fn(), + buildOutputFormatter: jest.fn(), } })