Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide meaningful list of units in item form #2312

Merged
merged 24 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions bundles/org.openhab.ui/web/src/assets/units.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Units defines the possible units for UI unit selection.
// If nothing is defined for an allowed dimension, the UI will fall back on the OH core default unit in the configured measurement system.
// For dimensions defined, any of the fields can be omitted. Logical defaults will be used.
// Units from curated units lists will always be added to the full unit list constructed from baseUnits and prefixes.
// So it is not necessary to explicitly add what is already in the curated units for the full units list.
// However, no prefixes will be applied to these. If you want prefixes to be applied, you should add them in the respective baseUnits Array as well.

/**
* @typedef Unit
* @property {string} dimension unit dimension (required)
* @property {string[]} [units] units used in a curated shortlist of units, not specific to SI or Imperial measurement system
* @property {string[]} [unitsSI] units used in a curated shortlist of units, specific to the SI measurement system
* @property {string[]} [unitsUS] units used in a curated shortlist of units, specific to the Imperial measurement system
* @property {string} [default] default unit, to be set to override core default unit, not specific to SI or Imperial measurement system
* @property {string} [defaultsSI] default unit in SI measurement system, to be set to override OH core default SI unit
* @property {string} [defaultUS] default unit in Imperial measurement system, to be set to override OH core default Imperial unit
* @property {string[]} [baseUnits] all supported base units that don't allow metric or binary prefixes
* @property {string[]} [baseUnitsMetric] metric base units, the full list of units will include all of these with all metric prefixes
* @property {string[]} [baseUnitsBinary] binary base units, the full list of units will include all of these with all binary prefixes
*/

/**
* Defines the possible units for UI unit selection.
* @type {Unit[]}
*/
export const Units = [{
dimension: 'Acceleration',
baseUnits: ['gₙ'],
baseUnitsSI: ['m/s²']
}, {
dimension: 'AmountOfSubstance',
baseUnits: ['°dH'],
baseUnitsSI: ['mol']
}, {
dimension: 'Angle',
units: ['°', '\'', '"', 'rad']
}, {
dimension: 'Area',
unitsSI: ['m²', 'km²', 'ha'],
unitsUS: ['ft²', 'mi²'],
baseUnits: ['ca', 'a', 'in²', 'ac'],
baseUnitsMetric: ['m²']
}, {
dimension: 'DataAmount',
units: ['bit', 'B', 'kB', 'kiB', 'MB', 'MiB', 'GB', 'GiB', 'TB', 'TiB'],
baseUnitsMetric: ['bit', 'B', 'o'],
baseUnitsBinary: ['bit', 'B', 'o']
}, {
dimension: 'DataTransferRate',
units: ['bit/s', 'kbit/s', 'Mbit/s', 'Gbit/s'],
baseUnitsMetric: ['bit/s'],
baseUnitsBinary: ['bit/s']
}, {
dimension: 'Density',
units: ['g/l', 'g/m³', 'kg/m³'],
baseUnits: ['lb/in³'],
baseUnitsMetric: ['g/m³', 'g/mm³', 'g/cm³', 'g/dm³', 'g/ml', 'g/cl', 'g/dl', 'g/l']
}, {
dimension: 'Dimensionless',
units: ['one', '%', 'dB', 'ppm', 'ppb'],
default: '%'
}, {
dimension: 'ElectricCapcitance',
baseUnitsMetric: ['F']
}, {
dimension: 'ElectricCharge',
units: ['Ah', 'C'],
baseUnitsMetric: ['C']
}, {
dimension: 'ElectricConductance',
baseUnitsMetric: ['S']
}, {
dimension: 'ElectricConductivity',
baseUnitsMetric: ['S/m']
}, {
dimension: 'ElectricCurrent',
baseUnitsMetric: ['A']
}, {
dimension: 'ElectricInductance',
baseUnitsMetric: ['H']
}, {
dimension: 'ElectricPotential',
baseUnitsMetric: ['V']
}, {
dimension: 'ElectricResistance',
baseUnitsMetric: ['Ω']
}, {
dimension: 'Energy',
units: ['kWh', 'Wh', 'J', 'kJ', 'cal', 'kcal'],
baseUnitsMetric: ['Ws', 'Wh', 'J', 'cal']
}, {
dimension: 'Force',
units: ['N', 'kN'],
baseUnitsMetric: ['N']
}, {
dimension: 'Frequency',
units: ['Hz', 'kHz', 'MHz', 'GHz', 'rpm'],
baseUnitsMetric: ['Hz']
}, {
dimension: 'Intensity',
units: ['W/m²', 'µW/cm²'],
baseUnitsMetric: ['W/mm²', 'W/cm²', 'W/dm²', 'W/m²']
}, {
dimension: 'Length',
unitsSI: ['mm', 'cm', 'dm', 'm', 'km'],
unitsUS: ['in', 'ft', 'mi'],
baseUnits: ['yd', 'ch', 'fur', 'lea']
}, {
dimension: 'LuminousFlux',
baseUnitsMetric: ['lm']
}, {
dimension: 'LuminousIntensity',
baseUnitsMetric: ['cd']
}, {
dimension: 'MagneticFlux',
baseUnitsMetric: ['T']
}, {
dimension: 'Mass',
unitsSI: ['mg', 'g', 'kg', 't'],
baseUnits: ['lb', 'oz', 'st'],
baseUnitsMetric: ['g', 't']
}, {
dimension: 'Power',
units: ['W', 'kW', 'VA', 'kVA', 'var', 'kvar', 'dBm'],
baseUnits: ['hp', 'kgf', 'lbf'],
baseUnitsMetric: ['W', 'VA', 'var']
}, {
dimension: 'Pressure',
unitsSI: ['Pa', 'hPa', 'bar', 'mbar', 'mmHg'],
unitsUS: ['inHg', 'psi'],
baseUnits: ['atm'],
baseUnitsMetric: ['Pa', 'bar']
}, {
dimension: 'RadiationAbsorbedDose',
baseUnitsMetric: ['Gy']
}, {
dimension: 'RadiationEffectiveDose',
baseUnitsMetric: ['Sv']
}, {
dimension: 'Radioactivity',
unitsSI: ['Bq', 'Ci'],
baseUnitsMetric: ['Ci']
}, {
dimension: 'Speed',
unitsSI: ['km/h', 'm/s'],
unitsUS: ['mph', 'in/h'],
baseUnits: ['kn'],
baseUnitsMetric: ['m/s', 'm/h']
}, {
dimension: 'Temperature',
unitsSI: ['°C', 'K'],
unitsUS: ['°F', 'K'],
baseUnits: ['mired']
}, {
dimension: 'Time',
units: ['s', 'min', 'h', 'd', 'wk', 'mo', 'y']
}, {
dimension: 'Volume',
unitsSI: ['ml', 'cl', 'l', 'm³'],
unitsUS: ['gal'],
baseUnits: ['in³', 'ft³'],
baseUnitsMetric: ['l', 'm³']
}, {
dimension: 'VolumetricFlowRate',
unitsSI: ['l/s', 'l/min', 'm³/s', 'm³/min', 'm³/h', 'm³/d'],
unitsUS: ['gal/min'],
baseUnitsMetric: ['m³/s', 'm³/min', 'm³/h', 'm³/d']
}]

/**
* Metric prefixes for metric base units.
* @type {string[]}
*/
export const MetricPrefixes = ['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', 'c', 'd', 'da', 'h', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']

/**
* Binary prefixes for binary base units.
* @type {string[]}
*/
export const BinaryPrefixes = ['ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']
132 changes: 111 additions & 21 deletions bundles/org.openhab.ui/web/src/components/item/group-form.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<template>
<div class="group-form no-padding">
<!-- Type -->
<f7-list-item v-if="item.type === 'Group'" :disabled="!editable" title="Members Base Type" class="align-popup-list-item" smart-select :smart-select-params="{searchbar: true, openIn: 'popup', closeOnSelect: true}">
<f7-list-item v-if="item.type === 'Group'" :disabled="!editable" title="Members Base Type" class="aligned-smart-select" smart-select :smart-select-params="{searchbar: true, openIn: 'popup', closeOnSelect: true}">
<select name="select-basetype" @change="groupType = $event.target.value">
<option v-for="type in types.GroupTypes" :key="type" :value="type" :selected="item.groupType ? type === item.groupType.split(':')[0] : false">
<option v-for="type in types.GroupTypes" :key="type" :value="type" :selected="item.groupType ? type === item.groupType : false">
{{ type }}
</option>
</select>
</f7-list-item>
<!-- Dimension -->
<f7-list-item v-if="dimensions.length && item.groupType && item.groupType.startsWith('Number')" :disabled="!editable" title="Dimension" class="align-popup-list-item" smart-select :smart-select-params="{searchbar: true, openIn: 'popup', closeOnSelect: true}">
<f7-list-item v-if="dimensions.length && item.groupType && item.groupType.startsWith('Number')" :disabled="!editable" title="Dimension" class="aligned-smart-select" smart-select :smart-select-params="{searchbar: true, openIn: 'popup', closeOnSelect: true}">
<select name="select-dimension" @change="groupDimension = $event.target.value">
<option key="" value="Number" :selected="item.type === 'Number'" />
<option v-for="d in dimensions" :key="d.name" :value="d.name" :selected="'Number:' + d.name === item.groupType">
Expand All @@ -18,18 +18,19 @@
</select>
</f7-list-item>
<!-- (Internal) Unit & State Description -->
<template v-if="createMode && groupType && groupDimension">
<f7-list-input label="Unit"
type="text"
info="Used internally, for persistence and external systems. It is independent from the state visualization in the UI, which is defined through the state description."
:value="item.unit"
@input="item.unit = $event.target.value" clear-button />
<f7-list-input label="State Description Pattern"
type="text"
info="Pattern or transformation applied to the state for display purposes. Only saved if you change the pre-filled default value."
:value="item.stateDescriptionPattern"
@input="item.stateDescriptionPattern = $event.target.value" clear-button />
</template>
<f7-list-input v-show="groupType && groupDimension && dimensionsReady"
ref="groupUnit"
label="Unit"
type="text"
:info="(createMode) ? 'Type a valid unit for the dimension or select from the proposed units. Used internally, for persistence and external systems. Is independent from state visualization in the UI, which is defined through the state description pattern.' : ''"
:value="groupDimension ? groupUnit : ''"
@change="groupUnit = $event.target.value" :clear-button="editable" />
<f7-list-input v-show="groupType && groupDimension"
label="State Description Pattern"
type="text"
:info="(createMode) ? 'Pattern or transformation applied to the state for display purposes. Only saved if you change the pre-filled default value.' : ''"
:value="getStateDescription()"
@input="item.stateDescriptionPattern = $event.target.value" :clear-button:="editable" />
<!-- Aggregation Functions -->
<f7-list-item v-if="aggregationFunctions" :disabled="!editable" title="Aggregation Function" class="align-popup-list-item" smart-select :smart-select-params="{openIn: 'popup', closeOnSelect: true}">
<select name="select-function" @change="groupFunctionKey = $event.target.value">
Expand Down Expand Up @@ -60,7 +61,15 @@ export default {
props: ['item', 'createMode'],
data () {
return {
types
types,
groupUnitAutocomplete: null,
oldGroupDimension: '',
oldGroupUnit: ''
}
},
watch: {
dimensionsReady (newValue, oldValue) {
if (oldValue === false && newValue === true) this.initializeAutocompleteGroupUnit()
}
},
computed: {
Expand All @@ -74,6 +83,9 @@ export default {
set (newType) {
const previousAggregationFunctions = this.aggregationFunctions
this.$set(this.item, 'groupType', '')
if (!this.createMode) {
this.oldGroupDimension = ''
}
this.$nextTick(() => {
if (newType !== 'None') {
this.$set(this.item, 'groupType', newType)
Expand All @@ -86,18 +98,32 @@ export default {
},
groupDimension: {
get () {
const parts = this.item.groupType.split(':')
return parts.length > 1 ? parts[1] : ''
const parts = this.item.groupType?.split(':')
return parts && parts.length > 1 ? parts[1] : ''
},
set (newDimension) {
if (!this.createMode) {
this.oldGroupDimension = this.groupDimension
}
if (!newDimension) {
this.groupType = 'Number'
return
}
const dimension = this.dimensions.find((d) => d.name === newDimension)
this.$set(this.item, 'groupType', 'Number:' + dimension.name)
this.$set(this.item, 'unit', dimension.systemUnit)
this.$set(this.item, 'stateDescriptionPattern', `%.0f ${dimension.systemUnit}`)
this.groupUnit = this.getUnitHint(dimension.name)
this.$set(this.item, 'stateDescriptionPattern', this.getStateDescription())
}
},
groupUnit: {
get () {
return this.unit
},
set (newUnit) {
if (!this.createMode) {
this.oldGroupUnit = this.unit
}
this.$set(this.item, 'unit', newUnit)
}
},
groupFunctionKey: {
Expand Down Expand Up @@ -146,7 +172,71 @@ export default {
this.item.functionKey += '_' + this.item.function.params.join('_')
}
} else {
this.$set(this.item, 'functionKey', '')
this.$set(this.item, 'functionKey', 'None')
}
},
methods: {
dimensionChanged () {
if (!this.oldGroupDimension) {
return false
}
return this.oldGroupDimension !== this.dimension
},
unitChanged () {
return this.oldGroupUnit && this.item.unit && this.oldGroupUnit !== this.item.unit
},
revertDimensionChange () {
if (!this.oldGroupDimension) {
this.groupType = 'Number'
this.$set(this.item, 'unit', '')
} else {
this.groupType = 'Number:' + this.oldGroupDimension
this.$set(this.item, 'unit', this.oldGroupUnit)
}
},
getStateDescription () {
return this.item.stateDescriptionPattern ? this.item.stateDescriptionPattern : '%.0f %unit%'
},
initializeAutocompleteGroupUnit () {
const self = this
const unitControl = this.$refs.groupUnit
if (!unitControl || !unitControl.$el) return
const inputElement = this.$$(unitControl.$el).find('input')
this.groupUnitAutocomplete = this.$f7.autocomplete.create({
inputEl: inputElement,
openIn: 'dropdown',
dropdownPlaceholderText: self.getUnitHint(this.dimension),
source (query, render) {
let curatedUnits = self.groupDimension ? self.getUnitList(self.groupDimension) : []
let allUnits = self.groupDimension ? self.getFullUnitList(self.groupDimension) : []
if (!query || !query.length) {
// Render curated list by default
render(curatedUnits)
} else {
let units = curatedUnits.filter(u => u.indexOf(query) >= 0)
if (units.length) {
// Show full curated list if in curated list
render(curatedUnits)
} else {
// If no match filter on full list
render(allUnits.filter(u => u.indexOf(query) >= 0))
}
}
}
})
}
},
mounted () {
if (!this.createMode && this.groupDimension) {
this.oldGroupDimension = this.groupDimension
this.oldGroupUnit = this.groupUnit
if (this.dimensionsReady) this.initializeAutocompleteGroupUnit()
}
},
beforeDestroy () {
if (this.groupUnitAutocomplete) {
this.$f7.autocomplete.destroy(this.groupUnitAutocomplete)
this.groupUnitAutocomplete = null
}
}
}
Expand Down
Loading
Loading