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?