Skip to content

Commit

Permalink
Discovery performance improvement + v2 backward compatibility
Browse files Browse the repository at this point in the history
* removed state field from getting pulled in discovery request
* added UoM default value logic based on dimension and regional settings
* streamlined rest request options now accepting gzip compressed data
* restructured project to clearly separate v2/v3
* fixed documentation missing quotes on metadata parameters examples

Signed-off-by: jsetton <[email protected]>
  • Loading branch information
jsetton committed Aug 26, 2019
1 parent 234a2d3 commit 73c62e7
Show file tree
Hide file tree
Showing 56 changed files with 2,120 additions and 164 deletions.
55 changes: 29 additions & 26 deletions USAGE.md

Large diffs are not rendered by default.

868 changes: 868 additions & 0 deletions lambda/smarthome/alexa/v2/ohConnector.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/**
* Amazon Smart Home Skill Capabilities for API V3
*/
const { CAPABILITIES, PROPERTY_SCHEMAS, ASSET_IDENTIFIERS, DISPLAY_CATEGORIES } = require('./config.js');
const { CAPABILITIES, PROPERTY_SCHEMAS, ASSET_IDENTIFIERS, DISPLAY_CATEGORIES, UNIT_OF_MEASUREMENT } = require('./config.js');

/**
* Returns alexa capability display category for a given interface
Expand Down Expand Up @@ -317,6 +317,33 @@ function getPropertyStateMap(property) {
return Object.keys(userMap).length > 0 ? userMap : customMap || defaultMap;
}

/**
* Returns unit of measurement based on given query
* @param {Object} query
* @return {*}
*/
function getUnitOfMeasure(query) {
let result;
// Find unit of measurement matching query
Object.keys(UNIT_OF_MEASUREMENT).some(dimension => {
if (!query.dimension || query.dimension === dimension) {
const values = UNIT_OF_MEASUREMENT[dimension].filter(measurement => query.id && measurement.id === query.id ||
query.symbol && measurement.symbol === query.symbol || query.unit && measurement.unit === query.unit);
result = values.find(measurement => measurement.system === query.system) || values.shift();
return result;
}
});
// Find unit of measurement default value if result empty and query dimension defined
if (!result && query.dimension) {
const values = UNIT_OF_MEASUREMENT[query.dimension].filter(measurement => measurement.default);
// Search based on query system fallback to international system (SI)
result = values.find(measurement => measurement.system === query.system) ||
values.find(measurement => measurement.system === 'SI');
}
// Return result property if defined, otherwise whole object
return result && query.property ? result[query.property] : result;
}

/**
* Determines if light endpoint is in color mode
* @param {Object} colorItem
Expand Down Expand Up @@ -357,6 +384,7 @@ module.exports = {
getPropertySchema: getPropertySchema,
getPropertySettings: getPropertySettings,
getPropertyStateMap: getPropertyStateMap,
getUnitOfMeasure: getUnitOfMeasure,
isInColorMode: isInColorMode,
isSupportedDisplayCategory: isSupportedDisplayCategory
};
Original file line number Diff line number Diff line change
Expand Up @@ -543,15 +543,16 @@ module.exports = Object.freeze({
* Defines alexa supported unit of measurement
* https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html#supported-values-for-unitofmeasure
* https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html (Alexa units)
* https://www.openhab.org/docs/concepts/units-of-measurement.html#list-of-units (OH symbols)
* https://www.openhab.org/docs/concepts/units-of-measurement.html#list-of-units (OH symbols + defaults)
*
* {
* '<ohItemTypeNumberDimension>': [
* {
* 'id': <alexaUnitOfMesureId>, (Alexa unitOfMeasure id used by RangeController interface)
* 'unit': <alexaUnit>, (Alexa unit properties naming convention)
* 'symbol': <ohUnitOfMeasureSymbol>, (OH unit of measurement item state symbol)
* 'system': <measurementSystem> (Measurement sytem)
* 'id': <alexaUnitOfMesureId>, (Alexa unitOfMeasure id used by RangeController interface)
* 'unit': <alexaUnit>, (Alexa unit properties naming convention)
* 'symbol': <ohUnitOfMeasureSymbol>, (OH unit of measurement item state symbol)
* 'system': <measurementSystem>, (Measurement sytem)
* 'default': <ohUnitOfMesureDefault>, (OH unit of measurement default by dimension & system)
* },
* ...
* ],
Expand All @@ -562,42 +563,42 @@ module.exports = Object.freeze({
*/
UNIT_OF_MEASUREMENT: {
'Angle': [
{'id': 'Angle.Degrees', 'unit': undefined, 'symbol': '°', 'system': 'SI'},
{'id': 'Angle.Radians', 'unit': undefined, 'symbol': 'rad', 'system': 'SI'},
{'id': 'Angle.Degrees', 'unit': undefined, 'symbol': '°', 'system': 'SI', 'default': true },
{'id': 'Angle.Radians', 'unit': undefined, 'symbol': 'rad', 'system': 'SI', 'default': false},
],
'Dimensionless': [
{'id': 'Percent', 'unit': undefined, 'symbol': '%', 'system': 'SI'},
{'id': 'Percent', 'unit': undefined, 'symbol': '%', 'system': 'SI', 'default': false},
],
'Length': [
{'id': 'Distance.Yards', 'unit': undefined, 'symbol': 'yd', 'system': 'US'},
{'id': 'Distance.Inches', 'unit': undefined, 'symbol': 'in', 'system': 'US'},
{'id': 'Distance.Meters', 'unit': undefined, 'symbol': 'm', 'system': 'SI'},
{'id': 'Distance.Feet', 'unit': undefined, 'symbol': 'ft', 'system': 'US'},
{'id': 'Distance.Miles', 'unit': undefined, 'symbol': 'mi', 'system': 'US'},
{'id': 'Distance.Kilometers', 'unit': undefined, 'symbol': 'km', 'system': 'SI'},
{'id': 'Distance.Yards', 'unit': undefined, 'symbol': 'yd', 'system': 'US', 'default': false},
{'id': 'Distance.Inches', 'unit': undefined, 'symbol': 'in', 'system': 'US', 'default': true },
{'id': 'Distance.Meters', 'unit': undefined, 'symbol': 'm', 'system': 'SI', 'default': true },
{'id': 'Distance.Feet', 'unit': undefined, 'symbol': 'ft', 'system': 'US', 'default': false},
{'id': 'Distance.Miles', 'unit': undefined, 'symbol': 'mi', 'system': 'US', 'default': false},
{'id': 'Distance.Kilometers', 'unit': undefined, 'symbol': 'km', 'system': 'SI', 'default': false},
],
'Mass': [
{'id': 'Mass.Kilograms', 'unit': 'KILOGRAM', 'symbol': 'kg', 'system': 'SI'},
{'id': 'Mass.Grams', 'unit': 'GRAM', 'symbol': 'g', 'system': 'SI'},
{'id': 'Weight.Pounds', 'unit': 'POUND', 'symbol': 'lb', 'system': 'US'},
{'id': 'Weight.Ounces', 'unit': 'OUNCE', 'symbol': 'oz', 'system': 'US'},
{'id': 'Mass.Kilograms', 'unit': 'KILOGRAM', 'symbol': 'kg', 'system': 'SI', 'default': false},
{'id': 'Mass.Grams', 'unit': 'GRAM', 'symbol': 'g', 'system': 'SI', 'default': false},
{'id': 'Weight.Pounds', 'unit': 'POUND', 'symbol': 'lb', 'system': 'US', 'default': false},
{'id': 'Weight.Ounces', 'unit': 'OUNCE', 'symbol': 'oz', 'system': 'US', 'default': false},
],
'Temperature': [
{'id': 'Temperature.Degrees', 'unit': undefined, 'symbol': '°', 'system': 'SI'},
{'id': 'Temperature.Celsius', 'unit': 'CELSIUS', 'symbol': '°C', 'system': 'SI'},
{'id': 'Temperature.Fahrenheit', 'unit': 'FAHRENHEIT', 'symbol': '°F', 'system': 'US'},
{'id': 'Temperature.Kelvin', 'unit': 'KELVIN', 'symbol': 'K', 'system': 'SI'},
{'id': 'Temperature.Degrees', 'unit': undefined, 'symbol': '°', 'system': 'SI', 'default': false},
{'id': 'Temperature.Celsius', 'unit': 'CELSIUS', 'symbol': '°C', 'system': 'SI', 'default': true },
{'id': 'Temperature.Fahrenheit', 'unit': 'FAHRENHEIT', 'symbol': '°F', 'system': 'US', 'default': true },
{'id': 'Temperature.Kelvin', 'unit': 'KELVIN', 'symbol': 'K', 'system': 'SI', 'default': false},
],
'Volume': [
{'id': 'Volume.Gallons', 'unit': 'UK_GALLON', 'symbol': 'gal', 'system': 'UK'},
{'id': 'Volume.Gallons', 'unit': 'US_FLUID_GALLON', 'symbol': 'gal', 'system': 'US'},
{'id': 'Volume.Pints', 'unit': 'UK_PINT', 'symbol': 'pt', 'system': 'UK'},
{'id': 'Volume.Pints', 'unit': 'US_FLUID_PINT', 'symbol': 'pt', 'system': 'US'},
{'id': 'Volume.Quarts', 'unit': 'UK_QUART', 'symbol': 'qt', 'system': 'UK'},
{'id': 'Volume.Quarts', 'unit': 'US_FLUID_QUART', 'symbol': 'qt', 'system': 'US'},
{'id': 'Volume.Liters', 'unit': 'LITER', 'symbol': 'l', 'system': 'SI'},
{'id': 'Volume.CubicMeters', 'unit': 'CUBIC_METER', 'symbol': 'm3', 'system': 'SI'},
{'id': 'Volume.CubicFeet', 'unit': 'CUBIC_FOOT', 'symbol': 'ft3', 'system': 'US'},
{'id': 'Volume.Gallons', 'unit': 'UK_GALLON', 'symbol': 'gal', 'system': 'UK', 'default': false},
{'id': 'Volume.Gallons', 'unit': 'US_FLUID_GALLON', 'symbol': 'gal', 'system': 'US', 'default': false},
{'id': 'Volume.Pints', 'unit': 'UK_PINT', 'symbol': 'pt', 'system': 'UK', 'default': false},
{'id': 'Volume.Pints', 'unit': 'US_FLUID_PINT', 'symbol': 'pt', 'system': 'US', 'default': false},
{'id': 'Volume.Quarts', 'unit': 'UK_QUART', 'symbol': 'qt', 'system': 'UK', 'default': false},
{'id': 'Volume.Quarts', 'unit': 'US_FLUID_QUART', 'symbol': 'qt', 'system': 'US', 'default': false},
{'id': 'Volume.Liters', 'unit': 'LITER', 'symbol': 'l', 'system': 'SI', 'default': false},
{'id': 'Volume.CubicMeters', 'unit': 'CUBIC_METER', 'symbol': 'm3', 'system': 'SI', 'default': false},
{'id': 'Volume.CubicFeet', 'unit': 'CUBIC_FOOT', 'symbol': 'ft3', 'system': 'US', 'default': false},
]
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class AlexaDirective extends AlexaResponse {
*/
postItemsAndReturn(items, parameters = {}) {
const promises = items.map(item =>
rest.postItemCommand(this.directive.endpoint.scope.token, this.timeout, item.name, item.state));
rest.postItemCommand(this.directive.endpoint.scope.token, item.name, item.state, this.timeout));
Promise.all(promises).then(() => {
this.getPropertiesResponseAndReturn(parameters);
}).catch((error) => {
Expand Down Expand Up @@ -149,7 +149,7 @@ class AlexaDirective extends AlexaResponse {
*/
getItemState(item) {
const itemName = item.sensor || item.name;
return rest.getItem(this.directive.endpoint.scope.token, this.timeout, itemName).then((result) =>
return rest.getItem(this.directive.endpoint.scope.token, itemName, this.timeout).then((result) =>
// Set state to undefined if uninitialized or undefined in oh, otherwise get formatted item state
Object.assign(result, {state: ['NULL', 'UNDEF'].includes(result.state) ? undefined : formatItemState(result)}));
}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class AlexaModeController extends AlexaDirective {
* Set mode
*/
setMode() {
const postItem = Object.assign(this.propertyMap[this.interface].mode.item, {
const postItem = Object.assign({}, this.propertyMap[this.interface].mode.item, {
state: this.directive.payload.mode
});
this.postItemsAndReturn([postItem]);
Expand All @@ -58,7 +58,7 @@ class AlexaModeController extends AlexaDirective {
const index = supportedModes.findIndex(mode => mode === item.state);

// Throw error if current mode not found
if (index === -1 ) {
if (index === -1) {
throw {cause: 'Current mode not found in supported list', item: item, supported: supportedModes};
}

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* Amazon Echo Smart Home Skill API implementation for openHAB (v3)
*/
const camelcase = require('camelcase');
const Directives = require('./alexa/v3');
const Directives = require('./directives');

/**
* Main entry point for all requests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
* Amazon Smart Home Skill Property Map for API V3
*/
const camelcase = require('camelcase');
const { sprintf } = require('sprintf-js');
const utils = require('@lib/utils.js');
const { getPropertySettings, getPropertyStateMap, isInColorMode, isSupportedDisplayCategory } = require('./capabilities.js');
const { CAPABILITY_PATTERN, UNIT_OF_MEASUREMENT } = require('./config.js');
const { getPropertySettings, getPropertyStateMap, getUnitOfMeasure, isInColorMode, isSupportedDisplayCategory } = require('./capabilities.js');
const { CAPABILITY_PATTERN } = require('./config.js');
const { normalize } = require('./propertyState.js');

/**
Expand Down Expand Up @@ -184,17 +185,16 @@ const normalizeParameters = {
return rangeValues[0] <= preset && preset <= rangeValues[1] && labels ?
[].concat(presets || [], match) : presets;
}, undefined);
// Use unit of measurement item state symbol and type dimension to determine unitOfMeasure if not defined
if (item.type.startsWith('Number:') && !property.parameters.unitOfMeasure) {
const symbol = item.state.split(' ').pop();
const dimension = item.type.split(':').pop();
const measurement = UNIT_OF_MEASUREMENT[dimension].find(meas => meas.symbol === symbol) || {};
property.parameters.unitOfMeasure = measurement.id;
}
// Remove unitOfMeasure parameter if not found in supported unit of measurement
if (property.parameters.unitOfMeasure && !Object.keys(UNIT_OF_MEASUREMENT).some(dimension =>
UNIT_OF_MEASUREMENT[dimension].find(meas => meas.id === property.parameters.unitOfMeasure))) {
delete property.parameters.unitOfMeasure;
// Use item state presentation symbol and type dimension to determine unitOfMeasure if not defined or valid
if (!getUnitOfMeasure({id: property.parameters.unitOfMeasure})) {
property.parameters.unitOfMeasure = getUnitOfMeasure({
dimension: item.type.split(':')[1],
symbol: sprintf(item.stateDescription && item.stateDescription.pattern, '42')
.split(/\d+\s*(?=\S)/).pop().trim(),
system: settings.regional &&
(settings.regional.measurementSystem || settings.regional.region),
property: 'id'
});
}
},

Expand All @@ -207,23 +207,19 @@ const normalizeParameters = {
temperature: function (property, item, settings) {
// Use scale parameter uppercased to determine temperature scale
let temperatureScale = (property.parameters.scale || '').toUpperCase();
// Use item state description pattern or unit of measurement item-typed state to determine state presentation
const statePresentation = item.stateDescription && item.stateDescription.pattern ||
item.type === 'Number:Temperature' && item.state;
// Use item state presentation symbol to determine temperature scale if not already defined
if (statePresentation && !temperatureScale) {
const symbol = statePresentation.split(' ').pop();
const measurement = UNIT_OF_MEASUREMENT['Temperature'].find(meas => meas.symbol === symbol) || {};
temperatureScale = measurement.unit;
}
// Use regional settings measurementSystem or region to determine temperature scale if not already defined
if (settings.regional && !temperatureScale) {
const setting = settings.regional.measurementSystem || settings.regional.region;
temperatureScale = setting === 'US' ? 'FAHRENHEIT' : setting === 'SI' ? 'CELSIUS' : undefined;
// Use item state presentation symbol and regional settings to determine temperature scale if not already defined
if (!temperatureScale) {
temperatureScale = getUnitOfMeasure({
dimension: 'Temperature',
symbol: sprintf(item.stateDescription && item.stateDescription.pattern, '42')
.split(/\d+\s*(?=\S)/).pop().trim(),
system: settings.regional &&
(settings.regional.measurementSystem || settings.regional.region),
property: 'unit'
});
}
// Set scale parameter if valid, otherwise default to Celsius
property.parameters.scale =
['CELSIUS', 'FAHRENHEIT'].includes(temperatureScale) ? temperatureScale : 'CELSIUS';
// Set scale parameter
property.parameters.scale = temperatureScale === 'FAHRENHEIT' ? 'FAHRENHEIT' : 'CELSIUS';
// Use setpoint range parameter to determine thermostat temperature range ([0] => minimum; [1] => maximum)
const setpointRange = (property.parameters.setpointRange || '').split(':').map(value => parseInt(value));
// Set setpoint range parameter if valid (min < max)
Expand Down Expand Up @@ -395,9 +391,11 @@ class AlexaPropertyMap {
}

// Set friendly names parameter on multi-instance property to use item label & synonyms, if not already defined
if (settings.property.multiInstance && !property.parameters.friendlyNames) {
property.parameters.friendlyNames = [
item.label, item.metadata.synonyms && item.metadata.synonyms.value].filter(Boolean).join(',');
if (settings.property.multiInstance) {
property.parameters.friendlyNames = property.parameters.friendlyNames ||
[item.label, item.metadata.synonyms && item.metadata.synonyms.value].filter(Boolean).join(',');
} else {
delete property.parameters.friendlyNames;
}

// Iterate over parameters
Expand Down
File renamed without changes.
File renamed without changes.
13 changes: 9 additions & 4 deletions lambda/smarthome/config_sample.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
* Default options, copy to config.js for deployment
* baseURL [https://myopenhab.org/rest]
* REST base URL, uncomment this to connect directly to a openHAB server.
* userpass
* Optional username:password for the REST server
* user
* Optional username for the REST server
* by default oauth2 tokens will be used for authentication, uncomment this
* to use standard BASIC auth when talking directly to a openHAB server.
* to use standard basic auth when talking directly to a openHAB server.
* pass
* Optional password for the REST server
* by default oauth2 tokens will be used for authentication, uncomment this
* to use standard basic auth when talking directly to a openHAB server.
* certFile [ssl/client.pfx]
* Optional SSL client certificate file path for the REST server
* use this for certificate auth when talking directly to a openHAB server.
Expand All @@ -30,6 +34,7 @@
module.exports = {
openhab: {
//baseURL: 'https://openhab.example.com/rest',
//userpass: '[email protected]:Password1'
//user: '[email protected]',
//pass: 'Password1'
}
};
6 changes: 5 additions & 1 deletion lambda/smarthome/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

require('module-alias/register');
const log = require('@lib/log.js');
const ohv3 = require('./ohConnectorV3.js');
const ohv2 = require('./alexa/v2/ohConnector.js');
const ohv3 = require('./alexa/v3/ohConnector.js');

/**
* Main entry point.
Expand All @@ -29,6 +30,9 @@ exports.handler = function (event, context, callback) {
case 3:
ohv3.handleRequest(event.directive, callback);
break;
case 2:
ohv2.handleRequest(event, context);
break;
default:
log.error(`No supported payloadVersion: ${version}`);
callback('No supported payloadVersion.');
Expand Down
Loading

0 comments on commit 73c62e7

Please sign in to comment.