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 Alexa integration with Gladys Plus #1396

Merged
merged 19 commits into from
May 20, 2022
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
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