From 9783acf94bc79328e2204042437d6a8209595905 Mon Sep 17 00:00:00 2001 From: sungmpar <72352750+sungmpar@users.noreply.github.com> Date: Wed, 2 Sep 2026 07:20:42 +0900 Subject: [PATCH] feat: support multiple devices in devices:history --- .changeset/bright-devices-share.md | 5 + README.md | 2 +- .../commands/devices/history.test.ts | 377 +++++++++++++++++- .../commands/locations/history.test.ts | 2 +- src/commands/devices/history.ts | 57 ++- 5 files changed, 428 insertions(+), 15 deletions(-) create mode 100644 .changeset/bright-devices-share.md diff --git a/.changeset/bright-devices-share.md b/.changeset/bright-devices-share.md new file mode 100644 index 00000000..789f6469 --- /dev/null +++ b/.changeset/bright-devices-share.md @@ -0,0 +1,5 @@ +--- +"@smartthings/cli": patch +--- + +Allow device history to be queried for multiple devices in one combined request. diff --git a/README.md b/README.md index f4fca84c..edff5c1c 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ smartthings edge:channels:create --help | devices:commands [id] [command] | execute a device command | | devices:delete [id] | delete a device | | devices:health [id-or-index] | get the current health status of a device | -| devices:history [id-or-index] | get device history by device | +| devices:history [id-or-index..] | get device history by device | | devices:preferences [id-or-index] | get the current preferences of a device | | devices:presentation [id-or-index] | get a device presentation | | devices:rename [id] [new-label] | rename a device | diff --git a/src/__tests__/commands/devices/history.test.ts b/src/__tests__/commands/devices/history.test.ts index 8c1079aa..2dafc3f5 100644 --- a/src/__tests__/commands/devices/history.test.ts +++ b/src/__tests__/commands/devices/history.test.ts @@ -2,7 +2,13 @@ import { jest } from '@jest/globals' import type { ArgumentsCamelCase, Argv } from 'yargs' -import type { DevicesEndpoint, HistoryEndpoint, DeviceActivity, Device, PaginatedList } from '@smartthings/core-sdk' +import type { + Device, + DeviceActivity, + DevicesEndpoint, + HistoryEndpoint, + PaginatedList, +} from '@smartthings/core-sdk' import type { CommandArgs } from '../../../commands/devices/history.js' import type { CLIConfig } from '../../../lib/cli-config.js' @@ -93,6 +99,7 @@ test('builder', () => { const builder = cmd.builder as (yargs: Argv) => Argv expect(builder(yargsMock)).toBe(argvMock) + expect(cmd.command).toBe('devices:history [id-or-index..]') expect(apiCommandBuilderMock).toHaveBeenCalledExactlyOnceWith(yargsMock) expect(buildOutputFormatterBuilderMock) @@ -100,20 +107,27 @@ test('builder', () => { expect(historyBuilderMock) .toHaveBeenCalledExactlyOnceWith(buildOutputFormatterBuilderArgvMock) - expect(positionalMock).toHaveBeenCalledTimes(1) + expect(positionalMock).toHaveBeenCalledExactlyOnceWith('id-or-index', { + array: true, + describe: 'the device id or number in list', + type: 'string', + }) expect(exampleMock).toHaveBeenCalledTimes(1) + expect(JSON.stringify(exampleMock.mock.calls[0][0])).toContain('$0 devices:history device-a device-b') expect(buildEpilogMock).toHaveBeenCalledTimes(1) expect(epilogMock).toHaveBeenCalledTimes(1) }) describe('handler', () => { const apiDevicesGetMock = jest.fn() + const apiDevicesListMock = jest.fn() const apiHistoryDevicesMock = jest.fn() const cliConfig = { profile: {} } as CLIConfig const command = { client: { devices: { get: apiDevicesGetMock, + list: apiDevicesListMock, }, history: { devices: apiHistoryDevicesMock, @@ -124,7 +138,9 @@ describe('handler', () => { apiCommandMock.mockResolvedValue(command) const device = { deviceId: 'device-id', locationId: 'location-id' } as Device - apiDevicesGetMock.mockResolvedValue(device) + const deviceA = { deviceId: 'device-a', label: 'Device A', locationId: 'location-a' } as Device + const deviceB = { deviceId: 'device-b', label: 'Device B', locationId: 'location-b' } as Device + const deviceC = { deviceId: 'device-c', label: 'Device C', locationId: 'location-a' } as Device const items = [{ deviceId: 'device-1' }] as DeviceActivity[] const deviceHistory = { @@ -137,6 +153,74 @@ describe('handler', () => { limit: 27, utc: false, } as ArgumentsCamelCase + const inputWithDevices = ( + ids: string[], + overrides: Record = {}, + ): ArgumentsCamelCase => ({ + ...defaultInputArgv, + ...overrides, + idOrIndex: ids, + }) as unknown as ArgumentsCamelCase + const setMetadata = (devices: Device[]): void => { + const devicesById = new Map(devices.map(device => [device.deviceId, device])) + apiDevicesGetMock.mockImplementation(async deviceId => { + const metadata = devicesById.get(deviceId) + if (metadata === undefined) { + throw Error(`missing test metadata for ${deviceId}`) + } + return metadata + }) + } + const noOutputStarted = (): void => { + expect(apiHistoryDevicesMock).not.toHaveBeenCalled() + expect(getHistoryMock).not.toHaveBeenCalled() + expect(writeDeviceEventsTableMock).not.toHaveBeenCalled() + expect(buildOutputFormatterMock).not.toHaveBeenCalled() + expect(writeOutputMock).not.toHaveBeenCalled() + } + const deferred = (): { + promise: Promise + reject: (reason?: unknown) => void + resolve: (value: T | PromiseLike) => void + } => { + let resolve!: (value: T | PromiseLike) => void + let reject!: (reason?: unknown) => void + const promise = new Promise((promiseResolve, promiseReject) => { + resolve = promiseResolve + reject = promiseReject + }) + return { promise, reject, resolve } + } + + beforeEach(() => { + apiDevicesGetMock.mockReset().mockResolvedValue(device) + apiDevicesListMock.mockReset() + apiHistoryDevicesMock.mockReset() + calculateOutputFormatMock.mockReset() + calculateRequestLimitMock.mockReset().mockReturnValue(30) + getHistoryMock.mockReset() + toEpochTimeMock.mockReset() + writeDeviceEventsTableMock.mockReset() + buildOutputFormatterMock.mockReset() + writeOutputMock.mockReset() + chooseDeviceMock.mockReset().mockImplementation(async (selectedCommand, idOrIndex, options) => { + if (idOrIndex === undefined) { + return 'chosen-device-id' + } + if (/^[1-9][0-9]*$/.test(idOrIndex)) { + const listedDevices = await options?.listItems?.(selectedCommand) + if (listedDevices === undefined) { + throw Error('numeric test input requires listItems') + } + const matchingDevice = listedDevices[Number(idOrIndex) - 1] + if (matchingDevice === undefined) { + throw Error(`invalid test index ${idOrIndex}`) + } + return matchingDevice.deviceId + } + return idOrIndex + }) + }) it('uses writeDeviceEventsTable for standard output', async () => { calculateOutputFormatMock.mockReturnValue('common') @@ -159,8 +243,217 @@ describe('handler', () => { expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('History is limited to')) }) + it('keeps one explicitly provided device scalar with the existing common columns', async () => { + calculateOutputFormatMock.mockReturnValue('common') + setMetadata([deviceA]) + apiHistoryDevicesMock.mockResolvedValueOnce(deviceHistory) + const inputArgv = inputWithDevices(['device-a']) + + await expect(cmd.handler(inputArgv)).resolves.not.toThrow() + + expect(chooseDeviceMock).toHaveBeenCalledExactlyOnceWith( + command, + 'device-a', + expect.objectContaining({ allowIndex: true }), + ) + expect(apiDevicesListMock).not.toHaveBeenCalled() + expect(apiDevicesGetMock).toHaveBeenCalledExactlyOnceWith('device-a') + expect(apiHistoryDevicesMock).toHaveBeenCalledExactlyOnceWith({ + deviceId: 'device-a', + locationId: 'location-a', + limit: 30, + before: undefined, + after: undefined, + }) + expect(writeDeviceEventsTableMock) + .toHaveBeenCalledExactlyOnceWith(command, deviceHistory, { utcTimeFormat: false }) + }) + + it('queries multiple direct IDs once and uses a scalar shared location', async () => { + calculateOutputFormatMock.mockReturnValue('common') + const deviceBInSameLocation = { ...deviceB, locationId: 'location-a' } + setMetadata([deviceA, deviceBInSameLocation]) + apiHistoryDevicesMock.mockResolvedValueOnce(deviceHistory) + + await expect(cmd.handler(inputWithDevices(['device-a', 'device-b']))).resolves.not.toThrow() + + expect(chooseDeviceMock).toHaveBeenCalledTimes(2) + expect(apiDevicesGetMock).toHaveBeenNthCalledWith(1, 'device-a') + expect(apiDevicesGetMock).toHaveBeenNthCalledWith(2, 'device-b') + expect(apiHistoryDevicesMock).toHaveBeenCalledExactlyOnceWith({ + deviceId: ['device-a', 'device-b'], + locationId: 'location-a', + limit: 30, + before: undefined, + after: undefined, + }) + expect(writeDeviceEventsTableMock).toHaveBeenCalledExactlyOnceWith( + command, + deviceHistory, + { includeName: true, utcTimeFormat: false }, + ) + expect(getHistoryMock).not.toHaveBeenCalled() + expect(buildOutputFormatterMock).not.toHaveBeenCalled() + expect(writeOutputMock).not.toHaveBeenCalled() + }) + + it('deduplicates locations in first-device order', async () => { + calculateOutputFormatMock.mockReturnValue('common') + setMetadata([deviceA, deviceB, deviceC]) + apiHistoryDevicesMock.mockResolvedValueOnce(deviceHistory) + + await expect(cmd.handler(inputWithDevices(['device-a', 'device-b', 'device-c']))) + .resolves.not.toThrow() + + expect(apiDevicesListMock).not.toHaveBeenCalled() + expect(apiHistoryDevicesMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + deviceId: ['device-a', 'device-b', 'device-c'], + locationId: ['location-a', 'location-b'], + })) + }) + + it('uses one shared device-list snapshot for multiple numeric inputs', async () => { + calculateOutputFormatMock.mockReturnValue('common') + apiDevicesListMock.mockResolvedValueOnce([deviceA, deviceB]) + setMetadata([deviceA, deviceB]) + apiHistoryDevicesMock.mockResolvedValueOnce(deviceHistory) + + await expect(cmd.handler(inputWithDevices(['1', '2']))).resolves.not.toThrow() + + expect(chooseDeviceMock).toHaveBeenCalledTimes(2) + expect(chooseDeviceMock).toHaveBeenNthCalledWith(1, command, '1', { + allowIndex: true, + listItems: expect.any(Function), + }) + expect(chooseDeviceMock).toHaveBeenNthCalledWith(2, command, '2', { + allowIndex: true, + listItems: expect.any(Function), + }) + expect(apiDevicesListMock).toHaveBeenCalledExactlyOnceWith() + expect(apiHistoryDevicesMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + deviceId: ['device-a', 'device-b'], + locationId: ['location-a', 'location-b'], + })) + }) + + it('deduplicates mixed ID and index inputs in first-resolved order', async () => { + calculateOutputFormatMock.mockReturnValue('common') + apiDevicesListMock.mockResolvedValueOnce([deviceA, deviceB]) + setMetadata([deviceA, deviceB]) + apiHistoryDevicesMock.mockResolvedValueOnce(deviceHistory) + + await expect(cmd.handler(inputWithDevices(['device-b', '1', 'device-b', '2']))) + .resolves.not.toThrow() + + expect(chooseDeviceMock).toHaveBeenCalledTimes(4) + expect(apiDevicesListMock).toHaveBeenCalledExactlyOnceWith() + expect(apiDevicesGetMock).toHaveBeenCalledTimes(2) + expect(apiDevicesGetMock).toHaveBeenNthCalledWith(1, 'device-b') + expect(apiDevicesGetMock).toHaveBeenNthCalledWith(2, 'device-a') + expect(apiHistoryDevicesMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + deviceId: ['device-b', 'device-a'], + locationId: ['location-b', 'location-a'], + })) + }) + + it('keeps duplicate inputs that resolve to one unique device scalar', async () => { + calculateOutputFormatMock.mockReturnValue('common') + setMetadata([deviceA]) + apiHistoryDevicesMock.mockResolvedValueOnce(deviceHistory) + + await expect(cmd.handler(inputWithDevices(['device-a', 'device-a']))).resolves.not.toThrow() + + expect(apiDevicesGetMock).toHaveBeenCalledExactlyOnceWith('device-a') + expect(apiHistoryDevicesMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + deviceId: 'device-a', + locationId: 'location-a', + })) + expect(writeDeviceEventsTableMock) + .toHaveBeenCalledExactlyOnceWith(command, deviceHistory, { utcTimeFormat: false }) + }) + + it('waits for all metadata and keeps location order when promises resolve in reverse', async () => { + calculateOutputFormatMock.mockReturnValue('common') + apiHistoryDevicesMock.mockResolvedValueOnce(deviceHistory) + const metadataA = deferred() + const metadataB = deferred() + apiDevicesGetMock.mockImplementation(deviceId => { + if (deviceId === 'device-a') { + return metadataA.promise + } + if (deviceId === 'device-b') { + return metadataB.promise + } + throw Error(`unexpected device id ${deviceId}`) + }) + + const handlerPromise = cmd.handler(inputWithDevices(['device-a', 'device-b'])) + await new Promise(resolve => setImmediate(resolve)) + + expect(apiDevicesGetMock).toHaveBeenCalledTimes(2) + noOutputStarted() + + metadataB.resolve(deviceB) + await new Promise(resolve => setImmediate(resolve)) + noOutputStarted() + + metadataA.resolve(deviceA) + await expect(handlerPromise).resolves.not.toThrow() + + expect(apiHistoryDevicesMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + deviceId: ['device-a', 'device-b'], + locationId: ['location-a', 'location-b'], + })) + }) + + it.each(['common', 'json'] as const)( + 'does not start history or output when metadata lookup fails for %s output', + async format => { + calculateOutputFormatMock.mockReturnValue(format) + apiDevicesGetMock + .mockResolvedValueOnce(deviceA) + .mockRejectedValueOnce(Error('metadata unavailable')) + + await expect(cmd.handler(inputWithDevices(['device-a', 'device-b']))) + .rejects.toThrow('metadata unavailable') + + expect(apiDevicesGetMock).toHaveBeenCalledTimes(2) + noOutputStarted() + }, + ) + + it.each([ + { format: 'common' as const, locationId: undefined }, + { format: 'json' as const, locationId: '' }, + ])('rejects location $locationId before starting $format output', async ({ format, locationId }) => { + calculateOutputFormatMock.mockReturnValue(format) + setMetadata([deviceA, { ...deviceB, locationId } as Device]) + + await expect(cmd.handler(inputWithDevices(['device-a', 'device-b']))) + .rejects.toThrow(/location/i) + + noOutputStarted() + }) + + it('handles an empty combined common response once', async () => { + calculateOutputFormatMock.mockReturnValue('common') + setMetadata([deviceA, deviceB]) + const emptyHistory = { ...deviceHistory, items: [] } as unknown as PaginatedList + apiHistoryDevicesMock.mockResolvedValueOnce(emptyHistory) + + await expect(cmd.handler(inputWithDevices(['device-a', 'device-b']))).resolves.not.toThrow() + + expect(apiHistoryDevicesMock).toHaveBeenCalledTimes(1) + expect(writeDeviceEventsTableMock).toHaveBeenCalledExactlyOnceWith( + command, + emptyHistory, + { includeName: true, utcTimeFormat: false }, + ) + }) + it('notifies user of max per-request limit if they request more', async () => { calculateOutputFormatMock.mockReturnValue('common') + calculateRequestLimitMock.mockReturnValueOnce(13) apiHistoryDevicesMock.mockResolvedValueOnce(deviceHistory) await expect(cmd.handler({ ...defaultInputArgv, limit: 31 })).resolves.not.toThrow() @@ -168,6 +461,40 @@ describe('handler', () => { expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('History is limited to')) }) + it('applies limit, before, after, UTC, and the common warning once to the combined stream', async () => { + calculateOutputFormatMock.mockReturnValue('common') + calculateRequestLimitMock.mockReturnValueOnce(13) + setMetadata([deviceA, deviceB]) + apiHistoryDevicesMock.mockResolvedValueOnce(deviceHistory) + toEpochTimeMock.mockImplementation(value => value === 'before-value' ? 101 : 202) + const inputArgv = inputWithDevices(['device-a', 'device-b'], { + after: 'after-value', + before: 'before-value', + limit: 31, + utc: true, + }) + + await expect(cmd.handler(inputArgv)).resolves.not.toThrow() + + expect(calculateRequestLimitMock).toHaveBeenCalledExactlyOnceWith(31) + expect(toEpochTimeMock).toHaveBeenCalledTimes(2) + expect(toEpochTimeMock).toHaveBeenNthCalledWith(1, 'before-value') + expect(toEpochTimeMock).toHaveBeenNthCalledWith(2, 'after-value') + expect(apiHistoryDevicesMock).toHaveBeenCalledExactlyOnceWith({ + deviceId: ['device-a', 'device-b'], + locationId: ['location-a', 'location-b'], + limit: 13, + before: 101, + after: 202, + }) + expect(writeDeviceEventsTableMock).toHaveBeenCalledExactlyOnceWith( + command, + deviceHistory, + { includeName: true, utcTimeFormat: true }, + ) + expect(consoleLogSpy).toHaveBeenCalledExactlyOnceWith('History is limited to 13 items per request.') + }) + it('writes non-table output when specified', async () => { calculateOutputFormatMock.mockReturnValue('json') getHistoryMock.mockResolvedValueOnce(items) @@ -196,4 +523,48 @@ describe('handler', () => { expect(writeDeviceEventsTableMock).not.toHaveBeenCalled() expect(consoleLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('History is limited to')) }) + + it.each([ + { + format: 'json' as const, + items, + name: 'JSON', + overrides: { json: true }, + output: undefined, + }, + { + format: 'yaml' as const, + items: [] as DeviceActivity[], + name: 'empty YAML output file', + overrides: { output: 'combined-history.yaml', yaml: true }, + output: 'combined-history.yaml', + }, + ])('writes one combined $name result', async ({ format, items: outputItems, output, overrides }) => { + calculateOutputFormatMock.mockReturnValue(format) + setMetadata([deviceA, deviceB]) + getHistoryMock.mockResolvedValueOnce(outputItems) + const outputFormatterMock = jest.fn>().mockReturnValue('formatted') + buildOutputFormatterMock.mockReturnValueOnce(outputFormatterMock) + const inputArgv = inputWithDevices(['device-a', 'device-b'], overrides) + + await expect(cmd.handler(inputArgv)).resolves.not.toThrow() + + expect(getHistoryMock).toHaveBeenCalledExactlyOnceWith( + command.client, + 27, + 30, + { + deviceId: ['device-a', 'device-b'], + locationId: ['location-a', 'location-b'], + limit: 30, + before: undefined, + after: undefined, + }, + ) + expect(buildOutputFormatterMock).toHaveBeenCalledExactlyOnceWith(inputArgv, cliConfig) + expect(outputFormatterMock).toHaveBeenCalledExactlyOnceWith(outputItems) + expect(writeOutputMock).toHaveBeenCalledExactlyOnceWith('formatted', output) + expect(apiHistoryDevicesMock).not.toHaveBeenCalled() + expect(writeDeviceEventsTableMock).not.toHaveBeenCalled() + }) }) diff --git a/src/__tests__/commands/locations/history.test.ts b/src/__tests__/commands/locations/history.test.ts index bf3c5e4b..179b662b 100644 --- a/src/__tests__/commands/locations/history.test.ts +++ b/src/__tests__/commands/locations/history.test.ts @@ -4,7 +4,7 @@ import type { ArgumentsCamelCase, Argv } from 'yargs' import type { DeviceActivity, HistoryEndpoint, PaginatedList } from '@smartthings/core-sdk' -import type { CommandArgs } from '../../../commands/devices/history.js' +import type { CommandArgs } from '../../../commands/locations/history.js' import type { CLIConfig } from '../../../lib/cli-config.js' import type { buildEpilog } from '../../../lib/help.js' import type { APICommand, APICommandFlags } from '../../../lib/command/api-command.js' diff --git a/src/commands/devices/history.ts b/src/commands/devices/history.ts index 60f8392f..c12eb394 100644 --- a/src/commands/devices/history.ts +++ b/src/commands/devices/history.ts @@ -1,6 +1,6 @@ import { type ArgumentsCamelCase, type Argv, type CommandModule } from 'yargs' -import { type DeviceActivity } from '@smartthings/core-sdk' +import { type Device, type DeviceActivity, type DeviceHistoryRequest } from '@smartthings/core-sdk' import { buildEpilog } from '../../lib/help.js' import { apiCommand, apiCommandBuilder, type APICommandFlags } from '../../lib/command/api-command.js' @@ -26,20 +26,21 @@ export type CommandArgs = & BuildOutputFormatterFlags & HistoryFlags & { - idOrIndex?: string + idOrIndex?: string[] } -const command = 'devices:history [id-or-index]' +const command = 'devices:history [id-or-index..]' const describe = 'get device history by device' const builder = (yargs: Argv): Argv => historyBuilder(buildOutputFormatterBuilder(apiCommandBuilder(yargs))) - .positional('id-or-index', { describe: 'the device id or number in list', type: 'string' }) + .positional('id-or-index', { array: true, describe: 'the device id or number in list', type: 'string' }) .example([ ['$0 devices:history', 'prompt for a device and display history for the chosen device'], ['$0 devices:history 2', 'display history for device listed second in output of devices command'], ['$0 devices:history 92f9920a-7629-40e3-8fdc-14924413897f', 'display history for device by id'], + ['$0 devices:history device-a device-b', 'display combined history for multiple devices'], ]) .epilog(buildEpilog({ command })) @@ -49,11 +50,44 @@ const handler = async (argv: ArgumentsCamelCase): Promise => const limit = argv.limit const perRequestLimit = calculateRequestLimit(limit) - const deviceId = await chooseDevice(command, argv.idOrIndex, { allowIndex: true }) - const device = await command.client.devices.get(deviceId) - const params = { - deviceId, - locationId: device.locationId, + const deviceIds: string[] = [] + const uniqueDeviceIds = new Set() + const addDeviceId = (deviceId: string): void => { + if (!uniqueDeviceIds.has(deviceId)) { + uniqueDeviceIds.add(deviceId) + deviceIds.push(deviceId) + } + } + + if (argv.idOrIndex === undefined || argv.idOrIndex.length === 0) { + addDeviceId(await chooseDevice(command, undefined, { allowIndex: true })) + } else { + let deviceListPromise: Promise | undefined + const listItems = (): Promise => + deviceListPromise ??= command.client.devices.list() + for (const idOrIndex of argv.idOrIndex) { + addDeviceId(await chooseDevice(command, idOrIndex, { allowIndex: true, listItems })) + } + } + + const devices = await Promise.all(deviceIds.map(async deviceId => command.client.devices.get(deviceId))) + const locationIds: string[] = [] + const uniqueLocationIds = new Set() + for (const device of devices) { + const locationId = device.locationId + if (typeof locationId !== 'string' || locationId.length === 0) { + throw Error('Unable to query history because a selected device has no location ID.') + } + if (!uniqueLocationIds.has(locationId)) { + uniqueLocationIds.add(locationId) + locationIds.push(locationId) + } + } + + const multipleDevices = deviceIds.length > 1 + const params: DeviceHistoryRequest = { + deviceId: multipleDevices ? deviceIds : deviceIds[0], + locationId: multipleDevices && locationIds.length > 1 ? locationIds : locationIds[0], limit: perRequestLimit, before: toEpochTime(argv.before), after: toEpochTime(argv.after), @@ -64,7 +98,10 @@ const handler = async (argv: ArgumentsCamelCase): Promise => console.log(`History is limited to ${maxItemsPerRequest} items per request.`) } const history = await command.client.history.devices(params) - await writeDeviceEventsTable(command, history, { utcTimeFormat: argv.utc }) + await writeDeviceEventsTable(command, history, { + ...(multipleDevices ? { includeName: true } : {}), + utcTimeFormat: argv.utc, + }) } else { const items = await getHistory(command.client, limit, perRequestLimit, params) const outputFormatter = buildOutputFormatter(argv, command.cliConfig)