import { UNITS_OF_MEASURE } from "../catalog.js"; import { labels } from "../resources.js"; import { s } from "../schema.js"; import { semanticsOf } from "../semantics.js"; import { DeclarationError, defineInterface } from "../types.js"; const directives = { SetRangeValue: { name: "SetRangeValue", payload: s.object({ rangeValue: s.number() }) }, AdjustRangeValue: { name: "AdjustRangeValue", payload: s.object({ rangeValueDelta: s.number(), rangeValueDeltaDefault: s.boolean() }), note: "rangeValueDeltaDefault is true when the user named no amount; rangeValueDelta is then the precision", }, }; // 0.1 + 0.2 is not 0.3: a value is on the grid when it is within a millionth of a step of it function onGrid(value, origin, step) { const steps = (value - origin) / step; return Math.abs(steps - Math.round(steps)) < 1e-6; } /** * A number in a range, under an instance name: the position of a blind, the speed of a fan. With nonControllable it * is a reading the user can ask for, like an air quality index. */ export const RangeController = defineInterface({ namespace: "Alexa.RangeController", version: "3", doc: "https://developer.amazon.com/docs/alexaplus/device-apis/alexa-rangecontroller.html", kind: "controller", tier: 1, instanced: true, properties: { rangeValue: { name: "rangeValue", value: s.number() }, }, directives, options: s.object({ /** precision is the step of the range and what "turn up the fan speed" changes it by. */ range: s.object({ min: s.number(), max: s.number(), precision: s.number({ gt: 0 }) }, { unknownKeys: "reject" }), unit: s.optional(s.oneOf(UNITS_OF_MEASURE, "a unit of measure like Alexa.Unit.Percent")), /** Names for values: "set the fan speed to maximum". */ presets: s.optional(s.array(s.object({ value: s.number(), friendlyNames: labels }, { unknownKeys: "reject" }))), semantics: s.optional(semanticsOf({ directives, stateValue: s.number(), ranges: true })), }, { unknownKeys: "reject" }), // A capability declared the 1.x way may be without a range; check() says so, discovery lists what there is discovery({ options: { range, unit, presets } }) { const configuration = { ...(range && { supportedRange: { minimumValue: range.min, maximumValue: range.max, precision: range.precision } }), ...(unit && { unitOfMeasure: unit }), ...(presets && { presets: presets.map(({ value, friendlyNames }) => ({ rangeValue: value, presetResources: { friendlyNames } })) }), }; return Object.keys(configuration).length > 0 ? { configuration } : {}; }, validate(capability) { const { range: { min, max, precision }, presets = [], semantics } = capability.options; const inRange = (value) => value >= min && value <= max; if (max <= min) throw new DeclarationError(capability, `range.max ${max} is not above range.min ${min}`); for (const { value } of presets) { // alexa-rangecontroller.html, "Preset object": "minimum range value + (n x precision) where n is an integer" if (!inRange(value) || !onGrid(value, min, precision)) { throw new DeclarationError(capability, `the preset ${value} is not ${min} + n x ${precision}, up to ${max}`); } } for (const { directive } of semantics?.actionMappings ?? []) { const { rangeValue } = directive.payload ?? {}; if (directive.name === "SetRangeValue" && !inRange(rangeValue)) { throw new DeclarationError(capability, `an action is mapped to SetRangeValue ${rangeValue}, the range is ${min} to ${max}`); } } }, // alexa-rangecontroller.html, "SetRangeValue directive error handling": "If your error is safety related, respond // with an Alexa.Safety.ErrorResponse" errorNamespace: "Alexa.Safety", errorTypes: ["OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED"], });