Skip to content

Commit

Permalink
Update channel controller support (#419)
Browse files Browse the repository at this point in the history
Signed-off-by: jsetton <jeremy.setton@gmail.com>
  • Loading branch information
jsetton authored Dec 1, 2021
1 parent d363443 commit 707c24a
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 67 deletions.
27 changes: 16 additions & 11 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,14 +729,18 @@ Items that represent a channel. It is important to note only well-known channel
* Number
* String
* Supported metadata parameters:
* channelMappings=`<channels>`
* each mapping formatted as `<channelName>=<channelNumber>` (e.g. `channelMappings="CBS=2,NBC=4,ABC=7,PBS=13"`)
* allows channel requests by name and incrementally otherwise only channel requests by number are supported if not defined
* defaults to no mappings
* channelMappings=`<mappings>`
* each mapping formatted as `<channelNumber>=<channelName>` (e.g. `channelMappings="2=CBS,4=NBC,7=ABC,13=PBS"`)
* allows channel requests by name otherwise only channel requests by number are supported if not defined
* defaults to item state description options `channelMappings="value1=label1,..."`, if defined, otherwise no mappings
* range=`<range>`
* range formatted as `<minValue>:<maxValue>` (e.g. `range="100:499"`)
* defaults to `"1:9999"`
* Utterance examples:
* *Alexa, change the channel to `<channel number>` on the `<device name>`.*
* *Alexa, change the channel to `<channel name>` on the `<device name>`.*
* *Alexa, next channel on the `<device name>`.*
* *Alexa, previous channel on the `<device name>`.*
* *Alexa, channel up on the `<device name>`.*
* *Alexa, channel down on the `<device name>`.*

Expand Down Expand Up @@ -780,14 +784,14 @@ Items that represent a volume level.
* increment=`<number>`
* defaults to 10 (standard value provided by Alexa)
* stepSpeaker=`<boolean>` (Number only)
* set to true for volume controlled in steps only and its state cannot be tracked by openHAB.
* set to true for volume controlled in incremental steps only and its state cannot be tracked by openHAB.
* defaults to false
* Utterance examples:
* *Alexa, set the volume of `<device name>` to 50.*
* *Alexa, set the volume of `<device name>` to 50.* (if not step speaker)
* *Alexa, turn the volume up on `<device name>`.*
* *Alexa, turn the volume down on `<device name>` by 20.*
* *Alexa, lower the volume on the `<device name>`.* (Step-speaker)
* *Alexa, volume up 20 on the `<device name>`.* (Step-speaker)
* *Alexa, increase the volume on the `<device name>`.*
* *Alexa, lower the volume on the `<device name>` by 20.*

<a name="speaker-muted"></a>
<a name="stepspeaker-muted"></a>
Expand Down Expand Up @@ -820,8 +824,9 @@ Items that represent the bass equalizer band range supported by an audio system.
* Dimmer
* Number
* Supported metadata parameters:
* range=`<minValue:maxValue>`
* range values should be synced across different bands if configured since same values used for all bands due to Alexa restriction.
* range=`<range>`
* range formatted as `<minValue>:<maxValue>`
* should be synced across different bands if configured since same values used for all bands due to Alexa restriction.
* defaults to `"0:100"` for Dimmer, otherwise `"-10:10"` for Number
* defaultLevel=`<number>`
* defaults to range midpoint
Expand Down Expand Up @@ -1426,7 +1431,7 @@ Items that represent a target setpoint for a thermostat.
* scale=`<scale>`
* Celsius or Fahrenheit
* defaults to [item unit of measurement](#item-unit-of-measurement), otherwise Celsius
* setpointRange=`<minValue:maxValue>`
* setpointRange=`<range>`
* range formatted as `<minValue>:<maxValue>` (e.g. `setpointRange="60:90"`)
* defaults to 4°C -> 32°C or 40°F -> 90°F
* Utterance examples:
Expand Down
39 changes: 20 additions & 19 deletions lambda/alexa/smarthome/handlers/channelController.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
* SPDX-License-Identifier: EPL-2.0
*/

const { clamp } = require('@root/utils');
const { Interface, Property } = require('../constants');
const { EndpointUnreachableError, InvalidValueError } = require('../errors');
const { EndpointUnreachableError, InvalidValueError, ValueOutOfRangeError } = require('../errors');
const AlexaHandler = require('./handler');

/**
Expand Down Expand Up @@ -54,39 +53,45 @@ class ChannelController extends AlexaHandler {
}

/**
* Changes channel to number or name
* Changes channel
* @param {Object} directive
* @param {Object} openhab
* @return {Promise}
*/
static async changeChannel(directive, openhab) {
const { item, channelMappings } = directive.endpoint.getCapabilityProperty({
const { item, channelMappings, range } = directive.endpoint.getCapabilityProperty({
interface: directive.namespace,
property: Property.CHANNEL
});
// Determine channel number using channel name if provided and defined in channelMappings parameter,
// otherwise use provided channel number
const channelName = directive.payload.channelMetadata.name || '';
const channelNumber = channelMappings[channelName.toUpperCase()] || directive.payload.channel.number;
// Determine channel number using channel name if provided, otherwise using provided channel number
const channelName = directive.payload.channelMetadata.name;
const channelNumber = channelName
? Object.keys(channelMappings).find((num) => channelMappings[num].toUpperCase() === channelName.toUpperCase())
: directive.payload.channel.number;

// Throw invalid value error if channel number not valid
if (isNaN(channelNumber)) {
throw new InvalidValueError(`The channel cannot be changed to ${channelNumber || channelName}.`);
}

// Throw value out of range error if channel number out of range
if (channelNumber < range[0] || channelNumber > range[1]) {
throw new ValueOutOfRangeError(`The channel cannot be changed to ${channelNumber}.`, { validRange: range });
}

await openhab.sendCommand(item.name, channelNumber);

return directive.response();
}

/**
* Adjusts channel number
* Adjusts channel
* @param {Object} directive
* @param {Object} openhab
* @return {Promise}
*/
static async adjustChannel(directive, openhab) {
const { item, channelMappings, isRetrievable } = directive.endpoint.getCapabilityProperty({
const { item, range, isRetrievable } = directive.endpoint.getCapabilityProperty({
interface: directive.namespace,
property: Property.CHANNEL
});
Expand All @@ -104,18 +109,14 @@ class ChannelController extends AlexaHandler {
throw new EndpointUnreachableError(`Could not get numeric state for item ${item.name}.`);
}

const channelCount = directive.payload.channelCount;
const channelNumbers = Object.values(channelMappings);
const channelIndex = channelNumbers.indexOf(state);
// Determine adjusted channel number adding directive payload channel count value to current state
const channelNumber = parseInt(state) + directive.payload.channelCount;

// Throw invalid value error if current channel not defined
if (channelIndex === -1) {
throw new InvalidValueError(`Current channel number ${state} is not defined in channel mappings.`);
// Throw value out of range error if adjusted channel number out of range
if (channelNumber < range[0] || channelNumber > range[1]) {
throw new ValueOutOfRangeError(`The channel cannot be adjusted to ${channelNumber}.`, { validRange: range });
}

const adjustedIndex = clamp(channelIndex + channelCount, 0, channelNumbers.length - 1);
const channelNumber = channelNumbers[adjustedIndex];

await openhab.sendCommand(item.name, channelNumber);

return directive.response();
Expand Down
29 changes: 25 additions & 4 deletions lambda/alexa/smarthome/properties/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class Channel extends AlexaProperty {
*/
get supportedParameters() {
return {
[Parameter.CHANNEL_MAPPINGS]: ParameterType.MAP
[Parameter.CHANNEL_MAPPINGS]: ParameterType.MAP,
[Parameter.RANGE]: ParameterType.RANGE
};
}

Expand All @@ -46,6 +47,14 @@ class Channel extends AlexaProperty {
return this.parameters[Parameter.CHANNEL_MAPPINGS] || {};
}

/**
* Returns range based on parameter
* @return {Array}
*/
get range() {
return this.parameters[Parameter.RANGE] || [1, 9999];
}

/**
* Returns alexa state
* @param {String} value
Expand All @@ -69,11 +78,23 @@ class Channel extends AlexaProperty {
// Update parameters from parent method
super.updateParameters(item, metadata, settings);

const channels = parameters[Parameter.CHANNEL_MAPPINGS] || {};
// Define channel mappings as follow:
// 1) using parameter if defined
// 2) using item state description options if available
// 3) empty object
const channels = parameters[Parameter.CHANNEL_MAPPINGS]
? parameters[Parameter.CHANNEL_MAPPINGS]
: item.stateDescription && item.stateDescription.options
? Object.fromEntries(item.stateDescription.options.map((option) => [option.value, option.label]))
: {};
// Update channel mappings parameter removing invalid mappings if defined
parameters[Parameter.CHANNEL_MAPPINGS] = Object.entries(channels)
.filter(([, number]) => !isNaN(number))
.reduce((channels, [name, number]) => ({ ...channels, [name.toUpperCase()]: number }), undefined);
.filter(([number]) => !isNaN(number))
.reduce((channels, [number, name]) => ({ ...channels, [number]: name }), undefined);

const range = parameters[Parameter.RANGE] || [];
// Update range parameter if valid (min < max), otherwise set to undefined
parameters[Parameter.RANGE] = range[0] < range[1] ? range.map((value) => Math.round(value)) : undefined;
}
}

Expand Down
Loading

0 comments on commit 707c24a

Please sign in to comment.