Skip to content

Commit

Permalink
Improve range value read only attribute default precision support (#650)
Browse files Browse the repository at this point in the history
Signed-off-by: jsetton <[email protected]>
  • Loading branch information
jsetton authored Jan 29, 2024
1 parent c2c54a2 commit 14ac403
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 25 deletions.
9 changes: 7 additions & 2 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1794,8 +1794,13 @@ Items that represent components of a device that are characterized by numbers wi
* defaults to no support
* supportedRange=`<range>`
* range formatted as `<minValue>:<maxValue>:<precision>` (e.g. `supportedRange="0:100:1"`)
* precision value used as default increment for adjusted range value request.
* defaults to item state description min, max & step values, if defined, otherwise `"0:100:1"` (Dimmer/Rollershutter); `"0:10:1"` (Number)
* precision value used as:
* default increment for adjusted range value requests
* item state rounding for range value state requests
* defaults to, in order of precedence:
* item state description min, max & step properties
* item state presentation precision for non-controllable Number
* `"0:100:1"` (Dimmer/Rollershutter); `"0:10:1"` (Number); `"0:10:0.01"` (Number Read-Only)
* presets=`<presets>`
* each preset formatted as `<presetValue>=<@assetIdOrName1>:...` (e.g. `presets="[email protected]:Lowest,[email protected]:Highest"`)
* limited to a maximum of 150 presets
Expand Down
18 changes: 13 additions & 5 deletions lambda/alexa/smarthome/properties/rangeValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
*/

import OpenHAB from '#openhab/index.js';
import { ItemType } from '#openhab/constants.js';
import { Parameter, ParameterType } from '../constants.js';
import { AlexaPresetResources } from '../resources.js';
Expand Down Expand Up @@ -63,7 +64,9 @@ export default class RangeValue extends Generic {
* @return {Array}
*/
get defaultRange() {
return this.item.type === ItemType.DIMMER || this.item.type === ItemType.ROLLERSHUTTER ? [0, 100, 1] : [0, 10, 1];
return this.item.type === ItemType.DIMMER || this.item.type === ItemType.ROLLERSHUTTER
? [0, 100, 1]
: [0, 10, this.isNonControllable ? 0.01 : 1];
}

/**
Expand Down Expand Up @@ -171,12 +174,17 @@ export default class RangeValue extends Generic {
// Define supported range as follow:
// 1) using parameter values if defined
// 2) using item state description minimum, maximum & step values if available
// 3) empty array
// 3) using item state presentation precision for number item type non-controllable property if available
const range = parameters[Parameter.SUPPORTED_RANGE]
? parameters[Parameter.SUPPORTED_RANGE]
: item.stateDescription
? [item.stateDescription.minimum, item.stateDescription.maximum, item.stateDescription.step]
: [];
: [
item.stateDescription?.minimum ?? this.defaultRange[0],
item.stateDescription?.maximum ?? this.defaultRange[1],
item.stateDescription?.step ??
(item.type.split(':')[0] === ItemType.NUMBER && this.isNonControllable
? 1 / 10 ** OpenHAB.getStatePresentationPrecision(item.stateDescription?.pattern)
: undefined)
];
// Update supported range values if valid (min < max; max - min > prec), otherwise set to undefined
parameters[Parameter.SUPPORTED_RANGE] =
range[0] < range[1] && range[1] - range[0] > Math.abs(range[2]) ? range : undefined;
Expand Down
9 changes: 3 additions & 6 deletions lambda/alexa/smarthome/unitOfMeasure.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* SPDX-License-Identifier: EPL-2.0
*/

import OpenHAB from '#openhab/index.js';
import { Dimension, UnitSymbol, UnitSystem } from '#openhab/constants.js';

/**
Expand Down Expand Up @@ -491,12 +492,8 @@ class UnitsOfMeasure {
* @return {Object}
*/
static getUnitOfMeasure({ dimension, unitSymbol, statePresentation, system = UnitSystem.METRIC }) {
// Determine symbol using item unit symbol or matching item state presentation with supported list
const symbol =
unitSymbol ??
Object.values(UnitSymbol).find((symbol) =>
new RegExp(`%\\d*(?:\\.\\d+)?[df]\\s*[%]?${symbol}$`).test(statePresentation)
);
// Determine symbol using item unit symbol or state presentation
const symbol = unitSymbol || OpenHAB.getStatePresentationUnitSymbol(statePresentation);
// Return unit of measure using symbol/dimension or fallback to default value using dimension/system
return (
this.#UOMS.find((uom) => uom.symbol === symbol && (!dimension || uom.dimension === dimension)) ||
Expand Down
30 changes: 26 additions & 4 deletions lambda/openhab/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import fs from 'node:fs';
import axios from 'axios';
import { HttpsAgent } from 'agentkeepalive';
import { validate as uuidValidate } from 'uuid';
import { ItemType, ItemValue } from './constants.js';
import { ItemType, ItemValue, UnitSymbol } from './constants.js';

/**
* Defines openHAB class
Expand Down Expand Up @@ -246,13 +246,35 @@ export default class OpenHAB {
const type = (item.groupType || item.type).split(':')[0];

if (type === ItemType.DIMMER || type === ItemType.NUMBER || type === ItemType.ROLLERSHUTTER) {
const { precision, specifier } =
item.stateDescription?.pattern?.match(/%\d*(?:\.(?<precision>\d+))?(?<specifier>[df])/)?.groups || {};
const precision = OpenHAB.getStatePresentationPrecision(item.stateDescription?.pattern);
const value = parseFloat(state);

return specifier === 'd' ? value.toFixed() : precision <= 16 ? value.toFixed(precision) : value.toString();
return isNaN(precision) ? value.toString() : value.toFixed(precision);
}

return state;
}

/**
* Returns state presentation precision for a given item state description pattern
*
* @param {String} pattern
* @return {Number}
*/
static getStatePresentationPrecision(pattern) {
const { precision, specifier } = pattern?.match(/%\d*(?:\.(?<precision>\d+))?(?<specifier>[df])/)?.groups || {};
return specifier === 'd' ? 0 : precision <= 16 ? parseInt(precision) : NaN;
}

/**
* Returns state presentation unit system for a given item state description pattern
*
* @param {String} pattern
* @return {String}
*/
static getStatePresentationUnitSymbol(pattern) {
return Object.values(UnitSymbol).find((symbol) =>
new RegExp(`%\\d*(?:\\.\\d+)?[df]\\s*[%]?${symbol}$`).test(pattern)
);
}
}
14 changes: 8 additions & 6 deletions lambda/test/alexa/cases/discovery/other.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,13 @@ export default {
type: 'Number:Mass',
name: 'range7',
label: 'Range Value 7',
stateDescription: {
pattern: '%.1f %unit%',
readOnly: true
},
metadata: {
alexa: {
value: 'RangeValue',
config: {
nonControllable: true
}
value: 'RangeValue'
}
}
},
Expand Down Expand Up @@ -852,7 +853,7 @@ export default {
},
configuration: {
'Alexa.RangeController:range6': {
supportedRange: { minimumValue: 0, maximumValue: 10, precision: 1 },
supportedRange: { minimumValue: 0, maximumValue: 10, precision: 0.01 },
unitOfMeasure: 'Alexa.Unit.Angle.Degrees'
}
},
Expand Down Expand Up @@ -889,7 +890,7 @@ export default {
},
configuration: {
'Alexa.RangeController:range7': {
supportedRange: { minimumValue: 0, maximumValue: 10, precision: 1 },
supportedRange: { minimumValue: 0, maximumValue: 10, precision: 0.1 },
unitOfMeasure: 'Alexa.Unit.Mass.Kilograms'
}
},
Expand All @@ -901,6 +902,7 @@ export default {
parameters: {
capabilityNames: ['@Setting.RangeValue'],
nonControllable: true,
supportedRange: [0, 10, 0.1],
unitOfMeasure: 'Mass.Kilograms'
},
item: { name: 'range7', type: 'Number:Mass' }
Expand Down
41 changes: 39 additions & 2 deletions lambda/test/openhab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,16 @@ describe('OpenHAB Tests', () => {
it('https client cert', async () => {
// set environment
const certFile = 'cert.pfx';
const certData = 'data';
const certPass = 'passphrase';
sinon.stub(fs, 'existsSync').withArgs(certFile).returns(true);
sinon.stub(fs, 'readFileSync').withArgs(certFile).returns('pfx');
sinon.stub(fs, 'readFileSync').withArgs(certFile).returns(certData);
nock(baseURL)
.get('/')
.reply(200)
.on('request', ({ headers, options, socket }) => {
expect(headers).to.not.have.property('authorization');
expect(options).to.nested.include({ 'agent.options.pfx': 'pfx', 'agent.options.passphrase': 'passphrase' });
expect(options).to.nested.include({ 'agent.options.pfx': certData, 'agent.options.passphrase': certPass });
expect(socket).to.include({ timeout });
});
// run test
Expand Down Expand Up @@ -415,4 +416,40 @@ describe('OpenHAB Tests', () => {
expect(nock.isDone()).to.be.true;
});
});

describe('get state presentation precision', () => {
it('integer', async () => {
expect(OpenHAB.getStatePresentationPrecision('%d %%')).to.equal(0);
});

it('float', async () => {
expect(OpenHAB.getStatePresentationPrecision('%.1f °F')).to.equal(1);
});

it('no precision', async () => {
expect(OpenHAB.getStatePresentationPrecision('foo')).to.be.NaN;
});

it('undefined', async () => {
expect(OpenHAB.getStatePresentationPrecision(undefined)).to.be.NaN;
});
});

describe('get state presentation unit symbol', () => {
it('percent', async () => {
expect(OpenHAB.getStatePresentationUnitSymbol('%d %%')).to.equal('%');
});

it('temperature', async () => {
expect(OpenHAB.getStatePresentationUnitSymbol('%.1f °F')).to.equal('°F');
});

it('no symbol', async () => {
expect(OpenHAB.getStatePresentationUnitSymbol('%.1f')).to.be.undefined;
});

it('undefined', async () => {
expect(OpenHAB.getStatePresentationUnitSymbol(undefined)).to.be.undefined;
});
});
});

0 comments on commit 14ac403

Please sign in to comment.