Skip to content

Commit

Permalink
Use data-attributes to cache service data
Browse files Browse the repository at this point in the history
Storing the whole selected service as a serialized JSON string to
the select box caused some issues with service identification in
frontend. Instead, use a hash id of the service for differentiating
the services and store the whole selected service details separately in
its own variable.
  • Loading branch information
pkronstrom committed Jan 21, 2019
1 parent fd51589 commit a0cfbe0
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 67 deletions.
60 changes: 34 additions & 26 deletions src/nodes/input-battery.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
</div>

<div class="form-row">
<label for="node-input-service"> Battery</label>
<select id="node-input-service"></select>
<label for="node-input-serviceId"> Battery</label>
<select id="node-input-serviceId"></select>
</div>

<div class="form-row">
Expand All @@ -26,7 +26,8 @@
RED.nodes.registerType('victron-battery', {
category: 'Victron Energy',
defaults: {
service: {value: ""},
serviceId: {value: ""},
service: {value: undefined},
path: {value: ""}
},
color: '#4790d0',
Expand All @@ -36,66 +37,73 @@
icon: "victronenergy.svg",
label: function() {
let altName = 'Battery';

if (this.service && this.path) {
let svc = JSON.parse(this.service).name;
let path = JSON.parse(this.path).name;
let svc = this.service.name;
let path = this.service.paths.find(p => p.path === this.path).name;
altName = `Battery - ${svc} - ${path}`;
}

return this.name || altName
},
oneditprepare: function() {
let _this = this;

let serviceElem = $('#node-input-service');
let serviceElem = $('#node-input-serviceId');
let pathElem = $('#node-input-path');

const buildOption = service => {
return $('<option/>')
.val(service.id || service.path)
.text(service.name)
.data(service);
}

const populateSelect = (selector, services) => {
selector[0].options.length = 0;
services.forEach(path => {
selector.append(
$('<option/>', {value: JSON.stringify(path), text: path.name})
);
services.forEach(service => {
selector.append(buildOption(service));
});
}

const updatePaths = () => {
let currentInterface = JSON.parse(serviceElem.val());
if (currentInterface)
populateSelect(pathElem, currentInterface.paths)
const data = serviceElem.find(':selected').data();
if (data && data.paths)
populateSelect(pathElem, data.paths);
}

// Upon receiving data, populate the edit form
$.getJSON('victron/services/battery', function(data) {
// 1. Populate edit options based on new data
if (data.length !== 0) {
// populate based on received data
populateSelect(serviceElem, data)
// the paths are set again when the service changes
serviceElem.change(updatePaths);
}
else {
$('#statusmessage').show();
// Show the form if we have previously set settings
if (!_this.service) $('.form-row').hide();
}

// show the previously deployed service even if it is currently unavailable
// 2. If the node was previously deployed,
// update the edit based on the old data
if (_this.service) {
let svc = JSON.parse(_this.service)
if (data.filter(o => o.service === svc.service).length == 0) {
$('<option/>', {value: _this.service, text: (svc.name + ' [disconnected]')})
// if it is not present anymore, still add it to the select list
if (!data.find(s => s.id === _this.service.id)) {
buildOption(_this.service)
.text(_this.service.name + " [disconnected]")
.appendTo(serviceElem);
}
serviceElem.val(_this.service);
serviceElem.val(_this.service.id);
}

updatePaths();
serviceElem.change(updatePaths);

if (_this.path) pathElem.val(_this.path);

if (_this.path)
pathElem.val(_this.path);
});

},
oneditsave: function() {
// save the current service to a variable
this.service = $('#node-input-serviceId').find(':selected').data()
}
});

Expand Down
6 changes: 3 additions & 3 deletions src/nodes/input-battery.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ module.exports = function(RED) {
RED.nodes.createNode(this, config);
var node = this;

this.service = JSON.parse(config.service);
this.path = JSON.parse(config.path);
this.service = config.service
this.path = config.path

this.subscription = null;
this.config = RED.nodes.getNode("victron-client-id");
this.client = this.config.client;

if (this.service && this.path) {
this.subscription = this.client.subscribe(this.service.service, this.path.path, (msg) => {
this.subscription = this.client.subscribe(this.service.service, this.path, (msg) => {
node.send({
payload: msg.value,
topic: `${this.service.service} - ${this.path.path}`
Expand Down
80 changes: 44 additions & 36 deletions src/nodes/output-relay.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
</div>

<div class="form-row">
<label for="node-input-service"> Relay </label>
<select id="node-input-service"></select>
<label for="node-input-serviceId"> Relay </label>
<select id="node-input-serviceId"></select>
</div>

<div class="form-warning hidden" id="warningmessage"></div>
Expand All @@ -31,8 +31,9 @@
RED.nodes.registerType('victron-relay', {
category: 'Victron Energy',
defaults: {
service: {value: ""},
state: {value: ""}
serviceId: {value: ""},
state: {value: ""},
service: {value: undefined}
},
color: '#4790d0',
inputs: 1,
Expand All @@ -42,47 +43,65 @@
label: function() {
let altName = 'Relay';
if (this.service) {
let svc = JSON.parse(this.service);
altName = svc.disabled
? `Relay - ${svc.name} - disabled`
: `Relay - ${svc.name} - ${this.state}`
altName = this.service.disabled
? `Relay - ${this.service.name} - disabled`
: `Relay - ${this.service.name} - ${this.state}`
}
return this.name || altName;
},
oneditprepare: function() {
let _this = this;
let serviceElem = $('#node-input-serviceId');

let selectElem = $('#node-input-service');
const buildOption = service => $('<option/>').val(service.id).text(service.name).data(service);

const populateSelect = (selector, services) => {
selector[0].options.length = 0;
services.forEach(path => {
selector.append(
$('<option/>', {value: JSON.stringify(path), text: path.name})
);
selector[0].options.length = 0; // clear options
services.forEach(service => {
selector.append(buildOption(service));
});
}

const updateWarning = () => {
const data = serviceElem.find(':selected').data();
if (data && data.disabled && data.warning) {
$('#warningmessage').text(data.warning).show()
$("input[name='value']").attr('disabled', true);
} else {
$('#warningmessage').hide();
$("input[name='value']").attr('disabled', false);
}
}

// Upon receiving data, populate the edit form
$.getJSON('victron/services/relay', function(data) {
// 1. Populate edit options based on new data
if (data.length !== 0) {
populateSelect(selectElem, data)
populateSelect(serviceElem, data)
}
else {
$('#statusmessage').show();
if (!_this.service) $('.form-row').hide();
if (!_this.serviceId) $('.form-row').hide();
}

// 2. If the node was previously deployed,
// update the edit based on the old data
if (_this.service) {
let svc = JSON.parse(_this.service)
if (data.filter(o => o.service === svc.service).length == 0) {
$('<option/>', {value: _this.service, text: (svc.name + ' [disconnected]')})
.appendTo(selectElem);
// if it is not present anymore, still add it to the select list
if (!data.find(s => s.id === _this.service.id)) {
buildOption(_this.service)
.text(_this.service.name + " [disconnected]")
.appendTo(serviceElem);
}
selectElem.val(_this.service);
serviceElem.val(_this.service.id);
}

updateWarning();
serviceElem.change(updateWarning);
});

// 3. Additional logic and state updates

// Set the radio button, if one has been already chosen
$(`input[name='value'][value='${this.state}']`).attr('checked', true);

Expand All @@ -91,21 +110,10 @@
$("input[name='value']").change(function() {
$("#node-input-state").val(this.value);
});

selectElem.change((e) => {
let selected = $(e.target).val() || _this.service;
// Ensure, that the selected relay isn't disabled
if (selected) {
let serviceObj = JSON.parse(selected);
if (serviceObj.disabled) {
$('#warningmessage').text(serviceObj.warning).show()
$("input[name='value']").attr('disabled', true);
} else {
$('#warningmessage').hide();
$("input[name='value']").attr('disabled', false);
}
}
});
},
oneditsave: function() {
// save the current service to a variable
this.service = $('#node-input-serviceId').find(':selected').data()
}
});

Expand Down
3 changes: 2 additions & 1 deletion src/nodes/output-relay.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ module.exports = function(RED) {
function OutputRelay(config) {
RED.nodes.createNode(this, config)

this.service = JSON.parse(config.service)
this.service = config.service
this.state = config.state

this.config = RED.nodes.getNode("victron-client-id")
this.client = this.config.client

Expand Down
19 changes: 18 additions & 1 deletion src/services/servicemapping.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@

/**
* Generates a unique hash from the given string.
* This is used to identify services from each other.
*
* @param {string} str a string the hash is generated from
*/
function getHash(str){
return str
.split('')
.reduce((a, b) => {
a = (( a << 5 ) - a) + b.charCodeAt(0)
return a & a
}, 0)
}

/**
* Constructs a battery object that is returned by
* configuration node's /victron/services REST endpoint.
Expand All @@ -8,6 +23,7 @@ const BATTERY = (service, name, paths) => {
return {
"service": `${service}`,
"name": `${name}`,
"id": getHash(service),
"paths": paths
}
}
Expand Down Expand Up @@ -59,7 +75,8 @@ const RELAY = (service, path, name) => {
return {
"service": `${service}`,
"name": `${name}`,
"paths": [
"id": getHash(service + path),
"paths": [
{
"name": "State (on/off)",
"path": `${path}`
Expand Down

0 comments on commit a0cfbe0

Please sign in to comment.