Anova Sous Vide Cooker Integretaion

Basic Idea

I wanted to add my anova sous vide stick (WiFi Version) status to openhab. Therefor we need to get the information from the (undocumented) API and parse it into openhab.

Later I will add some options to start / stop the stick, set timers etc.

Prerequisites

You need your credentials for the API. I found two ways

  1. Get the information from your rooted android phone (not tested since my phone is not rooted)

https://forum.fhem.de/index.php?topic=56697.0

$ cat /data/data/com.anovaculinary.android/shared_prefs/com.anovaculinary.android_preferences.xml

  1. Get the information via man in the midle attack in your home WiFi

https://github.com/bmedicke/anova.py/blob/master/readme.md


anova.rules

var String anova_cooker = "anova%20fxxxxyxxyxyxx2" 
var String anova_secret = "xxxxxxxx"
			
				
rule "Check Anova Status"
	when 
		Time cron "0 0/1 * * * ?"
	then
		logInfo( "anova.rules" , "Rule triggered")
		var String Anova_ApiCall = "https://api.anovaculinary.com/cookers/%s?secret=%s"
		var Integer minute	= now.getMinuteOfHour
		// Get Info every 5 Minutes or every minute if anova is running to avoid spamming API
		if( (minute%5) == 0 || Anova_Status.state == ON  ){
			logInfo( "anova.rules" , "Requesting API Data")
			var String Anova_Answer = sendHttpGetRequest(String::format(Anova_ApiCall, anova_cooker , anova_secret))
			
			// logInfo( "anova.rules" , Anova_Answer)
			
			// Check JSON answer for Status
			var String Anova_online = transform( "JSONPATH", "$.error.code" , Anova_Answer)
			
			if( Anova_online == "404" ){
				// Anova Offline
				Anova_Online.sendCommand( OFF )
				Anova_Status.sendCommand( OFF )
				Anova_Current_job.postUpdate( "-" )
				Anova_Timer_running.postUpdate( OFF )
				Anova_Timer_remaining.postUpdate( -1 )
				Anova_Target_temp.postUpdate(-1)
				Anova_Current_temp.postUpdate( -1 )
			}else{
				// Anova Online
				Anova_Online.sendCommand( ON )
				
				// Anova temperatures
				Anova_Target_temp.postUpdate( Double::parseDouble( transform( "JSONPATH" , "$.status.target_temp" , Anova_Answer) ))
				Anova_Current_temp.postUpdate( Double::parseDouble( transform( "JSONPATH" , "$.status.current_temp" , Anova_Answer) ))
				
				// Anova status (running or not)
				var String Anova_running = transform( "JSONPATH" , "$.status.is_running", Anova_Answer)
				if( Anova_running == "true" ){
					Anova_Status.sendCommand( ON ) 
					
					// Get job information 
					Anova_Current_job.postUpdate( transform( "JSONPATH" , "$.status.current_job_id", Anova_Answer) )
					
					// Timer
					var String anova_timer = transform( "JSONPATH" , "$.status.is_timer_running", Anova_Answer)
					
					if( anova_timer == "false"){
						// No timer running
						Anova_Timer_running.postUpdate( OFF )
						Anova_Timer_remaining.postUpdate( -1 ) // -1 as default if no timer running
					}else{
						Anova_Timer_running.postUpdate( ON )
						
						var Number anova_timer_remaining =  Integer::parseInt( transform( "JSONPATH" , "$.status.timer_length", Anova_Answer))/60
						Anova_Timer_remaining.postUpdate( anova_timer_remaining )		
					}
				}else{
					// Anova not running
					Anova_Status.sendCommand( OFF )
					Anova_Current_job.postUpdate( "-" )
					Anova_Timer_running.postUpdate( OFF )
					Anova_Timer_remaining.postUpdate( -1 )
				}
			}
		}
end
5 Likes

Did you manage to get any further with Anova integration to include the control functions etc.?

I’m also joining this topic.
Did anyone already implemented the cooker?
This api is working fine in my case:

https://anovaculinary.io/devices/XXXXXXXXXXXXX/states/?limit=1&max-age=10s
	{
		"body": {
			"boot-id": "43435004281963",
			"job": {
				"cook-time-seconds": 3600,
				"id": "98827197488379",
				"mode": "COOK",
				"ota-url": "",
				"target-temperature": 83.9,
				"temperature-unit": "C"
			},
			"job-status": {
				"cook-time-remaining": 2510,
				"job-start-systick": 1042,
				"provisioning-pairing-code": 0,
				"state": "COOKING",
				"state-change-systick": 1042
			},
			"network-info": {
				"bssid": "8483C22779XX",
				"connection-status": "connected-station",
				"is-provisioning": false,
				"mac-address": "78DB2FD452XX",
				"mode": "station",
				"security-type": "WPA2",
				"ssid": "XXXXXX"
			},
			"pin-info": {
				"device-safe": 0,
				"water-leak": 0,
				"water-level-critical": 0,
				"water-temp-too-high": 0
			},
			"system-info-3220": {
				"firmware-version": "1.4.4",
				"firmware-version-raw": "VM176_A_01.04.04",
				"largest-free-heap-size": 28008,
				"stack-low-level": 220,
				"stack-low-task": 7,
				"systick": 390055,
				"total-free-heap-size": 28784
			},
			"system-info-nxp": {
				"version-string": "VM171_A_01.04.04"
			},
			"temperature-info": {
				"heater-temperature": 84.16,
				"triac-temperature": 52.39,
				"water-temperature": 83.9
			}
		},
		"header": {
			"created-at": "2020-12-30T15:38:50.166901Z",
			"e-tag": "18bab891d351ccd6a8ea8995a10501c1c308ec3dee14f537b8f83e34e6fd7XX",
			"entity-id": "209594404101XX"
		}
	}
]

I gave it a first try. :wink:
The device is an ‘Anova Precision Cooker’ with bluetooth and WIFI.
And I’m running it on OH3, with JSON transformation.




Things to think about/improve?

  • Refresh time: I would like to have it only refreshing when the cooker is ON (online).
    I was thinking to have this checked by his IP (IP is mac based in my case).
    Not sure if I can control this through a thing at this moment? Maybe I need to convert it to a rule?

  • Job id: usefull? Maybe the id’s can be resolved to a name?

  • Device safe: usefull? What is the exact trigger here?

  • Commands: today it seems not to be possible to send commands (fe target temperature) to the device? Or I missed it.

  • Rules: some examples I think off?

    • pushover or google broadcast 5 minutes before endtime?
    • Recalls if still active after 15 minutes end of program?
    • Warnings when temperature is too high, water level to low…




My things:
(change XXXXX with your API (you can find this in your app):

Thing http:url:anova "Anova Cooker" [
        baseURL = "https://anovaculinary.io/devices/XXXXXXXXXX/states/",
        refresh = "60",
        ignoreSSLErrors = "true"
        ]
{
        Channels:
                Type number : anova_CookTimeSeconds "Job Cook Time Seconds" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.job.cook-time-seconds"
                ]
                Type string : anova_Id "Job ID" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.job.id"
                ]
                Type string : anova_Mode "Job Mode" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.job.mode"
                ]
                Type number : anova_TargetTemperature "Job Target Temperature" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.job.target-temperature"
                ]
                Type number : anova_CookTimeRemaining "Job Status Time Remaining" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.job-status.cook-time-remaining"
                ]
                Type string : anova_State "Job State" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.job-status.state"
                ]
                Type number : anova_DeviceSafe "Device Safe" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.pin-info.device-safe"
                ]
                Type number : anova_WaterLeak "Water Leak" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.pin-info.water-leak"
                ]
                Type number : anova_WaterLevelCritical "Water Level Critical" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.pin-info.water-level-critical"
                ]
                Type number : anova_WaterTempTooHigh "Water Temperature High" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.pin-info.water-temp-too-high"
                ]
                Type number : anova_HeaterTemp "Heater Temperature" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.temperature-info.heater-temperature"
                ]
                Type number : anova_TriacTemp "Triac Temperature" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.temperature-info.triac-temperature"
                ]
                Type number : anova_WaterTemp "Water Temperature" [
                        mode = "READONLY",
                        stateTransformation = "JSONPATH:$[0].body.temperature-info.water-temperature"
                ]
}

My items:

Number  anova_CookTimeSeconds           "Job Cook Time [%s s]"                  <time>           { channel="http:url:anova:anova_CookTimeSeconds" }
String  anova_Id                        "ID [%s]"                               <status>         { channel="http:url:anova:anova_Id" }
String  anova_Mode                      "Mode [%s]"                             <status>         { channel="http:url:anova:anova_Mode" }
Number  anova_TargetTemperature         "Target Temperature [%s °C]"            <temperature>    { channel="http:url:anova:anova_TargetTemperature" }
Number  anova_CookTimeRemaining         "Time Remaining [%s s]"                 <time>           { channel="http:url:anova:anova_CookTimeRemaining" }
String  anova_State                     "State [%s]"                            <status>         { channel="http:url:anova:anova_State" }
Switch  anova_DeviceSafe                "Device Safe"                           <status>         { channel="http:url:anova:anova_DeviceSafe" }
Switch  anova_WaterLeak                 "Water Leak"                            <water>          { channel="http:url:anova:anova_WaterLeak" }
Switch  anova_WaterLevelCritical        "Water Level Critical"                  <water>          { channel="http:url:anova:anova_WaterLevelCritical" }
Switch  anova_WaterTempTooHigh          "Water Temperature High"                <temperature>    { channel="http:url:anova:anova_WaterTempTooHigh" }
Number  anova_WaterTemp                 "Water Temperature [%s °C]"             <temperature>    { channel="http:url:anova:anova_WaterTemp" }
Number  anova_HeaterTemp                "Heater Temperature [%s °C]"            <temperature>    { channel="http:url:anova:anova_HeaterTemp" }
Number  anova_TriacTemp                 "Triac Temperature [%s °C]"             <temperature>    { channel="http:url:anova:anova_TriacTemp" }

My sitemap:

 Frame label="Anova Job" {
    Text item=anova_Mode
    Text item=anova_State
    Text item=anova_CookTimeSeconds
    Text item=anova_CookTimeRemaining
    Text item=anova_WaterTemp
    Text item=anova_TargetTemperature
    Text item=anova_Id
    }
Frame label="Anova Cooker" {
    Switch item=anova_DeviceSafe
    Switch item=anova_WaterLeak
    Switch item=anova_WaterLevelCritical
    Switch item=anova_WaterTempTooHigh
    Text item=anova_HeaterTemp
    Text item=anova_TriacTemp
    }

Feel free to improve/advice/… this.

What happens if you do a http PUT with the updated json, does that command/control the unit? You can use curl to test that. I love my unit and use it monthly, however when I tried using their app I never found it to be reliable but that was 3 years ago I last tried.

Have you seen this way of creating a MQTT bridge that works with the bluetooth capable models?

I noticed that topic. As well as a lot of others, and it seems that anova don’t support/document API very well. :blush: Sadly I’m not a real programmer, so I don’t have the skills to change/analyze those scripts.

Today, I wrote a small OH rule that runs only when the annova is online (IP is reachable throuh network binding). With this, I can see the current status of the device.

rule "Check Anova Status"
   when
      Item anovaCookerIP changed from OFF to ON
   then
      Thread::sleep(5000)
      while(anovaCookerIP.state == ON) {
         var String json = sendHttpGetRequest("https://anovaculinary.io/devices/XXXXXXX/states/")
         anova_CookTimeSeconds.postUpdate        (Double::parseDouble(transform("JSONPATH","$[0].body.job.cook-time-seconds", json)))
         anova_Mode.postUpdate                   ((transform("JSONPATH","$[0].body.job.mode", json)))
         anova_TargetTemperature.postUpdate      (Double::parseDouble(transform("JSONPATH","$[0].body.job.target-temperature", json)))
         anova_Id.postUpdate                     (Double::parseDouble(transform("JSONPATH","$[0].body.job.id", json)))
         anova_CookTimeRemaining.postUpdate      (Double::parseDouble(transform("JSONPATH","$[0].body.job-status.cook-time-remaining", json)))
         anova_State.postUpdate                  ((transform("JSONPATH","$[0].body.job-status.state", json)))
         anova_DeviceSafe.postUpdate             ((transform("JSONPATH","$[0].body.pin-info.device-safe", json)))
         anova_WaterLeak.postUpdate              ((transform("JSONPATH","$[0].body.pin-info.water-leak", json)))
         anova_WaterLevelCritical.postUpdate     ((transform("JSONPATH","$[0].body.pin-info.water-level-critical", json)))
         anova_WaterTempTooHigh.postUpdate       ((transform("JSONPATH","$[0].body.pin-info.water-temp-too-high", json)))
         anova_WaterTemp.postUpdate              (Double::parseDouble(transform("JSONPATH","$[0].body.temperature-info.water-temperature", json)))
         anova_HeaterTemp.postUpdate             (Double::parseDouble(transform("JSONPATH","$[0].body.temperature-info.heater-temperature", json)))
         anova_TriacTemp.postUpdate              (Double::parseDouble(transform("JSONPATH","$[0].body.temperature-info.triac-temperature", json)))
         anova_LastUpdate.postUpdate             ((transform("JSONPATH","$[0].header.created-at", json)))
         Thread::sleep(60000)
         }
end

I’m now trying to figure out if it’s possible to send commands to it. A first sight, not much is moving. This is the example I’m testing:

curl -X PUT -H “Content-Type: application/json” -d ‘{“target-temperature”:“25”}’ https://anovaculinary.io/devices/XXXXXXX/body/job

Looking at the API etc you have found here it should be fully possible to make a decent binding out of this.

I have a device since a few days back so if I have some time over I might look into it in the future.

2 Likes

Would love a binding for this!
Would be such a nice thing to have my anova completly integrated into openhab!

EDIT: ignore the link above, this is using the login details to for anova app, i think this one is connecting directly to it anova.py/readme.md at master · bmedicke/anova.py · GitHub?