diff --git a/USAGE.md b/USAGE.md index bfac9656..0577b033 100644 --- a/USAGE.md +++ b/USAGE.md @@ -241,7 +241,7 @@ Number:Temperature Temperature2 "Temperature" {alexa="TemperatureSenso * Rollershutter * Default category: OTHER * `ThermostatController.targetSetpoint` - * Items that represent a target setpoint for a thermostat. The scale is determined based on: (1) value set in parameter `scale="Fahrenheit"`; (2) unit of item state presentation (`°F`=Fahrenheit; `°C`=Celsius); (3) unit of measurement unit if Number:Temperature item type; (4) your openHAB server regional measurement system or region settings (US=Fahrenheit; SI=Celsius); (5) defaults to Celsius. By default, the temperature range is limited to predefined setpoint values based on the scale parameter. If necessary, the temperature range can be customized using parameter `setpointRange="60:90"`. + * Items that represent a target setpoint for a thermostat. The scale is determined based on: (1) value set in parameter `scale="Fahrenheit"`; (2) unit of item state presentation (`°F`=Fahrenheit; `°C`=Celsius); (3) your openHAB server regional measurement system or region settings (US=Fahrenheit; SI=Celsius); (4) defaults to Celsius. By default, the temperature range is limited to predefined setpoint values based on the scale parameter. If necessary, the temperature range can be customized using parameter `setpointRange="60:90"`. * Supported item type: * Number(:Temperature) * Supported metadata parameters: @@ -252,7 +252,7 @@ Number:Temperature Temperature2 "Temperature" {alexa="TemperatureSenso * defaults to defined scale range listed above if omitted * Default category: THERMOSTAT * `ThermostatController.upperSetpoint` - * Items that represent a upper or HEAT setpoint for a thermostat. The scale is determined based on: (1) value set in parameter `scale="Fahrenheit"`; (2) unit of item state presentation (`°F`=Fahrenheit; `°C`=Celsius); (3) unit of measurement unit if Number:Temperature item type; (4) your openHAB server regional measurement system or region settings (US=Fahrenheit; SI=Celsius); (5) defaults to Celsius. By default, the temperature range is limited to predefined setpoint values based on the scale parameter. If necessary, the temperature range can be customized using parameter `setpointRange="60:90"`. + * Items that represent a upper or HEAT setpoint for a thermostat. The scale is determined based on: (1) value set in parameter `scale="Fahrenheit"`; (2) unit of item state presentation (`°F`=Fahrenheit; `°C`=Celsius); (3) your openHAB server regional measurement system or region settings (US=Fahrenheit; SI=Celsius); (4) defaults to Celsius. By default, the temperature range is limited to predefined setpoint values based on the scale parameter. If necessary, the temperature range can be customized using parameter `setpointRange="60:90"`. * Supported item type: * Number(:Temperature) * Supported metadata parameters: @@ -265,7 +265,7 @@ Number:Temperature Temperature2 "Temperature" {alexa="TemperatureSenso * defaults to defined scale range listed above if omitted * Default category: THERMOSTAT * `ThermostatController.lowerSetpoint` - * Items that represent a lower or COOL setpoint for a thermostat. The scale is determined based on: (1) value set in parameter `scale="Fahrenheit"`; (2) unit of item state presentation (`°F`=Fahrenheit; `°C`=Celsius); (3) unit of measurement unit if Number:Temperature item type; (4) your openHAB server regional measurement system or region settings (US=Fahrenheit; SI=Celsius); (5) defaults to Celsius. By default, the temperature range is limited to predefined setpoint values based on the scale parameter. If necessary, the temperature range can be customized using parameter `setpointRange="60:90"`. + * Items that represent a lower or COOL setpoint for a thermostat. The scale is determined based on: (1) value set in parameter `scale="Fahrenheit"`; (2) unit of item state presentation (`°F`=Fahrenheit; `°C`=Celsius); (3) your openHAB server regional measurement system or region settings (US=Fahrenheit; SI=Celsius); (4) defaults to Celsius. By default, the temperature range is limited to predefined setpoint values based on the scale parameter. If necessary, the temperature range can be customized using parameter `setpointRange="60:90"`. * Supported item type: * Number(:Temperature) * Supported metadata parameters: @@ -302,7 +302,7 @@ Number:Temperature Temperature2 "Temperature" {alexa="TemperatureSenso * defaults to, depending on the parameters provided, either user-based, preset-based or default item type-based mapping. * Default category: THERMOSTAT * `TemperatureSensor.temperature` - * Items that represent the current temperature. The scale is determined based on: (1) value set in parameter `scale="Fahrenheit"`; (2) unit of item state presentation (`°F`=Fahrenheit; `°C`=Celsius); (3) unit of measurement unit if Number:Temperature item type; (4) your openHAB server regional measurement system or region settings (US=Fahrenheit; SI=Celsius); (5) defaults to Celsius. + * Items that represent the current temperature. The scale is determined based on: (1) value set in parameter `scale="Fahrenheit"`; (2) unit of item state presentation (`°F`=Fahrenheit; `°C`=Celsius); (3) your openHAB server regional measurement system or region settings (US=Fahrenheit; SI=Celsius); (4) defaults to Celsius. * Supported item type: * Number(:Temperature) * Supported metadata parameters: @@ -519,7 +519,7 @@ Number:Temperature Temperature2 "Temperature" {alexa="TemperatureSenso * defaults to false * Default category: OTHER * `RangeController.rangeValue` - * Items that represent components of a device that are characterized by numbers within a minimum and maximum range. Multiple instances can be configured in a group endpoint. By default, to ask for a specific range, the item label will be used as the friendly name. To configure it, use `friendlyNames` parameter and provide a comma delimited list of different labels (Keep in mind that some names are [not allowed](#friendly-names-not-allowed)). Additionally, pre-defined [asset ids](#asset-catalog) can be used to label a mode as well prefixing with an @ sign (e.g. `friendlyNames="@Setting.FanSpeed,Speed"`). To set the supported range, provide a column delimited list including minimum, maximum and precision values. The latter value will be use as default increment when requesting adjusted range values. Optionally, to name specific presets, like fan speeds low [1] & high value [10], can be added in `presets` parameter and provide a comma delimited list of preset mappings composed of range value and the associated names/asset ids they should be called, delimited by equal and column signs (e.g. `presets="1=@Value.Minimum:@Value.Low:Lowest,10=@Value.Maximum:@Value.High:Highest"`). Another optional settings is `unitOfMeasure` parameter which gives a unit of measure to the range values. By default if omitted, it is based on the unit of measurement number item type that have a supported unit, otherwise, a [unit id](#unit-of-measurement-catalog) can be used. (e.g. `unitOfMeasure=Angle.Degrees`) + * Items that represent components of a device that are characterized by numbers within a minimum and maximum range. Multiple instances can be configured in a group endpoint. By default, to ask for a specific range, the item label will be used as the friendly name. To configure it, use `friendlyNames` parameter and provide a comma delimited list of different labels (Keep in mind that some names are [not allowed](#friendly-names-not-allowed)). Additionally, pre-defined [asset ids](#asset-catalog) can be used to label a mode as well prefixing with an @ sign (e.g. `friendlyNames="@Setting.FanSpeed,Speed"`). To set the supported range, provide a column delimited list including minimum, maximum and precision values. The latter value will be use as default increment when requesting adjusted range values. Optionally, to name specific presets, like fan speeds low [1] & high value [10], can be added in `presets` parameter and provide a comma delimited list of preset mappings composed of range value and the associated names/asset ids they should be called, delimited by equal and column signs (e.g. `presets="1=@Value.Minimum:@Value.Low:Lowest,10=@Value.Maximum:@Value.High:Highest"`). Another optional settings is `unitOfMeasure` parameter which gives a unit of measure to the range values. It is determined based on: (1) [unit id](#unit-of-measurement-catalog) set in parameter `unitOfMeasure=Angle.Degrees`; (2) supported unit of item state presentation; (3) default unit of measurement for item type with dimension based on your openHAB server regional settings; (4) defaults to empty. * Supported item type: * Dimmer * Number @@ -539,7 +539,10 @@ Number:Temperature Temperature2 "Temperature" {alexa="TemperatureSenso * presets=`` (optional) * each preset formatted as `=<@assetIdOrName1>:<@assetIdOrName2>:...` * unitOfMeasure=`` (optional) - * defaults to item state unit of measurement symbol for Number:* item types + * defaults to unit of item state presentation or default unit of measurement for the Number:* item types listed below: + * Number:Angle [`Angle.Degrees`] + * Number:Length [`Distance.Meters` (SI); `Distance.Inches` (US)] + * Number:Temperature [`Temperature.Celsius` (SI); `Temperature.Fahrenheit` (US)] * Default category: OTHER * `ToggleController.toggleState` * Items that represent components of a device that can be turned on or off. Multiple instances can be configured in a group endpoint. By default, to ask for a specific range, the item label will be used as the friendly name. To configure it, use `friendlyNames` parameter and provide a comma delimited list of different labels (Keep in mind that some names are [not allowed](#friendly-names-not-allowed)). Additionally, pre-defined [asset ids](#asset-catalog) can be used to label a mode as well with an @ sign prefix (e.g. `friendlyNames="@Setting.Oscillate,Rotate"`). diff --git a/lambda/smarthome/alexa/capabilities.js b/lambda/smarthome/alexa/capabilities.js index ce98c1d0..2594571a 100644 --- a/lambda/smarthome/alexa/capabilities.js +++ b/lambda/smarthome/alexa/capabilities.js @@ -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 @@ -317,6 +317,37 @@ 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 + ); + // Search based on query system fallback to first value + 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 && UNIT_OF_MEASUREMENT[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 @@ -357,6 +388,7 @@ module.exports = { getPropertySchema: getPropertySchema, getPropertySettings: getPropertySettings, getPropertyStateMap: getPropertyStateMap, + getUnitOfMeasure: getUnitOfMeasure, isInColorMode: isInColorMode, isSupportedDisplayCategory: isSupportedDisplayCategory }; diff --git a/lambda/smarthome/alexa/config.js b/lambda/smarthome/alexa/config.js index cf596db1..df89bcab 100644 --- a/lambda/smarthome/alexa/config.js +++ b/lambda/smarthome/alexa/config.js @@ -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) * * { * '': [ * { - * 'id': , (Alexa unitOfMeasure id used by RangeController interface) - * 'unit': , (Alexa unit properties naming convention) - * 'symbol': , (OH unit of measurement item state symbol) - * 'system': (Measurement sytem) + * 'id': , (Alexa unitOfMeasure id used by RangeController interface) + * 'unit': , (Alexa unit properties naming convention) + * 'symbol': , (OH unit of measurement item state symbol) + * 'system': , (Measurement sytem) + * 'default': , (OH unit of measurement default boolean) * }, * ... * ], @@ -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}, ] }, diff --git a/lambda/smarthome/alexa/propertyMap.js b/lambda/smarthome/alexa/propertyMap.js index 56236509..6137cbc3 100644 --- a/lambda/smarthome/alexa/propertyMap.js +++ b/lambda/smarthome/alexa/propertyMap.js @@ -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'); /** @@ -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 (!property.parameters.unitOfMeasure || !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' + }); } }, @@ -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) @@ -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 diff --git a/lambda/smarthome/lib/rest.js b/lambda/smarthome/lib/rest.js index c7229d1a..7785a2f3 100644 --- a/lambda/smarthome/lib/rest.js +++ b/lambda/smarthome/lib/rest.js @@ -12,8 +12,11 @@ */ const fs = require('fs'); -const request = require('request-promise-native'); -const qs = require('querystring'); +// Import request module and set default options +const request = require('request-promise-native').defaults({ + forever: true, // Connection: keep-alive + gzip: true, // Accept-Encoding: gzip +}); /** * Defines configuration settings object @@ -30,10 +33,10 @@ function getConfig() { const config = { openhab: { baseURL: process.env.OPENHAB_BASE_URL || 'https://myopenhab.org/rest', - user: process.env.OPENHAB_USERNAME || null, - pass: process.env.OPENHAB_PASSWORD || null, + user: process.env.OPENHAB_USERNAME, + pass: process.env.OPENHAB_PASSWORD, certFile: process.env.OPENHAB_CERT_FILE || 'ssl/client.pfx', - certPass: process.env.OPENHAB_CERT_PASSPHRASE || null + certPass: process.env.OPENHAB_CERT_PASSPHRASE } }; // Merge config file settings with default ones @@ -44,7 +47,7 @@ function getConfig() { } // Load ssl client certificate if available if (fs.existsSync(config.openhab.certFile)) { - config.openhab.cert = fs.readFileSync(config.openhab.certFile); + config.openhab.cert = fs.readFileSync(`${process.cwd()}/${config.openhab.certFile}`); } return config; } @@ -62,20 +65,19 @@ function getConfigFileSettings() { } /** - * Returns request options object with openHAB authentication settings + * Returns request options with openHAB authentication settings * @param {String} token * @param {Object} options * @return {Object} */ -function getAuthenticationSettings(token, options) { +function ohAuthenticationSettings(token, options = {}) { if (config.openhab.cert) { // SSL Certificate Authentication - options.agentOptions = Object.assign({}, options.agentOptions, { - 'pfx': config.openhab.cert - }, config.openhab.certPass && { - 'passphrase': config.openhab.certPass - }); - } else if (config.openhab.userpass || token) { + options.agentOptions = { + pfx: config.openhab.cert, + passphrase: config.openhab.certPass + }; + } else { options.headers = Object.assign({}, options.headers, { 'Authorization': config.openhab.userpass ? // Basic Authentication @@ -106,7 +108,7 @@ function getItem(token, timeout, itemName) { */ function getItems(token, timeout) { const parameters = { - fields: 'editable,groupNames,groupType,name,label,metadata,state,stateDescription,tags,type', + fields: 'editable,groupNames,groupType,name,label,metadata,stateDescription,tags,type', metadata: 'alexa,channel,synonyms' }; return getItemOrItems(token, timeout, null, parameters); @@ -121,17 +123,13 @@ function getItems(token, timeout) { * @return {Promise} */ function getItemOrItems(token, timeout, itemName, parameters) { - const options = getAuthenticationSettings(token, Object.assign({ + const options = ohAuthenticationSettings(token, { method: 'GET', - uri: `${config.openhab.baseURL}/items${itemName ? '/' + itemName : ''}${parameters ? '?' + qs.stringify(parameters) : ''}`, - headers: { - 'Connection': 'keep-alive', - 'Content-Type': 'text/plain' - }, - json: true - }, parseInt(timeout) && { + uri: `${config.openhab.baseURL}/items/${itemName || ''}`, + qs: parameters, + json: true, timeout: parseInt(timeout) - })); + }); return request(options); } @@ -142,17 +140,12 @@ function getItemOrItems(token, timeout, itemName, parameters) { * @return {Promise} */ function getRegionalSettings(token, timeout) { - const options = getAuthenticationSettings(token, Object.assign({ - method: "GET", + const options = ohAuthenticationSettings(token, { + method: 'GET', uri: `${config.openhab.baseURL}/services/org.eclipse.smarthome.core.i18nprovider/config`, - headers: { - 'Connection': 'keep-alive', - 'Content-Type': 'text/plain' - }, - json: true - }, parseInt(timeout) && { + json: true, timeout: parseInt(timeout) - })); + }); return request(options); } @@ -165,17 +158,15 @@ function getRegionalSettings(token, timeout) { * @return {Promise} */ function postItemCommand(token, timeout, itemName, value) { - const options = getAuthenticationSettings(token, Object.assign({ + const options = ohAuthenticationSettings(token, { method: 'POST', uri: `${config.openhab.baseURL}/items/${itemName}`, headers: { - 'Connection': 'keep-alive', 'Content-Type': 'text/plain' }, - body: value.toString() - }, parseInt(timeout) && { + body: value.toString(), timeout: parseInt(timeout) - })); + }); return request(options); } diff --git a/lambda/smarthome/test/v3/test_discoverFan.js b/lambda/smarthome/test/v3/test_discoverFan.js index e045d5d4..40bc6dd7 100644 --- a/lambda/smarthome/test/v3/test_discoverFan.js +++ b/lambda/smarthome/test/v3/test_discoverFan.js @@ -15,15 +15,18 @@ module.exports = { description: "fan range/toggle components", mocked: [ { - "type": "Number", + "type": "Dimmer", "name": "TowerFanSpeed", + "stateDescription": { + "pattern": "%d %%" + }, "tags": [], "metadata": { "alexa": { "value": "RangeController.rangeValue", "config": { - "supportedRange": "1:10:1", - "presets": "1=@Value.Minimum:@Value.Low:Lowest,10=@Value.Maximum:@Value.High:Highest,99=invalid", + "supportedRange": "0:100:10", + "presets": "10=@Value.Minimum:@Value.Low:Lowest,100=@Value.Maximum:@Value.High:Highest,999=invalid", "friendlyNames": "@Setting.FanSpeed,Air Speed,Speed" } } @@ -45,7 +48,6 @@ module.exports = { "groupNames": ["gTowerFan"] }, { - "state": "80 °", "type": "Number:Angle", "name": "TowerFanAngle", "label": "Fan Angle", @@ -106,12 +108,13 @@ module.exports = { "unitOfMeasure": "Alexa.Unit.Angle.Degrees" }, "Alexa.RangeController.TowerFanSpeed": { - "supportedRange": {"minimumValue": 1, "maximumValue": 10, "precision": 1}, + "supportedRange": {"minimumValue": 0, "maximumValue": 100, "precision": 10}, + "unitOfMeasure": "Alexa.Unit.Percent", "presets": { - 1: { + 10: { "friendlyNames": ["asset:Alexa.Value.Minimum", "asset:Alexa.Value.Low", "text:Lowest:en-US"] }, - 10: { + 100: { "friendlyNames": ["asset:Alexa.Value.Maximum", "asset:Alexa.Value.High", "text:Highest:en-US"] } } @@ -132,11 +135,12 @@ module.exports = { "RangeController:TowerFanSpeed": { "rangeValue": { "parameters": { - "supportedRange": {"minimumValue": 1, "maximumValue": 10, "precision": 1}, - "presets": ["1=@Value.Minimum:@Value.Low:Lowest", "10=@Value.Maximum:@Value.High:Highest"], + "supportedRange": {"minimumValue": 0, "maximumValue": 100, "precision": 10}, + "unitOfMeasure": "Percent", + "presets": ["10=@Value.Minimum:@Value.Low:Lowest", "100=@Value.Maximum:@Value.High:Highest"], "friendlyNames": ["@Setting.FanSpeed", "Air Speed", "Speed"] }, - "item": {"name": "TowerFanSpeed", "type": "Number"}, + "item": {"name": "TowerFanSpeed", "type": "Dimmer"}, "schema": {"name": "rangeValue"} } }, diff --git a/lambda/smarthome/test/v3/test_discoverThermostat.js b/lambda/smarthome/test/v3/test_discoverThermostat.js index 785d0c96..99725152 100644 --- a/lambda/smarthome/test/v3/test_discoverThermostat.js +++ b/lambda/smarthome/test/v3/test_discoverThermostat.js @@ -230,7 +230,6 @@ module.exports = { "groupNames": [] }, { - "state": "20 °C", "type": "Number:Temperature", "name": "temperature2", "label": "Temperature 2", @@ -453,7 +452,7 @@ module.exports = { "propertyMap": { "TemperatureSensor": { "temperature": { - "parameters": {"scale": "CELSIUS"}, + "parameters": {"scale": "FAHRENHEIT"}, "item": {"name": "temperature2", "type": "Number:Temperature"}, "schema": {"name": "temperature"} }