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

Add thermostat hold setpoints support #428

Merged
merged 1 commit into from
Dec 7, 2021
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
18 changes: 10 additions & 8 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1432,7 +1432,7 @@ Items that represent a general device responding to percentage commands.

#### `TargetTemperature`

Items that represent a target setpoint for a thermostat.
Items that represent a target setpoint for a thermostat. For thermostat that requires hold to be set prior to manually changing setpoints, set parameter `requiresSetpointHold=true` on the [`ThermostatHold`](#thermostathold) item.

* Supported item types:
* Number(:Temperature)
Expand All @@ -1455,9 +1455,7 @@ Items that represent a target setpoint for a thermostat.

#### `CoolingSetpoint`

Items that represent an upper or cooling setpoint for a thermostat. This needs to be paired with [`HeatingSetpoint`](#heatingsetpoint).

By default, if the target, cooling and heating setpoints along with the thermostat mode are defined, the setpoint mode automation will be enabled limiting to dual mode interactions. For true triple mode support, set parameter `supportsSetpointMode=false` on the [`HeatingCoolingMode`](#heatingcoolingmode) item. It is important to note that only the target setpoint can be requested by voice. Therefore, when the thermostat is in dual mode, the cooling and heating setpoints will be updated based on the `comfortRange` parameter.
Items that represent an upper or cooling setpoint for a thermostat. This needs to be paired with [`HeatingSetpoint`](#heatingsetpoint). It is important to note that only the target setpoint can be requested by voice. Therefore, when the thermostat is in dual mode, the cooling and heating setpoints will be updated based on the `comfortRange` parameter. For thermostat that requires hold to be set prior to manually changing setpoints, set parameter `requiresSetpointHold=true` on the [`ThermostatHold`](#thermostathold) item. By default, if the target, cooling and heating setpoints along with the thermostat mode are defined, the setpoint mode automation will be enabled limiting to dual mode interactions. For true triple mode support, set parameter `supportsSetpointMode=false` on the [`HeatingCoolingMode`](#heatingcoolingmode) item.

* Supported item types:
* Number(:Temperature)
Expand Down Expand Up @@ -1549,11 +1547,15 @@ Items that represent the heating/cooling mode of a thermostat.
Items that represent the hold setting of a thermostat. This needs to be paired with [`HeatingCoolingMode`](#heatingcoolingmode). Only requests to resume schedule (turn off hold) are supported.

* Supported item types:
* Number [RESUME=0]
* String [RESUME="resume"]
* Switch [RESUME="OFF"]
* Number [OFF=0, ON=1]
* String [OFF="schedule", ON="hold"]
* Switch [OFF="OFF", ON="ON"]
* Supported metadata parameters:
* RESUME=`<state>`
* OFF=`<state>`
* ON=`<state>`
* requiresSetpointHold=`<boolean>`
* set to true to require thermostat hold to be set on setpoint requests
* defaults to false
* Utterance examples:
* *Alexa, resume the `<device name>` schedule.*

Expand Down
29 changes: 22 additions & 7 deletions lambda/alexa/smarthome/handlers/thermostatController.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class ThermostatController extends AlexaHandler {
static async setTargetTemperature(directive, openhab) {
const capability = directive.endpoint.getCapability({ interface: directive.namespace });
const properties = capability.getPropertyMap();
const thermostatHold = properties[Property.THERMOSTAT_HOLD];
const thermostatMode = properties[Property.THERMOSTAT_MODE];

// Throw schedule request error if schedule request
Expand Down Expand Up @@ -182,6 +183,11 @@ class ThermostatController extends AlexaHandler {
}
}

// Set thermostat hold prior to sending setpoint commands if required
if (thermostatHold && thermostatHold.requiresSetpointHold) {
await openhab.sendCommand(thermostatHold.item.name, thermostatHold.getCommand(ThermostatHold.ON));
}

// Define commands to send
const commands = items.map((item) => openhab.sendCommand(item.name, item.command));

Expand All @@ -199,6 +205,7 @@ class ThermostatController extends AlexaHandler {
static async adjustTargetTemperature(directive, openhab) {
const capability = directive.endpoint.getCapability({ interface: directive.namespace });
const properties = capability.getPropertyMap();
const thermostatHold = properties[Property.THERMOSTAT_HOLD];
const thermostatMode = properties[Property.THERMOSTAT_MODE];

// Get current alexa thermostat mode if property defined, retrievable and supports setpoint mode
Expand Down Expand Up @@ -251,6 +258,11 @@ class ThermostatController extends AlexaHandler {
});
});

// Set thermostat hold prior to sending setpoint commands if required
if (thermostatHold && thermostatHold.requiresSetpointHold) {
await openhab.sendCommand(thermostatHold.item.name, thermostatHold.getCommand(ThermostatHold.ON));
}

await Promise.all(commands);

return directive.response();
Expand All @@ -273,14 +285,12 @@ class ThermostatController extends AlexaHandler {
throw new InvalidValueError('The thermostat has no mode property.');
}

const thermostatMode = directive.payload.thermostatMode.value;
const command = property.getCommand(thermostatMode);
const mode = directive.payload.thermostatMode.value;
const command = property.getCommand(mode);

// Throw thermostat mode unsupported error if no command defined
if (typeof command === 'undefined') {
throw new ThermostatModeUnsupportedError(
`${property.item.name} doesn't support thermostat mode [${thermostatMode}].`
);
throw new ThermostatModeUnsupportedError(`${property.item.name} doesn't support thermostat mode [${mode}].`);
}

await openhab.sendCommand(property.item.name, command);
Expand All @@ -296,8 +306,14 @@ class ThermostatController extends AlexaHandler {
*/
static async resumeSchedule(directive, openhab) {
const properties = directive.endpoint.getCapabilityPropertyMap({ interface: directive.namespace });
const thermostatHold = properties[Property.THERMOSTAT_HOLD];
const thermostatMode = properties[Property.THERMOSTAT_MODE];

// Throw invalid value error if no thermostat hold property defined
if (typeof thermostatHold === 'undefined') {
throw new InvalidValueError('The thermostat has no hold property.');
}

// Get current alexa thermostat mode if property defined and retrievable
const mode =
thermostatMode && thermostatMode.isRetrievable
Expand All @@ -309,8 +325,7 @@ class ThermostatController extends AlexaHandler {
throw new ThermostatOffError('The thermostat is off.');
}

const thermostatHold = properties[Property.THERMOSTAT_HOLD];
const command = thermostatHold.getCommand(ThermostatHold.RESUME);
const command = thermostatHold.getCommand(ThermostatHold.OFF);

await openhab.sendCommand(thermostatHold.item.name, command);

Expand Down
1 change: 1 addition & 0 deletions lambda/alexa/smarthome/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const Parameter = Object.freeze({
PRIMARY_CONTROL: 'primaryControl',
PROXY_BASE_URL: 'proxyBaseUrl',
RANGE: 'range',
REQUIRES_SETPOINT_HOLD: 'requiresSetpointHold',
RESOLUTION: 'resolution',
RETRIEVABLE: 'retrievable',
SCALE: 'scale',
Expand Down
37 changes: 31 additions & 6 deletions lambda/alexa/smarthome/properties/thermostatHold.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

const { ItemType, ItemValue } = require('@openhab/constants');
const { Property } = require('../constants');
const { Parameter, ParameterType } = require('../metadata');
const AlexaProperty = require('./property');

/**
Expand All @@ -21,10 +22,16 @@ const AlexaProperty = require('./property');
*/
class ThermostatHold extends AlexaProperty {
/**
* Defines resume state
* Defines on state
* @type {String}
*/
static RESUME = 'RESUME';
static ON = 'ON';

/**
* Defines off state
* @type {String}
*/
static OFF = 'OFF';

/**
* Returns supported item types
Expand All @@ -34,12 +41,22 @@ class ThermostatHold extends AlexaProperty {
return [ItemType.NUMBER, ItemType.STRING, ItemType.SWITCH];
}

/**
* Returns supported parameters and their type
* @return {Object}
*/
get supportedParameters() {
return {
[Parameter.REQUIRES_SETPOINT_HOLD]: ParameterType.BOOLEAN
};
}

/**
* Returns supported values
* @return {Array}
*/
get supportedValues() {
return [ThermostatHold.RESUME];
return [ThermostatHold.ON, ThermostatHold.OFF];
}

/**
Expand All @@ -65,15 +82,23 @@ class ThermostatHold extends AlexaProperty {
get defaultValueMap() {
switch (this.item.type) {
case ItemType.NUMBER:
return { RESUME: 0 };
return { [ThermostatHold.OFF]: 0, [ThermostatHold.ON]: 1 };
case ItemType.STRING:
return { RESUME: 'resume' };
return { [ThermostatHold.OFF]: 'schedule', [ThermostatHold.ON]: 'hold' };
case ItemType.SWITCH:
return { RESUME: ItemValue.OFF };
return { [ThermostatHold.OFF]: ItemValue.OFF, [ThermostatHold.ON]: ItemValue.ON };
default:
return {};
}
}

/**
* Returns if requires setpoint hold based on parameter
* @return {Boolean}
*/
get requiresSetpointHold() {
return this.parameters[Parameter.REQUIRES_SETPOINT_HOLD] === true;
}
}

module.exports = ThermostatHold;
39 changes: 38 additions & 1 deletion lambda/test/alexa/cases/thermostatControllerMode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ module.exports = [
}
}
},
openhab: [{ name: 'thermostatHold', value: 'resume' }]
openhab: [{ name: 'thermostatHold', value: 'schedule' }]
}
},
{
Expand Down Expand Up @@ -455,6 +455,43 @@ module.exports = [
openhab: [{ name: 'thermostatHold', value: 0 }]
}
},
{
description: 'resume thermostat schedule invalid value error',
directive: {
header: {
namespace: 'Alexa.ThermostatController',
name: 'ResumeSchedule'
},
endpoint: {
endpointId: 'gThermostat',
cookie: {
capabilities: JSON.stringify([
{
name: 'ThermostatController',
property: 'thermostatMode',
parameters: { OFF: '0', HEAT: '1', COOL: '2', AUTO: '3' },
item: { name: 'thermostatMode', type: 'Number' }
}
])
}
},
payload: {}
},
expected: {
alexa: {
event: {
header: {
namespace: 'Alexa',
name: 'ErrorResponse'
},
payload: {
type: 'INVALID_VALUE',
message: 'The thermostat has no hold property.'
}
}
}
}
},
{
description: 'resume thermostat schedule thermostat off error',
directive: {
Expand Down
Loading