How to create a custom input with calibration using ADS1115 ADC and Gravity analog pH and EC sensors?

At present I have some EC and pH sensors (not from Atlas Scientific) that I’ve connected to an ADC (ADS1115) and it is possible to manipulate scaling constants and conversion equations by hand to obtain calibrated outputs. However, I’m hoping to create a custom input to compute/derive EC or pH readings from the ADC reading. These sensors have to be calibrated and I am hoping to somehow store these calibration constants inside some NVM available to Mycodo. Routines would need to be created to derive these constants from calibration experiments and update them. Is any of this possible?

1 Like

This is completely possible with the current system and it’s advisable to make a new Input, since you have a desire for functionality not currently existing. There are two functions I created to be able to store data in the database for each custom input that persists across activations/deactivations. There is also the ability to create extra buttons in the Input configuration menu that execute certain functions when pressed. There is already an Input that uses this to calibrate CO2 concentration (I believe it’s the MH_Z19B Input). This would allow you to build in the ability to calibrate to certain pH solutions with a single click from the Input menu. When I get home later, I’ll provide some references for those database functions I mentioned.

Inputs have the following functions available to them:

  1. self.set_custom_option(option, value), where option is a string, and value can be any JSON serializable variable type (dict, list, str, bool).

  2. self.get_custom_option(option), where option is the string used with set_custom_option(). If there are no values found in the database for the option, None is returned.

You can see these functions used in the Anyleaf pH Input.

For building the capacity for the Input module to perform calibration, you can also reference the Anyleaf pH Input, where you can see a message, a numerical input, and three buttons are defined in the INPUT_INFORMATION dictionary:

Further down in the module you can see there are functions named after the id of each of these buttons, calibrate_slot_1(args_dict), calibrate_slot_2(args_dict), and calibrate_slot_3(args_dict). args_dict that is passed to these functions contains the value from the numerical input that was defined along with the buttons. This allows you to pass values from the user to the function that is executed when the user presses each button. In conjunction with the set and get functions, you can then store values in the database to be used later. In this example, a calibration is performed, the values are stored in the database, and these values are queried from the database the next time the Input is activated.

The Anyleaf pH Input probably has all the information you need to get your custom Input calibration working (in addition to the ADS1115 Input). Let me know if you have any other questions. If you develop a working Input, please share and I’ll add it to the built-in set.

Thank you, I am working on an input module as i learn python. And this helps

1 Like

Thanks for your detailed reply. I would like to use a DS18B20’s temperature reading to correct the EC & pH values, and I also foresee using multiple ADS1115s to add ORP, etc. Is it possible to set a custom input as a combo sensor, i.e. does the ‘input_library’ field in INPUT_INFORMATION allow a list?

Let’s take the Anyleaf pH Input for example.

Within the INPUT_INFORMATION dictionary, input_library is only used for generating docs for the manual and serves to show the user the main library or libraries used by the input. For defining dependencies to be installed, you will want to edit dependencies_module.

For defining what values are to be stored in the measurement database, the measurements_dict dictionary is used. In this Input, there is only a single channel (channel 0) that has the measurement ion_concentration and unit pH selected. You can find lists of measurements and units on the Configuration → Measurements page of Mycodo. If a measurement or unit doesn’t exist for what you want to use, you can also create new ones on this page.

For your particular use, you will need to define all the dependencies for whichever libraries you are going to use, then initialize them in the initialize_input() function, then acquire measurements, perform calculations, and store any values within the get_measurement() function (which is executed every Input Period).

Check out, which is an example module that has comments throughout to help explain how it works. Also check out the Building a Custom Input Module Wiki page.

1 Like

@KyleGabriel Thanks for your help pointing the way, I’ve been successful at implementing custom inputs for analog pH and EC probes read using the ADS1115.

The probes I use are DFRobot’s Gravity: Analog pH Sensor / Meter Pro Kit V2 for pH, the Gravity: Analog Electrical Conductivity Sensor /Meter V2 (K=1) for EC, and the Gravity: Waterproof DS18B20 Sensor Kit for water temperature. It should work for any analog pH or EC probe, and you can choose from any temperature source you have available in your system.

Here’s a couple files if anyone’s interested to try it out. Seems to work OK so far, and I’ll be testing more over this week to see how they perform. Warning: may have bugs! (18.9 KB) (13.0 KB)


Thanks for sharing dookaloosy! I got gravity sensors a while ago, but I hadn’t tried using them yet. As soon as I can, this week I’ll try them!

Thanks for sharing. I’ll review them and incorporate them into the next Mycodo release. I’ll probably also add a single Input for the EC as well, so the user can choose a single sensor Input if they only have one and the dual Input if they have both.

That’s OK. Both input examples are set up to allow measurements to be disabled as needed, though the ADC channel assignments are hard-coded (0=pH, 1=EC). I suppose my wish for a future extension would allow the individual enabling of each measurement and also assignment of the type of measurement per channel, e.g. a generic sensor input that supports analog pH, EC, ORP, DOx, TDS and so on.

That’s a good point. One Input module should suffice then. I can add in the ability to select which ACD channel each input is connected to and all the sanity checks for user input. These are good exercises to see if what we want to accomplish breaks Mycodo, then I can update the code to allow it to work.

I just went through your ph_ec input and made some changes that I’d like you to review. I decided to only work on the combo input because it supports both sensors and you an independently select which measurements you want to store, so you can turn it into a single-sensor input this way. I also found some strange stuff in the module and went to reference the anyleaf Input to see what you were referencing and I found where the strangeness came from. It seems the person who made the anyleaf Input was a bit confused about the database storage mechanism. In any case, I made some changes that remove unnecessary code and make it a bit easier to read.

Essentially, I removed all the get_custom_option() functions in the initialization, as all these values are pulled from the database and set to the variables when setup_custom_options() is called. As an example, if you have a custom option with an id of “ph_cal_v1” and the default value is 5, then self.ph_cal_v1 will be set to 5 if a user has never saved a value. They are free to set a value in the dropdown settings of the Input, and if they save it as 6, then this value is stored in the database as 6 which will be automatically set the next time the Input is activated. Now, the set_custom_option() function saves values in the exact same dictionary as the user input, so if you have a custom option with an id of “ph_cal_v1” and you execute set_custom_option(“ph_cal_v1”, 7), this overwrites the current value of 6 in the database.

I noticed you modeled this module off the anyleaf module that was storing data in the database with the same names as the custom_options that were set, effectively overwriting the user options. Now, this is actually a pretty clever way of doing it, as the user can see what the calibration changed the values to. But, there was no way to reset the calibration back to the defaults without deleting and adding a new Input.

Here are the other changes I made:

  1. Add ability to select which ADC channel each sensor is connected to.
  2. Add clear calibration slots actions (which required the addition of a delete_custom_option() function)

I just pushed my changes and you can test the new module by upgrading to master.

I also just did a review of the Anyleaf pH and ORP Inputs and made some changes, similarly to your Input, to remove unnecessary code and also added the ability to select an external temperate compensation measurement.

I was able to upgrade to master, and your changes appear to work well. Will monitor it over the course of the next day or so and report back.

Just reading the code, found one minor correction at line 472:

    v = self.get_volt_data(int(self.adc_channel_ec))  # pH

The comment should say “# EC”.

Yes, I did copy most of the anyleaf module code over. Following your changes, I see that the calibration values don’t update until the Inputs page is refreshed in the browser. The new calibration does take effect at the next sensor read, as it should. The “live update” of the calibration data was a nice feature that I liked from the anyleaf module, but it’s not a dealbreaker if it’s not there anymore. I do like the calibration reset button.

Fixed. Thanks.

This is likely due to timing and the database values not changed until after the page has been requested and rendered.

I’m not sure what you mean, as there’s been no change in how data is stored during calibration.

In the old anyleaf module, the moment you click on any of the ‘Calibrate’ buttons, the ‘Cal data’ fields for that slot would immediately be updated with the voltage, pH/EC and temperature values. So it was nice UI feedback to know that the new calibration data had “taken effect”, without having to check the Data > Live page. Like you said, the author of the anyleaf module was pretty clever to do this – but it doesn’t matter, this feature (bug?) isn’t crucial to the utility of the input.

What I’m saying is I haven’t changed anything related to how data is saved or presented. None of my changes should have affected that behavior.

Has something changed about your setup, such as using an external temperature measurement for compensation?

Indeed, I don’t see any of your changes changing this behavior… unless the block of code containing all the get_custom_option() calls (that you’d removed) in initialize_input() was somehow getting called repeatedly in the custom input I shared?

No, I haven’t changed my setup – I am still connecting the same temperature source I previously used to your revised input module. I can load my old custom input again (I removed it to upgrade to master) if you’d like me to make a 1-to-1 comparison.