Transfering data between a list of dicts and an observableArray

Hi there,
I have developed a filamentscale inside a dry box. Unfortunately I am not good at programming. So I am stuck with a problem that exceeds my skills. Perhaps someone else is interested in my project and is willing to help.

The plugin stores some values about filament spools in a text file. These values are stored in a list of dicts server side and in an ObservableArray client side. When transferring the values from one side to the other I use api_command/json.loads and json.dumps/send_plugin_message. (I know that a socket solution would be better but I couldn't get it to work.)
I get an error when loading data from the text file or adding data though the plugin.
My workaround is transmitting the data back and forth. But I'd like to get rid of that.

You can find my code here:

How to reproduce:
Comment out line 236 in __ini__.py

	file_obj.close()
# 	self._plugin_manager.send_plugin_message(self._identifier, dict(filaments=self.filaments, active_filament=self.active_filament)) # my workaround
	except IOError:

Start Octoprint and add some filaments to the list on the SmartScaletab.
Reload the browser and add some more filaments.
When trying to activate the filaments with a click on their name I get an error in/static/js/SmartScale.js around line 100 - 1004.
This code works for the loaded data but the newly added data throws a "Mandatory parameter spoolweight missing for command load"

	self.newFilament(self.filaments()[self.active_filament()].fila)
	self.spool_weight(self.filaments()[self.active_filament()].spool);
	self.material_density(self.filaments()[self.active_filament()].density);

This code works for the newly added data but the loaded data throws a "self.filaments()[...].fila is not a function"

	self.newFilament(self.filaments()[self.active_filament()].fila())
	self.spool_weight(self.filaments()[self.active_filament()].spool());
	self.material_density(self.filaments()[self.active_filament()].density());

I must be doing something wrong when transferring/adding the data. I've read through the Knockout, Octoprint and lots of Java/JSON docs.

I know my code is a mess. Thanks for your effort.

Jebril.

Line 208 of __init__.py is interesting to me because you're prepending two of the three lookup values with u'something' like that. I'm guessing you might have gotten an error otherwise...?

				self.job=self._printer.get_current_job()
				self.job=self.job['filament'][u'tool0'][u'length']
				self.job=float(self.job)/100

So... what's this trying to do? It sort of looks like you're storing the output of a call to OctoPrint's get_current_job() to an object variable called job... then you're overwriting it with a subset of that. And then you're overwriting it again with a calculation of that.

I think a lot of us would instead write something more like...

				_currentJob = self._printer.get_current_job()
				_length = _currentJob['filament'][u'tool0'][u'length']
				self.job = float(_length)/100

And then we'd likely refactor that self.job to be more like self.jobLength perhaps.


I wonder if anything would change if you updated the first line of this file from what it is to:

# -*- coding: iso-8859-15 -*-
1 Like

I haven't looked at your code yet, but...

I've found that this type of conversion is tricky, because if you have a list of dictionaries when you assign the list to the observableArray the actual items within the dictionary themselves are not observable. There are a couple of ways around this by using knockout extenstions like this one, creating a separate view model for the individual dictionaries contained with the observableArray, and you might be able to utilize the ko.mapping.fromJSON or ko.mapping.fromJS to assign the observableArray.

Using the separate view model is probably the way to go though, I found the objservableDictionary to be a little tricky to work with. Also, when you add items to the array from the js side you use a new declaration of the separate view model. And of course, you don't necessarily have to create a separate viewmodel to achieve the same thing. I've done something similar in many of my plugin, just by pushing a dictionary of observables. An example can be seen here in my TPLink-Smartplug plugin.

1 Like

@OutsourcedGuru: The code around line 208 is checking whether there is enough filament left on the spool before starting a new print. I had to do it like this to get rid of errors. I cleaned up the code according to your suggestions. But it had no influence on my problem with the ObservableArray. Thanks for teaching me not to use the same variable for different purposes and for understanding conventions in selecting variable names.

@jneilliii:

... the actual items within ... are not observable ...

That seems to be exactly my problem. Thanks for pointing me to the right direction. After struggling around with James ObervableDictionary and outsourcing the problematic code to a separate viewmodel I come to the conclusion that my workaround will work good enough for the time being.

Thank you both,
Jebril

I agree with @jneilliii. Octoprint will deliver the objects as observables, but if you add new ones it is your job to make sure they are in the same format. Adding a viewmodel for this is a good way to go.

I recently pushed a new branch for the DropboxTimelapse plugin, and had just such an issue. Here are the default settings for the plugin:

        return dict(
            api_token=None,
            delete_after_upload=False,
            additional_upload_events=[
                {
                    'event_name': 'PLUGIN_OCTOLAPSE_MOVIE_DONE',
                    'payload_path_key': 'movie'
                },
                {
                    'event_name': 'PLUGIN_OCTOLAPSE_SNAPSHOT_ARCHIVE_DONE',
                    'payload_path_key': 'archive'
                }
            ]
        )

Not very efficient, but quite descriptive :slight_smile:

Here is the assignment of the OctoPrint settings to the viewmodel to make the jinja2 files a bit cleaner:

        self.settings = parameters[0];
        self.plugin_settings = null;

        self.onBeforeBinding = function() {
            // Make plugin setting access a little more terse
            self.plugin_settings = self.settings.settings.plugins.dropbox_timelapse;
        };

They can be bound in the usual way. Nothing special there really. Here's some jinja2 I used to bind each dictionary in the events array:

            <tbody data-bind="foreach: plugin_settings.additional_upload_events">
            <tr>
                <td>
                    <input class="input-text input-block-level" type="text" data-bind="value: $data.event_name">
                </td>
                <td>
                    <input class="input-text input-block-level" type="text" data-bind="value: $data.payload_path_key">
                </td>
                <td style="text-align:center;">
                    <button class="btn btn-small" type="button" title="Delete Custom Event" data-bind="click: function() { $parent.removeUploadEvent($index());}"><i class="fa fa-trash"></i></button>
                </td>
            </tr>
            </tbody>

Here is a custom viewmodel for the additional_upload_events:

    function AdditionalUploadEventViewModel(event_name, payload_path_key){
        var self = this;
        self.event_name = ko.observable(event_name);
        self.payload_path_key = ko.observable(payload_path_key);
    }

The only special code is for adding new events:

        self.addUploadEvent = function() {
            self.plugin_settings.additional_upload_events.push(
                new AdditionalUploadEventViewModel("","")
            );
        };

You can add any defaults you want in there. And the associated 'add new event button' jinja2:

<button class="btn" type="button" data-bind="click: addUploadEvent"><i class="fa fa-plus"></i> Add Custom Event</button>

You can view the full source here. Good luck!

2 Likes

Awesome example @FormerLurker.

1 Like

Sorry i got ill and didn't have the time to look into the code ever since. Thanks a lot for your effort. For now my workaround will do. I'll still eager to clean that up. but i'll have to wait a little bit longer. Thank you!

Jebril.