import {z} from 'zod' export type GetApiRulesArgs = {locationId: unknown, includeAllParents?: string | undefined, max?: number | undefined, offset?: number | undefined}; export type GetApiRulesOptions = {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 link = z.object({href: z.string().optional()}); export const links = z.object({next: link.optional(), previous: link.optional()}); export const pagedRules = z.object({items: z.array(rule).optional(), _links: links.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 }; /** * List Rules * * GET /rules */ export const getApiRules = async (args: GetApiRulesArgs, options?: GetApiRulesOptions) => { const {baseUrl = "https://api.smartthings.com/v1", fetch: fetchFn = fetch, Bearer} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('accept', "application/json") if (args.locationId != null) query.append("locationId", String(args.locationId)) if (args.includeAllParents != null) query.append("includeAllParents", String(args.includeAllParents)) if (args.max != null) query.append("max", String(args.max)) if (args.offset != null) query.append("offset", String(args.offset)) if (Bearer !== undefined) headers.set('authorization', `Bearer ${Bearer}`) const url = `${baseUrl.replace(/\/+$/, '')}/rules${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "GET", headers}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(pagedRules, await res.json()) };