Skip to content

Commit

Permalink
feat(api): New node for direct access to api
Browse files Browse the repository at this point in the history
New node to have direct access to home assistant api either by http or
websocket
  • Loading branch information
zachowj committed Mar 7, 2019
1 parent 635aa48 commit ed7341a
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 7 deletions.
23 changes: 23 additions & 0 deletions lib/base-node.js
Expand Up @@ -144,6 +144,29 @@ class BaseNode {
this.node.status(opts);
}

setStatusSuccess(text = 'Success') {
this.status({
fill: 'green',
shape: 'dot',
text: `${text} called at: ${this.getPrettyDate()}`
});
}

setStatusSending(text = 'Sending') {
this.status({
fill: 'yellow',
shape: 'dot',
text: `${text} at: ${this.getPrettyDate()}`
});
}
setStatusFailed(text = 'Failed') {
this.status({
fill: 'red',
shape: 'ring',
text: `${text} at: ${this.getPrettyDate()}`
});
}

updateConnectionStatus(additionalText) {
this.setConnectionStatus(this.connectionState, additionalText);
}
Expand Down
6 changes: 6 additions & 0 deletions lib/ha-websocket.js
Expand Up @@ -211,6 +211,12 @@ class HaWebsocket extends EventEmitter {
return result;
}

async send(data) {
const response = await this.client.sendMessagePromise(data);

return response;
}

/*
* Pretty much a copy from https://github.com/home-assistant/home-assistant-js-websocket
*/
Expand Down
2 changes: 1 addition & 1 deletion lib/node-home-assistant.js
Expand Up @@ -23,7 +23,7 @@ class HomeAssistant {
constructor(config) {
debug('Instantiating HomeAssistant');
this.config = Object.assign({}, DEFAULTS, config);
this.api = new HaHttp(this.config);
this.http = new HaHttp(this.config);
this.websocket = new HaWebsocket(this.config);
}

Expand Down
179 changes: 179 additions & 0 deletions nodes/api/api.html
@@ -0,0 +1,179 @@
<script type="text/javascript">
RED.nodes.registerType("ha-api", {
category: "home_assistant",
color: "#52C0F2",
inputs: 1,
outputs: 1,
icon: "server.png",
paletteLabel: "API",
label: function() {
return this.name || "API";
},
defaults: {
name: { value: "" },
server: { value: "", type: "server", required: true },
protocol: { value: "websocket" },
method: { value: "get" },
path: { value: "" },
data: { value: "" },
location: { value: "payload" },
locationType: { value: "msg" }
},
oneditprepare: function() {
const node = this;
const $server = $("#node-input-server");
const utils = {
setDefaultServerSelection: function() {
let defaultServer;
RED.nodes.eachConfig(n => {
if (n.type === "server" && !defaultServer) defaultServer = n.id;
});
if (defaultServer) $server.val(defaultServer);
}
};

if (!node.server) {
utils.setDefaultServerSelection();
}

$("#node-input-data")
.typedInput({
types: [
{
value: "json",
label: "JSON",
icon: "red/images/typedInput/json.png",
validate: function(v) {
if (!v) return true;
try {
JSON.parse(v);
return true;
} catch (e) {
return false;
}
},
expand: function() {
const that = this;
const value = this.value();
try {
value = JSON.stringify(JSON.parse(value), null, 4);
} catch (err) {}
RED.editor.editJSON({
value: value,
complete: function(v) {
const value = v;
try {
value = JSON.stringify(JSON.parse(v));
} catch (err) {}
that.value(value);
}
});
}
}
]
})
.typedInput("width", "68%");

$("#node-input-protocol")
.on("change", function() {
const isHttp = $(this).val() === "http";
$(".http").toggle(isHttp);
$("#node-input-method").trigger("change");
})
.trigger("change");

$("#node-input-method").on("change", function() {
const label =
$("#node-input-protocol").val() === "http" &&
$("#node-input-method").val() === "get"
? "Params"
: "Data";
$("#data-label").text(label);
});

$("#node-input-location")
.typedInput({
types: [
"msg",
"flow",
"global",
{ value: "none", label: "None", hasValue: false }
],
typeField: "#node-input-locationType"
})
.typedInput("width", "68%");
}
});
</script>

<script type="text/x-red" data-template-name="ha-api">
<div class="form-row">
<label for="node-input-name">Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>

<div class="form-row">
<label for="node-input-server">Server</label>
<input type="text" id="node-input-server" />
</div>

<div class="form-row">
<label for="node-input-protocol">Protocol</label>
<select type="text" id="node-input-protocol" style="width:70%;">
<option value="websocket">WebSocket</option>
<option value="http">HTTP</option>
</select>
</div>

<div class="form-row http">
<label for="node-input-method">Method</label>
<select type="text" id="node-input-method" style="width:70%;">
<option value="get">GET</option>
<option value="post">POST</option>
</select>
</div>

<div class="form-row http">
<label for="node-input-path">Path</label>
<input type="text" id="node-input-path" placeholder="config">
</div>

<div class="form-row">
<label id="data-label" for="node-input-data">Data</label>
<input type="text" id="node-input-data">
</div>

<div class="form-row">
<label for="node-input-location">Results</label>
<input type="text" id="node-input-location">
</div>
</script>

<script type="text/x-red" data-help-name="ha-api">
<h3>Configuration</h3>
<dl class="message-properties">
<dt>Protocol<span class="property-type">[websocket|http] string</span></dt>
<dd>Protocol to use to access Home Assistant API.</dd>

<dt>Method<span class="property-type">[get|post] string</span></dt>
<dd>Type of method to use to access the HTTP endpoint.</dd>

<dt>Path<span class="property-type">string</span></dt>
<dd>URL of the API endpoint.</dd>

<dt>Params<span class="property-type">JSON</span></dt>
<dd>JSON object with key/value pairs that will be converted into URL parameters.</dd>

<dt>Data<span class="property-type">JSON</span></dt>
<dd>JSON Object to send for WebSocket requests and HTTP posts.</dd>

<dt>Results<span class="property-type">string</span></dt>
<dd>Location to saved the API results.</dd>

<dt>Templates</dt>
<dd>Templates can be used in path, params and data fields.</dd>
</dl>

<h3>Outputs</h3>
Will output the results received from the API call to the location defined in the config.
</script>
117 changes: 117 additions & 0 deletions nodes/api/api.js
@@ -0,0 +1,117 @@
const RenderTemplate = require('../../lib/mustache-context');
const BaseNode = require('../../lib/base-node');

module.exports = function(RED) {
const nodeOptions = {
config: {
name: {},
server: { isNode: true },
protocol: {},
method: {},
path: {},
data: {},
location: {},
locationType: {}
}
};
class ApiNode extends BaseNode {
constructor(nodeDefinition) {
super(nodeDefinition, RED, nodeOptions);
}

onInput({ message }) {
const node = this;
const config = node.nodeConfig;
const serverName = node.utils.toCamelCase(config.server.name);
const data = RenderTemplate(
config.data,
message,
node.node.context(),
serverName
);
let apiCall;

if (config.protocol === 'http') {
const path = RenderTemplate(
config.path,
message,
node.node.context(),
serverName
).replace(/^\/(?:api\/)?/, '');

if (!path) {
node.error('HTTP request requires a valid path.');
return;
}

if (!['get', 'post'].includes(config.method)) {
node.error('HTTP request requires a valid method');
return;
}

apiCall = config.server.http[`_${config.method}`].bind(
config.server.http,
path,
data
);
} else {
try {
const json = JSON.parse(data);

if (!json.type) {
node.error(
`A WebSocket request requires a 'type' property in the data object.`
);
return null;
}

apiCall = config.server.websocket.send.bind(
config.server.websocket,
json
);
} catch (e) {
node.error(e.message);
node.setStatusFailed();
return;
}
}

node.setStatusSending();

return apiCall()
.then(results => {
node.setStatusSuccess(config.protocol);

const contextKey = RED.util.parseContextStore(
config.location
);
contextKey.key = contextKey.key || 'payload';
const locationType = config.location_type || 'msg';

if (locationType === 'flow' || locationType === 'global') {
node.node
.context()
[locationType].set(
contextKey.key,
results,
contextKey.store
);
} else if (locationType === 'msg') {
message[contextKey.key] = results;
}

node.send(message);
})
.catch(err => {
node.error(
'API Error. ' + err.message
? `Error Message: ${err.message}`
: ''
);
node.setStatusFailed('API Error');
});
}
}

RED.nodes.registerType('ha-api', ApiNode);
};
Binary file added nodes/api/icons/server.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion nodes/config-server/config-server.js
Expand Up @@ -158,7 +158,7 @@ module.exports = function(RED) {
.rejectUnauthorizedCerts,
connectionDelay: this.nodeConfig.connectionDelay
});
this.api = this.homeAssistant.api;
this.http = this.homeAssistant.http;
this.websocket = this.homeAssistant.websocket;

this.homeAssistant
Expand Down
2 changes: 1 addition & 1 deletion nodes/fire-event/fire-event.js
Expand Up @@ -66,7 +66,7 @@ module.exports = function(RED) {
text: `Sending at: ${this.getPrettyDate()}`
});

return this.nodeConfig.server.api
return this.nodeConfig.server.http
.fireEvent(eventType, eventData)
.then(() => {
this.status({
Expand Down
4 changes: 2 additions & 2 deletions nodes/get-history/get-history.js
Expand Up @@ -92,15 +92,15 @@ module.exports = function(RED) {

let apiRequest =
entityidtype.value === 'includes' && entityid
? this.nodeConfig.server.api.getHistory(
? this.nodeConfig.server.http.getHistory(
startdate,
null,
enddate,
{
include: new RegExp(entityid)
}
)
: this.nodeConfig.server.api.getHistory(
: this.nodeConfig.server.http.getHistory(
startdate,
entityid,
enddate
Expand Down
2 changes: 1 addition & 1 deletion nodes/render-template/render-template.js
Expand Up @@ -41,7 +41,7 @@ module.exports = function(RED) {
text: `Requesting at: ${this.getPrettyDate()}`
});

return this.nodeConfig.server.api
return this.nodeConfig.server.http
.renderTemplate(template)
.then(res => {
message.template = template;
Expand Down

0 comments on commit ed7341a

Please sign in to comment.