SettingsUpdated event only fires once per session

I'm seeing a weird issue where the SettingsUpdated event only fires once per session. This means that if a user changes their settings in my plugin & saves, then changes them again & saves, the changes in the second save will not take effect. If the page is refreshed the settings will reflect the settings from the first save. Then the user can make the 2nd set of changes and save again for them to take effect.

Has anyone else seen this? Any idea what to look into?

OctoPrint version: 1.7.2

Screen recording: L9AcKnt4BX - YouTube

Haven't seen that one before personally. Is there anything in the logs indicating something going wrong during the initial save process? Context is king when it comes to assisting with these types of questions, so if you have a link to your plugin code it could also help spot any irregularities.

I was just looking at octoprint.log (that's the file you see streamed on the left in the screen recording). Is there another I should look at?

sorry, I didn't actually watch the video, I see now what I think you may be running into. When binding a list of dictionaries on the knockout side, if you don't bind just quite right then changes to the list are only seen if the length of the list changes, not if properties of the dictionary are changed. So if you add another item to your list I bet it saves. There are a couple of ways to combat this.

One way to combat this is by using a pop-up editor rather than relying on inline editing of the dictionaries. I've done this in several of my plugins that involve lists of data, most notably Tasmota and TPLinkSmartplug.

Simpler example would be what I did with event manager in core octoprint. You basically need to make your individual items observable as you create them in the add function. Basically creating a secondary view model like here.

I see. I apologize - I'm still relatively unsure when it comes to DOM/web development - especially non-vanilla things like jinja/knockout. I'm referencing the js you linked. I already tried using self.name = ko.observable(customEvent.name); in the self.CustomEvent function, but that just gave me some text like "[object Object]" in the octoprint settings window.

Below is what I've got for relevant jinja2 & js - maybe you can spot something obvious I'm doing wrong? This is so close to working! It's just this updating of the setting issue that I've got left

<!-- ko foreach: customEvents -->
<tr data-bind="with: new $root.CustomEvent($data)">
    <td><input type="text" data-bind="value: name"></input></td>
    <td><input type="text" data-bind="value: message"></input></td>
    <td style="max-width: 37px;">
        <button class="btn btn-danger" title="Remove" data-bind="click: remove">
            <i class="fa fa-minus"></i>
        </button>
    </td>
</tr>
<!-- /ko -->
<tr style="justify-content: flex-end;"><td style="overflow: visible; flex-grow: 0; padding: 0; border: 0;">
    <button class="btn btn-primary" title="Add new" data-bind="click: $root.newCustomEvent">
        <i class="fa fa-plus"></i>
    </button>
self.newCustomEvent = () =>
    root.selectedHook().customEvents.push({
        name: ko.observable(''),
        message: ko.observable('')
    })
    
self.CustomEvent = function(customEvent){
    let self = this;    
    self.name = customEvent.name;
    self.message = customEvent.message;
    self.remove = () => root.selectedHook().customEvents.remove(customEvent);
}

Thanks for all your help!

sorry, I'm not super familiar with the => style of js programming and again, it's hard to tell without full context, but I would have something like this.

<!-- ko foreach: customEvents -->
<tr>
    <td><input type="text" data-bind="value: name"></input></td>
    <td><input type="text" data-bind="value: message"></input></td>
    <td style="max-width: 37px;">
        <button class="btn btn-danger" title="Remove" data-bind="click: $root.remove">
            <i class="fa fa-minus"></i>
        </button>
    </td>
</tr>
<!-- /ko -->
<tr style="justify-content: flex-end;"><td style="overflow: visible; flex-grow: 0; padding: 0; border: 0;">
    <button class="btn btn-primary" title="Add new" data-bind="click: newCustomEvent">
        <i class="fa fa-plus"></i>
    </button>
self.newCustomEvent = function() {
    self.settingsViewModel.plugins.pluginidentifier.customEvents.push({
        name: ko.observable(''),
        message: ko.observable('')
    });
}

self.remove = function(data) {
    self.settingsViewModel.plugins.pluginidentifier.customEvents.remove(data);
}

Thanks for the input (and sorry for the delay in my response).

I think what you provided is functionally equivalent to what I had in my answer.

If I were to guess at what the problem might be, the plugin I'm trying to modify uses an array of settings, where each element is a dictionary of settings for that webhook. I don't know if this is the norm, but the only other plugin I've looked at seems to just use a dictionary of settings (more similar to the examples in your answer).

Here's an example: (full file here: OctoPrint-Webhooks/webhooks.js at 61a8fa874610f01e28652f5afba9e30309e8726f · 2blane/OctoPrint-Webhooks · GitHub )

function WebhookSettingsCustomViewModel(parameters) {
    var self = this, root = self;
    self.settings = parameters[0]
    self.hooks = ko.observableArray();
    self.selectedHook = ko.observable();

Again, I'm a total noob to octoprint plugins, python, knockout, etc (and I'm only ok when it comes to HTML/JS/CSS), but reading a bit of the knockout docs it seems that marking array elements as observable is different than marking the array as observable.

Sorry - don't know if that was clear or makes sense. Appreciate the help

exactly, it's very weird that knockout doesn't natively see changes within an observableArray, only differences in number of items. At one point I did find this knockout plugin that would allow linking in a more intelligent way.

1 Like

Hi @jneilliii - it's been a while since I've looked at this! I've taken ownership of this plugin and am tracking this issue here: Fix settings saving · Issue #1 · derekantrican/OctoPrint-Webhooks · GitHub (there's a good screen recording example of the issue there).

Per the docs at Mixins — OctoPrint master documentation I've overridden the on_settings_save function to see if I can see what's happening:

def on_settings_save(self, data):
	self._logger.info(f"Saving settings: {data}")
	octoprint.plugin.SettingsPlugin.on_settings_save(self, data)

What I've found is that this log message is only printed once - meaning that the on_settings_save function is only being called the first time and not the second time.

Any ideas for troubleshooting this further?

It's probably something similar to the previous issue. For debugging this I'd try to check the js side of things using the onSettingsBeforeSave view model callback (example) and a breakpoint in your browser's developer tools. It helps to adjust your config.yaml with the following settings so the files don't get bundled and you can browse sources to your js file to add the breakpoint.

devel:
  webassets:
    bundle: false
    minify: false

Thanks for the tip! I've implemented the onSettingsBeforeSave callback. Looks like that is getting called twice, but isn't seeing the value change.

Here's a new screen recording with config.yaml, octoprint.log, octoprint web interface, and dev console all in view: pDhSHRXqUd - YouTube. You can see that the config.yaml only gets updated the first time & the on_settings_save python def only gets called once (to write to octoprint.log). However, the onSettingsBeforeSave gets called both times (the yellow text in the dev console) but the particular webhook_enabled boolean value I'm looking at hasn't changed.

Does on_settings_save (and - therefore - the update to the config.yaml) only run if values have changed? Maybe that's why those are only running once and it's something about the jinja/knockout relationship that's at fault here (not updating the second time). I'm going to have to read up on knockout.js and perhaps find a good forum for asking related questions.

correct, this was what I was saying relative to observableArrays before, etc. it seems like it might be a binding issue.

I found the issue! It appears all my bindings/observables were working correctly (which made this even harder to track down) but since the js is "shorthanding" settings.settings.plugins.webhooks.hooks()[self.selectedIndex()] as self.selectedHook that means selectedHook is no longer the same as the one in the settings once the settings are saved (because the settings - correct me if I'm wrong - creates new observables with the new settings once the settings are saved).

So I added the following to reassign selectedHook from the settings object (which happens within the selectHook function):

self.onSettingsShown = function () { 
    self.selectHook(self.selectedIndex());
}

So good to have a super old issue fixed that's been bugging me!

1 Like