Skip to content

Commit

Permalink
Merge branch 'release/v2.0.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
nikkow committed Aug 20, 2020
2 parents ffa8cf9 + c425440 commit f09c43e
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 76 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,7 @@ $RECYCLE.BIN/

# Windows shortcuts
*.lnk

# Misc
dist
.vscode
27 changes: 22 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
# Changelog
All notable changes to this project will be documented in this file.

## [2.0.2] - 2020-08-20

### Added

- Added error message when automatic session refresh is impossible (see [this article](https://github.com/nikkow/node-red-contrib-tahoma#i-received-a-session-expired-error-what-happned) for more information)
- Added support for blinds rotation for motors that support it ([#33](https://github.com/nikkow/node-red-contrib-tahoma/pull/33), thanks to [@marekhalmo](https://github.com/marekhalmo))
- Added a buffer when setting a custom position to consider close-enough values as valid ([#34](https://github.com/nikkow/node-red-contrib-tahoma/issues/34))

### Changed

- Fixed an issue that prevented tokens from being correctly refreshed
- Fixed an unhandled Promise rejection ([#31](https://github.com/nikkow/node-red-contrib-tahoma/issues/31))

### Refactoring

- Refactored the network layer to retrieve credentials from memory, instead of global context.

## [2.0.1] - 2020-03-01

### Changed

- Fix publication to NPM registry issue (See [#20](https://github.com/nikkow/node-red-contrib-tahoma/issues/20))
- Fix publication to NPM registry issue ([#20](https://github.com/nikkow/node-red-contrib-tahoma/issues/20))

## [2.0.0] - 2020-03-01

Expand Down Expand Up @@ -33,17 +50,17 @@ All notable changes to this project will be documented in this file.

### Changed

- Fix missing break statement (See [#8](https://github.com/nikkow/node-red-contrib-tahoma/pull/8), thanks to [@taucher4000](https://github.com/taucher4000))
- Fix missing break statement ([#8](https://github.com/nikkow/node-red-contrib-tahoma/pull/8), thanks to [@taucher4000](https://github.com/taucher4000))
- Fix login strategy, preventing "Too Many Requests" error returned by Somfy (See [#9](https://github.com/nikkow/node-red-contrib-tahoma/issues/9))

## [0.2.0] - 2018-10-21

### Added

- New node `tahoma-read` (See [#6](https://github.com/nikkow/node-red-contrib-tahoma/issues/6))
- New `stop` action to immediatly stop the current action on the devices (See [#5](https://github.com/nikkow/node-red-contrib-tahoma/pull/5), thanks to [@Genosse274](https://github.com/Genosse274))
- New node `tahoma-read` ([#6](https://github.com/nikkow/node-red-contrib-tahoma/issues/6))
- New `stop` action to immediatly stop the current action on the devices ([#5](https://github.com/nikkow/node-red-contrib-tahoma/pull/5), thanks to [@Genosse274](https://github.com/Genosse274))
- New CHANGELOG.md file to keep track of all updates.

### Changed

- Fix path in getSetup() (See [#7](https://github.com/nikkow/node-red-contrib-tahoma/pull/7), thanks to [@hobbyquaker](https://github.com/hobbyquaker))
- Fix path in getSetup() ([#7](https://github.com/nikkow/node-red-contrib-tahoma/pull/7), thanks to [@hobbyquaker](https://github.com/hobbyquaker))
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ This node accepts an object as input. The following properties will be parsed:
| -------- | ---- | --------- | ----------- |
| `action` | enum (see below) | **Yes** | The action to perform |
| `position` | int (0-100) | *No* | The position you want to set your blinds/door to |
| `rotation` | int (0-100) | *No* | The rotation you want to set your blinds to |
| `lowspeed` | boolean | *No* | Should the action be triggered in low-speed mode? |

#### Actions
Expand All @@ -59,6 +60,7 @@ Currently, only a few commands are understood by this node. The possible values
* `close`: This will close the device
* `stop`: This will stop all running actions
* `customPosition`: This will set the device to a custom position. The position is passed using the `position` property, which is required in this mode.
* `customRotation`: This will set the device (blinds) to a custom rotation. The rotation is passed using the `orientation` property, which is required in this mode.

#### Output

Expand All @@ -70,7 +72,8 @@ The node will output its original `msg.payload` enriched with the result of the

### Node `tahoma-read`

This node does not accept any input. You can specify the desired device by editing the node properties.
This node will ignore all data provided as input. You can specify the desired device by editing the node properties.
(Note: you can still trigger a `tahoma-read` instuction periodically by using an `inject` node. See [#28](https://github.com/nikkow/node-red-contrib-tahoma/issues/28#issuecomment-615755280))

#### Output

Expand Down
2 changes: 1 addition & 1 deletion 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 package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-red-contrib-tahoma",
"version": "2.0.1",
"version": "2.0.2",
"description": "Control a Somfy Tahoma box from Node RED",
"main": "index.js",
"scripts": {
Expand Down
34 changes: 15 additions & 19 deletions src/core/somfy-api.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import { Red } from 'node-red';
import axios, { AxiosRequestConfig, AxiosResponse, AxiosInstance, AxiosError } from 'axios';
import { INetworkError } from '../interfaces/network-error';
import { ICommand } from '../interfaces/command';
import { IDevice } from '../interfaces/device';
import { ICommandExecutionResponse } from '../interfaces/command-execution-response';
import { HttpResponse } from '../enums/http-response.enum';
import { Node } from 'node-red';

export class SomfyApi {
private static SOMFY_BASE_URL: string = 'https://api.somfy.com/api/v1';
private static SOMFY_AUTH_URL: string = 'https://accounts.somfy.com/oauth/oauth/v2';
private static HTTP_OK: number = 200;
private static HTTP_UNAUTHORIZED: number = 401;
private static HTTP_BAD_REQUEST: number = 400;
private context;
private configNode: Node;
private axiosInstance: AxiosInstance;

constructor(private readonly RED: Red, context: any, private readonly account: string) {
this.context = context;
constructor(configNode: Node) {
this.axiosInstance = axios.create();

const configNode = this.RED.nodes.getNode(account) as any; // TODO: Type this
this.configNode = configNode;

this.axiosInstance.interceptors.request.use(
(request: AxiosRequestConfig) => {
Expand All @@ -32,11 +28,11 @@ export class SomfyApi {
return response;
},
(error: INetworkError) => {
if (error.response.status !== SomfyApi.HTTP_UNAUTHORIZED) {
if (error.response.status !== HttpResponse.UNAUTHORIZED) {
return Promise.reject(error);
}

const refreshTokenUrl = `${SomfyApi.SOMFY_AUTH_URL}/token?client_id=${configNode.apikey}&client_secret=${configNode.apisecret}&grant_type=refresh_token&refresh_token=${this.getRefreshToken()}`;
const refreshTokenUrl = `${SomfyApi.SOMFY_AUTH_URL}/token?client_id=${this.configNode['apikey']}&client_secret=${this.configNode['apisecret']}&grant_type=refresh_token&refresh_token=${this.getRefreshToken()}`;

return axios({
url: refreshTokenUrl,
Expand All @@ -47,20 +43,20 @@ export class SomfyApi {
}
})
.then(response => {
this.context().global.set('somfy_api_access_token', response.data.access_token);
this.context().global.set('somfy_api_refresh_token', response.data.refresh_token);
this.configNode['accesstoken'] = response.data.access_token;
this.configNode['refreshtoken'] = response.data.refresh_token;

this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${response.data.access_token}`;

error.hasRefreshedToken = true;
return Promise.reject(error);
})
.catch((refreshTokenRequestError: AxiosError) => {
if (refreshTokenRequestError.response.status === SomfyApi.HTTP_BAD_REQUEST) {
if (refreshTokenRequestError.response.status === HttpResponse.BAD_REQUEST) {
const refreshTokenRequestErrorData = refreshTokenRequestError.response.data;
if (refreshTokenRequestErrorData.hasOwnProperty('message') && refreshTokenRequestErrorData.message === 'error.invalid.grant') {
this.context().global.set('somfy_api_access_token', null);
this.context().global.set('somfy_api_refresh_token', null);
this.configNode['accesstoken'] = null;
this.configNode['refreshtoken'] = null;
error.isRefreshTokenExpired = true;
}
}
Expand All @@ -74,7 +70,7 @@ export class SomfyApi {
private _request(options: AxiosRequestConfig): Promise<any> {
return this.axiosInstance(options)
.then((response: AxiosResponse) => {
if (response.status !== SomfyApi.HTTP_OK) {
if (response.status !== HttpResponse.OK) {
return 'http_error';
}

Expand All @@ -86,11 +82,11 @@ export class SomfyApi {
}

private getAccessToken(): string {
return this.context().global.get('somfy_api_access_token');
return this.configNode['accesstoken'];
}

private getRefreshToken(): string {
return this.context().global.get('somfy_api_refresh_token');
return this.configNode['refreshtoken'];
}

public getDevice(device: string): Promise<IDevice> {
Expand Down
6 changes: 6 additions & 0 deletions src/enums/http-response.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum HttpResponse {
OK = 200,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
SERVER_ERROR = 500
}
2 changes: 1 addition & 1 deletion src/interfaces/command-execution-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface ICommandExecutionResponse {

export interface ICommandExecutionFinalState {
finished: boolean;
tahomabox?: string;
account?: string;
device?: string;
expectedState?: object;
jobId?: string;
Expand Down
25 changes: 11 additions & 14 deletions src/nodes/tahoma-config.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,44 @@
import { Red } from 'node-red';
import * as fs from 'fs';
import { SomfyApi } from '../core/somfy-api';
import { HttpResponse } from '../enums/http-response.enum';

export = (RED: Red) => {
RED.nodes.registerType('tahoma-config', function(this, props: any ) {
// const config = props as ISomfyCredentialsProperties;
RED.nodes.createNode(this, props);
this.apikey = props.apikey;
this.apisecret = props.apisecret;
this.accesstoken = props.accesstoken;
this.refreshtoken = props.refreshtoken;

this.context().global.set('somfy_api_access_token', this.accesstoken);
this.context().global.set('somfy_api_refresh_token', this.refreshtoken);
});

RED.httpAdmin.get('/somfy/callback', (request, response) => {
const CALLBACK_BODY = fs.readFileSync(__dirname + '/somfy-callback.html');
const callbackBody = fs.readFileSync(__dirname + '/somfy-callback.html');
response.header('Content-Type', 'text/html');
response.write(CALLBACK_BODY.toString());
response.write(callbackBody.toString());
response.send();
});

RED.httpAdmin.get('/somfy/:account/sites', function (req, res) {
const configNode = RED.nodes.getNode(req.params.account) as any;
const somfyApiClient = new SomfyApi(RED, configNode.context, req.params.account);
const configNode = RED.nodes.getNode(req.params.account);
const somfyApiClient = new SomfyApi(configNode);

somfyApiClient.getSites()
.then((sites: any) => res.json(sites))
.catch(() => {
res.status(500);
.catch((error) => {
res.status(error.isRefreshTokenExpired ? HttpResponse.UNAUTHORIZED : HttpResponse.SERVER_ERROR);
res.send();
});
});

RED.httpAdmin.get('/somfy/:account/site/:siteid/devices', function (req, res) {
const configNode = RED.nodes.getNode(req.params.account) as any;
const somfyApiClient = new SomfyApi(RED, configNode.context, req.params.account);
const configNode = RED.nodes.getNode(req.params.account);
const somfyApiClient = new SomfyApi(configNode);

somfyApiClient.getDevicesForSite(req.params.siteid)
.then((devices: any) => res.json(devices))
.catch(() => {
res.status(500);
.catch((error) => {
res.status(error.isRefreshTokenExpired ? HttpResponse.UNAUTHORIZED : HttpResponse.SERVER_ERROR);
res.send();
});
});
Expand Down
8 changes: 7 additions & 1 deletion src/nodes/tahoma-read.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
$("#node-input-site").prop('disabled', false);
loadDevices();
}, function(error) {
if(error.status === 401) { $("#_session_exp_error").show(); }
$("#node-input-site").empty().prop('disabled', true).html(`<option value="${selected_site}">${selected_site}</option>`);
$("#node-input-device").empty().prop('disabled', true).html(`<option value="${selected_device}">${selected_device}</option>`);;
});
Expand Down Expand Up @@ -81,6 +82,7 @@
$("#node-input-device").empty().html(html);
$("#node-input-device").prop('disabled', false);
}, function(error) {
if(error.status === 401) { $("#_session_exp_error").show(); }
$("#node-input-device").empty().prop('disabled', true).html(`<option value="${selected_device}">${selected_device}</option>`);;
});
}
Expand All @@ -93,13 +95,17 @@
</script>

<script type="text/x-red" data-template-name="tahoma-read">
<div style="font-weight: normal; color: #AD1625; margin-bottom: 15px; display: none;" id="_session_exp_error">
<b><i class="fa fa-tag"></i> Session Expired</b><br />
Your session has expired and could not be automatically refreshed. This happens after a long period of inactivity. You can check <a href="https://github.com/nikkow/node-red-contrib-tahoma#i-received-a-session-expired-error-what-happned" target="_blank">this article</a> for more information.
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
<div class="form-row">
<label for="node-input-tahomabox"><i class="icon-tag"></i> Account</label>
<input type="text" id="node-input-tahomabox">
<input type="text" id="node-input-tahomabox">
</div>
<div class="form-row">
<label for="node-input-site"><i class="icon-tag"></i> Site</label>
Expand Down
6 changes: 4 additions & 2 deletions src/nodes/tahoma-read.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SomfyApi } from '../core/somfy-api';
import { NodeProperties, Red } from 'node-red';
import { NodeProperties, Red, Node } from 'node-red';
import { INodeConfiguration } from '../interfaces/node-config';
import { IMessage } from '../interfaces/message';
import { INetworkError } from '../interfaces/network-error';
Expand All @@ -15,7 +15,9 @@ export = (RED: Red) => {
this.tahomabox = config.tahomabox;

this.on('input', (msg: IMessage) => {
const somfyApiClient = new SomfyApi(RED, this.context, this.tahomabox);
const configNode = RED.nodes.getNode(this.tahomabox) as Node;
const somfyApiClient = new SomfyApi(configNode);

somfyApiClient.getDevice(this.device)
.then((deviceData) => {
msg.payload = deviceData;
Expand Down
6 changes: 6 additions & 0 deletions src/nodes/tahoma.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
$("#node-input-site").prop('disabled', false);
loadDevices();
}, function(error) {
if(error.status === 401) { $("#_session_exp_error").show(); }
$("#node-input-site").empty().prop('disabled', true).html(`<option value="${selected_site}">${selected_site}</option>`);
$("#node-input-device").empty().prop('disabled', true).html(`<option value="${selected_device}">${selected_device}</option>`);;
});
Expand Down Expand Up @@ -82,6 +83,7 @@
$("#node-input-device").empty().html(html);
$("#node-input-device").prop('disabled', false);
}, function(error) {
if(error.status === 401) { $("#_session_exp_error").show(); }
$("#node-input-device").empty().prop('disabled', true).html(`<option value="${selected_device}">${selected_device}</option>`);;
});
}
Expand All @@ -94,6 +96,10 @@
</script>

<script type="text/x-red" data-template-name="tahoma">
<div style="font-weight: normal; color: #AD1625; margin-bottom: 15px; display: none;" id="_session_exp_error">
<b><i class="fa fa-tag"></i> Session Expired</b><br />
Your session has expired and could not be automatically refreshed. This happens after a long period of inactivity. You can check <a href="https://github.com/nikkow/node-red-contrib-tahoma#i-received-a-session-expired-error-what-happned" target="_blank">this article</a> for more information.
</div>
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
<input type="text" id="node-input-name">
Expand Down
Loading

0 comments on commit f09c43e

Please sign in to comment.