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: <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
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 `<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]))
+}
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 <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
+  })
+}
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 @@
 <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]]
-      }
-      const params = []
-      this.classes.forEach((c) => {
-        for (const p of AlexaDefinitions[c]) {
-          if (!params.find(p2 => p2.name === p.name)) params.push(p)
+      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)
         }
-      })
-      return params
+        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`
+      }
     }
   },
   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 () {
diff --git a/bundles/org.openhab.ui/web/src/pages/settings/items/metadata/item-metadata-edit.vue b/bundles/org.openhab.ui/web/src/pages/settings/items/metadata/item-metadata-edit.vue
index 982dd126e..f4f1855e3 100644
--- a/bundles/org.openhab.ui/web/src/pages/settings/items/metadata/item-metadata-edit.vue
+++ b/bundles/org.openhab.ui/web/src/pages/settings/items/metadata/item-metadata-edit.vue
@@ -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) => {