Skip to content

Commit

Permalink
Add playback state & step attribute support (#544)
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 31, 2022
1 parent c90c8b5 commit 87099ab
Show file tree
Hide file tree
Showing 32 changed files with 973 additions and 117 deletions.
41 changes: 34 additions & 7 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ Number:Temperature Temperature2 "Temperature" {alexa="CurrentTemperatu

To interact with the networking capabilities of a router, such as controlling the network access for a specific device, the [networking attributes](#networking-attributes) are available to configure a representation of a home network and its connected devices.

In order to be take advantage of these capabilities, your router must be configured as a [group endpoint](#group-endpoint) based on [`HomeNetwork`](#homenetwork) supported device types. If it doesn't have any other capabilities, it can be an empty group. Likewise, connected devices must be configured based on [`ConnectedDevice`](#connecteddevice) supported device types and metadata parameters.
In order to take advantage of these capabilities, your router must be configured as a [group endpoint](#group-endpoint) based on [`HomeNetwork`](#homenetwork) supported device types. If it doesn't have any other capabilities, it can be an empty group. Likewise, connected devices must be configured based on [`ConnectedDevice`](#connecteddevice) supported device types and metadata parameters.

```xtend
Group Router "Router" {alexa="Router"}
Expand Down Expand Up @@ -478,7 +478,7 @@ Device Types | Supported Attributes | Description
`Automobile` | [`BatteryLevel`](#batterylevel), [`FanSpeed`](#fanspeed), [`LockState`](#lockstate), [`PowerState`](#powerstate), [`TargetTemperature`](#targettemperature), [`CurrentTemperature`](#currenttemperature) | A motor vehicle (automobile, car).
`AutomobileAccessory` | [`BatteryLevel`](#batterylevel), [`CameraStream`](#camerastream), [`FanSpeed`](#fanspeed), [`PowerState`](#powerstate) | A smart device in an automobile, such as a dash camera.
`Blind`, `Curtain`, `Shade` | *[`OpenState`](#openstate)*, *[`PositionState`](#positionstate)*, [`TiltAngle`](#tiltangle), [`TargetOpenState`](#targetopenstate), [`CurrentOpenState`](#currentopenstate) | A window covering on the inside of a structure.
`BluetoothSpeaker` | *[`PowerState`](#powerstate)*, *[`VolumeLevel`](#volumelevel)*, [`VolumeStep`](#volumestep), [`MuteState`](#mutestate), [`MuteStep`](#mutestep), [`EqualizerBass`](#equalizerbass), [`EqualizerMidrange`](#equalizermidrange), [`EqualizerTreble`](#equalizertreble), [`EqualizerMode`](#equalizermode), [`Channel`](#channel), [`ChannelStep`](#channelstep), [`Input`](#input), [`Playback`](#playback), [`PlaybackStop`](#playbackstop), [`BatteryLevel`](#batterylevel) | A speaker that connects to an audio source over Bluetooth.
`BluetoothSpeaker` | *[`PowerState`](#powerstate)*, *[`VolumeLevel`](#volumelevel)*, [`VolumeStep`](#volumestep), [`MuteState`](#mutestate), [`MuteStep`](#mutestep), [`EqualizerBass`](#equalizerbass), [`EqualizerMidrange`](#equalizermidrange), [`EqualizerTreble`](#equalizertreble), [`EqualizerMode`](#equalizermode), [`Channel`](#channel), [`ChannelStep`](#channelstep), [`Input`](#input), [`Playback`](#playback), [`PlaybackStop`](#playbackstop), [`PlaybackStep`](#playbackstep), [`BatteryLevel`](#batterylevel) | A speaker that connects to an audio source over Bluetooth.
`Camera` | *[`PowerState`](#powerstate)*, *[`CameraStream`](#camerastream)*, [`BatteryLevel`](#batterylevel) | A security device with video or photo functionality.
`ChristmasTree` | Same as `Light` | A religious holiday decoration that often contains lights.
`CoffeeMaker` | *[`PowerState`](#powerstate)* | A device that makes coffee.
Expand Down Expand Up @@ -511,11 +511,11 @@ Device Types | Supported Attributes | Description
`SecuritySystem` | Same as `SecurityPanel` | A security system.
`Shutter`, `Awning` | Same as `Blind` | A window covering on the outside of a structure.
`SlowCooker` | *[`PowerState`](#powerstate)* | An electric cooking device that sits on a countertop, cooks at low temperatures, and is often shaped like a cooking pot.
`Speaker` | *[`PowerState`](#powerstate)*, *[`VolumeLevel`](#volumelevel)*, [`VolumeStep`](#volumestep), [`MuteState`](#mutestate), [`MuteStep`](#mutestep), [`EqualizerBass`](#equalizerbass), [`EqualizerMidrange`](#equalizermidrange), [`EqualizerTreble`](#equalizertreble), [`EqualizerMode`](#equalizermode), [`Channel`](#channel), [`ChannelStep`](#channelstep), [`Input`](#input), [`Playback`](#playback), [`PlaybackStop`](#playbackstop) | A speaker or speaker system.
`StreamingDevice` | *[`PowerState`](#powerstate)*, *[`Playback`](#playback)*, [`PlaybackStop`](#playbackstop), [`Channel`](#channel), [`ChannelStep`](#channelstep), [`Input`](#input), [`VolumeLevel`](#volumelevel), [`VolumeStep`](#volumestep), [`MuteState`](#mutestate), [`MuteStep`](#mutestep), [`EqualizerBass`](#equalizerbass), [`EqualizerMidrange`](#equalizermidrange), [`EqualizerTreble`](#equalizertreble), [`EqualizerMode`](#equalizermode) | A streaming device such as Apple TV, Chromecast, or Roku.
`Speaker` | *[`PowerState`](#powerstate)*, *[`VolumeLevel`](#volumelevel)*, [`VolumeStep`](#volumestep), [`MuteState`](#mutestate), [`MuteStep`](#mutestep), [`EqualizerBass`](#equalizerbass), [`EqualizerMidrange`](#equalizermidrange), [`EqualizerTreble`](#equalizertreble), [`EqualizerMode`](#equalizermode), [`Channel`](#channel), [`ChannelStep`](#channelstep), [`Input`](#input), [`Playback`](#playback), [`PlaybackStop`](#playbackstop), [`PlaybackStep`](#playbackstep) | A speaker or speaker system.
`StreamingDevice` | *[`PowerState`](#powerstate)*, *[`Playback`](#playback)*, [`PlaybackStop`](#playbackstop), [`PlaybackStep`](#playbackstep), [`Channel`](#channel), [`ChannelStep`](#channelstep), [`Input`](#input), [`VolumeLevel`](#volumelevel), [`VolumeStep`](#volumestep), [`MuteState`](#mutestate), [`MuteStep`](#mutestep), [`EqualizerBass`](#equalizerbass), [`EqualizerMidrange`](#equalizermidrange), [`EqualizerTreble`](#equalizertreble), [`EqualizerMode`](#equalizermode) | A streaming device such as Apple TV, Chromecast, or Roku.
`Switch` | *[`PowerState`](#powerstate)*, *[`PowerLevel`](#powerlevel)*, *[`Percentage`](#percentage)* | A switch wired directly to the electrical system. A switch can control a variety of devices. For lighting devices, use `Light` instead.
`Tablet` | *[`PowerState`](#powerstate)*, [`BatteryLevel`](#batterylevel), [`NetworkAccess`](#networkaccess) | A tablet computer.
`Television` | *[`PowerState`](#powerstate)*, *[`Channel`](#channel)*, [`ChannelStep`](#channelstep), [`Input`](#input), [`VolumeLevel`](#volumelevel), [`VolumeStep`](#volumestep), [`MuteState`](#mutestate), [`MuteStep`](#mutestep), [`EqualizerBass`](#equalizerbass), [`EqualizerMidrange`](#equalizermidrange), [`EqualizerTreble`](#equalizertreble), [`EqualizerMode`](#equalizermode), [`Playback`](#playback), [`PlaybackStop`](#playbackstop) | A television.
`Television` | *[`PowerState`](#powerstate)*, *[`Channel`](#channel)*, [`ChannelStep`](#channelstep), [`Input`](#input), [`VolumeLevel`](#volumelevel), [`VolumeStep`](#volumestep), [`MuteState`](#mutestate), [`MuteStep`](#mutestep), [`EqualizerBass`](#equalizerbass), [`EqualizerMidrange`](#equalizermidrange), [`EqualizerTreble`](#equalizertreble), [`EqualizerMode`](#equalizermode), [`Playback`](#playback), [`PlaybackStop`](#playbackstop), [`PlaybackStep`](#playbackstep) | A television.
`TemperatureSensor` | *[`CurrentTemperature`](#currenttemperature)*, [`BatteryLevel`](#batterylevel) | An endpoint that reports temperature, but does not control it. The temperature data of the endpoint doesn't appear in the Alexa app. If your endpoint also controls temperature, use `Thermostat` instead.
`Thermostat` | *[`HeatingCoolingMode`](#heatingcoolingmode)*, [`TargetTemperature`](#targettemperature), [`CoolingSetpoint`](#coolingsetpoint), [`HeatingSetpoint`](#heatingsetpoint), [`EcoCoolingSetpoint`](#ecocoolingsetpoint), [`EcoHeatingSetpoint`](#ecoheatingsetpoint), [`ThermostatHold`](#thermostathold), [`ThermostatFan`](#thermostatfan), [`CurrentTemperature`](#currenttemperature), [`CurrentHumidity`](#currenthumidity), [`BatteryLevel`](#batterylevel) | An endpoint that controls temperature, stand-alone air conditioners, or heaters with direct temperature control. If your endpoint senses temperature but does not control it, use `TemperatureSensor` instead.
`VacuumCleaner` | *[`PowerState`](#powerstate)*, *[`VacuumMode`](#vacuummode)*, [`FanSpeed`](#fanspeed), [`BatteryLevel`](#batterylevel) | A vacuum cleaner.
Expand Down Expand Up @@ -913,7 +913,7 @@ Items that represent a list of equalizer modes supported by an audio system.

#### `Playback`

Items that represent the playback controls of a AV device. For stop command support, use [`PlaybackStop`](#playbackstop).
Items that represent the playback controls of a AV device. For stop command support, use [`PlaybackStop`](#playbackstop). For adjustment in incremental discrete steps, use [`PlaybackStep`](#playbackstep) instead.

* Supported item types:
* Player
Expand Down Expand Up @@ -943,6 +943,32 @@ Items that represent the playback stop command of a AV device. This needs to be
* Utterance examples:
* *Alexa, stop `<device name>`.*

#### `PlaybackStep`

Items that represent the playback controls of a AV device adjusted in discrete steps.

* Supported item types:
* String
* Supported metadata parameters:
* PLAY=`<command>`
* PAUSE=`<command>`
* STOP=`<command>`
* START_OVER=`<command>`
* PREVIOUS=`<command>`
* NEXT=`<command>`
* REWIND=`<command>`
* FAST_FORWARD=`<command>`
* Utterance examples:
* *Alexa, play `<device name>`.*
* *Alexa, resume `<device name>`.*
* *Alexa, pause `<device name>`.*
* *Alexa, stop `<device name>`.*
* *Alexa, start over on `<device name>`.*
* *Alexa, next on `<device name>`.*
* *Alexa, previous on `<device name>`.*
* *Alexa, fast forward on `<device name>`.*
* *Alexa, rewind on `<device name>`.*

### Fan Attributes

#### `FanSpeed`
Expand Down Expand Up @@ -1922,7 +1948,8 @@ ModeController | [`Mode`](#mode), [`FanDirection`](#fandirection), [`FanSpeed`](
MotionSensor | [`MotionDetectionState`](#motiondetectionstate) | `de-DE`, `en-AU`, `en-CA`, `en-GB`, `en-IN`, `en-US`, `es-ES`, `es-MX`, `es-US`, `fr-CA`, `fr-FR`, `hi-IN`, `it-IT`, `ja-JP`, `pt-BR`
Networking | [`HomeNetwork`](#homenetwork), [`ConnectedDevice`](#connecteddevice), [`NetworkAccess`](#networkaccess) | `en-US`
PercentageController | [`Percentage`](#percentage) | `de-DE`, `en-AU`, `en-CA`, `en-GB`, `en-IN`, `en-US`, `es-ES`, `es-US`, `fr-CA`, `fr-FR`, `hi-IN`, `it-IT`, `ja-JP`, `pt-BR`
PlaybackController | [`Playback`](#playback), [`PlaybackStop`](#playbackstop) | `ar-SA`, `de-DE`, `en-AU`, `en-CA`, `en-GB`, `en-IN`, `en-US`, `es-ES`, `es-MX`, `es-US`, `fr-CA`, `fr-FR`, `hi-IN`, `it-IT`, `ja-JP`, `pt-BR`
PlaybackController | [`Playback`](#playback), [`PlaybackStop`](#playbackstop), [`PlaybackStep`](#playbackstep) | `ar-SA`, `de-DE`, `en-AU`, `en-CA`, `en-GB`, `en-IN`, `en-US`, `es-ES`, `es-MX`, `es-US`, `fr-CA`, `fr-FR`, `hi-IN`, `it-IT`, `ja-JP`, `pt-BR`
PlaybackStateReporter | [`Playback`](#playback), [`PlaybackStop`](#playbackstop) | `ar-SA`, `de-DE`, `en-AU`, `en-CA`, `en-GB`, `en-IN`, `en-US`, `es-ES`, `es-MX`, `es-US`, `fr-CA`, `fr-FR`, `hi-IN`, `it-IT`, `ja-JP`, `pt-BR`
PowerController | [`PowerState`](#powerstate) | `ar-SA`, `de-DE`, `en-AU`, `en-CA`, `en-GB`, `en-IN`, `en-US`, `es-ES`, `es-MX`, `es-US`, `fr-CA`, `fr-FR`, `hi-IN`, `it-IT`, `ja-JP`, `pt-BR`
PowerLevelController | [`PowerLevel`](#powerlevel) | `de-DE`, `en-AU`, `en-CA`, `en-GB`, `en-IN`, `en-US`, `es-ES`, `es-MX`, `fr-CA`, `fr-FR`, `it-IT`, `ja-JP`
RangeController | [`RangeValue`](#rangevalue), [`BatteryLevel`](#batterylevel), [`CurrentHumidity`](#currenthumidity), [`FanSpeed`](#fanspeed), [`PositionState`](#positionstate), [`TiltAngle`](#tiltangle) | `de-DE`, `en-AU`, `en-CA`, `en-GB`, `en-IN`, `en-US`, `es-ES`, `es-MX`, `es-US`, `fr-CA`, `fr-FR`, `hi-IN`, `it-IT`, `ja-JP`, `pt-BR`
Expand Down
8 changes: 5 additions & 3 deletions lambda/alexa/smarthome/capabilities/capability.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,10 @@ export default class AlexaCapability {
* @return {Boolean}
*/
hasRequiredLinkedCapabilities(capabilities) {
return this.requiredLinkedCapabilities.every(({ name, instance }) =>
capabilities.find((capability) => capability.name === name && capability.instance === instance)
return this.requiredLinkedCapabilities.every(({ name, instance, property }) =>
capabilities.find(
(capability) => capability.name === name && capability.instance === instance && capability.hasProperty(property)
)
);
}

Expand Down Expand Up @@ -229,7 +231,7 @@ export default class AlexaCapability {
getContextProperties() {
return this.properties.filter(
// Filter decouple state tagged property if defined, otherwise fallback to standard property
(property) => (this.getProperty({ ...property, tag: DecoupleState.TAG_NAME }) || property) === property
(property) => (this.getProperty({ ...property, tag: DecoupleState.TAG_SENSOR }) || property) === property
);
}

Expand Down
1 change: 1 addition & 0 deletions lambda/alexa/smarthome/capabilities/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { default as NetworkingConnectedDevice } from './networkingConnectedDevic
export { default as NetworkingHomeNetworkController } from './networkingHomeNetworkController.js';
export { default as PercentageController } from './percentageController.js';
export { default as PlaybackController } from './playbackController.js';
export { default as PlaybackStateReporter } from './playbackStateReporter.js';
export { default as PowerController } from './powerController.js';
export { default as PowerLevelController } from './powerLevelController.js';
export { default as RangeController } from './rangeController.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default class NetworkingAccessController extends AlexaCapability {
* @return {Array}
*/
get requiredLinkedCapabilities() {
return [{ name: Capability.NETWORKING_CONNECTED_DEVICE, property: Property.CONNECTED_DEVICE }];
return [{ name: Capability.NETWORKING_CONNECTED_DEVICE, property: { name: Property.CONNECTED_DEVICE } }];
}

/**
Expand Down
3 changes: 2 additions & 1 deletion lambda/alexa/smarthome/capabilities/playbackController.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import AlexaCapability from './capability.js';
import AlexaDisplayCategory from '../category.js';
import { Capability, Interface, Property } from '../constants.js';
import { Playback, PlaybackStop, PlaybackAction } from '../properties/index.js';
import { Playback, PlaybackStop, PlaybackStep, PlaybackAction } from '../properties/index.js';

/**
* Defines Alexa.PlaybackController interface capability class
Expand Down Expand Up @@ -46,6 +46,7 @@ export default class PlaybackController extends AlexaCapability {
return {
[Property.PLAYBACK]: Playback,
[Property.PLAYBACK_STOP]: PlaybackStop,
[Property.PLAYBACK_STEP]: PlaybackStep,
[Property.PLAYBACK_ACTION]: PlaybackAction
};
}
Expand Down
80 changes: 80 additions & 0 deletions lambda/alexa/smarthome/capabilities/playbackStateReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/

import AlexaCapability from './capability.js';
import { Capability, Interface, Property } from '../constants.js';
import { PlaybackState } from '../properties/index.js';

/**
* Defines Alexa.PlaybackStateReporter interface capability class
* https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html#properties
* @extends AlexaCapability
*/
export default class PlaybackStateReporter extends AlexaCapability {
/**
* Returns name
* @return {String}
*/
static get name() {
return Capability.PLAYBACK_STATE_REPORTER;
}

/**
* Returns interface
* @return {String}
*/
get interface() {
return Interface.ALEXA_PLAYBACK_STATE_REPORTER;
}

/**
* Returns supported properties
* @return {Object}
*/
get supportedProperties() {
return {
[Property.PLAYBACK_STATE]: PlaybackState
};
}

/**
* Returns required linked capabilities
* @return {Array}
*/
get requiredLinkedCapabilities() {
return [{ name: Capability.PLAYBACK_CONTROLLER, property: { name: Property.PLAYBACK } }];
}

/**
* Returns reportable properties
* @param {Array} items
* @param {Object} properties
* @return {Array}
*/
getReportableProperties(items, properties) {
const playbackState = properties[Property.PLAYBACK_STATE];
const playbackStopState = properties[`${Property.PLAYBACK_STATE}#${PlaybackState.TAG_STOP}`];

if (playbackStopState) {
const item = items.find((item) => item.name === playbackStopState.item.name);
const { state } = playbackStopState.getState(item?.state) || {};
// Return playback stop state property if in stopped state
if (state === PlaybackState.STOPPED) {
return [playbackStopState];
}
}

// Return playback state property otherwise
return [playbackState];
}
}
2 changes: 1 addition & 1 deletion lambda/alexa/smarthome/capabilities/safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default class Safety extends AlexaCapability {
*/
get requiredLinkedCapabilities() {
// Requires open state attribute mode controller capability to be linked
return [{ name: Capability.MODE_CONTROLLER, instance: OpenState.name }];
return [{ name: Capability.MODE_CONTROLLER, instance: OpenState.name, property: { name: Property.MODE } }];
}

/**
Expand Down
4 changes: 4 additions & 0 deletions lambda/alexa/smarthome/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const Capability = Object.freeze({
NETWORKING_HOME_NETWORK_CONTROLLER: 'NetworkingHomeNetworkController',
PERCENTAGE_CONTROLLER: 'PercentageController',
PLAYBACK_CONTROLLER: 'PlaybackController',
PLAYBACK_STATE_REPORTER: 'PlaybackStateReporter',
POWER_CONTROLLER: 'PowerController',
POWER_LEVEL_CONTROLLER: 'PowerLevelController',
RANGE_CONTROLLER: 'RangeController',
Expand Down Expand Up @@ -72,6 +73,7 @@ export const Interface = Object.freeze({
ALEXA_NETWORKING_HOME_NETWORK_CONTROLLER: 'Alexa.Networking.HomeNetworkController',
ALEXA_PERCENTAGE_CONTROLLER: 'Alexa.PercentageController',
ALEXA_PLAYBACK_CONTROLLER: 'Alexa.PlaybackController',
ALEXA_PLAYBACK_STATE_REPORTER: 'Alexa.PlaybackStateReporter',
ALEXA_POWER_CONTROLLER: 'Alexa.PowerController',
ALEXA_POWER_LEVEL_CONTROLLER: 'Alexa.PowerLevelController',
ALEXA_RANGE_CONTROLLER: 'Alexa.RangeController',
Expand Down Expand Up @@ -116,6 +118,8 @@ export const Property = Object.freeze({
PERCENTAGE: 'percentage',
PLAYBACK: 'playback',
PLAYBACK_ACTION: 'playbackAction',
PLAYBACK_STATE: 'playbackState',
PLAYBACK_STEP: 'playbackStep',
PLAYBACK_STOP: 'playbackStop',
POWER_LEVEL: 'powerLevel',
POWER_STATE: 'powerState',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ export default class CurrentLockState extends LockState {
* @type {String}
*/
static get tag() {
return DecoupleState.TAG_NAME;
return DecoupleState.TAG_SENSOR;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ export default class CurrentOpenState extends OpenState {
* @type {String}
*/
static get tag() {
return DecoupleState.TAG_NAME;
return DecoupleState.TAG_SENSOR;
}
}
1 change: 1 addition & 0 deletions lambda/alexa/smarthome/device/attributes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export { default as ObstacleAlert } from './obstacleAlert.js';
export { default as OpenState } from './openState.js';
export { default as Percentage } from './percentage.js';
export { default as Playback } from './playback.js';
export { default as PlaybackStep } from './playbackStep.js';
export { default as PlaybackStop } from './playbackStop.js';
export { default as PositionState } from './positionState.js';
export { default as PowerLevel } from './powerLevel.js';
Expand Down
4 changes: 2 additions & 2 deletions lambda/alexa/smarthome/device/attributes/openState.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default class OpenState extends DeviceAttribute {
case ItemType.CONTACT:
case ItemType.NUMBER:
case ItemType.STRING:
if (this.tag !== DecoupleState.TAG_NAME) return;
if (this.tag !== DecoupleState.TAG_SENSOR) return;
// fall through
case ItemType.SWITCH:
return [
Expand All @@ -83,7 +83,7 @@ export default class OpenState extends DeviceAttribute {
[OpenState.CLOSED]: [AlexaAssetCatalog.VALUE_CLOSE]
},
// Add semantic mappings map if not decouple state tagged
...(this.tag !== DecoupleState.TAG_NAME && {
...(this.tag !== DecoupleState.TAG_SENSOR && {
actionMappings: {
[AlexaActionSemantic.CLOSE]: OpenState.CLOSED,
[AlexaActionSemantic.OPEN]: OpenState.OPEN,
Expand Down
Loading

0 comments on commit 87099ab

Please sign in to comment.