Getting Unknown controller: Function_Custom error in a Custom Function

I’ve added one of the examples functions on (/Mycodo/functions/examples/custom_function_generic.py), but when i click in any of the ‘custom_commands’ buttons, it raises the error:

  • ERROR - mycodo.daemon - Unknown controller: Function_Custom

I’ve been working in a Custom Function python code, but it gave me the same ERROR with all the buttons in custom_commands.

I’ve also have a question that can be related to this issue: does the variables in custom_commands, like button_one_value in this example, are passed as a self.button_one_value in the loop code, like the custom_options variables? Because they all return Nonetype for me…

Thanks in advance for your support!!

1 Like

This was fixed in the yet-to-be released version. You can upgrade to master to test the latest code.

I’ve made this Custom Function to test to test access to variables defined in the UI. Basicaly it displays the variables create in the ‘custom_options’, ‘custom_commands’ and thru the self.set_custom_function and get_custom_function, and show the result in the debug log.

import time

from mycodo.databases.models import CustomController
from mycodo.functions.base_function import AbstractFunction
from mycodo.mycodo_client import DaemonControl
from mycodo.utils.constraints_pass import constraints_pass_positive_value
from mycodo.utils.database import db_retrieve_table_daemon


FUNCTION_INFORMATION = {
    'function_name_unique': 'custom_var_test',
    'function_name': 'Var Test',

    'message': 'This is a test of the Function options. ',

    'options_enabled': [
        'custom_options'
    ],

    
    # These options will appear in the settings of the Function,
    # which the user can use to set different values and options for the Function.
    # These settings can only be changed when the Function is inactive.
    'custom_options': [
        {
            'id': 'period',
            'type': 'integer',
            'default_value': 60,
            'required': True,
            'name': 'Status Period (seconds)',
            'phrase': 'The duration (seconds) to update the Function status on the UI'
        },
        {
            'id': 'text_1',
            'type': 'text',
            'default_value': 'Text_1',
            'required': True,
            'name': 'Text 1',
            'phrase': 'Text 1 Description'
        },
        {
            'id': 'integer_1',
            'type': 'integer',
            'default_value': 100,
            'required': True,
            'constraints_pass': constraints_pass_positive_value,
            'name': 'Integer 1',
            'phrase': 'Integer 1 Description'
        }
        
    ],

    # Custom Actions give the user the ability to interact with the running Function Controller.
    # Each button will execute a different function within the Function Controller and pass any
    # input (text, numbers, selections, etc.) the user entered. This is useful for such things as
    # calibration.
    'custom_commands_message': 'This is a message for custom actions.',
    'custom_commands': [
        {
            'id': 'button_one_value',
            'type': 'integer',
            'default_value': 650,
            'name': 'Button One Value',
            'phrase': 'Value for button one.'
        },
        {
            'id': 'button_one',  # Do a search for the function "button_one(self, args_dict)"
            'type': 'button',
            'wait_for_return': True,  # The UI will wait until the function has returned the UI with a value to display
            'name': 'Button One',
            'phrase': "This is button one"
        }
    ]
}


class CustomModule(AbstractFunction):
    """
    Class to operate custom controller
    """
    def __init__(self, function, testing=False):
        super().__init__(function, testing=testing, name=__name__)

        self.control = DaemonControl()
        self.timer_loop = time.time()
        #
        # Initialize what you defined in custom_options, above
        #
        # Standard custom options inherit the name you defined in the "id" key
        self.text_1 = None
        self.integer_1 = None
        self.button_one_value = None
       
        #
        # Set custom options
        #
        custom_function = db_retrieve_table_daemon(
            CustomController, unique_id=self.unique_id)
        self.setup_custom_options(
            FUNCTION_INFORMATION['custom_options'], custom_function)

        if not testing:
            self.try_initialize()

    def initialize(self):
        # import controller-specific modules here
        # You may import something you defined in dependencies_module
        
        self.set_custom_option("integer_2", 5000)
        self.custom_1 = self.get_custom_option("integer_2")
        
        

    
    def loop(self):
        while self.timer_loop < time.time():
            self.timer_loop += self.period
        
        
        self.set_custom_option("integer_2", 5000)
        self.custom_1 = self.get_custom_option("integer_2")
            
        self.logger.debug(f'self.text_1: {self.text_1}')
        self.logger.debug(f'self.integer_1: {self.integer_1}')
        self.logger.debug(f'self.button_one_value: {self.button_one_value}')
        self.logger.debug(f'self.custom_1: {self.custom_1}')
        
        
        

    def button_one(self, args_dict):
        self.logger.error("Button One Pressed!: {}".format(int(args_dict['button_one_value'])))
        return "Here return message will be seen in the web UI. " \
               "This only works when 'wait_for_return' is set True."

    def button_two(self, args_dict):
        self.logger.error("Button Two Pressed!: {}".format(int(args_dict['button_two_value'])))
        return "This message will never be seen in the web UI because this process is threaded"

And i get this Log:

DEBUG - mycodo.function.custom_var_test_891adf8a - self.text_1: Text_1
DEBUG - mycodo.function.custom_var_test_891adf8a - self.integer_1: 100
DEBUG - mycodo.function.custom_var_test_891adf8a - self.button_one_value: None
DEBUG - mycodo.function.custom_var_test_891adf8a - self.custom_1: None

What i’m doing wrong? How do i access the button_one_value?

Does get and set_custom_option() works in Functions?

I’ve even tried to put the .py file in the main function folder but the results where the same…

1 Like

The Custom Command user inputs are only passed to the function executed by the button. This is the dictionary args_dict in the function that has the same name as the button ID:

Yes, those are available to use in any custom Inputs, Outputs, and Functions you create.

Ok! Very nice! You are a fast supporter, I’ve appreciated that.

I’ve done the upgrade to master branch and all works now. Thanks!

I’ve got another question for you… It Is possible to update the integer_1 field in the Function Configuration UI via the loop() code or ‘custom_commands’ or any other method?

1 Like

I’m not sure I understand the question. Could you describe what you’re trying to accomplish?

Let me try to show you, because i will only have access to the system in monday to send you screenshots…

if i run the code bellow, if i run button_one(), it sets ‘integer_1’ = ‘button_one_value’ - 1

Activating the function in ‘default_value’ and running button_one(), self.get_…(‘integer_1’) will be equal to 649 and self.get_…('button_one_value) to 650.

If you open the Function Configuration screen (engine icon of the Function) in the localhost>Setup>Functions, it will still be 100.

And if you Deactivate the Function ( I suppose the value should be stored between activations.). Change, for example ‘text_1’ and Save and Activate again, ‘integer_1’ will be 100 again.


import time

from mycodo.databases.models import CustomController
from mycodo.functions.base_function import AbstractFunction
from mycodo.mycodo_client import DaemonControl
from mycodo.utils.constraints_pass import constraints_pass_positive_value
from mycodo.utils.database import db_retrieve_table_daemon


FUNCTION_INFORMATION = {
    'function_name_unique': 'custom_var_test4',
    'function_name': 'Var Test4',

    'message': 'This is a test of the Function options. ',

    'options_enabled': [
        'custom_options'
    ],

    
    # These options will appear in the settings of the Function,
    # which the user can use to set different values and options for the Function.
    # These settings can only be changed when the Function is inactive.
    'custom_options': [
        {
            'id': 'period',
            'type': 'integer',
            'default_value': 5,
            'required': True,
            'name': 'Status Period (seconds)',
            'phrase': 'The duration (seconds) to update the Function status on the UI'
        },
        {
            'id': 'text_1',
            'type': 'text',
            'default_value': 'Text_1',
            'required': True,
            'name': 'Text 1',
            'phrase': 'Text 1 Description'
        },
        {
            'id': 'integer_1',
            'type': 'integer',
            'default_value': 100,
            'required': True,
            'constraints_pass': constraints_pass_positive_value,
            'name': 'Integer 1',
            'phrase': 'Integer 1 Description'
        }
        
    ],

    # Custom Actions give the user the ability to interact with the running Function Controller.
    # Each button will execute a different function within the Function Controller and pass any
    # input (text, numbers, selections, etc.) the user entered. This is useful for such things as
    # calibration.
    'custom_commands_message': 'This is a message for custom actions.',
    'custom_commands': [
        {
            'id': 'button_one_value',
            'type': 'integer',
            'default_value': 650,
            'required': True,
            'name': 'Button One Value',
            'phrase': 'Value for button one.'
        },
        {
            'id': 'button_one',  # Do a search for the function "button_one(self, args_dict)"
            'type': 'button',
            'wait_for_return': True,  # The UI will wait until the function has returned the UI with a value to display
            'name': 'Button One',
            'phrase': "This is button one"
        }
    ]
}


class CustomModule(AbstractFunction):
    """
    Class to operate custom controller
    """
    def __init__(self, function, testing=False):
        super().__init__(function, testing=testing, name=__name__)

        self.control = DaemonControl()
        self.timer_loop = time.time()
        #
        # Initialize what you defined in custom_options, above
        #
        # Standard custom options inherit the name you defined in the "id" key
        self.text_1 = None
        self.integer_1 = None
        self.period = None
        
       
        #
        # Set custom options
        #
        custom_function = db_retrieve_table_daemon(
            CustomController, unique_id=self.unique_id)
        self.setup_custom_options(
            FUNCTION_INFORMATION['custom_options'], custom_function)
        
        if not testing:
            self.try_initialize()

    def initialize(self):
        
        self.timer_loop = time.time()
        self.set_custom_option("integer_2", 5000)
        self.custom_1 = self.get_custom_option("integer_2")
        
        

    
    def loop(self):
        while self.timer_loop < time.time():
            self.timer_loop += self.period
            
       
        # sets new variable to monitor 'integer_1' custom option value  
        self.integer_1_update = self.get_custom_option("integer_1")
        self.button_one_value = self.get_custom_option("button_one_value")
            
            
        self.logger.debug(f'self.text_1: {self.text_1}')
        self.logger.debug(f'self.integer_1: {self.integer_1}')
        self.logger.debug(f'self.integer_1_update: {self.integer_1_update}')
        self.logger.debug(f'self.button_one_value: {self.button_one_value}')
        self.logger.debug(f'self.custom_1: {self.custom_1}')
        
        
        
        
        
    #Updates or create self.set_custom_option('button_one_value',) with button_one_value
    #updates integer_1 with button_one_value-1
    def button_one(self, args_dict):
        self.logger.error("Button One Pressed!: {}".format(int(args_dict['button_one_value'])))
        self.logger.debug("Button One Pressed!: {}".format(int(args_dict['button_one_value'])))
        try: 
            self.set_custom_option('button_one_value', int(args_dict['button_one_value']))
            self.set_custom_option('integer_1', int(args_dict['button_one_value']) - 1)
        except: self.logger.debug(f'Cannot set new button one value')
        
        return "Button One Pressed!"

And If i remember correctly, the self.get_…(‘button_one_value’) was None after the Reactivation after a save too, but no 100% sure.

I can send you some DEBUG Log monday…

Cheers!

Yes, self.set_custom_option() is how you would modify the variable values of custom_options.

You can see in the following Input a calibration function that changes the stored voltage and pH values in custom_options with buttons set up in custom_commands.

Keep in mind that if you press Save, you will change the value(s) to whatever you have filled in on the UI Configuration. If the module is changing the values in the background, you will need to refresh the page to see what new values the module has set them to. There is no automatic updating of the UI.

Ok!! Thanks for enlighting my doubts. I’ve runned some more tests… (i’m trying to help)

With the following code i got some weird behavior as you can see in the debug log at he end.

It’s basically the same, but i added 2 more ‘custom_options’, ‘integer_2’ (defined at init()) and ‘integer_3’ (defined in the button_one() command)

import time

from mycodo.databases.models import CustomController
from mycodo.functions.base_function import AbstractFunction
from mycodo.mycodo_client import DaemonControl
from mycodo.utils.constraints_pass import constraints_pass_positive_value
from mycodo.utils.database import db_retrieve_table_daemon


FUNCTION_INFORMATION = {
    'function_name_unique': 'custom_var_test3',
    'function_name': 'Var Test3',

    'message': 'This is a test of the Function options. ',

    'options_enabled': [
        'custom_options'
    ],

    
    # These options will appear in the settings of the Function,
    # which the user can use to set different values and options for the Function.
    # These settings can only be changed when the Function is inactive.
    'custom_options': [
        {
            'id': 'period',
            'type': 'integer',
            'default_value': 10,
            'required': True,
            'name': 'Function Period (seconds)',
            'phrase': 'The duration (seconds) to run the Function'
        },
        {
            'id': 'text_1',
            'type': 'text',
            'default_value': 'Text_1',
            'required': True,
            'name': 'Text 1',
            'phrase': 'Text 1 Description'
        },
        {
            'id': 'integer_1',
            'type': 'integer',
            'default_value': 100,
            'required': True,
            'constraints_pass': constraints_pass_positive_value,
            'name': 'Integer 1',
            'phrase': 'Integer 1 Description'
        }
        
    ],

    # Custom Actions give the user the ability to interact with the running Function Controller.
    # Each button will execute a different function within the Function Controller and pass any
    # input (text, numbers, selections, etc.) the user entered. This is useful for such things as
    # calibration.
    'custom_commands_message': 'This is a message for custom actions.',
    'custom_commands': [
        {
            'id': 'button_one_value',
            'type': 'integer',
            'default_value': 650,
            'required': True,
            'name': 'Button One Value',
            'phrase': 'Value for button one.'
        },
        {
            'id': 'button_one',  # Do a search for the function "button_one(self, args_dict)"
            'type': 'button',
            'wait_for_return': True,  # The UI will wait until the function has returned the UI with a value to display
            'name': 'Button One',
            'phrase': "This is button one"
        }
    ]
}


class CustomModule(AbstractFunction):
    """
    Class to operate custom controller
    """
    def __init__(self, function, testing=False):
        super().__init__(function, testing=testing, name=__name__)

        self.control = DaemonControl()
        self.timer_loop = time.time()
        #
        # Initialize what you defined in custom_options, above
        #
        # Standard custom options inherit the name you defined in the "id" key
        self.text_1 = None
        self.integer_1 = None
        self.period = None
        self.set_custom_option("integer_2", 2222) ##### WORKS!!!
        
       
        #
        # Set custom options
        #
        custom_function = db_retrieve_table_daemon(
            CustomController, unique_id=self.unique_id)
        self.setup_custom_options(
            FUNCTION_INFORMATION['custom_options'], custom_function)
        
        if not testing:
            self.try_initialize()

    def initialize(self):
        
        self.timer_loop = time.time()
        self.integer_2 = self.get_custom_option("integer_2")
        
        

    
    def loop(self):
        if self.timer_loop > time.time():
            return
        while self.timer_loop < time.time():
            self.timer_loop += self.period
            
       
        # sets new variable to monitor 'integer_1' custom option value  
        self.integer_1_update = self.get_custom_option("integer_1")
        self.button_one_value = self.get_custom_option("button_one_value")
        self.integer_2 = self.get_custom_option("integer_2")
        self.integer_3 = self.get_custom_option("integer_3")
        
        
        
        self.logger.debug(f'--------------------- Loop timer-------------------------')
        self.logger.debug(f'time.time(): {time.time()}')
        self.logger.debug(f'self.timer_loop: {self.timer_loop}')
        self.logger.debug(f'self.period: {self.period}')
        self.logger.debug(f'--------------------- Var -----------------------------')
        self.logger.debug(f'self.text_1: {self.text_1}')
        self.logger.debug(f'self.integer_1: {self.integer_1}')
        self.logger.debug(f'self.integer_1_update: {self.integer_1_update}')
        self.logger.debug(f'self.button_one_value: {self.button_one_value}')
        self.logger.debug(f'self.integer_2: {self.integer_2}')
        self.logger.debug(f'self.integer_3: {self.integer_3}')
        
        
        
        
        
    #Updates or create self.set_custom_option('button_one_value',) with button_one_value
    #updates integer_1 with button_one_value-1
    def button_one(self, args_dict):
        self.logger.error("Button One Pressed!: {}".format(int(args_dict['button_one_value'])))
        self.logger.debug("Button One Pressed!: {}".format(int(args_dict['button_one_value'])))
        try: 
            self.set_custom_option('button_one_value', int(args_dict['button_one_value']))
            self.set_custom_option('integer_1', int(args_dict['button_one_value']) - 1)
            self.set_custom_option("integer_3", 3333)
        except: self.logger.debug(f'Cannot set new button one value')
        
        return "Button One Pressed!"

The log is result of the operations:

1- Activate the functions with its default values.
2- Change to 10 the ‘button_one_value’ and press ‘button_one’ .
3- Deactivated the function and refresh browser. (screenshot)

2022-10-17 10:50:05,510 - INFO - mycodo.controllers.controller_function_dc18a35c - Activated in 1867.9 ms
2022-10-17 10:50:05,511 - DEBUG - mycodo.daemon - Function controller with ID dc18a35c-def0-4259-9578-0e22422e9261 activated.
2022-10-17 10:50:05,700 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - --------------------- Loop timer-------------------------
2022-10-17 10:50:05,702 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - time.time(): 1666014605.7024288
2022-10-17 10:50:05,703 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.timer_loop: 1666014615.4626243
2022-10-17 10:50:05,703 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.period: 10
2022-10-17 10:50:05,704 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - --------------------- Var -----------------------------
2022-10-17 10:50:05,704 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.text_1: Text_1
2022-10-17 10:50:05,710 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_1: 100
2022-10-17 10:50:05,715 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_1_update: 100
2022-10-17 10:50:05,721 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.button_one_value: None
2022-10-17 10:50:05,722 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_2: 2222
2022-10-17 10:50:05,723 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_3: None
2022-10-17 10:50:12,784 - ERROR - mycodo.function.custom_var_test3_dc18a35c - Button One Pressed!: 10
2022-10-17 10:50:12,785 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - Button One Pressed!: 10
2022-10-17 10:50:15,617 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - --------------------- Loop timer-------------------------
2022-10-17 10:50:15,617 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - time.time(): 1666014615.6176841
2022-10-17 10:50:15,618 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.timer_loop: 1666014625.4626243
2022-10-17 10:50:15,618 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.period: 10
2022-10-17 10:50:15,618 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - --------------------- Var -----------------------------
2022-10-17 10:50:15,618 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.text_1: Text_1
2022-10-17 10:50:15,618 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_1: 100
2022-10-17 10:50:15,618 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_1_update: 9
2022-10-17 10:50:15,619 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.button_one_value: 10
2022-10-17 10:50:15,619 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_2: 2222
2022-10-17 10:50:15,619 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_3: 3333
2022-10-17 10:50:19,148 - INFO - mycodo.controllers.controller_function_dc18a35c - Deactivated in 193.6 ms
2022-10-17 10:50:19,150 - DEBUG - mycodo.daemon - Function controller with ID dc18a35c-def0-4259-9578-0e22422e9261 deactivated.

4- Reactivated the function.
5- Deactivate the function and refresh browser. (screenshot)

2022-10-17 10:50:50,523 - INFO - mycodo.controllers.controller_function_dc18a35c - Activated in 558.1 ms
2022-10-17 10:50:50,523 - DEBUG - mycodo.daemon - Function controller with ID dc18a35c-def0-4259-9578-0e22422e9261 activated.
2022-10-17 10:50:50,751 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - --------------------- Loop timer-------------------------
2022-10-17 10:50:50,757 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - time.time(): 1666014650.7571816
2022-10-17 10:50:50,762 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.timer_loop: 1666014660.4774222
2022-10-17 10:50:50,768 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.period: 10
2022-10-17 10:50:50,770 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - --------------------- Var -----------------------------
2022-10-17 10:50:50,770 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.text_1: Text_1
2022-10-17 10:50:50,771 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_1: 9
2022-10-17 10:50:50,771 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_1_update: 9
2022-10-17 10:50:50,771 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.button_one_value: 10
2022-10-17 10:50:50,772 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_2: 2222
2022-10-17 10:50:50,772 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_3: 3333
2022-10-17 10:50:54,298 - INFO - mycodo.controllers.controller_function_dc18a35c - Deactivated in 108.2 ms
2022-10-17 10:50:54,304 - DEBUG - mycodo.daemon - Function controller with ID dc18a35c-def0-4259-9578-0e22422e9261 deactivated.

6- Change ‘integer_1’ from 9 to 100 and Save.
7- Activate the Function again.
8- Deactivate the function and refresh browser. (screenshot)

2022-10-17 10:51:45,356 - INFO - mycodo.controllers.controller_function_dc18a35c - Activated in 1032.6 ms
2022-10-17 10:51:45,356 - DEBUG - mycodo.daemon - Function controller with ID dc18a35c-def0-4259-9578-0e22422e9261 activated.
2022-10-17 10:51:45,547 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - --------------------- Loop timer-------------------------
2022-10-17 10:51:45,553 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - time.time(): 1666014705.5538113
2022-10-17 10:51:45,556 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.timer_loop: 1666014715.3125362
2022-10-17 10:51:45,557 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.period: 10
2022-10-17 10:51:45,557 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - --------------------- Var -----------------------------
2022-10-17 10:51:45,558 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.text_1: Text_1
2022-10-17 10:51:45,558 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_1: 100
2022-10-17 10:51:45,559 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_1_update: 100
2022-10-17 10:51:45,560 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.button_one_value: None
2022-10-17 10:51:45,560 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_2: 2222
2022-10-17 10:51:45,560 - DEBUG - mycodo.function.custom_var_test3_dc18a35c - self.integer_3: None

Am i doing something wrong again? ‘integer_3’ and ‘button_one_value’ disappeared from the ‘custom_options’ after i saved a new ‘integer_1’ value.

I’am running these tests because i’ve saw one post here in the forum saying that was possible to store “static variables” between reboots, but i’m not getting it.

Just to clarify: What I’m trying to do is like a Feed Schedule Function for a Drain-To-Waste hydroponics setup, where the user inputs the Start time, Span, Nº of feeds, Volume to feed, Reservoir volume, etc, and it do everything else for you, controlling Feed and recirculation pump and notificating the volume left in the reservoir with the help pf a Hall Flow meter. And my intention is to share the code here.



Post edited for clarification.

1 Like

It appears values set by set_custom_option() are erased when the configuration is saved. I found the issue and just pushed a fix. I haven’t performed extensive testing, but it appears to now work. If you would like to test, you’ll need to upgrade to the master branch. Please let me know if if resolves your issue or you encounter any other unexpected behavior.