From 1671c1aaaf6193e61def383537aaa45d85341972 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 14 Nov 2021 11:08:26 -0500 Subject: [PATCH] Update alexa integration with new metadata syntax (#1145) Signed-off-by: jsetton --- .../src/assets/definitions/metadata/alexa.js | 271 ----------- .../definitions/metadata/alexa/constants.js | 58 +++ .../metadata/alexa/deviceattributes.js | 411 ++++++++++++++++ .../definitions/metadata/alexa/devicetypes.js | 317 +++++++++++++ .../definitions/metadata/alexa/helpers.js | 63 +++ .../definitions/metadata/alexa/index.js | 32 ++ .../definitions/metadata/alexa/parameters.js | 440 ++++++++++++++++++ .../src/components/config/config-sheet.vue | 6 +- .../config/controls/parameter-text.vue | 1 + .../item/metadata/item-metadata-alexa.vue | 195 ++++++-- .../items/metadata/item-metadata-edit.vue | 1 + 11 files changed, 1486 insertions(+), 309 deletions(-) delete mode 100644 bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa.js create mode 100644 bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/constants.js create mode 100644 bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/deviceattributes.js create mode 100644 bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/devicetypes.js create mode 100644 bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/helpers.js create mode 100644 bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/index.js create mode 100644 bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/parameters.js diff --git a/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa.js b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa.js deleted file mode 100644 index 46b6e0e58..000000000 --- a/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa.js +++ /dev/null @@ -1,271 +0,0 @@ - -const categories = [ - 'ACTIVITY_TRIGGER', - 'CAMERA', - 'COMPUTER', - 'CONTACT_SENSOR', - 'DOOR', - 'DOORBELL', - 'EXTERIOR_BLIND', - 'FAN', - 'GAME_CONSOLE', - 'GARAGE_DOOR', - 'INTERIOR_BLIND', - 'LAPTOP', - 'LIGHT', - 'MICROWAVE', - 'MOBILE_PHONE', - 'MOTION_SENSOR', - 'MUSIC_SYSTEM', - 'NETWORK_HARDWARE', - 'OTHER', - 'OVEN', - 'PHONE', - 'SCENE_TRIGGER', - 'SCREEN', - 'SECURITY_PANEL', - 'SMARTLOCK', - 'SMARTPLUG', - 'SPEAKER', - 'STREAMING_DEVICE', - 'SWITCH', - 'TABLET', - 'TEMPERATURE_SENSOR', - 'THERMOSTAT', - 'TV', - 'WEARABLE' -] - -// Group endpoints are generated from the display categories. Example from the docs: SECURITY_PANEL => Endpoint.SecurityPanel. -const groupEndpoints = categories - .map(category => { - const convertedChars = [] - let capitalizeNext = false - for (let i = 0; i < category.length; i++) { - const currentChar = category.charAt(i) - if (i === 0) { - convertedChars.push(currentChar.toUpperCase()) - } else if (currentChar === '_') { - capitalizeNext = true - } else if (capitalizeNext) { - convertedChars.push(currentChar.toUpperCase()) - capitalizeNext = false - } else { - convertedChars.push(currentChar.toLocaleLowerCase()) - } - } - return 'Endpoint.' + convertedChars.join('') - }).reduce((endpoints, endpointName) => { - endpoints[endpointName] = [] - return endpoints - }, {}) - -const labels = { - 'Switchable': [], - 'Lighting': [], - 'Blind': [], - 'Door': [], - 'Lock': [], - 'Outlet': [], - 'CurrentHumidity': [], - 'CurrentTemperature': [], - 'TargetTemperature': [], - 'LowerTemperature': [], - 'UpperTemperature': [], - 'HeatingCoolingMode': [], - 'ColorTemperature': [], - 'Activity': [], - 'Scene': [], - 'EntertainmentChannel': [], - 'EntertainmentInput': [], - 'EqualizerBass': [], - 'EqualizerMidrange': [], - 'EqualizerTreble': [], - 'EqualizerMode': [], - 'MediaPlayer': [], - 'SpeakerMute': [], - 'SpeakerVolume': [], - 'ContactSensor': [], - 'MotionSensor': [], - 'SecurityAlarmMode': [], - 'BurglaryAlarm': [], - 'FireAlarm': [], - 'CarbonMonoxideAlarm': [], - 'WaterAlarm': [], - 'ModeComponent': [], - 'RangeComponent': [], - 'ToggleComponent': [] -} - -const p = (type, name, label, description, options, advanced) => { - return { - name, - type, - label, - description, - advanced, - limitToOptions: !!options, - options: (!options) ? undefined : options.split(',').map((o) => { - const parts = o.split('=') - return { value: parts[0], label: parts[1] || parts[0] } - }) - } -} - -const categoryParameter = p('TEXT', 'category', 'Category', 'Override the default category for the class', categories.join(','), true) -const scaleParameter = p('TEXT', 'scale', 'Scale', 'Temperature Unit', 'Celsius,Fahrenheit') -const comfortRangeParameter = p('TEXT', 'comfortRange', 'Comfort Range', 'Number to define the comfort range, defaults: 2°F or 1°C') -const setpointRangeParameter = p('TEXT', 'setpointRange', 'Setpoint Range', 'Format: minRange:maxRange') -const rangeParameter = p('TEXT', 'range', 'Range', 'Format: minRange:maxRange') -const volumeIncrementParameter = p('INTEGER', 'increment', 'Increment') -const friendlyNamesParameter = p('TEXT', 'friendlyNames', 'Friendly Names', 'each name formatted as @assetIdOrName, defaults to item label name') -const nonControllableParameter = p('BOOLEAN', 'nonControllable', 'Non Controllable') -const languageParameter = p('TEXT', 'language', 'Language', 'defaults to your server regional settings, if defined, otherwise en', 'de,en,es,fr,hi,it,ja,pt') -const actionMappingsParameter = p('TEXT', 'actionMappings', 'Action Mappings') -const stateMappingsParameter = p('TEXT', 'stateMappings', 'State Mappings') - -const capabilities = { - 'PowerController.powerState': [], - 'BrightnessController.brightness': [], - 'PowerLevelController.powerLevel': [], - 'PercentageController.percentage': [], - 'ThermostatController.targetSetpoint': [ - scaleParameter, - setpointRangeParameter - ], - 'ThermostatController.upperSetpoint': [ - scaleParameter, - comfortRangeParameter, - setpointRangeParameter - ], - 'ThermostatController.lowerSetpoint': [ - scaleParameter, - comfortRangeParameter, - setpointRangeParameter - ], - 'ThermostatController.thermostatMode': [ - p('TEXT', 'OFF', 'OFF State'), - p('TEXT', 'HEAT', 'HEAT State'), - p('TEXT', 'COOL', 'COOL State'), - p('TEXT', 'ECO', 'ECO State'), - p('TEXT', 'AUTO', 'AUTO State'), - p('TEXT', 'binding', 'Binding', 'Auto-configure modes for binding', 'daikin,max,nest,zwave'), - p('TEXT', 'supportedModes', 'Supported modes'), - p('BOOLEAN', 'supportsSetpointMode', 'Supports Setpoint Mode', '', null, true) - ], - 'TemperatureSensor.temperature': [scaleParameter], - 'LockController.lockState': [ - p('TEXT', 'LOCKED', 'Locked State'), - p('TEXT', 'UNLOCKED', 'Unlocked State'), - p('TEXT', 'JAMMED', 'Jammed State') - ], - 'ColorController.color': [], - 'ColorTemperatureController.colorTemperatureInKelvin': [ - p('INTEGER', 'increment', 'Increment'), - rangeParameter, - p('TEXT', 'binding', 'Binding', 'Auto-configure range for binding', 'hue,lifx,milight,tradfri,yeelight') - ], - 'SceneController.scene': [ - p('TEXT', 'supportsDeactivation', 'Supports deactivation') - ], - 'ChannelController.channel': [ - // User should switch to YAML for this one - ], - 'InputController.input': [ - p('TEXT', 'supportedInputs', 'required list of supported input values (e.g. "HMDI1,TV,XBOX")') - ], - 'Speaker.volume': [ - volumeIncrementParameter - ], - 'Speaker.muted': [], - 'StepSpeaker.volume': [ - volumeIncrementParameter - ], - 'StepSpeaker.muted': [], - 'PlaybackController.playback': [], - 'EqualizerController.bands:bass': [ - rangeParameter - ], - 'EqualizerController.bands:midrange': [ - rangeParameter - ], - 'EqualizerController.bands:treble': [ - rangeParameter - ], - 'EqualizerController.modes': [ - p('TEXT', 'MOVIE', 'MOVIE State'), - p('TEXT', 'MUSIC', 'MUSIC State'), - p('TEXT', 'NIGHT', 'NIGHT State'), - p('TEXT', 'SPORT', 'SPORT State'), - p('TEXT', 'TV', 'TV State'), - p('TEXT', 'supportedModes', 'Supported modes') - ], - - // TODO the rest - 'ContactSensor.detectionState': [], - 'MotionSensor.detectionState': [], - 'SecurityPanelController.armState': [ - p('TEXT', 'DISARMED', 'DISARMED State'), - p('TEXT', 'ARMED_STAY', 'ARMED_STAY State'), - p('TEXT', 'ARMED_AWAY', 'ARMED_AWAY State'), - p('TEXT', 'ARMED_NIGHT', 'ARMED_NIGHT State'), - p('TEXT', 'AUTHORIZATION_REQUIRED', 'AUTHORIZATION_REQUIRED State'), - p('TEXT', 'UNAUTHORIZED', 'UNAUTHORIZED State'), - p('TEXT', 'NOT_READY', 'NOT_READY State'), - p('TEXT', 'UNCLEARED_ALARM', 'UNCLEARED_ALARM State'), - p('TEXT', 'UNCLEARED_TROUBLE', 'UNCLEARED_TROUBLE State'), - p('TEXT', 'BYPASS_NEEDED', 'BYPASS_NEEDED State'), - p('TEXT', 'supportedArmStates', 'Supported arm states'), - p('BOOLEAN', 'supportsPinCodes', 'Supports pin codes'), - p('INTEGER', 'exitDelay', 'Exit Delay') - ], - 'SecurityPanelController.burglaryAlarm': [], - 'SecurityPanelController.fireAlarm': [], - 'SecurityPanelController.carbonMonoxideAlarm': [], - 'SecurityPanelController.waterAlarm': [], - 'ModeController.mode': [ - friendlyNamesParameter, - nonControllableParameter, - p('TEXT', 'supportedModes', 'Supported Modes'), - p('BOOLEAN', 'ordered', 'Ordered'), - languageParameter, - actionMappingsParameter, - stateMappingsParameter - ], - 'RangeController.rangeValue': [ - friendlyNamesParameter, - nonControllableParameter, - p('TEXT', 'supportedRange', 'Supported Range'), - p('TEXT', 'presets', 'Presets'), - p('TEXT', 'unitOfMeasure', 'Unit of Measure'), - languageParameter, - actionMappingsParameter, - stateMappingsParameter - ], - 'ToggleController.toggleState': [ - friendlyNamesParameter, - nonControllableParameter, - languageParameter, - actionMappingsParameter, - stateMappingsParameter - ] -} - -let classes = {} - -for (let l in labels) { - labels[l].unshift(categoryParameter) - classes['label:' + l] = labels[l] -} - -for (let l in groupEndpoints) { - groupEndpoints[l].unshift(categoryParameter) - classes['endpoint:' + l] = groupEndpoints[l] -} - -for (let c in capabilities) { - capabilities[c].unshift(categoryParameter) - classes[c] = capabilities[c] -} - -export default classes diff --git a/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/constants.js b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/constants.js new file mode 100644 index 000000000..b0292c1ab --- /dev/null +++ b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/constants.js @@ -0,0 +1,58 @@ +export const ARM_STATES = ['DISARMED', 'ARMED_STAY', 'ARMED_AWAY', 'ARMED_NIGHT'] + +export const EQUALIZER_MODES = ['MOVIE', 'MUSIC', 'NIGHT', 'SPORT', 'TV'] + +export const FAN_DIRECTIONS = ['FORWARD', 'REVERSE'] + +export const FAN_SPEEDS = ['OFF', 'LOW', 'MEDIUM', 'HIGH'] + +export const LANGUAGES = { + en: 'English', + fr: 'French', + de: 'German', + hi: 'Hindi', + it: 'Italian', + ja: 'Japanese', + pt: 'Portuguese', + es: 'Spanish' +} + +export const LOCK_STATES = ['LOCKED', 'UNLOCKED', 'JAMMED'] + +export const OPEN_STATES = ['CLOSED', 'OPEN'] + +export const PLAYBACK_OPERATIONS = ['Play', 'Pause', 'Next', 'Previous', 'FastForward', 'Rewind'] + +export const TEMPERATURE_SCALES = ['CELSIUS', 'FAHRENHEIT'] + +export const THERMOSTAT_MODES = ['OFF', 'HEAT', 'COOL', 'ECO', 'AUTO'] + +export const THERMOSTAT_FAN_MODES = ['AUTO', 'ON'] + +export const VACUUM_MODES = ['CLEAN', 'DOCK', 'SPOT', 'PAUSE', 'STOP'] + +export const UNITS_OF_MEASURE = { + 'Angle.Degrees': '°', + 'Angle.Radians': 'rad', + 'Percent': '%', + 'Distance.Yards': 'yd', + 'Distance.Inches': 'in', + 'Distance.Meters': 'm', + 'Distance.Feet': 'ft', + 'Distance.Miles': 'mi', + 'Distance.Kilometers': 'km', + 'Mass.Kilograms': 'kg', + 'Mass.Grams': 'g', + 'Weight.Pounds': 'lb', + 'Weight.Ounces': 'oz', + 'Temperature.Degrees': '°', + 'Temperature.Celsius': '°C', + 'Temperature.Fahrenheit': '°F', + 'Temperature.Kelvin': 'K', + 'Volume.Gallons': 'gal', + 'Volume.Pints': 'pt', + 'Volume.Quarts': 'qt', + 'Volume.Liters': 'l', + 'Volume.CubicMeters': 'm3', + 'Volume.CubicFeet': 'ft3' +} diff --git a/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/deviceattributes.js b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/deviceattributes.js new file mode 100644 index 000000000..56b8dbb6f --- /dev/null +++ b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/deviceattributes.js @@ -0,0 +1,411 @@ +import p from './parameters.js' +import { + ARM_STATES, + EQUALIZER_MODES, + FAN_DIRECTIONS, + FAN_SPEEDS, + LOCK_STATES, + OPEN_STATES, + THERMOSTAT_MODES, + THERMOSTAT_FAN_MODES, + VACUUM_MODES +} from './constants.js' + +export default { + // Camera Attributes + CameraStream: { + itemTypes: ['String'], + parameters: () => [p.proxyBaseUrl(), p.resolution(), p.basicAuth('username'), p.basicAuth('password')] + }, + + // Cover Attributes + OpenState: { + itemTypes: ['Switch'], + parameters: () => [p.inverted()] + }, + TargetOpenState: { + itemTypes: ['Switch'], + requires: ['CurrentOpenState'], + parameters: () => [p.inverted()] + }, + CurrentOpenState: { + itemTypes: ['Contact', 'Number', 'String', 'Switch'], + requires: ['TargetOpenState'], + parameters: (item) => + item.type === 'Contact' || item.type === 'Switch' + ? [p.inverted()] + : OPEN_STATES.map((state) => p.valueMapping(state)) + }, + ObstacleAlert: { + itemTypes: ['Contact ', 'Switch'], + requires: ['OpenState'], + parameters: () => [p.inverted()] + }, + PositionState: { + itemTypes: ['Dimmer', 'Rollershutter'], + parameters: (item) => [ + p.inverted(item.type === 'Rollershutter'), + p.presets(item.stateDescription, '20=Morning,60=Afternoon,80=Evening:@Setting.Night', true), + p.language(item.settings && item.settings.regional.language) + ] + }, + TiltAngle: { + itemTypes: ['Dimmer', 'Number', 'Number:Angle', 'Rollershutter'], + parameters: (item) => [ + p.inverted(item.type === 'Rollershutter'), + p.presets(item.stateDescription, '20=Morning,60=Afternoon,80=Evening:@Setting.Night', true), + p.language(item.settings && item.settings.regional.language) + ] + }, + + // Entertainment Attributes + Channel: { + itemTypes: ['Number', 'String'], + parameters: () => [p.channelMappings(), p.retrievable()] + }, + Input: { + itemTypes: ['Number', 'String'], + parameters: (item) => [ + p.supportedInputs(item.stateDescription, item.type === 'String' ? 'HDMI1=Cable,HDMI2=Kodi' : '1=Cable,2=Kodi'), + p.language(item.settings && item.settings.regional.language), + p.retrievable() + ] + }, + VolumeLevel: { + itemTypes: ['Dimmer', 'Number'], + parameters: (item) => [p.increment(10), ...(item.type === 'Number' ? [p.stepSpeaker()] : [])] + }, + MuteState: { + itemTypes: ['Switch'], + parameters: () => [p.inverted(), p.stepSpeaker()] + }, + EqualizerBass: { + itemTypes: ['Dimmer', 'Number'], + parameters: (item) => [ + p.equalizerRange(item.type === 'Dimmer' ? '0:100' : '-10:10'), + p.equalizerDefaultLevel(item.type === 'Dimmer' ? 50 : 0), + p.increment(item.type === 'Dimmer' ? 'INCREASE/DECREASE' : 1), + p.retrievable() + ] + }, + EqualizerMidrange: { + itemTypes: ['Dimmer', 'Number'], + parameters: (item) => [ + p.equalizerRange(item.type === 'Dimmer' ? '0:100' : '-10:10'), + p.equalizerDefaultLevel(item.type === 'Dimmer' ? 50 : 0), + p.increment(item.type === 'Dimmer' ? 'INCREASE/DECREASE' : 1), + p.retrievable() + ] + }, + EqualizerTreble: { + itemTypes: ['Dimmer', 'Number'], + parameters: (item) => [ + p.equalizerRange(item.type === 'Dimmer' ? '0:100' : '-10:10'), + p.equalizerDefaultLevel(item.type === 'Dimmer' ? 50 : 0), + p.increment(item.type === 'Dimmer' ? 'INCREASE/DECREASE' : 1), + p.retrievable() + ] + }, + EqualizerMode: { + itemTypes: ['Number', 'String'], + parameters: () => [ + ...EQUALIZER_MODES.map((mode) => p.valueMapping(mode)), + p.supportedEqualizerModes(), + p.retrievable() + ] + }, + Playback: { + itemTypes: ['Player'], + parameters: () => [p.supportedOperations()] + }, + PlaybackStop: { + itemTypes: ['Switch'], + requires: ['Playback'], + parameters: () => [p.inverted()] + }, + + // Fan Attributes + FanDirection: { + itemTypes: ['String', 'Switch'], + parameters: (item) => [ + ...(item.type === 'Switch' ? [p.inverted()] : FAN_DIRECTIONS.map((direction) => p.valueMapping(direction))), + p.retrievable() + ] + }, + FanOscillate: { + itemTypes: ['Switch'], + parameters: () => [p.inverted(), p.retrievable()] + }, + FanSpeed: { + itemTypes: ['Dimmer', 'Number', 'String'], + parameters: (item) => [ + ...(item.type === 'Dimmer' + ? [p.inverted()] + : item.type === 'Number' + ? [p.speedLevels()] + : FAN_SPEEDS.map((speed) => p.valueMapping(speed))), + p.retrievable() + ] + }, + + // Light Attributes + Brightness: { + itemTypes: ['Color', 'Dimmer'], + parameters: () => [p.retrievable()] + }, + Color: { + itemTypes: ['Color'], + parameters: () => [p.retrievable()] + }, + ColorTemperature: { + itemTypes: ['Dimmer', 'Number'], + parameters: (item) => [ + ...(item.type === 'Dimmer' ? [p.colorTemperatureBinding()] : []), + p.colorTemperatureRange(), + p.increment(item.type === 'Dimmer' ? 'INCREASE/DECREASE' : 500), + p.retrievable() + ] + }, + + // Networking Attributes + NetworkAccess: { + itemTypes: ['Switch'], + parameters: () => [p.inverted(), p.retrievable()], + visible: (item) => item.groups + .map((group) => group.metadata.alexa.config || {}) + .some((config) => !!config.macAddress) + }, + + // Scene Attributes + Scene: { + itemTypes: ['Switch'], + parameters: () => [p.supportsDeactivation()] + }, + + // Security Attributes + LockState: { + itemTypes: ['Switch'], + parameters: () => [p.inverted(), p.retrievable()] + }, + TargetLockState: { + itemTypes: ['Switch'], + requires: ['CurrentLockState'], + parameters: () => [p.inverted()] + }, + CurrentLockState: { + itemTypes: ['Contact', 'Number', 'String', 'Switch'], + requires: ['TargetLockState'], + parameters: (item) => + item.type === 'Contact' || item.type === 'Switch' + ? [p.inverted()] + : LOCK_STATES.map((state) => p.valueMapping(state)) + }, + ArmState: { + itemTypes: ['Number', 'String', 'Switch'], + parameters: () => [ + ...ARM_STATES.map((state) => p.valueMapping(state)), + p.supportedArmStates(), + p.pinCodes(), + p.exitDelay(), + p.retrievable() + ] + }, + BurglaryAlarm: { + itemTypes: ['Contact', 'Switch'], + parameters: () => [p.inverted()] + }, + CarbonMonoxideAlarm: { + itemTypes: ['Contact', 'Switch'], + parameters: () => [p.inverted()] + }, + FireAlarm: { + itemTypes: ['Contact', 'Switch'], + parameters: () => [p.inverted()] + }, + WaterAlarm: { + itemTypes: ['Contact', 'Switch'], + parameters: () => [p.inverted()] + }, + AlarmAlert: { + itemTypes: ['Contact', 'Switch'], + requires: ['ArmState'], + parameters: () => [p.inverted()] + }, + ReadyAlert: { + itemTypes: ['Contact', 'Switch'], + requires: ['ArmState'], + parameters: () => [p.inverted()] + }, + TroubleAlert: { + itemTypes: ['Contact', 'Switch'], + requires: ['ArmState'], + parameters: () => [p.inverted()] + }, + ZonesAlert: { + itemTypes: ['Contact', 'Switch'], + requires: ['ArmState'], + parameters: () => [p.inverted()] + }, + + // Sensor Attributes + BatteryLevel: { + itemTypes: ['Dimmer', 'Number', 'Number:Dimensionless'] + }, + CurrentHumidity: { + itemTypes: ['Dimmer', 'Number', 'Number:Dimensionless'] + }, + CurrentTemperature: { + itemTypes: ['Number', 'Number:Temperature'], + parameters: (item) => [p.scale(item)] + }, + ContactDetectionState: { + itemTypes: ['Contact', 'Switch'], + parameters: () => [p.inverted()] + }, + MotionDetectionState: { + itemTypes: ['Contact', 'Switch'], + parameters: () => [p.inverted()] + }, + + // Switchable Attributes + PowerState: { + itemTypes: ['Color', 'Dimmer', 'Number', 'String', 'Switch'], + parameters: (item) => [ + ...(item.type === 'Number' || item.type === 'String' + ? [p.valueMapping('OFF', true), p.valueMapping('ON', true)] + : []), + p.retrievable() + ] + }, + PowerLevel: { + itemTypes: ['Dimmer'], + parameters: () => [p.retrievable()] + }, + Percentage: { + itemTypes: ['Dimmer', 'Rollershutter'], + parameters: (item) => [p.inverted(item.type === 'Rollershutter'), p.retrievable()] + }, + + // Thermostat Attributes + TargetTemperature: { + itemTypes: ['Number', 'Number:Temperature'], + parameters: (item) => [p.scale(item), p.setpointRange(item), p.retrievable()] + }, + CoolingSetpoint: { + itemTypes: ['Number', 'Number:Temperature'], + requires: ['HeatingSetpoint'], + parameters: (item) => [p.scale(item), p.comfortRange(item), p.setpointRange(item), p.retrievable()] + }, + HeatingSetpoint: { + itemTypes: ['Number', 'Number:Temperature'], + requires: ['CoolingSetpoint'], + parameters: (item) => [p.scale(item), p.comfortRange(item), p.setpointRange(item), p.retrievable()] + }, + EcoCoolingSetpoint: { + itemTypes: ['Number', 'Number:Temperature'], + requires: ['EcoHeatingSetpoint'], + parameters: (item) => [p.scale(item), p.comfortRange(item), p.setpointRange(item), p.retrievable()] + }, + EcoHeatingSetpoint: { + itemTypes: ['Number', 'Number:Temperature'], + requires: ['EcoCoolingSetpoint'], + parameters: (item) => [p.scale(item), p.comfortRange(item), p.setpointRange(item), p.retrievable()] + }, + HeatingCoolingMode: { + itemTypes: ['Number', 'String', 'Switch'], + parameters: () => [ + ...THERMOSTAT_MODES.map((mode) => p.thermostatModeMapping(mode)), + p.thermostatModeBinding(), + p.supportedThermostatModes(), + p.supportsSetpointMode(), + p.retrievable() + ] + }, + ThermostatHold: { + itemTypes: ['Number', 'String', 'Switch'], + requires: ['HeatingCoolingMode'], + parameters: (item) => [p.valueMapping('RESUME')] + }, + ThermostatFan: { + itemTypes: ['String', 'Switch'], + parameters: (item) => [ + ...(item.type === 'Switch' ? [p.inverted()] : THERMOSTAT_FAN_MODES.map((mode) => p.valueMapping(mode))), + p.retrievable() + ] + }, + + // Vacuum Attributes + VacuumMode: { + itemTypes: ['Number', 'String'], + parameters: () => [...VACUUM_MODES.map((mode) => p.valueMapping(mode)), p.retrievable()] + }, + + // Generic Attributes + Mode: { + itemTypes: ['Number', 'String', 'Switch'], + supports: ['multiInstance'], + parameters: (item, config) => [ + p.capabilityNames( + item.groups.length ? item.label : '@Setting.Mode', + 'Wash Temperature,@Setting.WaterTemperature' + ), + p.nonControllable(item.stateDescription), + p.retrievable(), + p.supportedModes(item.stateDescription), + p.ordered(), + p.language(item.settings && item.settings.regional.language), + p.actionMappings( + { set: 'mode', ...(config.ordered && { adjust: '(±deltaValue)' }) }, + 'Close=Down,Open=Up,Lower=Down,Raise=Up' + ), + p.stateMappings(['mode'], 'Closed=Down,Open=Up') + ] + }, + RangeValue: { + itemTypes: [ + 'Dimmer', + 'Number', + 'Number:Angle', + 'Number:Dimensionless', + 'Number:Length', + 'Number:Mass', + 'Number:Temperature', + 'Number:Volume', + 'Rollershutter' + ], + supports: ['multiInstance'], + parameters: (item) => [ + p.capabilityNames(item.groups.length ? item.label : '@Setting.RangeValue', '@Setting.FanSpeed,Speed'), + p.nonControllable(item.stateDescription), + p.retrievable(), + p.inverted(item.type === 'Rollershutter'), + ...(item.type === 'Dimmer' + ? [p.supportedCommands(['ON', 'OFF', 'INCREASE', 'DECREASE'], 'INCREASE=@Value.Up,DECREASE=@Value.Down')] + : item.type === 'Rollershutter' + ? [p.supportedCommands(['UP', 'DOWN', 'MOVE', 'STOP'], 'UP=@Value.Open,DOWN=@Value.Close,STOP=@Value.Stop')] + : []), + p.supportedRange( + item.stateDescription, + item.type === 'Dimmer' || item.type === 'Rollershutter' ? '0:100:1' : '0:10:1' + ), + p.presets(item.stateDescription, '1=@Value.Low:Lowest,10=@Value.High:Highest'), + p.unitOfMeasure(item), + p.language(item.settings && item.settings.regional.language), + p.actionMappings({ set: 'value', adjust: '(±deltaValue)' }, 'Close=0,Open=100,Lower=(-10),Raise=(+10)'), + p.stateMappings({ default: 'value', range: 'minValue:maxValue' }, 'Closed=0,Open=1:100') + ] + }, + ToggleState: { + itemTypes: ['Switch'], + supports: ['multiInstance'], + parameters: (item) => [ + p.capabilityNames(item.groups.length ? item.label : '@Setting.ToggleState', '@Setting.Oscillate,Rotate'), + p.nonControllable(item.stateDescription), + p.retrievable(), + p.inverted(), + p.language(item.settings && item.settings.regional.language), + p.actionMappings(['ON', 'OFF'], 'Close=OFF,Open=ON'), + p.stateMappings(['ON', 'OFF'], 'Closed=OFF,Open=ON') + ] + } +} diff --git a/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/devicetypes.js b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/devicetypes.js new file mode 100644 index 000000000..bfb1bf39e --- /dev/null +++ b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/devicetypes.js @@ -0,0 +1,317 @@ +import attributes from './deviceattributes.js' +import p from './parameters.js' + +const genericAttributes = ['Mode', 'RangeValue', 'ToggleState'] +const genericDeviceAttributes = ['PowerState', ...genericAttributes] +const networkDeviceAttributes = ['NetworkAccess', ...genericDeviceAttributes] +const mobileDeviceAttributes = ['BatteryLevel', ...networkDeviceAttributes] +const sensorAttributes = ['BatteryLevel', ...genericAttributes] + +const cameraAttributes = ['CameraStream', 'BatteryLevel', ...genericDeviceAttributes] +const doorAttributes = ['OpenState', 'TargetOpenState', 'CurrentOpenState', ...genericAttributes] +const blindAttributes = ['PositionState', 'TiltAngle', ...doorAttributes] +const fanAttributes = ['FanDirection', 'FanOscillate', 'FanSpeed', ...genericDeviceAttributes] +const lightAttributes = ['Brightness', 'Color', 'ColorTemperature', ...genericDeviceAttributes] +const switchAttributes = ['PowerLevel', 'Percentage', ...genericDeviceAttributes] + +const entertainmentAttributes = [ + 'VolumeLevel', + 'MuteState', + 'Channel', + 'Input', + 'Playback', + 'PlaybackStop', + 'EqualizerBass', + 'EqualizerMidrange', + 'EqualizerTreble', + 'EqualizerMode', + ...genericDeviceAttributes +] +const securityAttributes = [ + 'ArmState', + 'BurglaryAlarm', + 'CarbonMonoxideAlarm', + 'FireAlarm', + 'WaterAlarm', + 'AlarmAlert', + 'ReadyAlert', + 'TroubleAlert', + 'ZonesAlert', + ...genericAttributes +] +const thermostatAttributes = [ + 'TargetTemperature', + 'CoolingSetpoint', + 'HeatingSetpoint', + 'EcoCoolingSetpoint', + 'EcoHeatingSetpoint', + 'HeatingCoolingMode', + 'ThermostatHold', + 'ThermostatFan', + 'CurrentTemperature', + 'CurrentHumidity', + 'BatteryLevel', + ...genericAttributes +] + +const blindParameters = (item) => { + const attributes = ['PositionState', 'TiltAngle'] + const metadata = item.members.map((mbr) => mbr.metadata && mbr.metadata.alexa.value).join(',') + return attributes.every((attr) => metadata.includes(attr)) ? [p.primaryControl()] : [] +} + +const networkParameters = (item) => { + const deviceTypes = ['NetworkHardware', 'Router'] + const connection = item.groups.find((g) => deviceTypes.includes(g.metadata.alexa.value)) + return connection ? [p.connectedTo(connection.label || connection.name), p.hostname(), p.macAddress()] : [] +} + +export const defaultParameters = (item) => { + const itemType = item.groupType || item.type + return itemType === 'Group' || !item.groups.length + ? [p.deviceName(item.label), p.deviceDescription(`${itemType} ${item.name}`)] + : [] +} + +export default { + Activity: { + defaultAttributes: ['Scene'], + supportsGroup: false + }, + AirConditioner: { + defaultAttributes: ['HeatingCoolingMode'], + supportedAttributes: ['HeatingCoolingMode', 'TargetTemperature', 'CurrentTemperature', ...fanAttributes] + }, + AirFreshener: { + defaultAttributes: ['FanSpeed'], + supportedAttributes: fanAttributes + }, + AirPurifier: { + defaultAttributes: ['FanSpeed'], + supportedAttributes: fanAttributes + }, + Automobile: { + supportedAttributes: [ + 'BatteryLevel', + 'FanSpeed', + 'LockState', + 'PowerState', + 'CurrentTemperature', + ...genericAttributes + ] + }, + AutomobileAccessory: { + supportedAttributes: ['BatteryLevel', 'CameraStream', 'FanSpeed', 'PowerState', ...genericAttributes] + }, + Awning: { + defaultAttributes: ['PositionState', 'OpenState'], + supportedAttributes: blindAttributes, + groupParameters: blindParameters + }, + Blind: { + defaultAttributes: ['PositionState', 'OpenState'], + supportedAttributes: blindAttributes, + groupParameters: blindParameters + }, + BluetoothSpeaker: { + defaultAttributes: ['VolumeLevel'], + supportedAttributes: ['BatteryLevel', ...entertainmentAttributes] + }, + Camera: { + defaultAttributes: ['CameraStream'], + supportedAttributes: cameraAttributes + }, + ChristmasTree: { + defaultAttributes: ['PowerState', 'Brightness', 'Color'], + supportedAttributes: lightAttributes + }, + CoffeeMaker: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Computer: { + defaultAttributes: ['PowerState'], + supportedAttributes: networkDeviceAttributes, + groupParameters: networkParameters + }, + ContactSensor: { + defaultAttributes: ['ContactDetectionState'], + supportedAttributes: ['ContactDetectionState', ...sensorAttributes] + }, + Curtain: { + defaultAttributes: ['PositionState', 'OpenState'], + supportedAttributes: blindAttributes, + groupParameters: blindParameters + }, + Dishwasher: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Door: { + defaultAttributes: ['OpenState'], + supportedAttributes: doorAttributes + }, + Doorbell: { + defaultAttributes: ['CameraStream'], + supportedAttributes: cameraAttributes + }, + Dryer: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Fan: { + defaultAttributes: ['FanSpeed'], + supportedAttributes: fanAttributes + }, + GameConsole: { + defaultAttributes: ['PowerState'], + supportedAttributes: networkDeviceAttributes, + groupParameters: networkParameters + }, + GarageDoor: { + defaultAttributes: ['OpenState'], + supportedAttributes: ['ObstacleAlert', ...doorAttributes] + }, + Headphones: { + defaultAttributes: ['VolumeLevel'], + supportedAttributes: ['BatteryLevel', ...entertainmentAttributes] + }, + Hub: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Laptop: { + defaultAttributes: ['PowerState'], + supportedAttributes: mobileDeviceAttributes, + groupParameters: networkParameters + }, + Light: { + defaultAttributes: ['PowerState', 'Brightness', 'Color'], + supportedAttributes: lightAttributes + }, + Lock: { + defaultAttributes: ['LockState'], + supportedAttributes: ['LockState', 'TargetLockState', 'CurrentLockState', 'BatteryLevel', ...genericAttributes] + }, + Microwave: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + MobilePhone: { + defaultAttributes: ['PowerState'], + supportedAttributes: mobileDeviceAttributes, + groupParameters: networkParameters + }, + MotionSensor: { + defaultAttributes: ['MotionDetectionState'], + supportedAttributes: ['MotionDetectionState', ...sensorAttributes] + }, + MusicSystem: { + defaultAttributes: ['Playback'], + supportedAttributes: entertainmentAttributes + }, + NetworkHardware: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Outlet: { + defaultAttributes: ['PowerState', 'PowerLevel', 'Percentage'], + supportedAttributes: switchAttributes + }, + Oven: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Phone: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Printer: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Router: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Scene: { + defaultAttributes: ['Scene'], + supportsGroup: false + }, + Screen: { + defaultAttributes: ['PowerState'], + supportedAttributes: entertainmentAttributes + }, + SecurityPanel: { + defaultAttributes: ['ArmState'], + supportedAttributes: securityAttributes + }, + SecuritySystem: { + defaultAttributes: ['ArmState'], + supportedAttributes: securityAttributes + }, + Shade: { + defaultAttributes: ['PositionState', 'OpenState'], + supportedAttributes: blindAttributes, + groupParameters: blindParameters + }, + Shutter: { + defaultAttributes: ['PositionState', 'OpenState'], + supportedAttributes: blindAttributes, + groupParameters: blindParameters + }, + SlowCooker: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + Speaker: { + defaultAttributes: ['VolumeLevel'], + supportedAttributes: entertainmentAttributes + }, + StreamingDevice: { + defaultAttributes: ['Playback'], + supportedAttributes: entertainmentAttributes + }, + Switch: { + defaultAttributes: ['PowerState', 'PowerLevel', 'Percentage'], + supportedAttributes: switchAttributes + }, + Tablet: { + defaultAttributes: ['PowerState'], + supportedAttributes: mobileDeviceAttributes, + groupParameters: networkParameters + }, + Television: { + defaultAttributes: ['Channel'], + supportedAttributes: entertainmentAttributes + }, + TemperatureSensor: { + defaultAttributes: ['CurrentTemperature'], + supportedAttributes: ['CurrentTemperature', ...sensorAttributes] + }, + Thermostat: { + defaultAttributes: ['HeatingCoolingMode'], + supportedAttributes: thermostatAttributes, + groupParameters: (item) => [p.scale(item, true)] + }, + VacuumCleaner: { + defaultAttributes: ['VacuumMode'], + supportedAttributes: ['VacuumMode', 'FanSpeed', 'BatteryLevel', ...genericDeviceAttributes] + }, + Washer: { + defaultAttributes: ['PowerState'], + supportedAttributes: genericDeviceAttributes + }, + WaterHeater: { + defaultAttributes: ['PowerState'], + supportedAttributes: ['TargetTemperature', 'CurrentTemperature', ...genericDeviceAttributes] + }, + Wearable: { + defaultAttributes: ['PowerState'], + supportedAttributes: mobileDeviceAttributes, + groupParameters: networkParameters + }, + Other: { + supportedAttributes: Object.keys(attributes).filter((attr) => attr !== 'NetworkAccess' && attr !== 'Scene') + } +} diff --git a/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/helpers.js b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/helpers.js new file mode 100644 index 000000000..98ff5e19c --- /dev/null +++ b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/helpers.js @@ -0,0 +1,63 @@ +import { UNITS_OF_MEASURE } from './constants.js' + +export const camelCase = (string, pascalCase = false) => + string + .toLowerCase() + .split(/[ _-]/) + .map((word, index) => (!index && !pascalCase ? word : word.charAt(0).toUpperCase() + word.slice(1))) + .join('') + +export const titleCase = (string) => + string + .toLowerCase() + .split(/[ _-]/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + +export const docLink = (title, anchor) => { + const link = `%DOC_URL%#${anchor || title.replace(/[. ]/g, '-').toLowerCase()}` + return `${title}` +} + +export const getGroupParameter = (parameter, groups) => { + for (const group of groups) { + const config = group.metadata.alexa.config || {} + if (parameter in config) return config[parameter] + } +} + +export const getOptions = (options, preserve = false) => + Array.isArray(options) + ? options.map((value) => ({ value, label: preserve ? value : titleCase(value) })) + : Object.keys(options).map((value) => ({ value, label: options[value] })) + +export const getSemanticFormat = (type, format) => + Object.keys(format).reduce( + (value, key, index, array) => + `${value}${index ? (index === array.length - 1 ? ' or ' : ', ') : ''}` + + `${Array.isArray(format) || key === 'default' ? type : camelCase(`${key}_${type}`)}=${format[key]}`, + '' + ) + +export const getTemperatureScale = (item) => { + const itemType = item.groupType || item.type + const state = (item.state !== 'NULL' && item.state !== 'UNDEF' && item.state) || '' + const statePresentation = (item.stateDescription && item.stateDescription.pattern) || '' + const format = (itemType === 'Number:Temperature' && state) || statePresentation + if (format.endsWith('°C')) return 'CELSIUS' + if (format.endsWith('°F')) return 'FAHRENHEIT' + const { measurementSystem } = (item.settings && item.settings.regional) || {} + if (measurementSystem === 'SI') return 'CELSIUS' + if (measurementSystem === 'US') return 'FAHRENHEIT' +} + +export const getUnitOfMeasure = (item) => { + const itemType = item.groupType || item.type + const state = (item.state !== 'NULL' && item.state !== 'UNDEF' && item.state) || '' + const statePresentation = (item.stateDescription && item.stateDescription.pattern) || '' + const format = + ((itemType === 'Dimmer' || itemType === 'Rollershutter') && '%') || + (itemType.startsWith('Number:') && state) || + statePresentation + return Object.keys(UNITS_OF_MEASURE).find((id) => format.endsWith(UNITS_OF_MEASURE[id])) +} diff --git a/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/index.js b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/index.js new file mode 100644 index 000000000..c5b66dc11 --- /dev/null +++ b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/index.js @@ -0,0 +1,32 @@ +import deviceAttributes from './deviceattributes.js' +import deviceTypes, { defaultParameters } from './devicetypes.js' + +const classes = {} + +for (const type of Object.keys(deviceTypes)) { + const { defaultAttributes = [], supportedAttributes = [], supportsGroup = true } = deviceTypes[type] + classes[type] = {} + + if (supportsGroup) { + const { groupParameters = [] } = deviceTypes[type] + classes[type]['Group'] = { parameters: [defaultParameters].concat(groupParameters) } + } + + for (const attribute of defaultAttributes) { + const { itemTypes = [], parameters } = deviceAttributes[attribute] + for (const itemType of itemTypes) { + if (!classes[type][itemType]) classes[type][itemType] = { parameters: [defaultParameters] } + if (parameters) classes[type][itemType].parameters.push(parameters) + } + } + + for (const attribute of supportedAttributes) { + const { itemTypes = [], parameters = [], ...properties } = deviceAttributes[attribute] + classes[`${type}.${attribute}`] = {} + for (const itemType of itemTypes) { + classes[`${type}.${attribute}`][itemType] = { parameters: [defaultParameters].concat(parameters), ...properties } + } + } +} + +export default classes diff --git a/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/parameters.js b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/parameters.js new file mode 100644 index 000000000..83b7b7ec1 --- /dev/null +++ b/bundles/org.openhab.ui/web/src/assets/definitions/metadata/alexa/parameters.js @@ -0,0 +1,440 @@ +import { + ARM_STATES, + EQUALIZER_MODES, + LANGUAGES, + PLAYBACK_OPERATIONS, + TEMPERATURE_SCALES, + THERMOSTAT_MODES, + UNITS_OF_MEASURE +} from './constants.js' +import { + docLink, + getGroupParameter, + getOptions, + getSemanticFormat, + getTemperatureScale, + getUnitOfMeasure, + titleCase +} from './helpers.js' + +export default { + actionMappings: (format, placeholder) => ({ + name: 'actionMappings', + label: 'Action Mappings', + description: `Each mapping formatted as ${getSemanticFormat('action', format)} (${docLink('Semantic Extensions')})`, + type: 'TEXT', + placeholder: placeholder.replace(/,/g, '\n'), + multiple: true + }), + basicAuth: (setting) => ({ + name: setting, + label: `Basic Authentication ${titleCase(setting)}`, + type: 'TEXT' + }), + capabilityNames: (defaultValue, placeholder) => ({ + name: 'capabilityNames', + label: 'Capability Names', + description: `Each name formatted as @assetIdOrName (${docLink('Asset Catalog')})`, + type: 'TEXT', + default: [defaultValue], + placeholder: placeholder.replace(/,/g, '\n'), + multiple: true, + required: !defaultValue + }), + channelMappings: () => ({ + name: 'channelMappings', + label: 'Channel Mappings', + description: 'Each mapping formatted as channelName=channelNumber', + type: 'TEXT', + placeholder: 'CBS=2\nNBC=4\nABC=7\nPBS=13', + multiple: true + }), + colorTemperatureBinding: () => ({ + name: 'binding', + label: 'Binding/Device Type', + description: 'Range binding presets', + type: 'TEXT', + options: getOptions({ + 'lifx:color': 'LIFX (Color)', + 'lifx:white': 'LIFX (White)', + 'milight:color': 'Milight/Easybulb/Limitless (Color)', + 'milight:white': 'Milight/Easybulb/Limitless (White)', + 'hue:color': 'Philips Hue (Color)', + 'hue:white': 'Philips Hue (White)', + 'tplinksmarthome:color': 'TP-Link Smart Home (Color)', + 'tplinksmarthome:white': 'TP-Link Smart Home (White)', + 'tradfri:color': 'TRÅDFRI (Color)', + 'tradfri:white': 'TRÅDFRI (White)', + 'yeelight:color': 'Yeelight (Color)', + 'yeelight:white': 'Yeelight (White)' + }), + limitToOptions: true, + visible: (_, config) => config.range === '1000:10000' + }), + colorTemperatureRange: () => ({ + name: 'range', + label: 'Temperature Range in Kelvin', + description: 'Formatted as minRange:maxRange', + type: 'TEXT', + default: '1000:10000', + pattern: '[0-9]+:[0-9]+', + visible: (_, config) => !config.binding + }), + comfortRange: (item) => ({ + name: 'comfortRange', + label: 'Comfort Range', + type: 'INTEGER', + min: 1, + default: (config) => { + const scale = config.scale || getGroupParameter('scale', item.groups) || getTemperatureScale(item) + if (scale === 'CELSIUS') return 1 + if (scale === 'FAHRENHEIT') return 2 + } + }), + connectedTo: (value) => ({ + name: 'connectedTo', + label: 'Connected To', + type: 'TEXT', + default: value, + readOnly: true + }), + deviceDescription: (defaultValue) => ({ + name: 'description', + label: 'Device Description', + type: 'TEXT', + default: defaultValue, + advanced: true + }), + deviceName: (defaultValue) => ({ + name: 'name', + label: 'Device Name', + type: 'TEXT', + default: defaultValue, + advanced: !!defaultValue, + required: !defaultValue + }), + equalizerDefaultLevel: (defaultValue) => ({ + name: 'defaultLevel', + label: 'Default Level', + description: 'Defaults to equalizer range midpoint', + type: 'INTEGER', + default: (config) => { + if (!config.range) return defaultValue + const range = config.range.split(':').map((n) => parseInt(n)) + if (range[0] < range[1]) return Math.round((range[0] + range[1]) / 2) + } + }), + equalizerRange: (defaultValue) => ({ + name: 'range', + label: 'Equalizer Range', + description: 'Formatted as minRange:maxRange', + type: 'TEXT', + default: defaultValue, + pattern: '[+-]?[0-9]+:[+-]?[0-9]+' + }), + exitDelay: () => ({ + name: 'exitDelay', + label: 'Exit Delay in Seconds', + type: 'INTEGER', + min: 0, + max: 255, + advanced: true + }), + hostname: () => ({ + name: 'hostname', + label: 'Hostname', + type: 'TEXT', + default: 'N/A', + advanced: true + }), + increment: (defaultValue) => ({ + name: 'increment', + label: 'Default Increment', + ...(isNaN(defaultValue) && { description: `Defaults to ${defaultValue}` }), + type: 'INTEGER', + min: 1, + ...(!isNaN(defaultValue) && { default: defaultValue }) + }), + inverted: (defaultValue = false) => ({ + name: 'inverted', + label: 'Inverted', + type: 'BOOLEAN', + default: defaultValue + }), + language: (defaultValue) => ({ + name: 'language', + label: 'Language', + description: 'Language for text-based names', + type: 'TEXT', + default: LANGUAGES[defaultValue] ? defaultValue : 'en', + options: getOptions(LANGUAGES), + limitToOptions: true, + advanced: true + }), + macAddress: () => ({ + name: 'macAddress', + label: 'MAC Address', + description: 'Formatted as EUI-48 or EUI-64 address with colon or dash separators', + type: 'TEXT', + pattern: '([0-9a-fA-F]{2}(-|:)){7}[0-9a-fA-F]{2}$|^([0-9a-fA-F]{2}(-|:)){5}[0-9a-fA-F]{2}' + }), + nonControllable: (stateDescription) => ({ + name: 'nonControllable', + label: 'Non-Controllable', + type: 'BOOLEAN', + default: (stateDescription && stateDescription.readOnly) === true, + visible: (_, config) => !!config.retrievable + }), + ordered: () => ({ + name: 'ordered', + label: 'Ordered', + description: 'If modes can be adjusted incrementally', + type: 'BOOLEAN', + default: false + }), + pinCodes: () => ({ + name: 'pinCodes', + label: 'Pin Codes', + description: 'Each code formatted as 4-digit pin', + type: 'TEXT', + placeholder: '1234\n9876', + multiple: true, + advanced: true + }), + presets: (stateDescription, placeholder, advanced = false) => ({ + name: 'presets', + label: 'Presets', + description: + 'Each preset formatted as presetValue=@assetIdOrName1:@assetIdOrName2:...' + + ` (${docLink('Asset Catalog')})`, + type: 'TEXT', + default: + stateDescription && + stateDescription.options && + stateDescription.options + .filter((option) => !isNaN(option.value)) + .map((option) => `${option.value}=${option.label}`), + placeholder: placeholder.replace(/,/g, '\n'), + multiple: true, + advanced + }), + primaryControl: () => ({ + name: 'primaryControl', + label: 'Primary Control', + description: 'Primary control for open/close/stop utterances', + type: 'TEXT', + default: 'position', + options: getOptions({ position: 'Position', tilt: 'Tilt' }), + limitToOptions: true + }), + proxyBaseUrl: () => ({ + name: 'proxyBaseUrl', + label: 'Proxy Base URL', + type: 'TEXT', + required: true, + pattern: 'https://.+' + }), + resolution: () => ({ + name: 'resolution', + label: 'Resolution', + type: 'TEXT', + default: '1080p', + options: getOptions(['480p', '720p', '1080p']), + limitToOptions: true + }), + retrievable: () => ({ + name: 'retrievable', + label: 'State Retrievable', + type: 'BOOLEAN', + default: true, + advanced: true, + visible: (_, config) => !config.nonControllable + }), + scale: (item, advanced = false) => ({ + name: 'scale', + label: 'Scale', + type: 'TEXT', + default: getGroupParameter('scale', item.groups) || getTemperatureScale(item), + options: getOptions(TEMPERATURE_SCALES), + limitToOptions: true, + advanced + }), + setpointRange: (item) => ({ + name: 'setpointRange', + label: 'Setpoint Range', + description: 'Formatted as minRange:maxRange', + type: 'TEXT', + default: (config) => { + const scale = config.scale || getGroupParameter('scale', item.groups) || getTemperatureScale(item) + if (scale === 'CELSIUS') return '4:32' + if (scale === 'FAHRENHEIT') return '40:90' + }, + pattern: '[+-]?[0-9]+:[+-]?[0-9]+' + }), + speedLevels: () => ({ + name: 'speedLevels', + label: 'Speed Levels', + type: 'INTEGER', + min: 2, + default: 3 + }), + stateMappings: (format, placeholder) => ({ + name: 'stateMappings', + label: 'State Mappings', + description: `Each mapping formatted as ${getSemanticFormat('state', format)} (${docLink('Semantic Extensions')})`, + type: 'TEXT', + placeholder: placeholder.replace(/,/g, '\n'), + multiple: true + }), + stepSpeaker: () => ({ + name: 'stepSpeaker', + label: 'Control Speaker in Discrete Steps', + type: 'BOOLEAN', + default: false, + advanced: true + }), + supportedArmStates: () => ({ + name: 'supportedArmStates', + label: 'Supported Arm States', + type: 'TEXT', + default: (config) => ARM_STATES.filter((state) => !!config[state]), + options: getOptions(ARM_STATES), + limitToOptions: true, + multiple: true, + advanced: true + }), + supportedCommands: (commands, placeholder) => ({ + name: 'supportedCommands', + label: 'Supported Commands', + description: + 'Each command formatted as command or command=@assetIdOrName1:...' + + ` (${docLink('Asset Catalog')})
Supported commands are ${commands.join(', ')}`, + type: 'TEXT', + placeholder: placeholder.replace(/,/g, '\n'), + multiple: true + }), + supportedEqualizerModes: () => ({ + name: 'supportedModes', + label: 'Supported Modes', + type: 'TEXT', + default: (config) => EQUALIZER_MODES.filter((mode) => !!config[mode]), + options: getOptions(EQUALIZER_MODES), + limitToOptions: true, + multiple: true, + advanced: true + }), + supportedInputs: (stateDescription, placeholder) => ({ + name: 'supportedInputs', + label: 'Supported Inputs', + description: 'Each input formatted as inputValue=inputName1:inputName2:...', + type: 'TEXT', + default: + stateDescription && + stateDescription.options && + stateDescription.options.map((option) => `${option.value}=${option.label}`), + placeholder: placeholder.replace(/,/g, '\n'), + multiple: true, + required: !stateDescription || !stateDescription.options || !stateDescription.options.length + }), + supportedModes: (stateDescription) => ({ + name: 'supportedModes', + label: 'Supported Modes', + description: + `Each mode formatted as mode=@assetIdOrName1:@assetIdOrName2:... (${docLink('Asset Catalog')})`, + type: 'TEXT', + default: + stateDescription && + stateDescription.options && + stateDescription.options.map((option) => `${option.value}=${option.label}`), + placeholder: 'Normal=Normal:Cottons\nWhites=Whites', + multiple: true, + required: !stateDescription || !stateDescription.options || !stateDescription.options.length + }), + supportedOperations: () => ({ + name: 'supportedOperations', + label: 'Supported Operations', + type: 'TEXT', + default: PLAYBACK_OPERATIONS, + options: getOptions(PLAYBACK_OPERATIONS, true), + limitToOptions: true, + multiple: true, + advanced: true + }), + supportedRange: (stateDescription, defaultValue) => ({ + name: 'supportedRange', + label: 'Supported Range', + description: 'Formatted as minValue:maxValue:precision', + type: 'TEXT', + default: + stateDescription && + !isNaN(stateDescription.minimum) && + !isNaN(stateDescription.maximum) && + !isNaN(stateDescription.step) + ? `${stateDescription.minimum}:${stateDescription.maximum}:${stateDescription.step}` + : defaultValue, + pattern: '[+-]?[0-9]+:[+-]?[0-9]+:[0-9]+' + }), + supportedThermostatModes: () => ({ + name: 'supportedModes', + label: 'Supported Modes', + type: 'TEXT', + default: (config) => THERMOSTAT_MODES.filter((mode) => !!config[mode]), + options: getOptions(THERMOSTAT_MODES), + limitToOptions: true, + multiple: true, + advanced: true, + visible: (_, config) => !config.binding + }), + supportsDeactivation: () => ({ + name: 'supportsDeactivation', + label: 'Supports Deactivation', + type: 'BOOLEAN', + default: true + }), + supportsSetpointMode: () => ({ + name: 'supportsSetpointMode', + label: 'Supports Setpoint Mode-aware Feature', + description: 'In most cases, this feature should remain enabled', + type: 'BOOLEAN', + default: true, + advanced: true + }), + thermostatModeBinding: () => ({ + name: 'binding', + label: 'Thermostat Binding', + type: 'TEXT', + options: getOptions({ + broadlinkthermostat: 'Broadlink Thermostat', + daikin: 'Daikin', + ecobee: 'ecobee', + insteon: 'Insteon', + max: 'MAX!', + nest: 'Nest', + radiothermostat: 'RadioThermostat', + venstarthermostat: 'Venstar Thermostat', + zwave: 'Z-Wave' + }), + limitToOptions: true, + visible: (_, config) => THERMOSTAT_MODES.every((mode) => !config[mode]) && !config.supportedModes.length + }), + thermostatModeMapping: (mode) => ({ + name: mode, + label: `${titleCase(mode)} Mapping`, + type: 'TEXT', + visible: (_, config) => !config.binding + }), + valueMapping: (value, required = false) => ({ + name: value, + label: `${titleCase(value)} Mapping`, + type: 'TEXT', + required + }), + unitOfMeasure: (item) => ({ + name: 'unitOfMeasure', + label: 'Unit of Measure', + type: 'TEXT', + default: getUnitOfMeasure(item), + options: getOptions(Object.keys(UNITS_OF_MEASURE), true), + limitToOptions: true + }) +} diff --git a/bundles/org.openhab.ui/web/src/components/config/config-sheet.vue b/bundles/org.openhab.ui/web/src/components/config/config-sheet.vue index c03fcd8e2..895563f5d 100644 --- a/bundles/org.openhab.ui/web/src/components/config/config-sheet.vue +++ b/bundles/org.openhab.ui/web/src/components/config/config-sheet.vue @@ -83,9 +83,11 @@ export default { }, computed: { configurationWithDefaults () { - let conf = Object.assign({}, this.configuration) + const conf = Object.assign({}, this.configuration) this.parameters.forEach((p) => { - if (conf[p.name] === undefined && p.default !== undefined) conf[p.name] = p.default + if (conf[p.name] === undefined && p.default !== undefined) { + conf[p.name] = typeof p.default === 'function' ? p.default(this.configuration) : p.default + } }) return conf }, diff --git a/bundles/org.openhab.ui/web/src/components/config/controls/parameter-text.vue b/bundles/org.openhab.ui/web/src/components/config/controls/parameter-text.vue index 7ad812e1c..6b4421900 100644 --- a/bundles/org.openhab.ui/web/src/components/config/controls/parameter-text.vue +++ b/bundles/org.openhab.ui/web/src/components/config/controls/parameter-text.vue @@ -7,6 +7,7 @@ :name="configDescription.name" :value="formattedValue" :autocomplete="autoCompleteOptions ? 'off' : ''" + :placeholder="configDescription.placeholder" :pattern="configDescription.pattern" :required="configDescription.required" validate :clear-button="!configDescription.required && configDescription.context !== 'password'" diff --git a/bundles/org.openhab.ui/web/src/components/item/metadata/item-metadata-alexa.vue b/bundles/org.openhab.ui/web/src/components/item/metadata/item-metadata-alexa.vue index bd265540b..e0483694d 100644 --- a/bundles/org.openhab.ui/web/src/components/item/metadata/item-metadata-alexa.vue +++ b/bundles/org.openhab.ui/web/src/components/item/metadata/item-metadata-alexa.vue @@ -1,39 +1,75 @@