Skip to content

Commit

Permalink
Add Alexa integration with Gladys Plus (#1396)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pierre-Gilles authored May 20, 2022
1 parent d45a7aa commit 1feac09
Show file tree
Hide file tree
Showing 32 changed files with 1,996 additions and 18 deletions.
12 changes: 6 additions & 6 deletions front/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"prettier": "^1.17.1"
},
"dependencies": {
"@gladysassistant/gladys-gateway-js": "^3.9.0",
"@gladysassistant/gladys-gateway-js": "^4.2.0",
"@gladysassistant/theme-optimized": "^1.0.3",
"@jaames/iro": "^5.5.2",
"@yaireo/tagify": "^4.5.0",
Expand Down
2 changes: 2 additions & 0 deletions front/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import GatewayForgotPassword from '../routes/gateway-forgot-password';
import GatewayResetPassword from '../routes/gateway-reset-password';
import GatewayConfirmEmail from '../routes/gateway-confirm-email';
import GoogleHomeGateway from '../routes/integration/all/google-home-gateway';
import AlexaGateway from '../routes/integration/all/alexa-gateway';

import SignupWelcomePage from '../routes/signup/1-welcome';
import SignupCreateAccountLocal from '../routes/signup/2-create-account-local';
Expand Down Expand Up @@ -234,6 +235,7 @@ const AppRouter = connect(
<BluetoothSettingsPage path="/dashboard/integration/device/bluetooth/config" />

<GoogleHomeGateway path="/dashboard/integration/device/google-home/authorize" />
<AlexaGateway path="/dashboard/integration/device/alexa/authorize" />

<ChatPage path="/dashboard/chat" />
<MapPage path="/dashboard/maps" />
Expand Down
3 changes: 2 additions & 1 deletion front/src/components/header/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const PAGES_WITHOUT_HEADER = [
'/subscribe-gateway',
'/gateway-configure-two-factor',
'/confirm-email',
'/dashboard/integration/device/google-home/authorize'
'/dashboard/integration/device/google-home/authorize',
'/dashboard/integration/device/alexa/authorize'
];

const Header = ({ ...props }) => {
Expand Down
14 changes: 14 additions & 0 deletions front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,20 @@
"cancelButton": "Cancel",
"connectButton": "Link"
},
"alexa": {
"title": "Gladys Assistant",
"cardTitle": "Do you want to connect Gladys Assistant to Amazon Alexa?",
"description": "By signin in, you are authorizing Alexa to access your devices.",
"error": "An error occured. Please retry !",
"connectedAs": "Connected as",
"googleWillBeAble": "Alexa will be able:",
"seeDevices": "List all your devices",
"controlDevices": "Control all your devices",
"getNewDeviceValues": "Periodically refresh your device states",
"privacyPolicy": "Read Alexa's <a href=\"https://www.alexa.com/help/privacy\">privacy policy</a>.",
"cancelButton": "Cancel",
"connectButton": "Link"
},
"zwave": {
"title": "Z-Wave",
"description": "Control your Z-Wave devices.",
Expand Down
14 changes: 14 additions & 0 deletions front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,20 @@
"cancelButton": "Annuler",
"connectButton": "Lier"
},
"alexa": {
"title": "Gladys Assistant",
"cardTitle": "Voulez-vous connecter Gladys Assistant à Amazon Alexa ?",
"description": "En vous connectant, vous authorizez Alexa à accéder à vos appareils.",
"error": "Une erreur est survenue. Merci de réessayer.",
"connectedAs": "Connecté en tant que",
"googleWillBeAble": "Amazon Alexa pourra :",
"seeDevices": "Lister les appareils présents chez vous",
"controlDevices": "Contrôler vos appareils",
"getNewDeviceValues": "Récupérer périodiquement les états de vos appareils",
"privacyPolicy": "Lire la <a href=\"https://www.alexa.com/help/privacy\">politique de confidentialité d'Alexa</a>.",
"cancelButton": "Annuler",
"connectButton": "Lier"
},
"zwave": {
"title": "Z-Wave",
"description": "Contrôlez vos appareils Z-Wave.",
Expand Down
15 changes: 15 additions & 0 deletions front/src/routes/integration/all/alexa-gateway/Layout.jsx
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 front/src/routes/integration/all/alexa-gateway/index.js
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);
3 changes: 3 additions & 0 deletions front/src/routes/integration/all/alexa-gateway/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.colWidth {
max-width: 35rem;
}
117 changes: 117 additions & 0 deletions server/lib/gateway/gateway.forwardDeviceStateToAlexa.js
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,
};
Loading

0 comments on commit 1feac09

Please sign in to comment.