YoLink Integration

Managed to use the YoLink server API to query my temperature sensor with a bit of Python. Now just gotta figure out how to translate this to Groovy and wrap it in a HE driver.

import requests
import time
import json

yolinkURL='https://api.yosmart.com/open/yolink/v2/api' 
tokenURL = "https://api.yosmart.com/open/yolink/token"

# Use the YoLink app to generate these authentication credentials
UaID = 'ua_*******************************'
SecID = 'sec_***************************'

def get_access_token(url, client_id, client_secret):
	response = requests.post(
		url,
		data={"grant_type": "client_credentials"},
		auth=(client_id, client_secret),
	)
	return response.json()['access_token']

def post_command(url, token, method, methodData ):
	commandHeaders = {}
	commandHeaders['Content-type'] = 'application/json'
	commandHeaders['Authorization'] = 'Bearer ' + token

	commandData = methodData
	commandData['method'] = method
	commandData['time'] = str(int(time.time()*1000))

	response = requests.post(
		url,
		data=json.dumps(commandData),
		headers=commandHeaders
	)
	return response.json()

#get transaction token
token = get_access_token(tokenURL, UaID, SecID)

#get device list from server
responseData = post_command(yolinkURL, token, 'Home.getDeviceList', {} );
responseData = responseData['data']
deviceList	 = responseData['devices']

#find the temperature sensor device in the list
for device in deviceList :
	if device['type'] == 'THSensor':
		break
	else:
		sys.exit( "No Temperature Sensor Found")	

#capture the temperature sensor device identifiers			
thsensorToken	 = device['token']
thsensorDeviceID = device['deviceId']

#query the temperature sensor for all its parameters
responseData = post_command(yolinkURL, token, 'THSensor.getState', {
	'token'		   : thsensorToken,
	'targetDevice' : thsensorDeviceID,
	'params'	   : {} }
);

#drill down for the temperature
responseData = responseData['data']
thsensorState = responseData['state']
thsensorTemp = thsensorState[ 'temperature']

if thsensorState[ 'mode'] == 'f':
	thsensorTemp = round((thsensorTemp * 9/5) + 32,2)
else:
	thsensorTemp = round(thsensorTemp,2)

print(thsensorTemp, thsensorState[ 'mode'] )
print('\n')
3 Likes

@steve.hoge, just want to thank you for trying to implement this directly into HE and encourage you in your efforts. This is something that will benefit many (not in the least me!). I have been a fan of LoRa and Yolink sensors (except for the current Cloud dependency). Code on!

Looks like Yolink has just come out with a relatively inexpensive new Hub with TTS (Yolink SpeakerHub). It is compatible with their existing system and can also be used in conjunction with an existing Yolink Hub. If the API can be accessed and wrapped into a HE driver, this might be a good inexpensive local TTS device for HE as opposed to Sonos, or cloud based integrations such as Alexa etc. Sound quality as reviewed by one person is not so good apparently (the Yolink TTS hub is about the same size as the HE hub).

Personally, at this time, not wanting to go through the hoops of setting up the Echo Speaks App with certificate renewals etc given the risk that Alexa can cut this off at any time of their choosing, and the expense of compatible speakers in each room. So my current HE announcements go through the Alexa cloud via Alexa routines. However, with a limit of 200 routines, I am constantly having to weigh which routines I need to cull. Moving TTS announcements to the HE would free me from this limitation as well as freeing up Routine space on Alexa as well. Having TTS local on HE would be great but I don’t want to go through the expense of having a Sonos in each room in addition to my Echos.

If the Yolink API is made available (Yolink has hinted that it is working on local integration) then these new TTS hubs MAY be acceptable alternatives to more expensive speakers (if just used for announcements).

BTW, related, any news on Z-Wave LR?

@steve.hoge thanks I'm new to Hubitat and Groovy (like yesterday new :smile: ) but gave this a try based on your Python code (I love Python). This Driver so far will get the access token after you enter in your UAID and Secret Key, save, then press configure. This will also then populate the devicesList state variable after you refresh. Then you can plugin the deviceToken and deviceID in the preferences and save, and now use the refresh button to pull those state variables from the sensor. Note I'm using this with a motion sensor, so be sure to change the part to your device type if not using MotionSenor in the getMotionSensorDetails command and also the updateState logic to reflect your state variables.

I don't quite know how to setup polling or use the HTTP Callback API for YoLink I see mentioned on the API docs so that the state would update based on a motion event. Sorry if it is a little rough, still learning everything but wanted to share in case it helps anyone else get closer to something working.

/*
 * YoLink API
 *
 * Talks to the YoLink API using user credentials from mobile app
 * Based off Python code by @steve.hoge and using snippets of various project examples including: https://github.com/hubitat/HubitatPublic/tree/master/examples/drivers
 * 
 */
metadata {
    definition(name: "YoLink API", namespace: "community", author: "Dev Dre4ms") {
        capability "Configuration"
        command "refresh"
        
        attribute "access_token", "text"
        attribute "deviceList", "text"
       
        attribute "alertInterval", "text"
        attribute "battery", "text"
        attribute "devTemperature", "text"
        attribute "ledAlarm", "text"
        attribute "nomotionDelay", "text"
        attribute "sensitivity", "text"
        attribute "state", "text"
        attribute "lastMotion", "text"
        
    }
}

preferences
{
    section
    {
        input "uaid", "text", title: "YoLink UAID", required: true
        input "secret", "password", title: "YoLink Secret Key", required: true
        input "deviceID", "text", title: "Device ID", required: false
        input "deviceToken", "text", title: "Device Token", required: false
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
    }
}


def refresh() {
    def motionState = getMotionSensorDetails(state.access_token, deviceToken, deviceID)
    updateState(motionState)
    logDebug("Motion state status:${motionState}")
}

def updateState(Map motionState)
{
    state.alertInterval = motionState.data.state.alertInterval
    state.battery = motionState.data.state.battery
    state.devTemperature = motionState.data.state.devTemperature
    state.ledAlarm = motionState.data.state.ledAlarm
    state.nomotionDelay = motionState.data.state.nomotionDelay
    state.sensitivity = motionState.data.state.sensitivity
    state.state = motionState.data.state.state
    state.lastMotion = motionState.data.reportAt
    
    
}

def logDebug(msg) 
{
    if (logEnable)
    {
        log.debug(msg)
    }
}


def configure()
{
    getAccessToken()

    //Get devicelist and update attribute to see deviceToken/deviceId
    def command_response = post_command(state.access_token, "Home.getDeviceList", [:])
    logDebug("Devices List: ${command_response}")
    state.devicesList = command_response
    
}

def getAccessToken()
{
    def postParams = genParamsPre()
    postParams['body'] = ["grant_type": "client_credentials"]
    try
    {
        token_response = httpPostExec(postParams, false)      
        logDebug("Received access token: ${token_response.access_token}")
        state.access_token = token_response.access_token
        
        return token_response.access_token
    }
    catch (Exception e)
    {
        logDebug("Error retrieving access token from YoLinkAPI")
    }
    
}

def getTokenURI()
{
    return "https://api.yosmart.com/open/yolink/token"
}

def getAPIURI()
{
    return "https://api.yosmart.com/open/yolink/v2/api"
}

def genParamsPre()
{
    def params =
        [
            uri: getTokenURI(),
            headers:
            [
                'Authorization': "Basic " + ("${uaid}:${secret}").bytes.encodeBase64().toString()
            ],
            contentType: 'application/json'
        ]
 
    return params
}

def genCommandParams(String token)
{
    def params =
        [
            uri: getAPIURI(),
            headers:
            [
                'Authorization': "Bearer " + token
            ],
            contentType: 'application/json'
        ]
 
    return params
}

def httpPostExec(params, throwToCaller = false)
{
    logDebug("httpPostExec(${params})")
    def response = [:]
    try
    {
        httpPost(params) { resp ->
            if (resp.success)
            {
                
                response = resp.getData()
                logDebug("Success: ${response}")
            }
            
        }
        
    }
    catch (Exception e)
    {
        logDebug("httpPostExec() failed: ${e.message}")
        if(throwToCaller)
        {
            throw(e)
        }
    }
    return response
}

def getCurrentTime() {
    now = new Date()
    return now.getTime()
}

def post_command(String token, String method, Map methodData) {
	def params = genCommandParams(token)

	def commandData = methodData
	commandData['method'] = method
    commandData['time'] = getCurrentTime().toString()
    params["body"] = commandData
    
	return httpPostExec(params, true)
}

def getMotionSensorDetails(String token, String deviceToken, String deviceID)
{
    //This needs to be updated for the type of sensor you'd like to get the state from, replace MotionSensor.getState with your device type
    def deviceData = post_command(token, 'MotionSensor.getState', [
    'token': deviceToken,
    'targetDevice': deviceID,
    'params': [:]])
}

def logsOff() {
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable", [value: "false", type: "bool"])
}
1 Like

Very cool! :smiley: I got it basically installed and talking to my Hub and it's listing my devices!

But I don't have a motion sensor and don't know enough about Groovy drivers yet to exactly know how to modify this based on querying my THSensor and my DoorSensor, so it's throwing the expected errors until I can modify it to talk to them.

Thanks for including the link to HubitatPublic/examples/drivers at master · hubitat/HubitatPublic · GitHub , I will be perusing that to try and understand Groovy driver semantics and control flow.

Google App Script to read temperature Sensor and better understand how the API works.

Enjoy !,  James


   // Google Apps Script
  // coded by James Wiser version 2-21-22 contact james@wisermail.com

function myFunction() {
  
  // console.logs are included to help you gain clarity in what is going on
  
  // API Reference Guide http://doc.yosmart.com/docs/overall/intro
  // yolinkURL='https://api.yosmart.com/open/yolink/v2/api' 
  // tokenURL = "https://api.yosmart.com/open/yolink/token"
 
  // client_id and client_secret are obtained from the app on your phone.  keep these private
  var client_id = 'ua_********************************';
  var client_secret = 'sec_***************************';
  
  var access_token_url = 'https://api.yosmart.com/open/yolink/token';
  var yolinkurl = "https://api.yosmart.com/open/yolink/v2/api";

  
  // authentication to get token from Yolink
  var data = {
    'grant_type' : 'client_credentials', 
   };

  var options = {
    'method': 'post',
    'payload': data,
    'headers': {
      'Authorization': 'Basic ' + Utilities.base64Encode(`${client_id}:${client_secret}`),
    },
  };
  var resp = UrlFetchApp.fetch(access_token_url, options);
  
  console.log(JSON.parse(resp.getContentText()));
  var data = JSON.parse(resp.getContentText());
  var token = data['access_token'];
  console.log(token);

// call API to get a valid list of devices
var now = new Date();
var timems = now.getTime();
console.log(timems);

// payload elements can be referenced in API Reference Guide
var payload = {
            
            "time": timems,
            "method": "Home.getDeviceList",
            
        }; 


// use the token that Yolink gave you
var options = {
    "method": "post",
     "Content-Type": "application/json",
     "headers": {'Authorization': 'Bearer '+  token},
     "payload" : payload  
    }


  var resp2 = UrlFetchApp.fetch(yolinkurl, options);
  
  console.log(JSON.parse(resp2.getContentText()));
  
  var respdata = JSON.parse(resp2.getContentText());
  var yolinkdata = respdata['data'];
  var devicelist = respdata['data']['devices'];
  // 0 id the 1st device in the data set, 1 would be the next and so on...
  var device = respdata['data']['devices'][0].deviceId;
    
  console.log(yolinkdata);
  console.log(devicelist);
  console.log(device);

  // find the first device that is a tempreature sensor
  var numofdevices = devicelist.length;
  for (var counter = 0; counter < numofdevices; counter = counter + 1) {
    console.log(respdata['data']['devices'][counter].deviceId);
    // type comes from API Reference Guide
    if (respdata['data']['devices'][counter].type == 'THSensor') {
        var ThSensor_token  = respdata['data']['devices'][counter].token;
        var ThSensor_deviceId  = respdata['data']['devices'][counter].deviceId;
        var TheSensor_name = respdata['data']['devices'][counter].name;
        break;
    }   
  }
  
 // format the next request with the required elements for the device. refer to API Reference Guide
 var payload = {
            
            "time": timems,
            "method": "THSensor.getState",
            "targetDevice": ThSensor_deviceId,
            "token" : ThSensor_token,
            
        }; 


var options = {
    "method": "post",
     "Content-Type": "application/json",
     "headers": {'Authorization': 'Bearer '+  token},
     "payload" : payload  
    }


  var resp3 = UrlFetchApp.fetch(yolinkurl, options);
  console.log(JSON.parse(resp3.getContentText()));
  
  var respdata = JSON.parse(resp3.getContentText());

  var tempsens = respdata['data']['state'].temperature
  var tempmode = respdata['data']['state'].mode
  var TheSensor_ts = respdata['data']['reportAt'];
  if (tempmode == 'f' ){
     temp = (tempsens * 9/5) + 32;
     temp = temp.toFixed(1);
  }
 else{
      temp = temp.toFixed(1);
      tempmode = 'c';
     }
  
  // use the date/time stamp that comes from the data set to accurately report the last reading
  var date = new Date(TheSensor_ts);
  // use the timezone you are working in
  var fdate = Utilities.formatDate(date, 'America/Chicago','MM-dd-yy hh:mm:ss aa');
  
  console.log(TheSensor_name + " is " +  temp + " " + tempmode + " " + fdate);

   
  }


2 Likes

They claim they will come out with an API in 2022.

If they come out with a LOCAL API, I'd be all over their devices. If cloud... Not so much.

Hi @steve.hoge,
Any further progress on the cloud API integration with Hubitat? I realize that the current API exposed by Yolink is cloud based, but for now, I am OK with this at this point unlike some here in the community. Even if cloud based, an integration app for noob users like me (non-coders) which could be uploaded like the Ecobee Hubitat cloud integration would be fantastic! Thanks for your contributions to this effort!

Have taken a break from Hubitat projects for awhile but will get back to it eventually. I was kind of hoping based on the proof-of-concept prototypes here that @Hubitat_Staff might pick up the YoLink ball and run with it :wink:

Yes, was hoping that Hubitat staff would pick up on this as well but I think that they are philosophically set on local only integrations (which I understand) and so are relying on community based integrations for anything that has to access API that are cloud based. Anyway, thanks for all your efforts and let us all know if (and hopefully not just if, but when) you decide to develop this further. Thanks again.

1 Like

Anyone have a good Z-wave water thermostat solution that integrates with Hubitat. Need it to monitor lake temperature in the summer. Probably installing it under my dock.

2 Likes

Any news on yolink and their cloud API? Is anyone working on a cloud integration driver for HE?

I don't think so...

1 Like

Integrate your YoLink devices with Home Assistant, because YoLink has created a cloud integration for HA.

There is an excellent Home Assistant integration for Hubitat called Home Assistant Device Bridge, linked to below, that permits Home Assistant entities to be available as Hubitat devices.

Alright, I was hoping to not have to do this but I'll spin up a HA machine for this. I also have some other sensors that seem to only work with HA. Thank you for the pointer!

1 Like

There are many of us who integrate “incompatible” devices into Hubitat using other hubs. In the instance of HA, the integration is local (both hubs are on the same LAN), and therefore fast enough that there’s no noticeable effect on automation speed.

1 Like
2 Likes

Just saw your post -- I've been thinking about this to monitor lake temperature close to the freeze point to trigger my bubbler to turn on so that I don't get a freeze in front of the wall. But, a benefit would be lake temperature in the summer!

Download the Hubitat app