import {z} from 'zod' export type OperandAggregationMode = 'None'; export type ArrayOperand = {operands: Array, aggregation?: OperandAggregationMode | undefined}; export type MapOperand = {[key: string]: Operand}; export type TriggerMode = 'Auto' | 'Always' | 'Never'; export type DeviceOperand = {devices: Array, component: string, capability: string, attribute: string, path?: string | undefined, aggregation?: OperandAggregationMode | undefined, trigger?: TriggerMode | undefined}; export type LocationAttribute = 'FineDust' | 'FineDustIndex' | 'Humidity' | 'Mode' | 'Security' | 'Temperature' | 'TemperatureC' | 'TemperatureF' | 'UltraFineDust' | 'UltraFineDustIndex' | 'Weather' | 'WeatherAlertSeverity'; export type LocationOperand = {locationId?: unknown | undefined, postalCode?: string | undefined, attribute: LocationAttribute, trigger?: TriggerMode | undefined}; export type DayOfWeek = 'Sun' | 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat'; export type DateReference = 'Today' | 'Holiday'; export type DateOperand = {timeZoneId?: string | undefined, daysOfWeek?: Array | undefined, year?: number | undefined, month?: number | undefined, day?: number | undefined, reference?: DateReference | undefined}; export type TimeReference = 'Now' | 'Midnight' | 'Sunrise' | 'Noon' | 'Sunset'; export type IntervalUnit = 'Second' | 'Minute' | 'Hour' | 'Day' | 'Week' | 'Month' | 'Year'; export type Interval = {value: Operand, unit: IntervalUnit}; export type TimeOperand = {timeZoneId?: string | undefined, daysOfWeek?: Array | undefined, reference: TimeReference, offset?: Interval | undefined}; export type DateTimeOperand = {timeZoneId?: string | undefined, locationId?: unknown | undefined, daysOfWeek?: Array | undefined, year?: number | undefined, month?: number | undefined, day?: number | undefined, reference: TimeReference, offset?: Interval | undefined}; export type Operand = {boolean?: boolean | undefined, decimal?: number | undefined, integer?: number | undefined, string?: string | undefined, array?: ArrayOperand | undefined, map?: MapOperand | undefined, device?: DeviceOperand | undefined, location?: LocationOperand | undefined, date?: DateOperand | undefined, time?: TimeOperand | undefined, datetime?: DateTimeOperand | undefined}; export type ConditionAggregationMode = 'Any' | 'All'; export type EqualsCondition = {left: Operand, right: Operand, aggregation?: ConditionAggregationMode | undefined, changesOnly?: boolean | undefined}; export type GreaterThanCondition = {left: Operand, right: Operand, aggregation?: ConditionAggregationMode | undefined, changesOnly?: boolean | undefined}; export type GreaterThanOrEqualsCondition = {left: Operand, right: Operand, aggregation?: ConditionAggregationMode | undefined, changesOnly?: boolean | undefined}; export type LessThanCondition = {left: Operand, right: Operand, aggregation?: ConditionAggregationMode | undefined, changesOnly?: boolean | undefined}; export type LessThanOrEqualsCondition = {left: Operand, right: Operand, aggregation?: ConditionAggregationMode | undefined, changesOnly?: boolean | undefined}; export type BetweenCondition = {value: Operand, start: Operand, end: Operand, aggregation?: ConditionAggregationMode | undefined, changesOnly?: boolean | undefined}; export type ChangesCondition = {and?: Array | undefined, or?: Array | undefined, not?: Condition | undefined, equals?: EqualsCondition | undefined, greaterThan?: GreaterThanCondition | undefined, greaterThanOrEquals?: GreaterThanOrEqualsCondition | undefined, lessThan?: LessThanCondition | undefined, lessThanOrEquals?: LessThanOrEqualsCondition | undefined, between?: BetweenCondition | undefined, id: unknown, operand?: Operand | undefined}; export type RemainsCondition = {and?: Array | undefined, or?: Array | undefined, not?: Condition | undefined, equals?: EqualsCondition | undefined, greaterThan?: GreaterThanCondition | undefined, greaterThanOrEquals?: GreaterThanOrEqualsCondition | undefined, lessThan?: LessThanCondition | undefined, lessThanOrEquals?: LessThanOrEqualsCondition | undefined, between?: BetweenCondition | undefined, id: unknown, operand?: Operand | undefined, duration: Interval, latching?: boolean | undefined}; export type WasCondition = {and?: Array | undefined, or?: Array | undefined, not?: Condition | undefined, equals?: EqualsCondition | undefined, greaterThan?: GreaterThanCondition | undefined, greaterThanOrEquals?: GreaterThanOrEqualsCondition | undefined, lessThan?: LessThanCondition | undefined, lessThanOrEquals?: LessThanOrEqualsCondition | undefined, between?: BetweenCondition | undefined, id: unknown, duration: Interval, operand?: Operand | undefined}; export type Condition = {and?: Array | undefined, or?: Array | undefined, not?: Condition | undefined, equals?: EqualsCondition | undefined, greaterThan?: GreaterThanCondition | undefined, greaterThanOrEquals?: GreaterThanOrEqualsCondition | undefined, lessThan?: LessThanCondition | undefined, lessThanOrEquals?: LessThanOrEqualsCondition | undefined, between?: BetweenCondition | undefined, changes?: ChangesCondition | undefined, remains?: RemainsCondition | undefined, was?: WasCondition | undefined}; export type Sequence = 'Serial' | 'Parallel'; export type IfActionSequence = {then?: Sequence | undefined, else?: Sequence | undefined}; export type IfAction = {and?: Array | undefined, or?: Array | undefined, not?: Condition | undefined, equals?: EqualsCondition | undefined, greaterThan?: GreaterThanCondition | undefined, greaterThanOrEquals?: GreaterThanOrEqualsCondition | undefined, lessThan?: LessThanCondition | undefined, lessThanOrEquals?: LessThanOrEqualsCondition | undefined, between?: BetweenCondition | undefined, changes?: ChangesCondition | undefined, remains?: RemainsCondition | undefined, was?: WasCondition | undefined, then?: Array | undefined, else?: Array | undefined, sequence?: IfActionSequence | undefined}; export type SleepAction = {duration: Interval}; export type DeviceCommand = {component?: unknown | undefined, capability: string, command: string, arguments?: Array> | undefined, commandId?: string | undefined}; export type CommandSequence = {commands?: Sequence | undefined, devices?: Sequence | undefined}; export type CommandAction = {devices: Array, commands: Array, sequence?: CommandSequence | undefined}; export type SceneArgument = {name?: string | undefined, schema?: Record | undefined, value?: Record | undefined}; export type SceneCommand = {arguments?: Array | undefined}; export type SceneCapability = {capabilityId?: string | undefined, status?: ('proposed' | 'live' | 'deprecated' | 'dead') | undefined, commands?: {[key: string]: SceneCommand} | undefined}; export type SceneComponent = {componentId?: string | undefined, capabilities?: Array | undefined}; export type SceneDeviceRequest = {deviceId?: unknown | undefined, actionId?: unknown | undefined, components?: Array | undefined}; export type SceneModeRequest = {modeId: unknown, actionId?: unknown | undefined, modeName?: string | undefined}; export type SceneSleepRequest = {seconds: number}; export type SceneDeviceGroupRequest = {deviceGroupId: unknown, actionId?: unknown | undefined, capability?: SceneCapability | undefined}; export type SceneAction = {deviceRequest?: SceneDeviceRequest | undefined, modeRequest?: SceneModeRequest | undefined, sleepRequest?: SceneSleepRequest | undefined, deviceGroupRequest?: SceneDeviceGroupRequest | undefined}; export type ActionSequence = {actions?: Sequence | undefined}; export type EveryAction = {interval?: Interval | undefined, specific?: DateTimeOperand | undefined, actions: Array, sequence?: ActionSequence | undefined}; export type LocationAction = {locationId?: unknown | undefined, mode?: string | undefined}; export type LimitAction = {count: number, period: 'Hour' | 'Day' | 'Week' | 'Month' | 'Lifetime', actions: Array, sequence?: ActionSequence | undefined}; export type ToggleAction = {devices: Array, component: string, capability: string, attribute: string}; export type Action = {if?: IfAction | undefined, sleep?: SleepAction | undefined, command?: CommandAction | undefined, scene?: SceneAction | undefined, every?: EveryAction | undefined, location?: LocationAction | undefined, limit?: LimitAction | undefined, toggle?: ToggleAction | undefined}; export type RuleRequest = {name: string, actions: Array, sequence?: ActionSequence | undefined, timeZoneId?: string | undefined}; export type CreateApiRulesArgs = {locationId: unknown, body: RuleRequest}; export type CreateApiRulesOptions = {baseUrl?: string | undefined, Bearer?: string | undefined, fetch?: typeof fetch | undefined}; export const operandAggregationMode = z.literal("None"); export const arrayOperand = z.object({operands: z.array(z.lazy(() => operand)), aggregation: operandAggregationMode.optional()}); export const mapOperand = z.record(z.string(), z.lazy(() => operand)); export const triggerMode = z.enum(["Auto", "Always", "Never"]); export const deviceOperand = z.object({devices: z.array(z.string()), component: z.string(), capability: z.string(), attribute: z.string(), path: z.string().optional(), aggregation: operandAggregationMode.optional(), trigger: triggerMode.optional()}); export const locationAttribute = z.enum(["FineDust", "FineDustIndex", "Humidity", "Mode", "Security", "Temperature", "TemperatureC", "TemperatureF", "UltraFineDust", "UltraFineDustIndex", "Weather", "WeatherAlertSeverity"]); export const locationOperand = z.object({locationId: z.string().optional(), postalCode: z.string().optional(), attribute: locationAttribute, trigger: triggerMode.optional()}); export const dayOfWeek = z.enum(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]); export const dateReference = z.enum(["Today", "Holiday"]); export const dateOperand = z.object({timeZoneId: z.string().optional(), daysOfWeek: z.array(dayOfWeek).optional(), year: z.number().int().optional(), month: z.number().int().gte(1).lte(12).optional(), day: z.number().int().gte(1).lte(31).optional(), reference: dateReference.optional()}); export const timeReference = z.enum(["Now", "Midnight", "Sunrise", "Noon", "Sunset"]); export const intervalUnit = z.enum(["Second", "Minute", "Hour", "Day", "Week", "Month", "Year"]); export const interval = z.object({value: z.lazy(() => operand), unit: intervalUnit}); export const timeOperand = z.object({timeZoneId: z.string().optional(), daysOfWeek: z.array(dayOfWeek).optional(), reference: timeReference, offset: interval.optional()}); export const dateTimeOperand = z.object({timeZoneId: z.string().optional(), locationId: z.string().optional(), daysOfWeek: z.array(dayOfWeek).optional(), year: z.number().int().optional(), month: z.number().int().gte(1).lte(12).optional(), day: z.number().int().gte(1).lte(31).optional(), reference: timeReference, offset: interval.optional()}); export const operand: z.ZodType = z.object({boolean: z.boolean().optional(), decimal: z.number().optional(), integer: z.number().int().optional(), string: z.string().optional(), array: arrayOperand.optional(), map: mapOperand.optional(), device: deviceOperand.optional(), location: locationOperand.optional(), date: dateOperand.optional(), time: timeOperand.optional(), datetime: dateTimeOperand.optional()}); export const conditionAggregationMode = z.enum(["Any", "All"]); export const equalsCondition = z.object({left: operand, right: operand, aggregation: conditionAggregationMode.optional(), changesOnly: z.boolean().optional()}); export const greaterThanCondition = z.object({left: operand, right: operand, aggregation: conditionAggregationMode.optional(), changesOnly: z.boolean().optional()}); export const greaterThanOrEqualsCondition = z.object({left: operand, right: operand, aggregation: conditionAggregationMode.optional(), changesOnly: z.boolean().optional()}); export const lessThanCondition = z.object({left: operand, right: operand, aggregation: conditionAggregationMode.optional(), changesOnly: z.boolean().optional()}); export const lessThanOrEqualsCondition = z.object({left: operand, right: operand, aggregation: conditionAggregationMode.optional(), changesOnly: z.boolean().optional()}); export const betweenCondition = z.object({value: operand, start: operand, end: operand, aggregation: conditionAggregationMode.optional(), changesOnly: z.boolean().optional()}); export const changesCondition = z.object({and: z.array(z.lazy(() => condition)).optional(), or: z.array(z.lazy(() => condition)).optional(), not: z.lazy(() => condition.optional()), equals: equalsCondition.optional(), greaterThan: greaterThanCondition.optional(), greaterThanOrEquals: greaterThanOrEqualsCondition.optional(), lessThan: lessThanCondition.optional(), lessThanOrEquals: lessThanOrEqualsCondition.optional(), between: betweenCondition.optional(), id: z.string(), operand: operand.optional()}); export const remainsCondition = z.object({and: z.array(z.lazy(() => condition)).optional(), or: z.array(z.lazy(() => condition)).optional(), not: z.lazy(() => condition.optional()), equals: equalsCondition.optional(), greaterThan: greaterThanCondition.optional(), greaterThanOrEquals: greaterThanOrEqualsCondition.optional(), lessThan: lessThanCondition.optional(), lessThanOrEquals: lessThanOrEqualsCondition.optional(), between: betweenCondition.optional(), id: z.string(), operand: operand.optional(), duration: interval, latching: z.boolean().optional()}); export const wasCondition = z.object({and: z.array(z.lazy(() => condition)).optional(), or: z.array(z.lazy(() => condition)).optional(), not: z.lazy(() => condition.optional()), equals: equalsCondition.optional(), greaterThan: greaterThanCondition.optional(), greaterThanOrEquals: greaterThanOrEqualsCondition.optional(), lessThan: lessThanCondition.optional(), lessThanOrEquals: lessThanOrEqualsCondition.optional(), between: betweenCondition.optional(), id: z.string(), duration: interval, operand: operand.optional()}); export const condition: z.ZodType = z.object({and: z.array(z.lazy(() => condition)).optional(), or: z.array(z.lazy(() => condition)).optional(), not: z.lazy(() => condition.optional()), equals: equalsCondition.optional(), greaterThan: greaterThanCondition.optional(), greaterThanOrEquals: greaterThanOrEqualsCondition.optional(), lessThan: lessThanCondition.optional(), lessThanOrEquals: lessThanOrEqualsCondition.optional(), between: betweenCondition.optional(), changes: changesCondition.optional(), remains: remainsCondition.optional(), was: wasCondition.optional()}); export const sequence = z.enum(["Serial", "Parallel"]); export const ifActionSequence = z.object({then: sequence.optional(), else: sequence.optional()}); export const ifAction = z.object({and: z.array(condition).optional(), or: z.array(condition).optional(), not: condition.optional(), equals: equalsCondition.optional(), greaterThan: greaterThanCondition.optional(), greaterThanOrEquals: greaterThanOrEqualsCondition.optional(), lessThan: lessThanCondition.optional(), lessThanOrEquals: lessThanOrEqualsCondition.optional(), between: betweenCondition.optional(), changes: changesCondition.optional(), remains: remainsCondition.optional(), was: wasCondition.optional(), then: z.array(z.lazy(() => action)).optional(), else: z.array(z.lazy(() => action)).optional(), sequence: ifActionSequence.optional()}); export const sleepAction = z.object({duration: interval}); export const deviceCommand = z.object({component: z.string().optional(), capability: z.string(), command: z.string(), arguments: z.array(z.object({})).optional(), commandId: z.string().optional()}); export const commandSequence = z.object({commands: sequence.optional(), devices: sequence.optional()}); export const commandAction = z.object({devices: z.array(z.string()), commands: z.array(deviceCommand), sequence: commandSequence.optional()}); export const sceneArgument = z.object({name: z.string().optional(), schema: z.object({}).optional(), value: z.object({}).optional()}); export const sceneCommand = z.object({arguments: z.array(sceneArgument).optional()}); export const sceneCapability = z.object({capabilityId: z.string().optional(), status: z.enum(["proposed", "live", "deprecated", "dead"]).optional(), commands: z.record(z.string(), sceneCommand).optional()}); export const sceneComponent = z.object({componentId: z.string().optional(), capabilities: z.array(sceneCapability).optional()}); export const sceneDeviceRequest = z.object({deviceId: z.string().optional(), actionId: z.string().optional(), components: z.array(sceneComponent).optional()}); export const sceneModeRequest = z.object({modeId: z.string(), actionId: z.string().optional(), modeName: z.string().optional()}); export const sceneSleepRequest = z.object({seconds: z.number().int()}); export const sceneDeviceGroupRequest = z.object({deviceGroupId: z.string(), actionId: z.string().optional(), capability: sceneCapability.optional()}); export const sceneAction = z.object({deviceRequest: sceneDeviceRequest.optional(), modeRequest: sceneModeRequest.optional(), sleepRequest: sceneSleepRequest.optional(), deviceGroupRequest: sceneDeviceGroupRequest.optional()}); export const actionSequence = z.object({actions: sequence.optional()}); export const everyAction = z.object({interval: interval.optional(), specific: dateTimeOperand.optional(), actions: z.array(z.lazy(() => action)), sequence: actionSequence.optional()}); export const locationAction = z.object({locationId: z.string().optional(), mode: z.string().optional()}); export const limitAction = z.object({count: z.number().int(), period: z.enum(["Hour", "Day", "Week", "Month", "Lifetime"]), actions: z.array(z.lazy(() => action)), sequence: actionSequence.optional()}); export const toggleAction = z.object({devices: z.array(z.string()), component: z.string(), capability: z.string(), attribute: z.string()}); export const action: z.ZodType = z.object({if: ifAction.optional(), sleep: sleepAction.optional(), command: commandAction.optional(), scene: sceneAction.optional(), every: everyAction.optional(), location: locationAction.optional(), limit: limitAction.optional(), toggle: toggleAction.optional()}); export const creator = z.enum(["SMARTTHINGS", "ARB", "RECIPE", "UNDEFINED"]); export const rule = z.object({name: z.string(), actions: z.array(action), sequence: actionSequence.optional(), timeZoneId: z.string().optional(), id: z.string(), status: z.enum(["Enabled", "Disabled"]).optional(), executionLocation: z.enum(["Cloud", "Local"]).optional(), ownerId: z.string(), ownerType: z.enum(["Location", "User"]), creator: creator.optional(), dateCreated: z.string(), dateUpdated: z.string(), dateExecuted: z.string().optional(), allowed: z.array(z.string()).optional()}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * Create a Rule * * POST /rules */ export const createApiRules = async (args: CreateApiRulesArgs, options?: CreateApiRulesOptions) => { const {baseUrl = "https://api.smartthings.com/v1", fetch: fetchFn = fetch, Bearer} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('content-type', "application/json") headers.set('accept', "application/json") if (args.locationId != null) query.append("locationId", String(args.locationId)) if (Bearer !== undefined) headers.set('authorization', `Bearer ${Bearer}`) const url = `${baseUrl.replace(/\/+$/, '')}/rules${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "POST", headers, body: JSON.stringify(args.body)}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(rule, await res.json()) };