Update alexa integration with new metadata syntax (#1145)

Signed-off-by: jsetton <jeremy.setton@gmail.com>
pull/1206/head
Jeremy 2021-11-14 11:08:26 -05:00 committed by GitHub
parent 9aeb301771
commit 1671c1aaaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1486 additions and 309 deletions

View File

@ -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: <code>minRange:maxRange</code>')
const rangeParameter = p('TEXT', 'range', 'Range', 'Format: <code>minRange:maxRange</code>')
const volumeIncrementParameter = p('INTEGER', 'increment', 'Increment')
const friendlyNamesParameter = p('TEXT', 'friendlyNames', 'Friendly Names', 'each name formatted as <code>@assetIdOrName</code>, 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

View File

@ -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'
}

View File

@ -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')
]
}
}

View File

@ -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')
}
}

View File

@ -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 `<a class="external text-color-blue" target="_blank" href="${link}">${title}</a>`
}
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 ' : ', ') : ''}` +
`<code>${Array.isArray(format) || key === 'default' ? type : camelCase(`${key}_${type}`)}=${format[key]}</code>`,
''
)
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]))
}

View File

@ -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

View File

@ -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 <code>@assetIdOrName</code> (${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 <code>channelName=channelNumber<code>',
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 <code>minRange:maxRange</code>',
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 <code>minRange:maxRange</code>',
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 <code>presetValue=@assetIdOrName1:@assetIdOrName2:...</code>' +
` (${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 <code>minRange:maxRange</code>',
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 <code>command</code> or <code>command=@assetIdOrName1:...</code>' +
` (${docLink('Asset Catalog')})<br />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 <code>inputValue=inputName1:inputName2:...</code>',
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 <code>mode=@assetIdOrName1:@assetIdOrName2:...</code> (${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 <code>minValue:maxValue:precision</code>',
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
})
}

View File

@ -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
},

View File

@ -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'"

View File

@ -1,39 +1,75 @@
<template>
<div>
<div v-if="ready">
<div style="text-align:right" class="padding-right" v-if="itemType !== 'Group'">
<label @click="toggleMultiple" style="cursor:pointer">Multiple</label> <f7-checkbox :checked="multiple" @change="toggleMultiple" />
<label @click="toggleMultiple" style="cursor:pointer">Multiple</label>
<f7-checkbox :checked="multiple" @change="toggleMultiple" />
</div>
<f7-list>
<f7-list-item :key="classSelectKey"
:title="(multiple) ? 'Alexa Classes' : 'Alexa Class'" smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, closeOnSelect: !multiple, scrollToSelectedItem: true }" ref="classes">
<select name="parameters" @change="updateClasses" :multiple="multiple">
<f7-list-item
:key="classSelectKey"
:title="'Alexa Device Type' + (itemType !== 'Group' ? (!multiple ? '/Attribute' : '/Attributes') : '')"
smart-select
:smart-select-params="{ openIn: 'popup', searchbar: true, closeOnSelect: !multiple, scrollToSelectedItem: true }"
ref="classes">
<select v-if="itemType === 'Group'" name="classes" @change="updateClasses">
<option value="" />
<option v-for="cl in orderedClasses" :value="cl" :key="cl" :selected="isSelected(cl)">
{{ cl }}
</option>
</select>
<select v-else name="classes" @change="updateClasses" :multiple="multiple">
<option v-if="!multiple" value="" />
<optgroup label="Labels" v-if="!multiple && itemType !== 'Group'">
<option v-for="cl in orderedClasses.filter((c) => c.indexOf('label:') === 0)"
:value="cl.replace('label:', '')" :key="cl"
:selected="isSelected(cl.replace('label:', ''))">
{{ cl.replace('label:', '') }}
<optgroup label="Default Attributes" v-if="!multiple">
<option v-for="cl in defaultClasses" :value="cl" :key="cl" :selected="isSelected(cl)">
{{ cl }}
</option>
</optgroup>
<optgroup label="Capabilities">
<option v-for="cl in orderedClasses.filter((c) => c.indexOf('label:') !== 0 && c.indexOf('endpoint:') === (itemType === 'Group'? 0 : -1))"
:value="cl.replace('endpoint:', '')" :key="cl"
:selected="isSelected(cl.replace('endpoint:', ''))">
{{ cl.replace('endpoint:', '') }}
<optgroup label="Specific Attributes">
<option v-for="cl in specificClasses" :value="cl" :key="cl" :selected="isSelected(cl)" :disabled="isDefined(cl)">
{{ cl }}
</option>
</optgroup>
<optgroup label="Generic Attributes" v-if="!multiple">
<option v-for="cl in genericClasses" :value="cl" :key="cl" :selected="isSelected(cl)">
{{ cl }}
</option>
</optgroup>
</select>
</f7-list-item>
<f7-block-footer class="padding-left no-padding no-margin" v-if="isPartOfGroupEndpoint">
<small v-html="`Part of group endpoint${item.groups.length > 1 ? 's' : ''}: ${groupLinks}`" />
</f7-block-footer>
</f7-list>
<div>
<config-sheet :parameterGroups="[]" :parameters="parameters" :configuration="metadata.config" />
</div>
<f7-block class="padding-top no-padding no-margin" v-if="itemType === 'Group' && classes.length">
<f7-block-title class="padding-left">
Group Endpoint Capabilities
</f7-block-title>
<f7-list>
<f7-list-item
v-for="cap in groupCapabilities"
:title="cap.name + (cap.isIgnored ? ' (Ignored)' : '')"
:after="cap.item"
:key="`${cap.name}:${cap.item}`"
:disabled="cap.isIgnored"
:link="`/settings/items/${cap.item}/metadata/alexa`" />
</f7-list>
<f7-block-footer class="padding-left" v-if="!groupCapabilities.length">
No direct group members of {{ item.name }} configured for Alexa
</f7-block-footer>
</f7-block>
<p class="padding">
<f7-link color="blue" external target="_blank" href="https://www.openhab.org/link/alexa">
<f7-link color="blue" external target="_blank" :href="docLink">
Alexa Integration Documentation
</f7-link>
</p>
</div>
<div v-else class="text-align-center">
<f7-preloader />
<div>Loading...</div>
</div>
</template>
<script>
@ -47,43 +83,130 @@ export default {
},
data () {
return {
itemType: this.item.type,
classesDefs: Object.keys(AlexaDefinitions),
multiple: this.item.type !== 'Group' && !!this.metadata.value && this.metadata.value.indexOf(',') > 0,
classSelectKey: this.$f7.utils.id()
itemType: this.item.groupType || this.item.type,
multiple: !!this.metadata.value && this.metadata.value.indexOf(',') > 0,
classSelectKey: this.$f7.utils.id(),
docUrl:
`https://${this.$store.state.runtimeInfo.buildString === 'Release Build' ? 'www' : 'next'}.openhab.org` +
'/link/alexa',
ready: false
}
},
mounted () {
Promise.all([
this.$oh.api.get('/rest/services/org.openhab.i18n/config'),
...this.item.groupNames.map((groupName) => this.$oh.api.get(`/rest/items/${groupName}?metadata=alexa`))
]).then(([regional, ...groups]) => {
this.item.groups = groups
.map((g) => ({ ...g, members: g.members.filter((mbr) => mbr.name !== this.item.name && mbr.metadata) }))
.filter((g) => g.metadata && !g.groupType)
this.item.settings = { regional }
this.ready = true
})
},
computed: {
classes () {
if (!this.multiple) return this.metadata.value
return (this.metadata.value) ? this.metadata.value.split(',') : []
return this.metadata.value ? this.metadata.value.split(',') : []
},
orderedClasses () {
return [...this.classesDefs].sort((a, b) => {
return a.localeCompare(b)
})
return [...this.classesDefs]
.filter((cl) => this.isVisible(cl) && this.supportsGroupType(cl) && !this.requiresGroupAttributes(cl))
.sort((a, b) => a.localeCompare(b))
},
defaultClasses () {
return this.orderedClasses.filter((cl) => cl.split('.').length === 1)
},
genericClasses () {
return this.orderedClasses.filter((cl) => cl.split('.').length === 2 && this.supportsMultiInstance(cl))
},
specificClasses () {
return this.orderedClasses.filter((cl) => cl.split('.').length === 2 && !this.supportsMultiInstance(cl))
},
parameters () {
if (!this.classes) return []
if (!this.multiple) {
return AlexaDefinitions['label:' + this.classes] || AlexaDefinitions['endpoint:' + this.classes] || [...AlexaDefinitions[this.classes]]
return this.classes.reduce((parameters, cl) => {
const { parameters: params = [] } = this.getDefinition(cl)
for (const p of params.map((p) => p(this.item, this.metadata.config)).flat()) {
if (p.description) p.description = p.description.replace('%DOC_URL%', this.docUrl)
if (!parameters.find((e) => e.name === p.name)) parameters.push(p)
}
const params = []
this.classes.forEach((c) => {
for (const p of AlexaDefinitions[c]) {
if (!params.find(p2 => p2.name === p.name)) params.push(p)
return parameters
}, [])
},
groupCapabilities () {
return this.item.members
.filter((mbr) => mbr.metadata && (mbr.groupType || mbr.type) !== 'Group')
.reduce((caps, mbr, idx, arr) => caps.concat(
mbr.metadata.alexa.value.split(',').map((cl) => ({
name: cl.split('.').pop().trim() || 'N/A',
item: mbr.name,
isIgnored:
!this.isSupportedGroupAttribute(cl) ||
!this.hasRequiredGroupAttributes(cl, mbr, arr) ||
(!this.supportsMultiInstance(cl) &&
arr.findIndex((mbr) => mbr.metadata.alexa.value.split(',').includes(cl)) !== idx)
}))
), [])
},
groupLinks () {
return this.item.groups
.map((g) => `<a class="text-color-blue" href="/settings/items/${g.name}/metadata/alexa">${g.label || g.name}</a>`)
.join(', ')
},
isPartOfGroupEndpoint () {
return this.itemType !== 'Group' && this.item.groups.length > 0
},
docLink () {
if (this.itemType === 'Group') {
return `${this.docUrl}#group-endpoint`
} else if (this.classes.length === 0 || !this.classesDefs.includes(this.classes[0])) {
return `${this.docUrl}#${this.isPartOfGroupEndpoint ? 'group-endpoint' : 'single-endpoint'}`
} else if (this.classes[0].indexOf('.') >= 0) {
return `${this.docUrl}#${this.classes[0].split('.')[1].toLowerCase()}`
} else if (this.classes[0] === 'Scene' || this.classes[0] === 'Activity') {
return `${this.docUrl}#${this.classes[0].toLowerCase()}`
} else {
return `${this.docUrl}#device-types`
}
})
return params
}
},
methods: {
isSelected (cl) {
return (this.multiple) ? this.classes.indexOf(cl) >= 0 : this.classes === cl
return this.classes.indexOf(cl) >= 0
},
isDefined (cl) {
return this.item.groups.some((g) => g.members.some((mbr) => mbr.metadata.alexa.value.split(',').includes(cl)))
},
isSupportedGroupAttribute (cl) {
return this.metadata.value === cl.split('.')[0] && this.classesDefs.includes(cl)
},
isVisible (cl) {
const { visible = () => true } = this.getDefinition(cl)
return !!AlexaDefinitions[cl] && !!AlexaDefinitions[cl][this.itemType] && visible(this.item)
},
getDefinition (cl, item) {
const itemType = item ? item.groupType || item.type : this.itemType
return (AlexaDefinitions[cl] && AlexaDefinitions[cl][itemType]) || {}
},
hasRequiredGroupAttributes (cl, item, items) {
const { requires = [] } = this.getDefinition(cl, item)
const type = cl.split('.')[0]
return requires.every((attr) => items.find((i) => i.metadata.alexa.value.split(',').includes(`${type}.${attr}`)))
},
requiresGroupAttributes (cl) {
const { requires = [] } = this.getDefinition(cl)
return !this.isPartOfGroupEndpoint && requires.length > 0
},
supportsGroupType (cl) {
return !this.isPartOfGroupEndpoint || this.item.groups.some((g) => cl.startsWith(`${g.metadata.alexa.value}.`))
},
supportsMultiInstance (cl) {
const { supports = [] } = this.getDefinition(cl)
return supports.includes('multiInstance')
},
toggleMultiple () {
this.multiple = !this.multiple
this.metadata.value = ''
if (this.metadata.value.indexOf(',') > 0) this.metadata.value = ''
this.classSelectKey = this.$f7.utils.id()
},
updateClasses () {

View File

@ -139,6 +139,7 @@ export default {
methods: {
onPageBeforeIn () {
this.generic = MetadataNamespaces.map((n) => n.name).indexOf(this.namespace) < 0
this.ready = false
},
onPageAfterIn () {
this.$oh.api.get(`/rest/items/${this.itemName}?metadata=${this.namespace}`).then((data) => {