Adding support for Seneye Aquarium & Pond Sensors (removing the need for a Seneye Web Server)


Image courtesy of wikipedia.

I’ve seen a few people who’ve integrated into the cloud based API of the Seneye system before. The problem with this is it involves either having to keep a M$ PC running the Seneye Connect app or spending 2-3 times as much money to pick up the Seneye Web Server or Web Server WiFi, as well as the Seneye sensor…well I had a better idea.

First up I wanted to write a native Python driver for the Seneye USB Device (SUD). The manufacturer released some sample code, in C++ and C# along with some accompanying docs of the underlying message structures…these left a lot to be desired but were a good place to start. I won’t go into too much detail but some late nights and lot’s of frustration with the bugs/gaps in the manufacturers documentation lead to this python module, pyseneye.

I’d like to add support for this base into HA, eventually but as an initial step I wanted to implement a custom component. The code for that is on github, here. I’ve currently implemented pH, NH3 and temp as a POC. I’ll be adding results for light readings shortly. The readings are throttled to once every 30 mins, as per Seneyes recommendations, which is why the code is slightly more complex than I initially thought it would be.

image
The result

When using the Seneye via HA, you won’t get your results synced up to the Seneye.me platform so you will need to implement alerts with Twilio or your favorite notifications platform, to get the similar functionality. The USB interface has a flag to say if the pH/NH3 slide is expired, which I’m currently ignoring…what will be interesting is to see if the readings still come back after the expiry time. I only wrote the driver and the component in the last week, so testing has been minimal so far but it would be good to get some early feedback.

The configuration is as easy as it gets:

sensor:
	- platform: seneye

The up-to-date code is on [Github]https://github.com/mcclown/home-assistant-custom-components/blob/master/seneye/sensor.py) but for a quick skim, it’s below.

"""
Support for the Seneye range of aquarium and pond sensors.
For more details about this platform, please refer to the documentation at
https://github.com/mcclown/home-assistant-custom-components

This custom component is based on the Awair component in HA.
https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/sensor/awair.py
"""

from datetime import timedelta
import logging

from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle, dt

REQUIREMENTS = ['https://github.com/mcclown/pyseneye/archive/0.0.1.zip#pyseneye==0.0.1']

_LOGGER = logging.getLogger(__name__)

ATTR_TIMESTAMP = 'timestamp'
ATTR_LAST_SLIDE_READ = 'last_slide_update'
ATTR_SENEYE_DEVICE_TYPE = 'seneye_device_type'


DEVICE_CLASS_PH = 'PH'
DEVICE_CLASS_FREE_AMMONIA = 'NH3'

UNIT_POWER_OF_HYDROGEN = 'pH'
UNIT_PARTS_PER_MILLION = 'ppm'

SENSOR_TYPES = {
	'temperature': {'device_class': DEVICE_CLASS_TEMPERATURE,
			 'unit_of_measurement': TEMP_CELSIUS,
			 'icon': 'mdi:thermometer'},
	'ph': {'device_class': DEVICE_CLASS_PH,
			  'unit_of_measurement': UNIT_POWER_OF_HYDROGEN,
			  'icon': 'mdi:alpha-h-box-outline'},
	'nh3': {'device_class': DEVICE_CLASS_FREE_AMMONIA,
			'unit_of_measurement': UNIT_PARTS_PER_MILLION,
			'icon': 'mdi:alpha-n-box-outline'}
}


SCAN_INTERVAL = timedelta(minutes=5)
SENEYE_SLIDE_READ_INTERVAL = timedelta(minutes=30)

async def async_setup_platform(hass, config, async_add_entities,
							   discovery_info=None):
	"""Setup Seneye objects"""

	try:

		seneye_data = SeneyeData(SENEYE_SLIDE_READ_INTERVAL)
		
		await seneye_data.async_update()
		
		all_sensors = []

		for sensor in SENSOR_TYPES:
			if sensor in seneye_data.data:
				seneye_sensor = SeneyeSensor(seneye_data, sensor, SENEYE_SLIDE_READ_INTERVAL)
				all_sensors.append(seneye_sensor)

		async_add_entities(all_sensors, True)

		return

	except Exception as e:
		_LOGGER.error("Error: {0}".format(e))

	raise PlatformNotReady


class SeneyeSensor(Entity):
	"""Implementation of a Seneye sensor."""

	def __init__(self, data, sensor_type, throttle):
		"""Initialize the sensor."""
		self._device_class = SENSOR_TYPES[sensor_type]['device_class']
		self._name = 'Seneye {}'.format(self._device_class)
		unit = SENSOR_TYPES[sensor_type]['unit_of_measurement']
		self._unit_of_measurement = unit
		self._data = data
		self._type = sensor_type
		self._throttle = throttle

	@property
	def name(self):
		"""Return the name of the sensor."""
		return self._name

	@property
	def device_class(self):
		"""Return the device class."""
		return self._device_class   

	@property
	def icon(self):
		"""Icon to use in the frontend."""
		return SENSOR_TYPES[self._type]['icon']

	@property
	def state(self):
		"""Return the state of the device."""
		return self._data.data[self._type]

	@property
	def device_state_attributes(self):
		"""Return additional attributes, preprocess before returning."""
		raw_dt = self._data.attrs[ATTR_LAST_SLIDE_READ]

		# Convert timestamp to local time, then format it.
		local_dt = dt.as_local(raw_dt)
		formatted_dt = local_dt.strftime("%Y-%m-%d %H:%M:%S")

		seneye_device_type = self._data.attrs[ATTR_SENEYE_DEVICE_TYPE]
	   
		return {ATTR_LAST_SLIDE_READ: formatted_dt, 
				ATTR_SENEYE_DEVICE_TYPE: seneye_device_type}

	@property
	def available(self):
		"""Device availability based on the last update timestamp.
		
		Data should be updating every 30mins, so we'll say it's unavailable
		if it takes over an hour to update.
		"""
		if ATTR_LAST_SLIDE_READ not in self._data.attrs:
			return False

		last_api_data = self._data.attrs[ATTR_LAST_SLIDE_READ]
		return (dt.utcnow() - last_api_data) < (2 * self._throttle)

	@property
	def unique_id(self):
		"""Return the unique id of this entity."""
		return "seneye_{}".format(self._type)

	@property
	def unit_of_measurement(self):
		"""Return the unit of measurement of this entity."""
		return self._unit_of_measurement

	async def async_update(self):
		"""Get the latest data."""
		await self._data.async_update()


class SeneyeData:
	"""Get data from Seneye device."""

	def __init__(self, throttle):
		"""Initialize the data object."""
		self.data = {}
		self.attrs = {}
		self.async_update = Throttle(throttle)(self._async_update)

	async def _async_update(self):
		"""Get the data from SUD."""
		from pyseneye.sud import SUDevice, Action, DeviceType
		
		device = SUDevice()
		device_type = None
		resp = None
		try:
			data = device.action(Action.ENTER_INTERACTIVE_MODE)
			device_type = data.device_type.name

			resp = device.action(Action.SENSOR_READING)
			if not resp:
				return

		finally:
			device.close()

		self.attrs[ATTR_LAST_SLIDE_READ] = dt.utcnow()
		self.attrs[ATTR_SENEYE_DEVICE_TYPE] = device_type

		for sensor in SENSOR_TYPES:

			self.data[sensor] = getattr(resp, sensor, None)

		_LOGGER.debug("Got Seneye data")
3 Likes

This is brilliant! Thank you for taking the time to do this.

I have no idea how to install it yet but will get around to it eventually. It’s been on my wish list for a while.

Thanks again.

@ashscott No problem, it is working pretty well for me, so far. I had some issue on 0.87.0 or HASS, which required me to restart the raspberry pi every 3 days. Haven’t seen the same issue since 0.89.0.

It’s pretty easy to add custom components. I can give you instructions if that helps. Feel free to reach out.

Another thing I’ve noticed. I can keep reading even after the slide expires. Obviously it will wear out eventually and the accuracy but I might try benchmarking it against a lab quality PH meter I have on one of my other tanks…see how long the slide can stay accurate for, beyond 30 days.

2 Likes

Some instructions would be great, thank you.

I use Seneye’s on our koi ponds. I have them connected via ethernet rather than the USB method. Is that what you are doing?

I’m not totally convinced in the accuracy but the alerts are really handy. I use a Hanna bench photometer to check the water parameters twice a week and monitor daily with the Seneye for trends.

I think it will mean pushing the slide changing to beyond 30 days will be possible if trends and alerts are more important than accuracy.

Well done!

@ashscott so are you running Seneye sensors, connected to a seneye web server that is connected to your network via ethernet?

Yes, exactly that. I have three of them.

@ashscott Ah, so what I’ve built so far isn’t really meant for your scenario. What I’ve built expects the Seneye to be plugged directly into the device running HA. I intend to extend it to support your scenario though and I’ll update this thread when I do.

1 Like

OK, great. I’ll watch out for it.

@mcclown Seriously, your contribution has taken an item out of my own bucket list, and made a dream of mine come true: to have a solution for getting rid of my only Windows installation (deployed only for Seneye) without having to buy a Seneye WebServer. And best part is I don’t have to turn my data over to Seneye to get the core functionality.

Windows VM is now shutdown. :slight_smile:
12

I have your pyseneye & HA component installed and working on one of my remote node/slave RaspberryPi’s forwarding the data collected over MQTT to my primary HA VM Server & works like a charm.

31

Any chance you know the formula to calculate NH4 / O2 values from the NH3 / pH / Temp values? Otherwise, I’ll ask / search around and see if I can find out how to do it.

Again, great job & a huge thank you! This is epic!

Yeah, I’ve looked into the NH4 calculation but haven’t had any luck so far. Everywhere I see approximations of it, they’re oversimplified. Usually you see a graph like below, but temperature also effects the NH3 vs NH4 ratio as well, so this graph misses that.

The other conversion tables you see, like below are better but I’m not keen to have to store a conversion table in the code and also they miss another variable that should be taken account of, the salinity of the water.

The best I’ve seen is the Hamza Reef Free Ammonia Calculator. This takes into account all of the above variables. I’ve emailed the creator to see if they’ll share the formulae, so fingers crossed they’ll come back with something.

1 Like

@cowboy glad I could help you streamline your setup :slight_smile:

I’ve been testing here with my setup for the last 2 months and it’s worked pretty well so far. I’ve hit an issue where ResinOS (ie. old HASS) seems to disconnect the USB device, randomly, after a few days and then the underlying HUD driver fails to work until the OS is restarted. I haven’t had a chance to try HASS OS (new HASS) yet but I really should migrate to that soon and find out.

I’m not seeing this issue on another Raspberry Pi I have, running Raspbian, so it will be interesting to hear your results. I’ve had this Raspbian instance running for the last 37 days (without a slide change), I’m seeing how far I can push it at the moment and if there’s anything built in to disable the slide readings, after a set time.

1 Like

Dude… even my wife was impressed when I finally told her what’d you’d gone off and done & that I got your creation running on our HA instance. She’d seen that laptop next to the aquarium (for Seneye) for years; it was very much a part of the furniture until last year when I migrated it to a VM (where Windows demands 2GB of ram).

I’ll keep my fingers crossed that Hamza Reef will help us out. Are you on Facebook & associated Reefing Groups there (Worldwide Reefing, etc)? If not, I’ll start dropping posts with inquiries for a marine chemist / biologist who might know what the formula is we are looking for.

On the flip side, the Seneye doesn’t know / have a means to measure the salinity of the water. (I wish it did tho).

Yeah, I had a play with Hass.IO awhile back and found it a bit too restrictive for my taste, and stuck with hassbian instead. Just FYI - I do note that everytime the pyseneye code gets called, I get the following entry in /var/log/syslog:

[47111.261738] hid-generic 0003:24F7:2204.001C: hiddev96,hidraw0: USB HID v1.00 Device [Seneye ltd Seneye SUD v 2.0.16] on usb-3f980000.usb-1.5/input0
[47111.354406] usb 1-1.5: reset full-speed USB device number 5 using dwc_otg

On a different but related note, have you heard anything else about the Seneye KH monitor? I think they previewed that almost two years ago…and still no further updates as to a release date. :frowning: I’d love to have one of those & tie it to the Kalkwasser - Reverse Osmosis switch point of my AutoTopUp Water setup in HA.

Meanwhile, I’ll have a look into seeing what needs to be done to convert the icons to MDI format & get them contributed there.

A member on the Dutch MyReef forum pointed me in the right direction.

He also mentioned Seneye’s calculations for DO and NH3 are based on crappy assumptions (like salinity) made in this formula. Probably not worth the time unless we consider external inputs (like a BME280 for altitude / atmospheric pressure) and still then only a theoretical maximum.

And more from the same MyReef member :slight_smile:
http://butane.chem.uiuc.edu/pshapley/GenChem1/L23/web-L23.pdf

@cowboy Thanks for the info…I had a feeling it was a case like that. I suppose what really matters in the NH3, since NH4 is tolerated to much higher levels.

I’m hoping to go to a local fragging event in the next few weeks. There will be a few people there that I can pick their brain about this as well.

The GitHub repo has been updated to support HA 0.92.0 and later versions. There was breaking changes for custom_components but I’ve fixed that now.

@cowboy On another quick note, Hamza Reef kindly shared the formula behind their Free Ammonia calculator

$A = 10.08690 + (0.0025 * $salinity_in_ppt) - (0.034 * $temperature_in_celcius) - $ph;
$B = 1.0 / (1.0 + pow(10.0, $A));

$nh3_in_ppm = $B * $total_ammonia_in_ppm;
$nh4_in_ppm = (1.0 - $B) * $total_ammonia_in_ppm;

if (total_ammonia_in_ppm was entered as NH-N rather than NH)
{
$nh3_in_ppm *= 1.2158706;
$nh4_in_ppm *= 1.2878275;
}

I’ll have to think about that, once I have a chance.

1 Like

Hello @mcclown,

I also have a Koi pond monitored by an Seneye Sensor connected to a Seneye web server, this is near the filter installation and my home assistant server is in the basement running virtually on a server. So no option to connect the usb part of the Seneye directly to the server here.

Is there something that can be done with the Seneye Webserver, when you connect to it you have SWS settings (Seneye Webserver settings), there you find a setting to export the data localy, there is also a link with a github repo

is there anything you can do with this? It looks quite ‘simple’ to do something with this, but I really have no experience with these things.

Regards.
Dirk

Hi @cowboy,

I managed to install hassio and play around with it but I have no clue how to install pyseneye & HA component, any chance you would like to give me any instructions how this works? Or otherwise a link to some resources? Thanks!

Hi @DirkTas67,

I don’t have a SWS and never planned to buy one (but I have 3 Seneye sensors) so I don’t know how the Local Data Exchange is formatted.

But…if you can find out if the LDE is or is not formatted and returned in the same way as the Cloud API data is returned, and if the LDE is in the same format, then let me know and I might can help you realise a different but working solution to get data off your SWS.

It’s a different solution than the “native RPI support” @mcclown developed, but I used it for more than a year until we had native RPI support. I just need to know if the SWS and Cloud API formats are exactly the same or not.

Cheers

@Casper_Bours,

I think @mcclown already produced some documentation how to install it, which can be found here:

I’m probably not the best Hass.io authority to query as I don’t use Hass.io, preferring Hassbian on RPI’s and “Rolling my own HA” on Xen-based Virtual Machines.