Can I have a luup.variable_watch module for all my watched variables?

Hi

I’m trying to see if I can create a separate file/module with all my watched variables and functions in ? But I’m not having much luck ? Is it even possible ?

I have a file called xxluupvariablewatch.lua uploaded and the following entry in the lua startup

require "xxluupvariablewatch"

And then with the module/Lua file itself I have the following.

module("xxluupvariablewatch", package.seeall)

function IP_power_state_change(dev_id,service, variable, old_val, new_val)
	luup.log("A watched variable has changed - check and update status")
	luup.log(dev_id,service, variable, old_val, new_val)
   -- called when the Office Desk Socket is switched on or off - from a variable_watch
	if new_val == "1" then
        luup.log("IPPower has been turned ON check socket status")
		-- code here to do stuff..
	else
		luup.log("IPPower has been turned OFF check socket status")
		-- code here to do stuff
   end
end

luup.variable_watch("IP_power_state_change", "urn:upnp-org:serviceId:SwitchPower1","Status", 375)

No matter what I do , while the log records the watched variable change, but the called function does not work, I continue to get the same error. (See below)

06	09/04/21 10:30:03.805	Device_Variable::m_szValue_set device: 375 service: urn:upnp-org:serviceId:SwitchPower1 variable: Status was: 0 now: 1 #hooks: 1 upnp: 0 skip: 0 v:0x10f6ae8/NONE duplicate:0 <0x76eda520>
01	09/04/21 10:30:03.805	LuaInterface::CallFunction_Variable func: IP_power_state_change Device_Variable 375 urn:upnp-org:serviceId:SwitchPower1:Status failed attempt to call a nil value <0x76eda520>

Any ideas ?

Whatever the name is that you pass to luup.variable_watch(), it has to be a global. Your function is defined in a module, so it’s scope is the module, which is not global.

A simple fix is to define a global name for the module’s function to function as an alias for it in the global scope, and pass that name to luup.variable_watch():

module("xxluupvariablewatch", package.seeall)

function IP_power_state_change(dev_id,service, variable, old_val, new_val)
    -- function body removed for brevity/clarity
end

-- I'm using _G here, a reference to the global scope, for clarity and to express the explicit
-- intent to set a global variable (as opposed to simply looking like a coding error where 
-- the "local" declaration was forgotten (i.e. it wasn't, we don't want it, we just don't want 
-- the code to look like it was).
_G.global_ip_power_state_change = IP_power_state_change

luup.variable_watch("global_ip_power_state_change", "urn:upnp-org:serviceId:SwitchPower1",
    "Status", 375)

Thanks so much @rigpapa !

I’ve always been hesitant using the _G route, assuming the lack of the ‘local’ designation would ultimately make it global by default.

To help my on-going education…

I’m curious - I also tried the following too, and adding the module name first, to see if that helped the function to be found, but that didn’t work either… So, to summarise, can nothing be found and/or held outside the module it’s declared within ? (Unless it’s defined with _G)

luup.variable_watch("xxluupvariablewatch.IP_power_state_change", "urn:upnp-org:serviceId:SwitchPower1","Status", 375)

To confirm the only way to make something truly ‘global’ would be declare it in the Lua startup, or to apply _G at the start (Programming in Lua : 14)?

That’s correct, but sometimes you want to know/remember whether you are actually intending to set a global variable, or you just forgot to use “local” in that spot. Using _G is just a way of documenting/affirming intent. A simple comment reminder would also suffice. Whatever works for you, but I always recommend leaving yourself a trail of crumbs – if you come back to that code months later, they’re great reminders of important little details like that.

Yeah, that doesn’t work in Luup. I suppose it would be nice if it did, but really, what many APIs do, and what could still be done in Luup (but if they haven’t made the leap yet, they’re not likely going to ever) is to simply pass a function reference or closure. This would have two advantages: scope would not matter (because you are literally passing the function to be called, not just the name of a function to be called), and the function would/could have access to upvalues. That would look like this:

function foo()
    -- some implementation
end

luup.variable_watch( foo, serviceId, varName, devNum )

Notice in this example (that doesn’t work because they haven’t done what I’m talking about) that the function name in the call to variable_watch() is not in quotes, and does not have () after it. This is a function reference – it is passing a reference to the function where it lives in memory, not just the function’s name. So the scope wouldn’t matter in this context. You could pass any function from any scope (as long as the function in question is in scope) – from a module, global, even a “closure” like this:

luup.variable_watch( function( dev, svc, var, oldval, newval)
    luup.log('Variable ' .. var .. ' modified from ' .. oldval .. " to " .. newval)
end, "urn:upnp-org:serviceId:SwitchPower1", "Status", 123 )

In this example, you are creating a function on the fly and passing it to variable_watch as the thing to execute. Having this ability would smooth out a lot of code structure and scoping issues in Luup, but alas, they didn’t take this approach, so we don’t get these benefits. Onward…

Sort of… there is a gotcha: if you’re writing a plugin, if you are executing plugin code, then _G does not refer to the same global scope as startup Lua/scene Lua. Scene Lua and startup Lua are in their own global scope, and each plugin executes in its own separate global scope, and they are not shared or mutually accessible, so _G actually refers to the global scope of whatever execution context (environment) is running, and there are several of those in Luup. Fortunately, Luup calls like variable_watch() still work correctly, because they refer to whatever global scope is defined by the environment they are used in. But you can’t define _G.x in startup Lua and see it in any plugin code; and plugin A cannot define _G.y and have it be visible to plugin B or startup Lua – _G is a different scope in each, because each plugin and startup/scene Lua are running in different environments. Basically, plugins are sandboxed, for security (to keep them from stomping on each other). The key here: _G is global to the environment, not global to the entire system.

Many thanks @rigpapa , every day is a school day for me with Lua…

A few hopefully quick questions…

Within which global scope/environment does the _G.global_ip_power_state_change = IP_power_state_change We created earlier exist ?
Does every ‘module’ have its own global scope/environment too, like plugins, scenes and start up ?
Can we see/list all the global variables created within each?

That’s the trick… if you define the function in startup Lua and assign _G.global_ip_power_state_change there, then it’s in the startup Lua/scene Lua environment. If a plugin loads your module, then _G.global_ip_power_state_change is in the plugin’s environment (different from startup/scene Lua and every other plugin).

Awesome thanks

One more question; why do we declare (link) the global function separately? i.e.

_G.global_ip_power_state_change = IP_power_state_change 

Why not name the required function with a _G. prefix originally ? I.e

function _G.global_ip_power_state_change(dev_id,service, variable, old_val, new_val)
	luup.log("A watched variable has changed - check and update status")
	luup.log(dev_id,service, variable, old_val, new_val)
   -- called when the Office Desk Socket is switched on or off - from a variable_watch
	if new_val == "1" then
        luup.log("IPPower has been turned ON check socket status")
		-- code here to do stuff..
	else
		luup.log("IPPower has been turned OFF check socket status")
		-- code here to do stuff
   end
end

That should work as well. My point was to illustrate what was needed. There are many ways to get it done. I, for example, prefer to do the work outside the module, making the assignment from the module-qualified function name.

One word of caution also to your final code… in my experience, the variable_watch() callback can be a great source of deadlocks. There are locking problems and race conditions in Luup around it that are exposed in circumstances such as this: you watch a variable on device A, and when it changes to a certain value, your watch callback issues actions to device A. This causes further variable changes on device A, and if those include a watched variables while the first handler is still resident and executing, things start going south. My approach to using variable_watch() is that I keep my handler as short as possible and do no actions in it – I treat it as one normally would an interrupt handler on a microprocessor: get out as fast as possible. What I do in the handler is use call_delay() with a very short delay (0 secs in fact – that works) to schedule another function to execute the needed actions that the handler would have performed. This allows the watch handler to return immediately and be cleaned up before the actions run.