Plugin dev: cannot figure out how to save my conf

Hello,

I am developing a plugin for volumio to get a rotary encoder to change the volume. At the moment I am stuck on the basics, i.e. how to save and load the configuration.
I managed to get my plugin to show on theUI, I can enable or disable it but when I save, the configuration does not stick.
The first time I show the conf page it does not read from the default config.json file either.

I never see the config.json in /data/configuration/miscellanea/myplugin.

I am developing this on a Rpi2 B.

I would really apreciate if someone could point me in the right direction.

config.json

{
  "enabled": {
    "type": "boolean",
    "value": false
  },
  "pina": {
    "type": "number",
    "value": 2
  },
  "pinb": {
    "type": "number",
    "value": 3
  },
  "pinc": {
    "type": "number",
    "value": 4
  }
}

UIConfig.json

{
  "page": {
    "label": "GPIO Rotary Encoder"
  },
  "sections": [
    {
      "id": "re_setup",
      "element": "section",
      "label": "Setup your Rotary Encoder PINs",
      "icon": "fa-plug",
      "onSave": { "type":"controller", "endpoint":"miscellanea/gpio-rotary", "method":"saveTriggers" },
      "saveButton": {
        "label": "Save",
        "data": [
          "pina_setting",
          "pinb_setting",
          "pinc_setting"
        ]
      },
      "content": [
        {
          "id": "pina_setting",
          "element": "select",
          "label": "GPIO for pin A",
          "value": { "value": 2, "label": "2" },
          "options": [
            { "value": 2, "label": "2" },
            { "value": 3, "label": "3" },
            { "value": 4, "label": "4" },
            { "value": 5, "label": "5" },
            { "value": 6, "label": "6" },
            { "value": 7, "label": "7" },
            { "value": 8, "label": "8" },
            { "value": 9, "label": "9" },
            { "value": 10, "label": "10" },
            { "value": 11, "label": "11" },
            { "value": 12, "label": "12" },
            { "value": 13, "label": "13" },
            { "value": 14, "label": "14" },
            { "value": 15, "label": "15" },
            { "value": 16, "label": "16" },
            { "value": 17, "label": "17" },
            { "value": 18, "label": "18" },
            { "value": 19, "label": "19" },
            { "value": 20, "label": "20" },
            { "value": 21, "label": "21" },
            { "value": 22, "label": "22" },
            { "value": 23, "label": "23" },
            { "value": 24, "label": "24" },
            { "value": 25, "label": "25" },
            { "value": 26, "label": "26" },
            { "value": 27, "label": "27" }
          ]
        },
        {
          "id": "pinb_setting",
          "element": "select",
          "label": "GPIO for pin B",
          "value": { "value": 3, "label": "3" },
          "options": [
            { "value": 2, "label": "2" },
            { "value": 3, "label": "3" },
            { "value": 4, "label": "4" },
            { "value": 5, "label": "5" },
            { "value": 6, "label": "6" },
            { "value": 7, "label": "7" },
            { "value": 8, "label": "8" },
            { "value": 9, "label": "9" },
            { "value": 10, "label": "10" },
            { "value": 11, "label": "11" },
            { "value": 12, "label": "12" },
            { "value": 13, "label": "13" },
            { "value": 14, "label": "14" },
            { "value": 15, "label": "15" },
            { "value": 16, "label": "16" },
            { "value": 17, "label": "17" },
            { "value": 18, "label": "18" },
            { "value": 19, "label": "19" },
            { "value": 20, "label": "20" },
            { "value": 21, "label": "21" },
            { "value": 22, "label": "22" },
            { "value": 23, "label": "23" },
            { "value": 24, "label": "24" },
            { "value": 25, "label": "25" },
            { "value": 26, "label": "26" },
            { "value": 27, "label": "27" }
          ]
        },
        {
          "id": "pinc_setting",
          "element": "select",
          "label": "GPIO for pin C",
          "value": { "value": 4, "label": "4" },
          "options": [
            { "value": 2, "label": "2" },
            { "value": 3, "label": "3" },
            { "value": 4, "label": "4" },
            { "value": 5, "label": "5" },
            { "value": 6, "label": "6" },
            { "value": 7, "label": "7" },
            { "value": 8, "label": "8" },
            { "value": 9, "label": "9" },
            { "value": 10, "label": "10" },
            { "value": 11, "label": "11" },
            { "value": 12, "label": "12" },
            { "value": 13, "label": "13" },
            { "value": 14, "label": "14" },
            { "value": 15, "label": "15" },
            { "value": 16, "label": "16" },
            { "value": 17, "label": "17" },
            { "value": 18, "label": "18" },
            { "value": 19, "label": "19" },
            { "value": 20, "label": "20" },
            { "value": 21, "label": "21" },
            { "value": 22, "label": "22" },
            { "value": 23, "label": "23" },
            { "value": 24, "label": "24" },
            { "value": 25, "label": "25" },
            { "value": 26, "label": "26" },
            { "value": 27, "label": "27" }
          ]
        }
      ]
    }
  ]
}

index.js

'use strict';

var libQ = require('kew');
//var fs = require('fs-extra');
var Gpio = require('onoff').Gpio;
var io = require('socket.io-client');
var socket = io.connect('http://localhost:3000');

module.exports = GPIORotary;

function GPIORotary(context) {
	var self = this;

	this.context = context;
	this.commandRouter = this.context.coreCommand;
	this.logger = this.context.logger;
	this.configManager = this.context.configManager;
}
// demarrage de Volumio
GPIORotary.prototype.onVolumioStart = function() {
	var self = this;
	var configFile=self.commandRouter.pluginManager.getConfigurationFile(self.context,'config.json');

	self.config = new (require('v-conf'))();
	self.logger.info('GPIO-Rotary pre loadFile ' + configFile);
	self.config.loadFile(configFile);
	self.logger.info('GPIO-Rotary Initialized');
	self.config.print();
};

// charge le fichier config.json
// c'est ce bout de bidule qui doit creer /data/configuration/miscellanea/gpio-rotary/config.json
// si il n'existe pas deja
// depuis /Volumio/app/pluginmanager.js
GPIORotary.prototype.getConfigurationFiles = function() {
	this.logger.info('GPIO-Rotary Got config.json');
	return['config.json'];
};

// Demarrage du plugin
GPIORotary.prototype.onStart = function() {
	var self = this;
	var defer = libQ.defer();

	self.logger.info('GPIO-Rotary Started');
	defer.resolve();
	return defer.promise;
};

// Arret du plugin
GPIORotary.prototype.onStop = function() {
	var self = this;
	var defer = libQ.defer();

	self.logger.info('GPIO-Rotary Stopped');
	defer.resolve();
	return defer.promise;
};

// on utilise cette fonction lorsque on veut acceder a la configuration du plugin
GPIORotary.prototype.getUIConfig = function() {
	var defer = libQ.defer();
	var self = this;

	var lang_code = 'en';

	//self.logger.info('GPIO Rotary - getUIConfig - Start');

	self.commandRouter.i18nJson(__dirname+'/i18n/strings_'+lang_code+'.json',
		__dirname+'/i18n/strings_en.json',
		__dirname +'/UIConfig.json')
		.then(function(uiconf)
		{
			uiconf.sections[0].content[0].value.value = self.config.get('pina');
			uiconf.sections[0].content[0].value.label = self.config.get('pina').toString();
			uiconf.sections[0].content[1].value.value = self.config.get('pinb');
			uiconf.sections[0].content[1].value.label = self.config.get('pinb').toString();
			uiconf.sections[0].content[2].value.value = self.config.get('pinc');
			uiconf.sections[0].content[2].value.label = self.config.get('pinc').toString();
			defer.resolve(uiconf);
		})
		.fail(function()
		{
			defer.reject(new Error());
		});
	return defer.promise;
};

// cette fonction est appelee depuis la page de conf pour enregistrer les parametres
GPIORotary.prototype.saveTriggers = function(data) {
	var defer = libQ.defer();
	var self = this;

	//self.logger.info('GPIO Rotary: Print conf pre save.')
	//self.config.print();

	self.logger.info('GPIO Rotary - Saving PINs');
	//self.config.set('pina', data.pina.value);
	self.config.set('pina', data['pina_setting']['value']);
	self.logger.info('GPIO Rotary - Saved PIN A, value: ' + data.pina_setting.value);
	//self.config.set('pinb', data.pinb.value);
	self.config.set('pinb', data['pinb_setting']['value']);
	//self.config.set('pinc', data.pinc.value);
	self.config.set('pinc', data['pinc_setting']['value']);
	self.logger.info('GPIO Rotary - PINs saved');

	self.commandRouter.pushToastMessage('success',"GPIO Rotary", "Configuration saved");
	defer.resolve();

	//self.logger.info('GPIO Rotary: Print conf post save.')
	//self.config.print();

	return defer.promise;
};

Thanks,

Vincent

Hi,

I faced the same problem a week ago.

Check the config.json file in

/data/plugins/yourCategory/yourPlugin

I guess it is out of sync with your config.json file. If thats true it does not save the configuration (thats what I figured out).
Actually it should save the configuration even if the files are out of sync (see https://github.com/fanciulli/v-conf/blob/master/index.js#L153).

You can also manually call

self.config.save();

to trigger v-conf to write the config into the file.

For easy debugging of your config you can print the content of the config file to console:

self.logger.info(self.config.print());

When you are connected to your pi you can simply run

sudo journalctl -f

to show the ongoing logs of the system to your console.

Hope that helps. :slight_smile:

Stefan

EDIT:
Saw that you have updated your question (?).

You do not see your configuration file in /data/configurations/…?
Thats strange.

What is the output when you change the following method by adding a print of your configuration to console?

[code]GPIORotary.prototype.onVolumioStart = function() {
var self = this;
var configFile=this.commandRouter.pluginManager.getConfigurationFile(this.context,‘config.json’);

this.config = new (require(‘v-conf’))();
self.logger.info('GPIO-Rotary pre loadFile ’ + configFile);
this.config.loadFile(configFile);
self.logger.info(‘GPIO-Rotary Initialized’);

//ADD THIS
self.logger.info(self.config.print());
};[/code]

Hi Stefan,

Thanks for the suggestion, I’ll try this tonight and report back.

Vincent

You are welcome. Let me know if you could fix it.

Well,

Adding that line showed me that the returned configuration was empty, basically because it can’t read /data/configuration/miscellanea/gpio-rotary/config.json.
So I created it copying the config.json from the plugin folder. (But I though it was supposed to use the config.json from the plugin directory as a default conf and set that in the /data/configuration directory when hitting save)

Now I am able to save the conf but it does not appear in the UI.

I have had to change my save routine to things like

self.config.set('pina', data.pina.value);

instead of the

self.config.set('pina', data['pina']);

that I see in other plugins, if I use that I get an error :

info: GPIO Rotary - Saving PINs Mar 13 18:22:46 volumio volumio[2306]: /data/plugins/miscellanea/gpio-rotary/node_modules/v-conf/index.js:197 Mar 13 18:22:46 volumio volumio[2306]: throw Error('The value '+value+' is not a number'); Mar 13 18:22:46 volumio volumio[2306]: ^ Mar 13 18:22:46 volumio volumio[2306]: Error: The value [object Object] is not a number Mar 13 18:22:46 volumio volumio[2306]: at Error (native) Mar 13 18:22:46 volumio volumio[2306]: at Config.forceToType (/data/plugins/miscellanea/gpio-rotary/node_modules/v-conf/index.js:197:20) Mar 13 18:22:46 volumio volumio[2306]: at Config.set (/data/plugins/miscellanea/gpio-rotary/node_modules/v-conf/index.js:106:25) Mar 13 18:22:46 volumio volumio[2306]: at GPIORotary.saveTriggers (/data/plugins/miscellanea/gpio-rotary/index.js:92:14) Mar 13 18:22:46 volumio volumio[2306]: at CoreCommandRouter.executeOnPlugin (/volumio/app/index.js:931:29) Mar 13 18:22:46 volumio volumio[2306]: at Socket.<anonymous> (/volumio/app/plugins/user_interface/websocket/index.js:479:38) Mar 13 18:22:46 volumio volumio[2306]: at emitTwo (events.js:106:13) Mar 13 18:22:46 volumio volumio[2306]: at Socket.emit (events.js:191:7) Mar 13 18:22:46 volumio volumio[2306]: at Socket.onevent (/volumio/node_modules/socket.io/lib/socket.js:348:8) Mar 13 18:22:46 volumio volumio[2306]: at Socket.onpacket (/volumio/node_modules/socket.io/lib/socket.js:308:12)

So I’m assuming there is something wrong in the way my conf is handled but what?

I’ll try to get more time on this later this week.

Thanks a lot for your input!

Vincent

Hello Vincent,

yes, you have a mistake in your config:

I have marked the fields red. Remove the quotes (") here. Take a look on the value of “enabled”. There are no quotes either. :slight_smile:
Remove them and then everything should work even with

self.config.set('pina', data['pina']);

:slight_smile:

Hope that helps.

Stefan

Sorry to drag up this old topic but i’m having the exact same issue. I cannot seem to save values whether I use a string or a number.

Oct 05 07:58:35 dining-room volumio[20024]: info: CoreCommandRouter::executeOnPlugin: teac-dab-controls , saveOptions 
Oct 05 07:58:35 dining-room volumio[20024]: info: teacdabcontrols - saving settings 
Oct 05 07:58:35 dining-room volumio[20024]: info: 17 
Oct 05 07:58:35 dining-room volumio[20024]: info: string 
Oct 05 07:58:35 dining-room volumio[20024]: error: Failed callmethod call: Error: The value undefined is not a number 

Here’s part of my UIConfig.json

    {
      "id": "encoder",
      "element": "section",
      "label": "encoder",
      "icon": "fa-cogs",
      "description": "Pin configuration for the rotary encoder",
      "onSave": {
        "type": "controller",
        "endpoint":"user_interface/teac-dab-controls",
        "method":"saveOptions"
      },
      "saveButton": {
        "label": "Save",
        "data": [
          "rot_enc_A",
          "rot_enc_B"
        ]
      },
      "content": [
        {
          "id": "rot_enc_A",
          "type":"string",
          "element": "input",
          "label": "Rotary encoder A pin",
          "value": ""
        },
        {
          "id": "rot_enc_B",
          "type":"string",
          "element": "input",
          "label": "Rotary encoder B pin",
          "value": ""
        }
      ]
    },

my config.json

    "rot_enc_A": {
        "type": "string",
        "value": "17"
    },
    "rot_enc_B": {
        "type": "string",
        "value": "27"
 },

my load and save functions. Loading values works without issue and populates them in the UI.

// Configuration Methods -----------------------------------------------------------------------------

teacdabcontrols.prototype.getUIConfig = function() {
    const self = this;
    const defer = libQ.defer();

    this.logger.info('Teac DAB Controls - getUIConfig');

    const lang_code = this.commandRouter.sharedVars.get('language_code');

    this.commandRouter.i18nJson(__dirname + '/i18n/strings_' + lang_code + '.json',
        __dirname + '/i18n/strings_en.json',
        __dirname + '/UIConfig.json')
        .then(function (uiconf) {

            uiconf.sections[0].content[0].value = self.config.get('buttons_clk');
            uiconf.sections[0].content[1].value = self.config.get('buttons_miso');
            uiconf.sections[0].content[2].value = self.config.get('buttons_mosi');
            uiconf.sections[0].content[3].value = self.config.get('buttons_cs');
            uiconf.sections[0].content[4].value = self.config.get('buttons_channel1');
            uiconf.sections[0].content[5].value = self.config.get('buttons_channel2');
            uiconf.sections[1].content[0].value = self.config.get('rot_enc_A');
            uiconf.sections[1].content[1].value = self.config.get('rot_enc_B');
            uiconf.sections[2].content[0].value = self.config.get('lcd_rs');
            uiconf.sections[2].content[1].value = self.config.get('lcd_e');
            uiconf.sections[2].content[2].value = self.config.get('lcd_d4');
            uiconf.sections[2].content[3].value = self.config.get('lcd_d5');
            uiconf.sections[2].content[4].value = self.config.get('lcd_d6');
            uiconf.sections[2].content[5].value = self.config.get('lcd_d7');

            defer.resolve(uiconf);
        })
        .fail(function () {
            self.logger.error('teacdabcontrols - Failed to parse UI Configuration page: ' + error);
            defer.reject(new Error());
        });

    return defer.promise;
};

teacdabcontrols.prototype.saveOptions = function (data) {
    const self = this;

    this.logger.info('teacdabcontrols - saving settings');

    const buttons_clk = data['buttons_clk'];
    const buttons_miso = data['buttons_miso'];
    const buttons_mosi = data['buttons_mosi'];
    const buttons_cs = data['buttons_cs'];
    const buttons_channel1 = data['buttons_channel1'];
    const buttons_channel2 = data['buttons_channel2'];

    const rot_enc_A = data['rot_enc_A'];
    this.logger.info(rot_enc_A);
    this.logger.info(typeof rot_enc_A);  // Output: object

    const rot_enc_B = data['rot_enc_B'];

    const lcd_rs = data['lcd_rs'];
    const lcd_e = data['lcd_e'];
    const lcd_d4 = data['lcd_d4'];
    const lcd_d5 = data['lcd_d5'];
    const lcd_d6 = data['lcd_d6'];
    const lcd_d7 = data['lcd_d7'];


    self.config.set('buttons_clk', buttons_clk);
    self.config.set('buttons_miso', buttons_miso);
    self.config.set('buttons_mosi', buttons_mosi);
    self.config.set('buttons_cs', buttons_cs);
    self.config.set('buttons_channel1', buttons_channel1);
    self.config.set('buttons_channel2', buttons_channel2);

    self.config.set('rot_enc_A', rot_enc_A);
    self.config.set('rot_enc_B', rot_enc_B);

    self.config.set('lcd_rs', lcd_rs);
    self.config.set('lcd_e', lcd_e);
    self.config.set('lcd_d4', lcd_d4);
    self.config.set('lcd_d5', lcd_d5);
    self.config.set('lcd_d6', lcd_d6);
    self.config.set('lcd_d7', lcd_d7);

    this.commandRouter.pushToastMessage('success', 'teacdabcontrols', this.commandRouter.getI18nString("COMMON.CONFIGURATION_UPDATE_DESCRIPTION"));

    this.logger.info('teacdabcontrols - settings saved');

    return libQ.resolve();
};

Why does it think my string is a number? I’ve tried using both the number type and string. The result is always the same. I’ve logged the data that should be getting written and it’s a string with the correct number in. I can’t see whats going wrong here.

Resolved this myself in the end using the following save function. I was only sending some of the configuration with each save. Assuming your config.json keys are the same as your UIConfig.json id’s, the following will save valid values. My other issue was that the config.json is copied to a path in /data/. I also believed that json.conf in the plugin directory would get updated but it does not. Chaning the values in the plugin and refreshing the plugin DOES NOT update the /data/ config.json, and if you change a type from number to string, you’ll be stuck with a number value until you bin the file in/data/. Hopefully this helps someone.

teacdabcontrols.prototype.saveOptions = function (data) {
    const self = this;

    function isNumeric(str) {
        // Use parseFloat or parseInt to attempt conversion to a number
        const num = parseFloat(str);
      
        // Check if the conversion is a valid number and not NaN
        return !isNaN(num);
      }

    this.logger.info('teac-dab-controls - saving settings');

    const formattedJsonString = JSON.stringify(data, null, 2);
    console.log(formattedJsonString);

    // Parse JSON string into a JavaScript object
    const jsonObject = JSON.parse(formattedJsonString);

    // Iterate through the object and save if the item is valid
    for (const key in jsonObject) {
    if (jsonObject.hasOwnProperty(key)) {
        const value = jsonObject[key];
        // console.log(`${key}: ${value}`);
        if (isNumeric(value)) {
            console.log(`${value} is a valid number. Saving ${key}.`);
            self.config.set(key, value);
        } else {
            console.log(`${value} is not a valid number. Not saving ${key}.`);
        }
    }
    }
    
    this.commandRouter.pushToastMessage('success', 'teac-dab-controls', this.commandRouter.getI18nString("COMMON.CONFIGURATION_UPDATE_DESCRIPTION"));

    this.logger.info('teac-dab-controls - settings saved');

    return libQ.resolve();
};
1 Like