Skip to content

Commit

Permalink
feat: Able to access flow global and states in templates
Browse files Browse the repository at this point in the history
You can use templates in the Entity Id and data fields. When using
templates the top level is a property of the message object: msg.payload
would be {{payload}}. You can also access the flow, global and states
contexts {{flow.foobar}} {{global.something}}. For the states context
you can use the {{states.domain.entity_id}} to just get the state or
drill further down like
{{states.light.kitchen.attributes.friendly_name}}.
{{states.light.kitchen}} and {{states.light.kitchen.state}} are
equivalent.
  • Loading branch information
zachowj committed Feb 13, 2019
1 parent c6343a9 commit e0de7cb
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 51 deletions.
79 changes: 79 additions & 0 deletions lib/mustache-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Modified from https://github.com/node-red/node-red/blob/master/nodes/core/core/80-template.js
*/

const mustache = require('mustache');
const selectn = require('selectn');

function parseContext(key) {
var match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key);
if (match) {
var parts = {};
parts.type = match[1];
parts.store = match[3] === '' ? 'default' : match[3];
parts.field = match[4];
return parts;
}
return undefined;
}

/**
* Custom Mustache Context capable to collect message property and node
* flow and global context
*/

function NodeContext(msg, parent, nodeContext, serverName) {
this.msgContext = new mustache.Context(msg, parent);
this.nodeContext = nodeContext;
this.serverName = serverName;
}

NodeContext.prototype = new mustache.Context();

NodeContext.prototype.lookup = function(name) {
// try message first:
try {
const value = this.msgContext.lookup(name);
if (value !== undefined) {
return value;
}

// try flow/global context:
const context = parseContext(name);
if (context) {
const type = context.type;
const store = context.store;
const field = context.field;
const target = this.nodeContext[type];
if (target) {
return target.get(field, store);
}
}

// try state entities
const match = /^states\.(\w+\.\w+)(?:\.(.+))?/.exec(name);
if (match) {
const gHomeAssistant = this.nodeContext.global.get('homeassistant');
const states = gHomeAssistant[this.serverName].states;
const entityId = match[1];
const path = match[2] || 'state';

return selectn(path, states[entityId]) || '';
}

return '';
} catch (err) {
throw err;
}
};

NodeContext.prototype.push = function push(view) {
return new NodeContext(
view,
this.nodeContext,
this.msgContext,
this.serverName
);
};

module.exports = NodeContext;
12 changes: 3 additions & 9 deletions nodes/call-service/call-service.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
}
}
},
render_data: { value: false },
mergecontext: { value: null },
output_location: { value: "payload" },
output_location_type: { value: "msg" }
Expand Down Expand Up @@ -362,11 +361,6 @@
<input type="text" id="node-input-data" />
</div>

<div class="form-row">
<input type="checkbox" id="node-input-render_data" style="margin-left: 105px;display:inline-block; width:15px; vertical-align:baseline;">
<label for="node-input-render_data" style="width: auto;">Render templates in Data</label>
</div>

<div class="form-row">
<label for="node-input-mergecontext">Merge Context</label>
<input type="text" id="node-input-mergecontext" placeholder="lightOptions"/>
Expand Down Expand Up @@ -408,9 +402,9 @@ <h3>Config</h3>
<dt>Entity Id <span class="property-type">string</span></dt>
<dd>A comma delimited list of entity ids</dd>
<dt class="optional">data <span class="property-type">JSON</span></dt>
<dd>When using templates the top level is a property of the message object: <code>msg.payload</code> would be <code>{{payload}}</code></dd>
<dt>Render Templates <span class="property-type">boolean</span></dt>
<dd>When checked will attempt to render templates within the data element including entity ids</dd>
<dd>JSON object to pass along.</dd>
<dt>Templates</dt>
<dd>You can use templates in the <code>Entity Id</code> and <code>data</code> fields. When using templates the top level is a property of the message object: <code>msg.payload</code> would be <code>{{payload}}</code>. You can also access the <code>flow, global and states</code> contexts <code>{{flow.foobar}}</code> <code>{{global.something}}</code>. For the <code>states</code> context you can use the <code>{{states.domain.entity_id}}</code> to just get the state or drill further down like <code>{{states.light.kitchen.attributes.friendly_name}}</code>. <code>{{states.light.kitchen}}</code> and <code>{{states.light.kitchen.state}}</code> are equivalent.</dd>
</dl>

<h3>Inputs</h3>
Expand Down
20 changes: 13 additions & 7 deletions nodes/call-service/call-service.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable camelcase */
const BaseNode = require('../../lib/base-node');
const mustache = require('mustache');
const Context = require('../../lib/mustache-context');

module.exports = function(RED) {
const nodeOptions = {
Expand All @@ -9,7 +10,6 @@ module.exports = function(RED) {
service_domain: {},
service: {},
data: {},
render_data: {},
mergecontext: {},
name: {},
server: { isNode: true },
Expand Down Expand Up @@ -78,11 +78,17 @@ module.exports = function(RED) {
);
}

if (this.nodeConfig.render_data) {
apiData = JSON.parse(
mustache.render(JSON.stringify(apiData), message)
);
}
apiData = JSON.parse(
mustache.render(
JSON.stringify(apiData),
new Context(
message,
null,
this.node.context(),
this.utils.toCamelCase(this.nodeConfig.server.name)
)
)
);

this.debug(
`Calling Service: ${apiDomain}:${apiService} -- ${JSON.stringify(
Expand Down Expand Up @@ -158,7 +164,7 @@ module.exports = function(RED) {
payloadData = payloadData || {};
configData = configData || {};

// Cacluate payload to send end priority ends up being 'Config, Global Ctx, Flow Ctx, Payload' with right most winning
// Calculate payload to send end priority ends up being 'Config, Global Ctx, Flow Ctx, Payload' with right most winning
if (this.nodeConfig.mergecontext) {
const ctx = this.node.context();
let flowVal = ctx.flow.get(this.nodeConfig.mergecontext);
Expand Down
46 changes: 20 additions & 26 deletions nodes/fire-event/fire-event.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,9 @@
return this.name || `Event: ${this.event}`;
},
defaults: {
name: {
value: ""
},
server: {
value: "",
type: "server",
required: true
},
event: {
value: ""
},
name: { value: "" },
server: { value: "", type: "server", required: true },
event: { value: "" },
data: {
value: "",
validate: function(v) {
Expand All @@ -37,10 +29,7 @@
}
}
},
render_data: { value: false },
mergecontext: {
value: null
}
mergecontext: { value: null }
},
oneditprepare: function() {
const NODE = this;
Expand Down Expand Up @@ -91,40 +80,45 @@
<input type="text" id="node-input-data" placeholder="{ }"/>
</div>

<div class="form-row">
<input type="checkbox" id="node-input-render_data" style="margin-left: 105px;display:inline-block; width:15px; vertical-align:baseline;">
<label for="node-input-render_data" style="width: auto;">Render templates in Data</label>
</div>

<div class="form-row">
<label for="node-input-mergecontext"><i class="fa fa-dot-circle-o"></i> Merge Context</label>
<input type="text" id="node-input-mergecontext" placeholder="lightOptions"/>
</div>
</script>

<script type="text/x-red" data-help-name="ha-fire-event">
<p>Call a Home Assistant service</p>
<p>Fire an event to Home Assistants event bus</p>

<h3>Config</h3>
<dl class="message-properties">
<dt>Event <span class="property-type">string</span></dt>
<dd>Event name to fire</dd>
<dt class="optional">data <span class="property-type">JSON</span></dt>
<dd>JSON object to pass along</dd>
<dt>Templates</dt>
<dd>You can use templates in the <code>Entity Id</code> and <code>data</code> fields. When using templates the top level is a property of the message object: <code>msg.payload</code> would be <code>{{payload}}</code>. You can also access the <code>flow, global and states</code> contexts <code>{{flow.foobar}}</code> <code>{{global.something}}</code>. For the <code>states</code> context you can use the <code>{{states.domain.entity_id}}</code> to just get the state or drill further down like <code>{{states.light.kitchen.attributes.friendly_name}}</code>. <code>{{states.light.kitchen}}</code> and <code>{{states.light.kitchen.state}}</code> are equivalent.</dd>
</dl>

<h3>Inputs</h3>
<dl class="message-properties">
<dt class="optional">
payload.event <span class="property-type">string</span>
payload.event <span class="property-type">string</span>
</dt>
<dd>Event to fire</dd>
<dt class="optional">
payload.data <span class="property-type">object</span>
payload.data <span class="property-type">object</span>
</dt>
<dd>Event data to send</dd>
</dl>

<h3>Outputs</h3>
<dl class="message-properties">
<dt>
payload.event_type <span class="property-type">string</span>
payload.event_type <span class="property-type">string</span>
</dt>
<dd>Event Type that was fired</dd>
<dt class="optional">
payload.data <span class="property-type">object</span>
payload.data <span class="property-type">object</span>
</dt>
<dd>Event <code>data</code> sent, if one was used</dd>
</dl>
Expand All @@ -140,6 +134,6 @@ <h3>Merge Resolution</h3>

<h3>References</h3>
<ul>
<li><a href="https://developers.home-assistant.io/docs/en/dev_101_events.html#firing-events">HA Events</a></li>
<li><a href="https://developers.home-assistant.io/docs/en/dev_101_events.html#firing-events">HA Events</a></li>
</ul>
</script>
23 changes: 14 additions & 9 deletions nodes/fire-event/fire-event.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const BaseNode = require('../../lib/base-node');
const mustache = require('mustache');
const Context = require('../../lib/mustache-context');

module.exports = function(RED) {
const nodeOptions = {
Expand All @@ -10,9 +11,7 @@ module.exports = function(RED) {
render_data: {},
mergecontext: {},
name: {},
server: {
isNode: true
}
server: { isNode: true }
}
};

Expand Down Expand Up @@ -52,11 +51,17 @@ module.exports = function(RED) {

this.debug(`Fire Event: ${eventType} -- ${JSON.stringify({})}`);

if (this.nodeConfig.render_data) {
eventData = JSON.parse(
mustache.render(JSON.stringify(eventData), message)
);
}
eventData = JSON.parse(
mustache.render(
JSON.stringify(eventData),
new Context(
message,
null,
this.node.context(),
this.utils.toCamelCase(this.nodeConfig.server.name)
)
)
);

message.payload = {
event: eventType,
Expand Down Expand Up @@ -103,7 +108,7 @@ module.exports = function(RED) {
payloadData = payloadData || {};
configData = configData || {};

// Cacluate payload to send end priority ends up being 'Config, Global Ctx, Flow Ctx, Payload' with right most winning
// Calculate payload to send end priority ends up being 'Config, Global Ctx, Flow Ctx, Payload' with right most winning
if (this.nodeConfig.mergecontext) {
const ctx = this.node.context();
let flowVal = ctx.flow.get(this.nodeConfig.mergecontext);
Expand Down

0 comments on commit e0de7cb

Please sign in to comment.