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


#1


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 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")

Going to next level of Aquarium Automation...who's with me?
#2

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.


#3

@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.


#4

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!


#5

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


#6

Yes, exactly that. I have three of them.


#7

@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.


#8

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