Working with pi’s is pretty easy and I can always cycle them into something else if I go another route with carrier boards but I am fully commited to their EZO ecosystem. I’m happy with the sensor package. I probably didnt need individual screens for each nutrient tank but its what we were used to with previous solutions. The only thing that really hurts financially was my decision to use their peristatic pumps rather than driving cheaper pumps with a relay board. Ultimately it’ll be easier and I’ll have better quantity and time logging of the pumps, things I don’t know how to do with gpio.
I agree with you, Atlas Scientific’s EZO ecosystem is great. It basically includes everything I need or would ever consider adding, with only one exception. It would be nice if they offered a full-spectrum quantum PAR sensor. I know their EZO-RGB can measure LUX, which can supposedly be converted to PAR, but I’m not sure if there are any drawbacks to doing that.
I also looked into what changes I’d need to make if I had to switch to their AtlasDesktop platform because they abounded AtlasIoT for the RPi. From what I can tell, the RPi would have to be replaced with a PC stick, and the i3 InterLink or any other carrier boards currently in use would need to be swapped for USB EZO carrier boards. If those are the only changes, it wouldn’t be too bad.
Regarding peristaltic pumps, I’m thinking about purchasing one or two, and if I like how they perform, I’ll get more. I have two hydroponic systems that would each require five pumps if I were to automate all of the dosing. Atlas sells two kits with the regular-sized pumps and all the necessary components. From what I can see, the only significant difference between the kits is that one is a single pump kit and the other is a triple pump kit. The triple kit also has a larger enclosure and uses just one data cable and one power supply. However, I don’t understand the pricing. Normally, buying more of something results in a slight discount, but buying three single pump kits is actually $72 cheaper than purchasing the triple pump kit. Am I missing something?
You also mentioned that you considered using a cheaper pump to save money but felt the benefits of the Atlas pumps justified the higher cost. After using them, do you still feel the same way?
Lastly, I recently switched to using dry nutrients that I premix before adding to my solution. They usually dissolve completely, but sometimes I notice small remnants left behind. Do you think this could pose an issue for these pumps, or do you believe they’ll perform as needed without any problems?
For the triple pump kit I’ve come to figure Atlas charges about triple what you would expect for each component, expecially wires and cables. You need to consider there is a sensor bridge($10) inside, a higher output power supply, and a project box, possibly with screen printing or stickers. Plus the labor to assemble it. So it seems accurate to their pricing. I use to mix dry ferts at 4:1 with water but I would get precipitates after a day. 6:1 is stable but I ultimately use 10:1 so I can consolidate to parts A and B rather than separating epsom salts. Plut its makes my dosing really easy to figure out as I can directly copy grams of my formulas to ml of liquid dose. I use a tool called Hydro Buddy to make my formulas. Took a while to learn to use it but I have my lab analysis of my water supply built into it and I use nutrient formulas from colleges. You may have better luck mixing all your dry ferts into individual wet mixes before combining. At least separating anything with calcium and magnesium, and never mix your calcium nitrate with anything but water before adding to the reservoir.
As for the pumps I’ve been smashing my head against the wall trying to figure out how to send commands to them. While the atlasiot service is running their sample code is interfered with buy the service polling all the sensors and pumps. I dont know enough about python to code anything myself and I cant find a way to send commands with mqtt. It was great being able to calibrate the pumps with a UI but I haven’t found a path to automations at all. Adding microcontrollers and controlling them with esphome looks like the easiest path right now. I have an email out to their support team looking for some direction.
Okay. I just wanted to make sure that I wasn’t missing something just in case I decide to buy three individual units. Right about now, a little extra wiring might be worth saving $72.
How do you measure the ratio, by weight.? I use a magnetic mixer with a 1000 mL glass flask to to premix parts A and B. I fill it up with RO water in between 500-800 mL depending on how grams of dry mix I’m using. How would I apply the 10:1 ratio to see where I’m at?
I’m familiar with the Hydro Buddy but never used it. My nute mixtures are premade from CropSalt and I use the scale they provide as a reference. I then adjust accordingly based on the PPM reading.
I forgot all about this but actually did it before and got it to work. I only have a pH and DO sensor so I could use AtlasIoT for everything but wanted to know if I could configure everything seperate and could both be run at the same time or while in use would AtlasIoT lock everything else out. It took me a while to figure it out but I eventually did. I’ll look for the scripts I used for the RPi and you should be able to alter them to work for yoy.
So there’s no automation with the pumps when using AtlasIoT? What options does it actually provide? I would have assumed the pumps would be able to use an attached pH or Conductivity sensor to control the dosing. However, from what you’re saying it sounds like they don’t. If not, then you should be able to use HA to automate based on your sensor readings and send the MQTT command to the pump to turn on until the desired level is met and then shutoff. I’ll post it as soon as I find it and makes sure it works.
You should get a response back from Atlas by their tech Charles. He does all of the coding for the Atlas IoT RPi and Desktop version. He recently emailed me and said he’s currently bringing the Desktop version up-to-date and afterwards he’ll implement the updates and changes into the RPi version.
I found the scripts that work and I know how to get the pH reading but anything else I can’t do. I’ve been trying to shutoff the led on my pH sensor using the L, 0 command as stated in the docs but it’s returning the pH value instead. I didn’t use the image version of AtlasIoT, instead I installed it manually with the Nginx auto-restart so I’m wondering now if AtlasIoT is actually locking me out or due to the constant polling and restarting when i kill the process.
I bastardize things and use gallons and g and ml due to containers i use but I’ll translate it to all metric to make it easier. 10:1 isnt exactly acurate since i focus on nutrient quantity. I take 100g of dry part A, add water to the 1000ml mark and mix. I use my formulas to figure out how many grams of A and B I need in the reservoir. (in my case my reservoirs are 40gal each). If you need 10g of part A you would add 100ml. General guideline is good growing conditions should be around 20% uptake a take so the daily dosing would be 20ml for this example. Adjust based on your EC target and plant health. In theory if things are all balanced out your pH shouldnt move much if your EC is correct for the system but the internet loves to argue about that. I currently use Masterblend premade but I supplement with Potassium Nitrate, Zinc EDTA, Manganese EDTA and Iron EDDHA. Important note here Iron EDTA that is widely used fully precipitates out by 6.5pH and up, and starts leaving above 6. Oh and EDDHA is VERY red.
There’s an on screen calibration, continuous dispensing, volume dispensing, volume over time dispensing, and constant flow rate. I mostly use volume over time and distribute a dose over the full daylight cycle since plants pull nitrogen out of the water within minutes. This way they get an even nitrogen dose all day and that alone is a big upgrade for me. There is no logic paths built in for automation from a control aspect. There are alarm functions for the inputs which look as though integrating actions would be a straight forward. I’m fairly confident throwing commands at the components from a 2nd script while the atlasiot or sample code environments are active would be a bad idea.
I installed manually as well, I had a lot of issues with the image on my first my first test run. I had to tweak things myself to launch the browser on startup and the RPI5’s start the browser before the atlas service gets running and I get a 502 bad gateway error from nginx. Goes away when I reload or x out the browser and it reloads. I haven’t figured out a way to delay the browser loading on startup yet.
So I should be able to help you a little. I used a shell script that is run when the RPi starts with a sleep command. That should solve your 502 bad gateway error.
#!/bin/bash
sleep 15
chromium-browser http://localhost:5000
I was also able to get the single commands of “R” and “i” on the pH sensor to work when AtlasIoT is running but I’m having problems with the others. I tried both with the AtlasIoT service killed and running. All you have to do is create the script, change the permissions to “Anyone”, and execute it. The script it not written to be triggered via MQTT from Home Assistant. I wrote them just to test them within the RPi to make sure they work.
Edit: The device info script does NOT work with the AtlasIoT service running. Below are the three commands to kill, check the sttatus, and restart the service. To see the difference, try running the device info script with the service running and then after you kill it. When the service is running the response will either be the pH value or an error.
kill - sudo systemctl stop AtlasIoT
restart - sudo systemctl start AtlasIoT
status - sudo systemctl status AtlasIoT
import smbus
import time
# I2C configuration
I2C_BUS = 1 # I2C bus 1 on most Raspberry Pi models
I2C_ADDRESS = 0x63 # Default I2C address of the pH EZO sensor
def read_ph():
try:
bus = smbus.SMBus(I2C_BUS)
# Send the 'R' command to request a reading
bus.write_byte(I2C_ADDRESS, ord('R'))
time.sleep(1.5) # Wait for the sensor to process
# Read 7 bytes of data
data = bus.read_i2c_block_data(I2C_ADDRESS, 0, 7)
# Check the status byte
if data[0] == 1: # Successful reading
# Decode the pH value
ph_string = ''.join(chr(b) for b in data[1:] if chr(b).isdigit() or chr(b) == '.')
return float(ph_string)
else:
print(f"Error: Status byte {data[0]}")
return None
except Exception as e:
print(f"Error reading pH: {e}")
return None
# Test the function
ph_value = read_ph()
if ph_value is not None:
print(f"pH: {ph_value}")
import smbus
import time
# I2C configuration
I2C_BUS = 1 # I2C bus 1 on most Raspberry Pi models
I2C_ADDRESS = 0x63 # Default I2C address of the pH EZO sensor
def send_command(command):
try:
# Send the command to the device as a single byte
bus.write_byte(I2C_ADDRESS, ord(command)) # Use write_byte instead of write_i2c_block_data
time.sleep(0.3) # Allow the sensor to process the command
print(f"Command '{command}' sent successfully.")
except Exception as e:
print(f"Error sending command '{command}': {e}")
def read_response():
try:
# Increase response length to 16 or more bytes
response = bus.read_i2c_block_data(I2C_ADDRESS, 0, 16) # Adjusted to 16 bytes
# Clean up the response and join the data
response_str = "".join(chr(b) for b in response if b != 0).strip()
return response_str
except Exception as e:
print(f"Error reading response: {e}")
return None
def get_device_info():
# Send 'i' command to get device info
send_command("i")
# Read the response
response = read_response()
if response:
print(f"Raw Device Info: {response}")
# Split response into parts
response_parts = response.split(',')
# Clean up the first part to remove the status byte
cleaned_device_info = response_parts[1:] # Remove the first item (status byte)
# Output the cleaned information
print(f"Device Info (cleaned): {cleaned_device_info}")
else:
print("No response received.")
if __name__ == '__main__':
bus = smbus.SMBus(I2C_BUS) # Re-initialize the bus
get_device_info()
I’ve tried to alter them for the “Status” and the “L,?” command which go through but I’m not getting a response. See what you can come up with and let me know.
I also found this but haven’t had a chance to really try anything from it.
Okay. I’m making progress. This file is found on the Atlas github page…
…but you could just copy the code below and make a file name AtlasI2C.py
.
#!/usr/bin/python
import io
import sys
import fcntl
import time
import copy
import string
class AtlasI2C:
# the timeout needed to query readings and calibrations
LONG_TIMEOUT = 1.5
# timeout for regular commands
SHORT_TIMEOUT = .3
# the default bus for I2C on the newer Raspberry Pis,
# certain older boards use bus 0
DEFAULT_BUS = 1
# the default address for the sensor
DEFAULT_ADDRESS = 98
LONG_TIMEOUT_COMMANDS = ("R", "CAL")
SLEEP_COMMANDS = ("SLEEP", )
def __init__(self, address=None, moduletype = "", name = "", bus=None):
'''
open two file streams, one for reading and one for writing
the specific I2C channel is selected with bus
it is usually 1, except for older revisions where its 0
wb and rb indicate binary read and write
'''
self._address = address or self.DEFAULT_ADDRESS
self.bus = bus or self.DEFAULT_BUS
self._long_timeout = self.LONG_TIMEOUT
self._short_timeout = self.SHORT_TIMEOUT
self.file_read = io.open(file="/dev/i2c-{}".format(self.bus),
mode="rb",
buffering=0)
self.file_write = io.open(file="/dev/i2c-{}".format(self.bus),
mode="wb",
buffering=0)
self.set_i2c_address(self._address)
self._name = name
self._module = moduletype
@property
def long_timeout(self):
return self._long_timeout
@property
def short_timeout(self):
return self._short_timeout
@property
def name(self):
return self._name
@property
def address(self):
return self._address
@property
def moduletype(self):
return self._module
def set_i2c_address(self, addr):
'''
set the I2C communications to the slave specified by the address
the commands for I2C dev using the ioctl functions are specified in
the i2c-dev.h file from i2c-tools
'''
I2C_SLAVE = 0x703
fcntl.ioctl(self.file_read, I2C_SLAVE, addr)
fcntl.ioctl(self.file_write, I2C_SLAVE, addr)
self._address = addr
def write(self, cmd):
'''
appends the null character and sends the string over I2C
'''
cmd += "\00"
self.file_write.write(cmd.encode('latin-1'))
def handle_raspi_glitch(self, response):
'''
Change MSB to 0 for all received characters except the first
and get a list of characters
NOTE: having to change the MSB to 0 is a glitch in the raspberry pi,
and you shouldn't have to do this!
'''
if self.app_using_python_two():
return list(map(lambda x: chr(ord(x) & ~0x80), list(response)))
else:
return list(map(lambda x: chr(x & ~0x80), list(response)))
def app_using_python_two(self):
return sys.version_info[0] < 3
def get_response(self, raw_data):
if self.app_using_python_two():
response = [i for i in raw_data if i != '\x00']
else:
response = raw_data
return response
def response_valid(self, response):
valid = True
error_code = None
if(len(response) > 0):
if self.app_using_python_two():
error_code = str(ord(response[0]))
else:
error_code = str(response[0])
if error_code != '1': #1:
valid = False
return valid, error_code
def get_device_info(self):
if(self._name == ""):
return self._module + " " + str(self.address)
else:
return self._module + " " + str(self.address) + " " + self._name
def read(self, num_of_bytes=31):
'''
reads a specified number of bytes from I2C, then parses and displays the result
'''
raw_data = self.file_read.read(num_of_bytes)
response = self.get_response(raw_data=raw_data)
#print(response)
is_valid, error_code = self.response_valid(response=response)
if is_valid:
char_list = self.handle_raspi_glitch(response[1:])
result = "Success " + self.get_device_info() + ": " + str(''.join(char_list))
#result = "Success: " + str(''.join(char_list))
else:
result = "Error " + self.get_device_info() + ": " + error_code
return result
def get_command_timeout(self, command):
timeout = None
if command.upper().startswith(self.LONG_TIMEOUT_COMMANDS):
timeout = self._long_timeout
elif not command.upper().startswith(self.SLEEP_COMMANDS):
timeout = self.short_timeout
return timeout
def query(self, command):
'''
write a command to the board, wait the correct timeout,
and read the response
'''
self.write(command)
current_timeout = self.get_command_timeout(command=command)
if not current_timeout:
return "sleep mode"
else:
time.sleep(current_timeout)
return self.read()
def close(self):
self.file_read.close()
self.file_write.close()
def list_i2c_devices(self):
'''
save the current address so we can restore it after
'''
prev_addr = copy.deepcopy(self._address)
i2c_devices = []
for i in range(0, 128):
try:
self.set_i2c_address(i)
self.read(1)
i2c_devices.append(i)
except IOError:
pass
# restore the address we were using
self.set_i2c_address(prev_addr)
return i2c_devices
Put it in the same directory as the following script. Run the script and you should get responses for several tests commands I tried.
import time
from AtlasI2C import AtlasI2C # Assuming the second script is saved as atlas_i2c.py
# I2C configuration
I2C_BUS = 1 # I2C bus 1 on most Raspberry Pi models
I2C_ADDRESS = 0x63 # Default I2C address of the pH EZO sensor
def send_command(command):
try:
# Initialize the AtlasI2C object
atlas = AtlasI2C(address=I2C_ADDRESS, bus=I2C_BUS)
# Send the command and get the response
response = atlas.query(command)
print(f"Command '{command}' sent successfully.")
print(f"Response: {response}")
except Exception as e:
print(f"Error sending command '{command}': {e}")
def test_command(command):
# Send the specified command
send_command(command)
if __name__ == '__main__':
# Test the 'Status' command
print("Testing 'Status' command:")
test_command("Status")
# Test the 'i' command
print("\nTesting 'i' command:")
test_command("i")
# Test the 'R' command
print("\nTesting 'R' command:")
test_command("R")
# Test the 'L,?' command
print("\nTesting 'L,?' command:")
test_command("L,?")
You should be able to look at my script and see what you need to change to test your other sensors. It should just be the I2C address and the test commands at the bottom.
I have not tried this with the AtlasIoT service running. I “killed” the process and then got these working. Now that they’re working I’m going to try to send commands via MQTT from Home Assistant and see if I could get a response that could be used in an automation. I’m currently working 12-hour shifts doing security so i don’t have a lot of time to try so if you beat me to it, let me know.
I got it working.
Create a script on your RPi with the following code. Change the permissions to ANYONE. Then execute in a terminal window. It will continue to print a statement showing that it is running. You can always change this later and have it run in the background. You’ll also need to create a service to start the script on boot. Add your info for the ip, username, and password.
import paho.mqtt.client as mqtt
import time
from AtlasI2C import AtlasI2C # Import the AtlasI2C class from your module
# Define MQTT settings with username and password
MQTT_BROKER = "your home assistant ip" # IP or hostname of your MQTT broker
MQTT_PORT = 1883
MQTT_USERNAME = "your mqtt user name" # Replace with your MQTT username
MQTT_PASSWORD = "your mqtt password" # Replace with your MQTT password
MQTT_TOPIC_COMMAND = "ezo/command" # Topic to receive commands
MQTT_TOPIC_RESPONSE = "ezo/response" # Topic to send responses back
# Callback when a message is received
def on_message(client, userdata, msg):
print("Message received callback triggered")
payload = msg.payload.decode()
print(f"Received message: {payload}")
if ":" in payload:
address_str, command = payload.split(":", 1)
try:
address = int(address_str)
print(f"Using address: {address}")
except ValueError:
print("Invalid address format.")
return
ezo_sensor = AtlasI2C(address=address)
try:
response = ezo_sensor.query(command)
client.publish(MQTT_TOPIC_RESPONSE, response)
print(f"Response sent: {response}")
except Exception as e:
print(f"Error querying sensor: {e}")
client.publish(MQTT_TOPIC_RESPONSE, f"Error: {e}")
else:
print("Invalid message format. Expected 'address:command'.")
# Create MQTT client instance
client = mqtt.Client()
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) # Set username and password
client.on_message = on_message
# Connect to MQTT broker
client.connect(MQTT_BROKER, MQTT_PORT, 60)
# Subscribe to the command topic
client.subscribe(MQTT_TOPIC_COMMAND)
# Loop forever to handle incoming messages
client.loop_start()
# Add periodic print statements
while True:
print("Script is running...")
time.sleep(60) # Sleep for 60 seconds
Make the the AtlasIoT script from my previous comment is in the same folder as this script.
In Home Assistant create a MQTT sensor in your configuration file or the file you point to for this. Depending on how you have it set up it will look like one of the below examples. You might have to restart HA.
- sensor:
- name: "EZO Sensor Response"
state_topic: "ezo/response"
qos: 1
sensor:
- platform: mqtt
name: "EZO Sensor Response"
state_topic: "ezo/response"
qos: 1
Then use the Developer Tools or whatever you want to run the below action. Enter the decimal for the sensor followed by a colon followed by the command. For example, for a pH sensor use 99 not the 0x63. The below action gets the status of the LED, Change it to 99:Status or 99:R or 99:i or whatever command you want from the datasheet. Change the 99 for whatever sensor you want. After you send the command check the state of the sensor you created and it should have a response if the command you sent provides one.
action: mqtt.publish
data:
topic: "ezo/command"
payload: "99:L,?"
You could probably get away with using only one sensor and add a delay in between commands in your automation. For example, an automation to control dosage of pH up and pH down would use a time trigger to check to publish a MQTT command to check the pH every 30 minutes. Then use conditions based on whether the level is to high or to low that send a command to trigger the pump to add a certain amount. Then in 30 minutes when its had time to mix it’ll check again and trigger the pump again if needed. You might have to strip away some of the response and tweak a fee other things but I think the biggest issue is solved.
Here’s how to start the script on system boot and automatically restart it if it stops.
Created a Systemd Service File:
sudo nano /etc/systemd/system/mqtt_ezo_script.service
Add the following content to the service file but with the correct path to your script and the working directory that it is in:
[Unit]
Description=MQTT EZO Script Service
After=network.target
[Service]
ExecStart=/usr/bin/python3 /path/to/script.py # Enter the path to your script.
WorkingDirectory=/path/to/ # Enter the path to the directory your script is in.
StandardOutput=inherit
StandardError=inherit
Restart=always
RestartSec=5
User=pi # Enter your username if you use something different.
[Install]
WantedBy=multi-user.target
Reloaded the Systemd Daemon:
sudo systemctl daemon-reload
Enabled the Service to Start on Boot:
sudo systemctl enable mqtt_ezo_script.service
Started the Service Immediately:
sudo systemctl start mqtt_ezo_script.service
Verified the Service Status:
sudo systemctl status mqtt_ezo_script.service
To temporarily stop the service so you could edit your script and do whatever and not worry about it restarting.
sudo systemctl stop mqtt_ezo_script.service
To start it back up.
sudo systemctl start mqtt_ezo_script.service
The AtlasIoT service needs to be stopped for the script and all of the sensor commands to work. I thought about adding a command to the script that stops and then restarts the AtlasIoT service to work around sending commands via MQTT but it seems like too much trouble. Maybe I’ll send an email to Atlas tech support to see if there’s a way for both to function at the same time. However, if I had your setup I don’t even think i would use the AtlasIoT and instead handle everything in Home Assistant. Instead of having the AtlasIoT UI on the touchscreens, I’d replace it with HA in kiosk mode. I’m sure you could design a better UI and have it display and function the same as AtlasIoT but with additional functionality to handle the pumps or anything else you might want. Then you’ll no longer have to do it from your computer or wherever you primarily work on HA at. You could have four different views, one for each screen, that all use the same exact yaml by using the decluttering card and passing a variable from each view/screen so the everything goes to the correct rpi.
I actually started making one several months ago just to see if i could duplicate the UI of AtlasIoT. I didn’t put too much time into it but as you can see it looks similar.
Your sensor test script works just fine but you knew that. I ran into a few hiccups. One was the silly mistake of removing the " " when i put my password in the mqtt script. The 2nd is that paho-mqtt needs to be installed for the import. I had success using
sudo apt install python3-paho-mqtt
The script runs but I dont think its actually communicating with my broker. I tried running it side by side with the atlas sample code with non stop polling on all sensors and I couldnt read anything with mqtt explorer. I’ve tried using homeassistant.local and the actual ip address of the HA server. I’m doing something wrong here. Is there somthing I don’t know about in the paho-mqtt install maybe?
Oh and setting up the service I learned you cant name the script with .py. I couldn’t get the service to run for some reason. (code=exited, status=200/CHDIR) every time but I ran CHMOD 777 on the script. It’s just like the AtlasIOTCore install so I dont know why it doesn’t want to work for me.
So just to let you know, I only have a basic understanding of coding so I do cheat a little and use AI. The two that have been the most helpful is ChatGPT and the Github’s Copilot. I use the free version for both and just write a short sentence about what I’m trying to do and paste my code after it. It helps a lot. I basically use it as a tool to help me learn. I bring that up because I remember I had an issue before with something I used either from that or something that was in the AS documents when it came to setting permissions using CHMOD. So when I created the scripts associated with this I just used the file explorer on the RPi and right-clicked on it, opened the properties, went to the permission settings, and then changed the three of them to ANYONE. If you use the CHMOD command this is also a great way to check to make sure it worked.
Yes, unlike the scripts that end in .py the service ends in .service.
FYI : Just so you’re aware of it, I know there are small things in the documents that are not written correctly so if you try to copy and paste them they will not work. For example, if you try to copy and paste the below command to execute the script you’ll get an error.
When I first installed AtlasIoT, this drove me crazy because I couldn’t figure out what i was doing wrong. I finally realized that the dash in “—url” is actually supposed to be “–url”, two dashes but whatever they used to write the documents must have autocorrected it and combined it into a single long dash. I’m only bringing it up just so you’re aware of it. I’ve wasted hours because of something small like that which is why i like plugging the code into one of the AI services, sometimes both of them, because it helps catch the little things.
I use the actual ip address. The first thing that comes to mind is similar to the issue you had when you needed to install the python3-paho-mqtt package. I apologize, i did that a while ago and forgot to include that in my explanation. Did you install the MQTT client? Try updating and then installing it if you haven’t already. This could be the reason you’re not seeing it in MQTT Explorer.
sudo apt update
sudo apt install mosquitto-clients
You can also open a terminal window on the RPi and run the following to listen for a response.
mosquitto_sub -h "YOUR HA IP ADDRESS" -t ezo/response -u "YOUR USERNAME" -P "YOUR PASSWORD"
Then open a separate terminal window on the RPi and run the following to publish a command. You should see it appear in the first terminal window. I’ve also made the same mistake multiple times when it comes to removing the " " or things I thought were just placeholders things for entering my own information when they actually needed to be included. However, this time you do need to remove them when you enter your own info.
mosquitto_pub -h "YOUR HA IP ADDRESS" -t ezo/command -u "YOUR USERNAME" -P "YOUR PASSWORD" -m "99:R"
I have a feeling that your issue might be that you haven’t installed the MQTT client because I don’t think this is done for installing AtlasIoT.
Let me know if this solves the problem.
Yup I did just that. Starting to remember some linux from years ago. Speaking of remembering things, don’t forget after doing apt update you need to
sudo apt upgrade
I noticed that when I did the first install. I have a streamlined google doc of the entire process I use since I did 4 of these in a row. If I remember right that double dash turned long hyphen turned into a ? when i pasted it though remote screenshare.
I was able to see the test publish come through explorer. I haven’t had any luck getting a response from a published command yet but I haven’t spent much time on it. I’ve been trying to troubleshoot why this error persists on the service script. I’ve tried dozens of things including chatgpt’s suggestions. BTW you have an “INI” above the [Unit] in that script that seems to serve no purpose and was triping an ignore error for me. My brain is spent for now. I’ll try more later.
Thanks for pointing that out. I went back and edited my post and removed it.
If you’re still refering to the (code=exited, status=200/CHDIR) error and since I haven’t seen your code I tried asking Github Copilot just for the heck of it to see what it says. Here’s what I asked.
Here’s the response I received.
I actually had an issue with the WorkingDirectory when I first created the service but I can’t remember the error. My issue was that I entered the directory path incorrectly. Most of my errors usually turn out to be syntax errors which drives me crazy. I have a feeling that the path for the WorkingDirectory is probably your issue too. Also, since Copilot used generic code in the example i went back and looked at the code I provided and noticed there’s actually three things that you need to enter your own info for. I’ll go back and edit the comment to point them out better.
[Unit]
Description=MQTT EZO Script Service
After=network.target
[Service]
ExecStart=/usr/bin/python3 /path/to/script.py # Enter the path to your script.
WorkingDirectory=/path/to/ # Enter the path to the directory your script is in.
StandardOutput=inherit
StandardError=inherit
Restart=always
RestartSec=5
User=pi # Enter your username if you use something different.
[Install]
WantedBy=multi-user.target
Woke up with a fresh brain and immediately saw the issue. You cant have a comment like this after a line in a service script. I’m not sure you can use comments in them at all. It works fine now. I’ll update again after I play with command and response testing more.
I got some chatgpt help troubleshooting and the script runs returns a correct response to commands now. Turned out to be another ghost ? that was added from copy and paste.
I also got an email from Charlie detailing that they maded the decision 2-3 years ago to never api or mqtt inputs due to security concerns. I think this is something we could get some key encryption into pretty easily to address those concerns but I’ll have to talk to cybersec friends for a better perspective on that. Is this something you would be interesting in developing into a github project? I’ve never done one before but I think its doable.
Noting some problems to solve for the future. Temperature compensation on the pH and EC isnt happening natively anymore. It wouldnt be very hard to let HA do it with an automation and regularly send a temp value and the compensation command to the other sensors. My issue is that I have my temp output in fahrenheit instead of celcius but that could be changed back and a template could be used to convert c to f in HA.
The EC sensor by default outputs mS, TDS, salinity and more. These can all be turned on or off but I currently have the script parsing out the first value after the : sending it as a payload to variable topic based on the command payload. Example: if ezo/command receives 99:R, it will read the pH value and then take the output “Success 99:5.8” and send 5.8 to /ezo/99 This makes it easier to set up sensors in HA. I have no clue how to send out the whole JSON template the way atlas does since we only get back basic outputs from an R command. Something to keep in mind for the future if needed.
I have it running an automation where trigger: every 5min, condition if: ph sensore numerical value is above a set threshold it sends “1:D,2” and doses 2ml of diluted phosphoric acid. I’m doing the same with EC publishing 2 msgs, one for A and B dosing pumps.
import paho.mqtt.client as mqtt
import time
from AtlasI2C import AtlasI2C # Import the AtlasI2C class from your module
import re # To use regular expressions for extracting numbers
import logging
# Configure logging to print logs to the console
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# MQTT Settings (Grouped together for better organization)
MQTT_BROKER = "your home assistant ip" # IP or hostname of your MQTT broker
MQTT_PORT = 1883
MQTT_USERNAME = "your mqtt user name" # Replace with your MQTT username
MQTT_PASSWORD = "your mqtt password" # Replace with your MQTT password
MQTT_TOPIC_COMMAND = "ezo/command" # Topic to receive commands
MQTT_TOPIC_BASE = "ezo" # Base topic for publishing responses, e.g., "ezo/{address}"
# Callback when a message is received
def on_message(client, userdata, msg):
logging.debug("Message received callback triggered")
payload = msg.payload.decode()
logging.debug(f"Received message: {payload}")
if ":" in payload:
address_str, command = payload.split(":", 1)
try:
address = int(address_str)
logging.debug(f"Using address: {address}")
except ValueError:
logging.error("Invalid address format.")
return
ezo_sensor = AtlasI2C(address=address)
try:
logging.debug(f"Sending command: {command} to sensor at address {address}")
response = ezo_sensor.query(command)
logging.debug(f"Response from sensor: {response}")
# Modify regex to account for multiple spaces between components
match = re.search(r"Success\s+\d+:\s+([-+]?\d*\.\d+|\d+)", response) # Allow for multiple spaces
if match:
numeric_value = match.group(1) # Extract the numeric value from the match
logging.debug(f"Extracted numeric value: {numeric_value}")
else:
numeric_value = "Error: No numeric value found"
logging.error(f"Error extracting numeric value from response: {response}")
# Define the topic for publishing based on the address
response_topic = f"{MQTT_TOPIC_BASE}/{address}" # Example: ezo/{address}
# Publish the cleaned response (only numeric value) to the appropriate topic based on address
client.publish(response_topic, numeric_value)
logging.debug(f"Response sent to topic '{response_topic}': {numeric_value}")
except Exception as e:
logging.error(f"Error querying sensor: {e}")
response_topic = f"{MQTT_TOPIC_BASE}/{address}" # Error topic will also use the same base
client.publish(response_topic, f"Error: {e}")
logging.debug(f"Error response sent to topic '{response_topic}': Error: {e}")
else:
logging.warning("Invalid message format. Expected 'address:command'.")
# Callback when the client connects to the MQTT broker
def on_connect(client, userdata, flags, rc):
logging.debug(f"Connected with result code {rc}")
# Subscribe to the command topic after connection is established
client.subscribe(MQTT_TOPIC_COMMAND) # Subscribe using the topic provided in MQTT_TOPIC_COMMAND
# Create MQTT client instance
client = mqtt.Client()
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) # Set username and password
client.on_connect = on_connect # Register on_connect callback
client.on_message = on_message # Register on_message callback
# Connect to MQTT broker
logging.debug(f"Connecting to MQTT broker at {MQTT_BROKER}...")
client.connect(MQTT_BROKER, MQTT_PORT, 60)
# Start the MQTT loop
client.loop_start()
# Add a delay to let the client connect and subscribe
time.sleep(2)
# Main loop for running the script and printing log messages
try:
while True:
logging.debug("Script is running...")
time.sleep(60) # Sleep for 60 seconds between loops
except KeyboardInterrupt:
logging.info("Script interrupted by user.")
finally:
# Cleanup before exiting
client.loop_stop()
logging.debug("MQTT loop stopped.")
This is my updated version of the script. its very verbose if run from command line so you can see whats happening live.
This makes sense and would explain why the example I used was written without hashtag comments and instead like it is below.
ExecStart=/usr/bin/python3 /path/to/script.py
WorkingDirectory=/path/to/
So in order to bring additional attention to it I added my own hashtag comments. I’ll go back and fix that in my prior posts to explain that the comment needs to be removed.
This is understandable, plus most users probably don’t need anything other than what AtlasIoT currently provides. Did you ask or did he mention if there’s anyway to avoid terminating AtlasIot when using your own script to communicate via I2C with the sensors? I don’t know enough about the process to know if this is something AS did intentionally or if it’s something that technically just can’t be done.
I’ve never created a Github project and my coding experience is very basic. I have a degree in Computer Science but never went into the field professionally so I don’t know how much help I would be. However, I do enjoy a challenge and would definitely contribute if I can. Are you thinking of something like AtlasIoT but for HA as a custom card with full communication to any Atlas sensor installed on the RPi?
Did all of their pH probes handle it natively? Because I’ve only seen their industrial pH probes manage both pH and temperature. I also don’t know the range your solution temperature falls within, but the lowest mine falls to is 65°F. According to the pH temperature compensation calculators I’ve used, at this temperature the pH would only be off by 0.023, which seems minimal. However, like you said, an automation should easily solve the problem.
Yes, this is something I haven’t done yet but definitely need to do. I took a quick look at your script and noticed that you expanded on the original one. Later today, I’ll take a closer look at it. I’m interested to see the changes you made and will probably incorporate a few of them into my own.
Something I did manage to do was create shell commands in HA so I could quickly stop AtlasIoT when I want to test something with the script and then start it back up. I normally use TigerVNC Viewer to remotely access my RPi and make changes, but by using the shell commands it saves time and simplifies my workflow.
I didn’t ask about a software interrupt. Forgot and kinda figured if we can get this much working, we can probably get every function of their software working on our own. I asked if there were definate plans to add pump automation logic into atlasIoT in the future. I pointed out that their store and the advertisement of atlasIoT led me to setting everything up on the RPI when it would absolutely have been better to use the esp32 hydroponics barebones kit they sell. (I didnt know it existed when buying beacuse their store doesnt push it like it does the software). He wanted me to check new progress of the desktop app to see the direction development is going but since I have no way to hook my sensors to it, it shows me nothing. I decided I cant really expect them to develop anything that will integrate with external controls if they already made the security desiction to no allow api or mqtt commands.
The temp compensation is handled by the embedded EZO chip and works on all pH and EC probes. Maybe a few others. I heat and cool my tanks with a copper coil loop to stay between 65-75F. I think you’re right the compensation slope change is minimal. I had a lot of early problems when nutrient water got up to 85. I’m actually getting my first test against more extreme cold this week and I’ve seen outside temps get down to mid 20s at night so I’m using a propane heater at night to keep it abouve 40. Plants are all still thriving. Even cold sensitive cucumbers are mostly ok. The heated water makes a big difference since cucumber vines will split open if the water gets into the 50s.
For right now I’ve abandoned atlasIot. I have decent looking dashboards on each system. The monitoring is more stable than atlasiot was. I haven’t had a system stop updating yet. With atlas I had to restart one of the rpi at least once a day. I think developing further I would like to do polling and responses on the script side rather than tons of published commands from HA automations.
I was thinking this is getting to be a pretty lengthy back and forth and if you’re interested we can setup a discord for development and just put updates here for the few people that are interested. We’ve strayed a good bit from the original topic.
I’ve actually never used an ESP32 without going into great detail why do you feel that it would have been a better choice?
Yes, they’re definitely pushing Atlas Desktop but there’s not much of a difference or at least nothing that I consider beneficial.
I did find out that by entering your RPi address like below you can get a JSON containing all of the connected EZO sensors and by adding an address you can get a JSON for a specific sensor.
<RPi IP ADDRESS>/api/values/
<RPi IP ADDRESS>/api/values/99
I thought you referring to the ability of the sensor to automatically compensate for temperature which only the Industrial pH probes are capable of doing.
Are you basing this off of the slope data provided by the sensor or your own when comparing the temperature compensation difference? I’m only asking because I just learned that the slope percentage provided by AtlasIoT is actually based on the “ideal” pH probe and shows the amount of calibration the probe needed to provide the correct reading. This indicates that the probe might need to be cleaned or replaced.
I know the feeling. I grow indoors and last year my AC went out in the middle of the summer and my water temperature also reached 85. Since then I added a chiller and have experimented with using a wort (the same as your copper coil) and a counterflow wort (which is more efficient). The only good thing is that I don’t have to deal with the cold and heat the water like you.
I meant to ask you about this but forgot. I have the same issue and didn’t know if it was due to something i did wrong or an issue with AtlasIoT. You’ve confirmed for me that it’s definitely an issue with AtlasIoT. In case you ever have to deal with it again, I found that by going to the MQTT setting and clicking on “Save” that it will start updating again. I’m going to try it without AtlasIoT and see if I have better results like you.
That’s fine with me. I don’t use Discord much but I can start.
If you can improve on this let me know. It uses polling like you suggested, parsing for the value, and an added config file to add sensors and easy implementation. AtlasIoT cannot be running while this is or it will result in occasional errors.
Configuration:
{
"mqtt": {
"broker": "HOME ASSISTANT IP ADDRESS",
"port": 1883,
"username": "USERNAME",
"password": "PASSWORD"
},
"polling_interval": 30,
"sensors": [
{
"address": 97,
"topic": "atlas/sensor97_DO_value",
"polling_delay": 1
},
{
"address": 99,
"topic": "atlas/sensor99_pH_value",
"polling_delay": 1
}
]
}
Script:
import paho.mqtt.client as mqtt
import time
import signal
import logging
from AtlasI2C import AtlasI2C # Import the AtlasI2C class from your module
import json
# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# Load configurations from a JSON file
CONFIG_FILE = "config.json"
with open(CONFIG_FILE, "r") as file:
config = json.load(file)
MQTT_BROKER = config["mqtt"]["broker"]
MQTT_PORT = config["mqtt"]["port"]
MQTT_USERNAME = config["mqtt"]["username"]
MQTT_PASSWORD = config["mqtt"]["password"]
POLLING_INTERVAL = config["polling_interval"]
SENSORS = config["sensors"]
def parse_sensor_response(response):
"""
Parses the sensor response to extract the relevant value.
Args:
response (str): The raw response from the sensor.
Returns:
str: The extracted sensor value.
"""
# Assuming the response is in the format "SOME_PREFIX: value"
# Modify this parsing logic based on the actual response format
try:
# Split the response and take the last part as the value
value = response.split(":")[-1].strip()
return value
except Exception as e:
logging.error(f"Error parsing sensor response: {e}")
return None
def poll_sensor(client, address, topic):
"""
Polls the sensor at the specified I2C address and publishes the data to the given MQTT topic.
Args:
client (mqtt.Client): The MQTT client instance.
address (int): The I2C address of the sensor.
topic (str): The MQTT topic to publish the sensor data to.
"""
ezo_sensor = AtlasI2C(address=address)
try:
response = ezo_sensor.query("R") # Query the sensor with the "R" command
value = parse_sensor_response(response)
if value is not None:
client.publish(topic, value) # Publish the parsed value to the MQTT topic
logging.info(f"Published to {topic}: {value}")
else:
logging.error(f"Failed to parse value from sensor at address {address}")
except Exception as e:
logging.error(f"Error querying sensor at address {address}: {e}")
def graceful_exit(signum, frame):
"""
Gracefully stops the MQTT client loop and exits the script when a termination signal is received.
"""
logging.info("Termination signal received. Exiting...")
client.loop_stop()
client.disconnect()
exit(0)
# Attach signal handlers for graceful exit
signal.signal(signal.SIGINT, graceful_exit)
signal.signal(signal.SIGTERM, graceful_exit)
# Create MQTT client instance
client = mqtt.Client()
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) # Set username and password
# Connect to MQTT broker
client.connect(MQTT_BROKER, MQTT_PORT, 60)
# Start the MQTT client loop
client.loop_start()
# Main polling loop
try:
while True:
for sensor in SENSORS:
poll_sensor(client, sensor["address"], sensor["topic"])
# Add a per-sensor delay to avoid overloading sensors with frequent polling
time.sleep(sensor.get("polling_delay", 1)) # Default to 1 second if not specified
time.sleep(POLLING_INTERVAL) # Delay before starting the next polling cycle
except KeyboardInterrupt:
graceful_exit(None, None)
Service:
[Unit]
Description=Atlas MQTT Polling Script Service
After=network.target
[Service]
ExecStart=/usr/bin/python3 /home/pi/AtlasHA/atlas_mqtt_polling_script.py
WorkingDirectory=/home/pi/AtlasHA
StandardOutput=inherit
StandardError=inherit
Restart=always
RestartSec=5
User=pi
[Install]
WantedBy=multi-user.target