-
-
Notifications
You must be signed in to change notification settings - Fork 288
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Alexa integration with Gladys Plus (#1396)
- Loading branch information
1 parent
d45a7aa
commit 1feac09
Showing
32 changed files
with
1,996 additions
and
18 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
const Layout = ({ children }) => ( | ||
<div class="page"> | ||
<div class="page-main"> | ||
<div class="my-3 my-md-5"> | ||
<div class="container"> | ||
<div class="row"> | ||
<div class="col-lg-12">{children}</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
|
||
export default Layout; |
135 changes: 135 additions & 0 deletions
135
front/src/routes/integration/all/alexa-gateway/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { Component } from 'preact'; | ||
import { connect } from 'unistore/preact'; | ||
import cx from 'classnames'; | ||
import { Text, Localizer, MarkupText } from 'preact-i18n'; | ||
import Layout from './Layout'; | ||
import style from './style.css'; | ||
|
||
class AlexaGateway extends Component { | ||
cancel = async e => { | ||
e.preventDefault(); | ||
await this.setState({ loading: true }); | ||
if (this.props.redirect_uri && this.props.state) { | ||
const redirectUrl = `${this.props.redirect_uri}?state=${this.props.state}&error=cancelled`; | ||
window.location.replace(redirectUrl); | ||
} else { | ||
this.setState({ loading: false, error: true }); | ||
} | ||
}; | ||
link = async e => { | ||
e.preventDefault(); | ||
try { | ||
await this.setState({ loading: true, error: false }); | ||
const responseAuthorize = await this.props.session.gatewayClient.alexaAuthorize({ | ||
client_id: this.props.client_id, | ||
redirect_uri: this.props.redirect_uri, | ||
state: this.props.state | ||
}); | ||
window.location.replace(responseAuthorize.redirectUrl); | ||
} catch (e) { | ||
await this.setState({ loading: false, error: true }); | ||
console.error(e); | ||
if (this.props.redirect_uri && this.props.state) { | ||
const redirectUrl = `${this.props.redirect_uri}?state=${this.props.state}&error=errored`; | ||
window.location.replace(redirectUrl); | ||
} | ||
} | ||
}; | ||
|
||
render(props, { loading, error }) { | ||
return ( | ||
<Layout> | ||
<div class="container mt-4"> | ||
<div class="row"> | ||
<div class={cx('col mx-auto', style.colWidth)}> | ||
<div class="text-center mb-6"> | ||
<h2> | ||
<Localizer> | ||
<img | ||
src="/assets/icons/favicon-96x96.png" | ||
class="header-brand-img" | ||
alt={<Text id="global.logoAlt" />} | ||
/> | ||
</Localizer> | ||
<Text id="integration.alexa.title" /> | ||
</h2> | ||
</div> | ||
<form class="card"> | ||
<div class="card-body p-6"> | ||
<div class="card-title"> | ||
<h3> | ||
<Text id="integration.alexa.cardTitle" /> | ||
</h3> | ||
</div> | ||
|
||
<div | ||
class={cx('dimmer', { | ||
active: loading | ||
})} | ||
> | ||
<div class="loader" /> | ||
<div class="dimmer-content"> | ||
{error && ( | ||
<p class="alert alert-danger"> | ||
<Text id="integration.alexa.error" /> | ||
</p> | ||
)} | ||
<p> | ||
<Text id="integration.alexa.description" /> | ||
</p> | ||
|
||
<p> | ||
<Text id="integration.alexa.connectedAs" /> <b>{props.user && props.user.email}</b> | ||
</p> | ||
|
||
<div class="form-group"> | ||
<h4> | ||
<Text id="integration.alexa.googleWillBeAble" /> | ||
</h4> | ||
<ul class="list-unstyled leading-loose"> | ||
<li> | ||
<i class="fe fe-check text-success mr-2" aria-hidden="true" />{' '} | ||
<Text id="integration.alexa.seeDevices" /> | ||
</li> | ||
<li> | ||
<i class="fe fe-check text-success mr-2" aria-hidden="true" />{' '} | ||
<Text id="integration.alexa.controlDevices" /> | ||
</li> | ||
<li> | ||
<i class="fe fe-check text-success mr-2" aria-hidden="true" />{' '} | ||
<Text id="integration.alexa.getNewDeviceValues" /> | ||
</li> | ||
</ul> | ||
</div> | ||
|
||
<p> | ||
<MarkupText id="integration.alexa.privacyPolicy" /> | ||
</p> | ||
|
||
<div class="form-footer"> | ||
<div class="row"> | ||
<div class="col-6"> | ||
<button class="btn btn-secondary btn-block" onClick={this.cancel}> | ||
<Text id="integration.alexa.cancelButton" /> | ||
</button> | ||
</div> | ||
<div class="col-6"> | ||
<button class="btn btn-primary btn-block" onClick={this.link}> | ||
<Text id="integration.alexa.connectButton" /> | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</form> | ||
</div> | ||
</div> | ||
</div> | ||
</Layout> | ||
); | ||
} | ||
} | ||
|
||
export default connect('user,session', {})(AlexaGateway); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.colWidth { | ||
max-width: 35rem; | ||
} |
117 changes: 117 additions & 0 deletions
117
server/lib/gateway/gateway.forwardDeviceStateToAlexa.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
const get = require('get-value'); | ||
const uuid = require('uuid'); | ||
|
||
const logger = require('../../utils/logger'); | ||
const { EVENTS } = require('../../utils/constants'); | ||
const { mappings, readValues } = require('../../services/alexa/lib/deviceMappings'); | ||
const { syncDeviceConverter } = require('../../services/alexa/lib/syncDeviceConverter'); | ||
|
||
// eslint-disable-next-line jsdoc/require-returns | ||
/** | ||
* @description send a current state to google | ||
* @param {Object} stateManager - The state manager. | ||
* @param {Object} gladysGatewayClient - The gladysGatewayClient. | ||
* @param {string} deviceFeatureSelector - The selector of the device feature to send. | ||
* @example | ||
* sendCurrentState(stateManager, 'light'); | ||
*/ | ||
async function sendCurrentState(stateManager, gladysGatewayClient, deviceFeatureSelector) { | ||
logger.debug(`Gladys Gateway: Forwarding state to Alexa: ${deviceFeatureSelector}`); | ||
try { | ||
// if the event is a DEVICE.NEW_STATE event | ||
const gladysFeature = stateManager.get('deviceFeature', deviceFeatureSelector); | ||
const gladysDevice = stateManager.get('deviceById', gladysFeature.device_id); | ||
|
||
const device = syncDeviceConverter(gladysDevice); | ||
|
||
if (!device) { | ||
logger.debug(`Gladys Gateway: Not forwarding state, device feature doesnt seems handled.`); | ||
return; | ||
} | ||
|
||
const func = get(readValues, `${gladysFeature.category}.${gladysFeature.type}`); | ||
const mapping = get(mappings, `${gladysFeature.category}.capabilities.${gladysFeature.type}`); | ||
|
||
if (!func || !mapping) { | ||
logger.debug(`Gladys Gateway: Not forwarding state, device feature doesnt seems handled.`); | ||
return; | ||
} | ||
|
||
const now = new Date().toISOString(); | ||
|
||
const properties = [ | ||
{ | ||
namespace: mapping.interface, | ||
name: get(mapping, 'properties.supported.0.name'), | ||
value: func(gladysFeature.last_value), | ||
timeOfSample: now, | ||
uncertaintyInMilliseconds: 0, | ||
}, | ||
]; | ||
|
||
const payload = { | ||
event: { | ||
header: { | ||
namespace: 'Alexa', | ||
name: 'ChangeReport', | ||
messageId: uuid.v4(), | ||
payloadVersion: '3', | ||
}, | ||
endpoint: { | ||
endpointId: gladysDevice.selector, | ||
}, | ||
payload: { | ||
change: { | ||
cause: { | ||
type: 'PHYSICAL_INTERACTION', | ||
}, | ||
properties, | ||
}, | ||
}, | ||
}, | ||
context: { | ||
properties, | ||
}, | ||
}; | ||
|
||
await gladysGatewayClient.alexaReportState(payload); | ||
} catch (e) { | ||
logger.warn(`Gladys Gateway: Unable to forward alexa reportState`); | ||
logger.warn(e); | ||
} | ||
} | ||
|
||
/** | ||
* @description Forward websocket message to Gateway. | ||
* @param {Object} event - Websocket event. | ||
* @returns {Promise} - Resolve when finished. | ||
* @example | ||
* forwardWebsockets({ | ||
* type: '' | ||
* payload: {} | ||
* }); | ||
*/ | ||
async function forwardDeviceStateToAlexa(event) { | ||
if (!this.connected) { | ||
logger.debug('Gateway: not connected. Prevent forwarding device new state.'); | ||
return null; | ||
} | ||
if (!this.alexaConnected) { | ||
logger.debug('Gateway: Alexa not connected. Prevent forwarding device new state.'); | ||
return null; | ||
} | ||
if (event.type === EVENTS.DEVICE.NEW_STATE && event.device_feature) { | ||
if (this.forwardStateToAlexaTimeouts.has(event.device_feature)) { | ||
clearTimeout(this.forwardStateToAlexaTimeouts.get(event.device_feature)); | ||
} | ||
const newTimeout = setTimeout(() => { | ||
sendCurrentState(this.stateManager, this.gladysGatewayClient, event.device_feature); | ||
}, this.alexaForwardStateTimeout); | ||
this.forwardStateToAlexaTimeouts.set(event.device_feature, newTimeout); | ||
} | ||
return null; | ||
} | ||
|
||
module.exports = { | ||
forwardDeviceStateToAlexa, | ||
}; |
Oops, something went wrong.