YoLink API Integration Advice

Hello! @marielle and I are interested in integrating YoLink devices via the YoLink API into PyLabRobot, and we wanted to open up a discussion about this. The YoLink API allows users to read from and write to a wide range of YoLink devices, and we believe it holds significant potential for affordable and versatile lab automation. The API is also OS-agnostic and open-source.

What is YoLink?

YoLink is a smart home and IoT platform that specializes in long-range, low-power wireless devices. They offer a diverse ecosystem of sensors and controllers, including:

  • Environmental Sensors: Temperature and humidity sensors, water leak sensors, motion sensors, and more.
  • Actuators/Controllers: Smart power strips, in-wall outlets, water shut-off valves, and various relay modules.
  • Security Devices: Door/window sensors, sirens, and remote controls.

The core of the YoLink system is the YoLink Hub, which acts as a bridge between the devices and the internet. Devices communicate with the hub using LoRa technology, enabling better range and penetration compared to Wi-Fi or Bluetooth.

The YoLink API allows developers to interact with their devices remotely in Python. Through the API, you can:

  • Read data from various sensors (e.g., current temperature, water leak status).
  • Send commands to control specific devices (e.g., turn a power strip outlet on/off, open/close a water valve).

Why is YoLink useful for Lab Automation?

The combination of affordable devices, long-range communication, and a robust API makes YoLink appealing for lab automation. Here are a few examples of potential applications:

  • Temperature Control: Imagine using YoLink temperature sensors to monitor the environment within an incubator or a specific experiment. You could then integrate these readings with YoLink smart power strips connected to space heaters or cooling pads to precisely maintain desired temperatures, creating a cost-effective and flexible environmental control system.
  • Liquid Level Management: YoLink’s water leak sensors and smart water valves could be used to monitor and control liquid levels in reservoirs, bioreactors, or chemical dispensing systems, preventing overflows or ensuring accurate dispensing.
  • General Device Control & Monitoring: Remotely power cycling equipment, monitoring room conditions, or even setting up basic safety interlocks can all be setup with YoLink.

The low cost of YoLink devices, combined with their strong support network, makes them an attractive alternative to more specialized and expensive lab equipment for certain applications.

Integrating YoLink into PyLabRobot

We’re particularly interested in discussing the best approach for integrating the YoLink API into PyLabRobot. Our initial thoughts are:

  1. IO Layer Integration: The YoLink API would likely sit within PyLabRobot’s IO layer, allowing for direct communication with YoLink devices.
  2. Device Abstraction: How should individual YoLink devices be represented?
  • One option is to create a separate class for each type of YoLink device (e.g., YoLinkTemperatureSensor, YoLinkSmartPowerStrip). This might become unwieldy with the wide range of devices.
  • Alternatively, we could primarily focus on integrating the YoLink Hub as its own class within PyLabRobot. This hub class would then expose methods to interact with the connected YoLink devices.
  1. API Handling for Read vs. Write:
  • For purely reading data from sensors, the YoLink API can often handle this straightforwardly. PyLabRobot would simply make the appropriate API calls and parse the responses.
  • For writing commands to devices (e.g., controlling a smart power strip or water valve), we envision creating a more structured interface or “setup” within PyLabRobot that allows users to define and interact with these writable devices in a consistent manner. For instance, it could inherit from the machine class in PyLabRobot.

We would love to hear your thoughts and advice on these points and any other considerations for integrating YoLink into PyLabRobot.

Looking forward to your feedback!

2 Likes

This is very interesting, I have never heard of YoLink - it seems like it is mostly focused on home automation?

Does this require a YoLink account to be generated and verified via the API?

How is communication established with the various machines?

Loads to talk about here and I will give your 3 points a bit more thought before giving some suggestions :slight_smile:

2 Likes

Thank you and looking forward to your feedback!

YoLink is mostly focused for home automation, but we believe that the affordability and flexibility makes it a competitive option for open-source lab automation.

The API requires an access token that can be obtained through a series of steps with Open API V2 (Steps to work with UAC | YoSmart Doc). The first step involves accessing your YoLink account and getting the information needed for generating the token.

I believe that all communications are routed through the central YoLink Hub. Essentially you can pair YoLink devices to the YoLink hub using the mobile app. In the sample scripts that I have written using the API, I am able to interact with all linked devices using a python method associated with the YoLink hub.

is there a python sdk or is it an http api? or something else?

knowing this is crucial to answering question 1

a big power of PLR is providing universal classes for a “machine-group”, e.g. LiquidHandler or PlateReader. For sensors I believe we should do the same.

The hairy part is many sensors measure so many different things with one device.

I could imagine a pattern similar to HeaterShaker that inherits from both TemperatureController and Shaker. However, with sensors the possibilities quickly explode. It is currently an unanswered question.

One possible implementation is a giant Sensor class with different attributes (loaded by the backend at runtime). Draft:

class Sensor:
  can_read_humidity = True
  can_read_temperature = True
  can_read_pressure = False

  def get_humidity(self) -> float:
    if not self.can_read_humidity:
      raise NotImplementedError("Humidity reading is not supported by this sensor.")
    ...
  
  def get_temperature(self) -> float:
    if not self.can_read_temperature:
      raise NotImplementedError("Temperature reading is not supported by this sensor.")
    ...
  
  def get_pressure(self) -> float:
    if not self.can_read_pressure:
      raise NotImplementedError("Pressure reading is not supported by this sensor.")
    ...

Not sure how I feel about this.

without having looked at implementation details, this does not sound like a bad idea.

I am not sure if those two are drastically different. A Scale is essentially a read-only device (zero/tare are technically commands on most scales, but this argument holds even when those are not implemented). Yet it is still implemented as a Machine since it has an io and we send commands and receive responses. We should have a similar thing for YoLink.


Zooming out, my motivation for wanting to accept these machines into PLR (and the maintenance cost that comes with it) is that these machines are one instance of a ‘universal lab machine class’ (like temperature sensor). Many protocols would benefit from having a sensor to call, and PLR could provide a singular front end for that so that people don’t all have to write an abstraction layer for it. When someone publishes a protocol, it is as easy to switch out say an incubator as it is a specific sensor. That is the PLR vision.

The universal layer is PLR’s only value-add here, as far as I can tell. (For other devices it might be universal + unique firmware driver that you can’t get anywhere else.)

I am not sure about the exact scope of machines yolink makes, but in any case we are gonna have to constrain ourselves to what reasonably makes sense in a lab automation protocol. My basic heurisitc for this is: is someone actually using this machine in a production protocol? If yes, it’s probably not stupid, others might do the same, and we should have it. (*im not providing a guarantee for any device here). Our goal should not necessarily be to have 1:1 mapping to their SDK. However, we can “work ahead” and implement some devices we would like to have integrated even if no one in our community currently has a device like that ← due to the api being predictable and well documented.

You can find details on the API here: Understanding the API | YoSmart Doc

And the python API interface here: GitHub - YoSmart-Inc/yolink-api: HomeAssistant YoLink Client

We will follow up later with a more thorough response to your points

1 Like

we should probably use the python sdk to build upon code that’s already developed (it’s actively developed and async, nice!)

I played around with the YoLink API a little bit so I can provide some insights as to how it works:

Below is a simple script I wrote to fetch the humidity and temperature from all available TempHumiditySensors linked to the YoLink hub:

# testing the THSensor.getState method
async def get_th_sensor_states():
    ACCESS_TOKEN = ACCESS_KEY
    
    async with aiohttp.ClientSession() as session:
        auth_mgr = MyYoLinkAuthMgr(session, ACCESS_TOKEN)
        listener = MyMessageListener()
        home = YoLinkHome()
        
        try:
            await home.async_setup(auth_mgr, listener)
            
            # Fetch all THSensor devices from hub
            devices = list(home.get_devices())
            th_sensors = [d for d in devices if d.device_type == "THSensor"]
            
            print(f"{len(th_sensors)} THSensor device(s) Detected")
            
            for th_sensor in th_sensors:
                print(f"\n{th_sensor.device_name} ({th_sensor.device_model_name})")
                
                try:
                    # Fetch the current state of the THSensor(s)
                    state = await th_sensor.fetch_state()
                    print(f"Temperature: {state.data['state']['temperature']}")
                    print(f"Humidity: {state.data['state']['humidity']}")

                except Exception as e:
                    print(f"Error getting state: {e}")
            
            if th_sensors:
                await asyncio.sleep(2)
                
        except Exception as e:
            print(f"Error: {e}")
        finally:
            await home.async_unload()

await get_th_sensor_states()

Output:

INFO:yolink.mqtt_client:[US] connecting to yolink mqtt broker.
4 THSensor device(s) Detected

Coldspot Fridge Sensor (YS8003-UC)
Temperature: -11.9
Humidity: 64.5

Kenmore Fridge Sensor (YS8003-UC)
Temperature: -12.9
Humidity: 78.2

Philco Fridge Sensor (YS8003-UC)
INFO:yolink.mqtt_client:[US] yolink mqtt client connected.
Temperature: -28.1
Humidity: 62.6

Upstreman Fridge Sensor (YS8003-UC)
Temperature: 3.5
Humidity: 56.7
INFO:yolink.home_manager:[US] shutting down yolink mqtt client.
INFO:yolink.home_manager:[US] yolink mqtt client disconnected.

I am not sure if there is a better way, but what we can do after establishing a connection to the hub is to call .get_devices(), which would return a list of YoLinkDevice objects.

We can then parse through the list and pick out the devices we want (in the example above, I picked out all THSensors, but you can get even more specific by using the name assigned to the sensor).

Now that we have our sensors of interest, we can call the .fetch_state() method on the YoLinkDevice object to get the real time current state. We can parse through the dict and extract the data we want.

Writing commands to YoLink to control devices (power strip outlets in this case) is slightly more involved. Below can give some insight as to how it works:

# Testing smart power strip
from yolink.outlet_request_builder import OutletRequestBuilder

async def test_multioutlet_control():
    multioutlets = [d for d in devices if d.device_type == "MultiOutlet"]
    
    if not multioutlets:
        print("No MultiOutlet devices found")
        return
    
    for outlet_device in multioutlets:
        print(f"\n--- Controlling {outlet_device.device_name} ---")
        
        # Get current state
        state = await outlet_device.fetch_state()
        print(f"Current state: {state.data}")
        
        try:
            # Control individual outlets
            # Note: outlet indices are 0-based (0, 1, 2, etc.)
            
            # Turn ON outlet 1
            print("Turning ON outlet 1...")
            request = OutletRequestBuilder.set_state_request("open", 0)
            response = await outlet_device.call_device(request)
            print(f"Response: {response.data}")
            
            await asyncio.sleep(2)  # Wait 2 seconds
            
            # Turn OFF outlet 1
            print("Turning OFF outlet 1...")
            request = OutletRequestBuilder.set_state_request("close", 0)
            response = await outlet_device.call_device(request)
            print(f"Response: {response.data}")
            
            # Turn ON all outlets
            print("Turning ON all outlets...")
            request = OutletRequestBuilder.set_state_request("open", None)
            response = await outlet_device.call_device(request)
            print(f"Response: {response.data}")
            
            await asyncio.sleep(2)  # Wait 2 seconds
            
            # Turn OFF all outlets
            print("Turning OFF all outlets...")
            request = OutletRequestBuilder.set_state_request("close", None)
            response = await outlet_device.call_device(request)
            print(f"Response: {response.data}")
            
        except Exception as e:
            print(f"Error controlling device: {e}")

await test_multioutlet_control()

For this example, OutletRequestBuilder is used to create a request, which is then sent to the device using the .call_device() method.

Putting these pieces together allows us to do more interesting things that can be relevant for lab automation. Here is a concept script I wrote that mixes both reading and writing together that would manage temperature in an environment. The idea is that after a certain temp is reached, the power strip can power a heater for example until the desired temperatre is reached (actual implementation will probably need more consideration :slight_smile: )

while True:
    coldspot_th_sensor_state = await coldspot_th_sensor.fetch_state()
    coldspot_temp = coldspot_th_sensor_state.data['state']['temperature']
    coldspot_humidity = coldspot_th_sensor_state.data['state']['humidity']

    power_strip_multioutlet_state = await power_strip_multioutlet.fetch_state()
    power_strip_outlet_states = power_strip_multioutlet_state.data['state']['state']

    print(f"Temperature: {coldspot_temp}")
    print(f"Humidity: {coldspot_humidity}")
    print(f"Power Strip States: {power_strip_outlet_states}")

    if coldspot_temp < 0 and power_strip_outlet_states[0] != 'open':
        print("Coldspot temperature is below 0°C and Outlet 0 is closed, turning on Outlet 0")
        
        # Turn ON outlet 0
        request = OutletRequestBuilder.set_state_request("open", 0)
        response = await power_strip_multioutlet.call_device(request)
    
    elif coldspot_temp >= 0 and power_strip_outlet_states[0] != 'closed':
        print("Coldspot temperature is above or equal to 0°C and Outlet 0 is open, turning off Outlet 0")
        
        # Turn OFF outlet 0
        request = OutletRequestBuilder.set_state_request("close", 0)
        response = await power_strip_multioutlet.call_device(request)

    # check every 3 seconds
    await asyncio.sleep(3)

I hope these scripts can help to provide some insight into how it can be integrated into pylabrobot!

here is how i could imagine this working in PLR:

s = Sensor(backend=YoLink(API_KEY, sensor_name))
await s.setup()
await s.get_temperature()
await s.get_humidity()
await s.stop()

sensor.setup()

  • get auth, listener, home
  • async setup
  • list devices and get correct device
  • get correct sensor

sensor.stop()

  • await home.async_unload()

analogous for the multi outlet device

2 Likes

@rickwierenga I finally got around to getting the YoLink API integration started and I put down my thoughts in the draft PR above - any feedback would be greatly appreaciated!

1 Like

awesome! thank you so much. left a comment in the PR thread. Let’s have implementation specific discussions there

1 Like