c3V6a2Vy / pyanova

Anova Sous Vide Bluetooth API Python Wrapper

Home Page:https://c3v6a2vy.github.io/pyanova/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support Anova Nano

c3V6a2Vy opened this issue · comments

Borrowed a Anova NANO and found out the library is not working. A quick sniffing reveals change in device_notification_uuid and device_primary_uuid

image

using the standard discovery can find the device, without needing to press any button. And quite opposite, if you long press the "target temp" button til it blinks the BLE scan won't be able to find the device at all.

from pyanova import pyanova
pa = pyanova.PyAnova(auto_connect=False)
devices = pa.discover(list_all=True, timeout=6)

yields: {'name': None, 'address': u'58:93:D8:9C:51:4A'}
However, connecting with the address directly failed at:

>>> pa.connect_device( {'name': None, 'address': u'58:93:D8:9C:51:4A'})
2020-06-03 00:41:54,678 - INFO - Starting PyAnova BLE adapter
2020-06-03 00:41:55,316 - INFO - Connecting to Anova device: {'name': None, 'address': u'58:93:D8:9C:51:4A'}
2020-06-03 00:41:55,897 - INFO - Connected to: {'name': None, 'address': u'58:93:D8:9C:51:4A'}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/pyanova-0.1.3-py2.7.egg/pyanova/pyanova.py", line 209, in connect_device
    self._dev.subscribe(notification_uuid, callback=PyAnova.indication_callback, indication=True)
  File "/usr/local/lib/python2.7/dist-packages/pygatt/device.py", line 144, in subscribe
    self._notification_handles(uuid)
  File "/usr/local/lib/python2.7/dist-packages/pygatt/device.py", line 120, in _notification_handles
    value_handle = self.get_handle(uuid)
  File "/usr/local/lib/python2.7/dist-packages/pygatt/device.py", line 208, in get_handle
    raise exceptions.BLEError(message)
pygatt.exceptions.BLEError: No characteristic found matching 0000ffe1-0000-1000-8000-00805f9b34fb

Through pygatt we found the following characteristics:

import pygatt
adapter = pygatt.GATTToolBackend()
adapter.start()
dev = adapter.connect(u'58:93:D8:9C:51:4A')
char_list = dev.discover_characteristics()
for c,v in char_list.items(): print(v.__dict__)

yield

...
{'descriptors': {}, 'handle': 14, 'uuid': '0e140002-0af1-4582-a242-773e63054c68'}
{'descriptors': {}, 'handle': 18, 'uuid': '0e140003-0af1-4582-a242-773e63054c68'}
{'descriptors': {}, 'handle': 5, 'uuid': '00002a01-0000-1000-8000-00805f9b34fb'}
{'descriptors': {}, 'handle': 11, 'uuid': '0e140001-0af1-4582-a242-773e63054c68'}
{'descriptors': {}, 'handle': 7, 'uuid': '00002a04-0000-1000-8000-00805f9b34fb'}
{'descriptors': {}, 'handle': 3, 'uuid': '00002a00-0000-1000-8000-00805f9b34fb'}

which pretty much aligns with the one we discovered in Windows.

0e140001-0af1-4582-a242-773e63054c68 seemed to be the primary characteristics since it's user description was "tx-char" with handle 0xA . 0e140003-0af1-4582-a242-773e63054c68 was marked as "rx-char" so we tried this and the asyn-char as well for receiving. None of the combination worked.

Will need to get my old Anova back to do more analysis on what that was like. And though unlikely, it could also be that the commands had changed.

Hey @c3V6a2Vy did you have any luck in sorting out how to control the Nano? I have one and please let me know if you need some help for testing stuff.

Writing to handle 18 in seemed to 0e140002-0af1-4582-a242-773e63054c68 be not working...

handle = 14
dev.char_write_handle(handle , bytearray("%s\r"%('status'), 'utf8'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.4/dist-packages/pygatt-4.0.5-py3.4.egg/pygatt/backends/gatttool/device.py", line 17, in wrapper
    return func(self, *args, **kwargs)
  File "/usr/local/lib/python3.4/dist-packages/pygatt-4.0.5-py3.4.egg/pygatt/backends/gatttool/device.py", line 48, in char_write_handle
    self._backend.char_write_handle(self, handle, *args, **kwargs)
  File "/usr/local/lib/python3.4/dist-packages/pygatt-4.0.5-py3.4.egg/pygatt/backends/gatttool/gatttool.py", line 50, in wrapper
    return func(self, *args, **kwargs)
  File "/usr/local/lib/python3.4/dist-packages/pygatt-4.0.5-py3.4.egg/pygatt/backends/gatttool/gatttool.py", line 574, in char_write_handle
    self.sendline(cmd)
  File "/usr/lib/python3.4/contextlib.py", line 66, in __exit__
    next(self.gen)
  File "/usr/local/lib/python3.4/dist-packages/pygatt-4.0.5-py3.4.egg/pygatt/backends/gatttool/gatttool.py", line 191, in event
    self.wait(event, timeout)
  File "/usr/local/lib/python3.4/dist-packages/pygatt-4.0.5-py3.4.egg/pygatt/backends/gatttool/gatttool.py", line 157, in wait
    raise NotificationTimeout()
pygatt.exceptions.NotificationTimeout: None

Actually, subscription attempts to 0e140002-0af1-4582-a242-773e63054c68 would failed as well with indication turned on:

dev.subscribe("0e140002-0af1-4582-a242-773e63054c68", indication=True)

Hey @c3V6a2Vy did you have any luck in sorting out how to control the Nano? I have one and please let me know if you need some help for testing stuff.

thanks mate, sorry for the late reply, didn't see your message nor have time to make more progress. I am going through the some details... stay tuned.

ok you can write to handle 11 (character: 0e140001-0af1-4582-a242-773e63054c68) but nothing happened.

 dev.char_write_handle(11, bytearray("%s\r"%('set unit c'), 'utf8'))

@c3V6a2Vy sorry I am not expert at all in the issue as I am not a programmer. Are you perhaps making any progress? I have a nano so if you need a double check I can help but I would need a detailed guide... :)

From some secret details I managed to trace down the sendTemperature command and found it's actually sending Protobuf msg bytes now instead of the string protocol used in the first generation.

Good news: we (sort of) know what comprises a valid command.
Not so good news: we have to find out its proto definition and have to port it to this library. I cannot find the original "proto" definitions in the package (which makes sense), so need to find a way to convert the generated codes back to proto definitions. I think it's doable just takes time..

I guess unfortunately there are still no updates on the nano support? :(

From some secret details I managed to trace down the sendTemperature command and found it's actually sending Protobuf msg bytes now instead of the string protocol used in the first generation.

Good news: we (sort of) know what comprises a valid command.
Not so good news: we have to find out its proto definition and have to port it to this library. I cannot find the original "proto" definitions in the package (which makes sense), so need to find a way to convert the generated codes back to proto definitions. I think it's doable just takes time..

could you describe in a little more detail how you're going about converting the definitions?
i've been following this issue since realizing that my Nano wouldn't work with existing packages, and have also decompiled the anova app and started poking around.
depending on the process you're using to convert the proto definitions, i'm hoping that maybe Xposed Framework could access the information you need directly since it hooks into an app and can modify its flow at runtime.

@myleskeller @garret
sorry for the lack of updates.. was busy with other projects.

i was combing through the decompiled codes from the app code, confirmed with the uuid and handle, and i was able to connect to the device via BT.

the challenge was the content being different from the the 1st gen device (which is basically byte array of string commands), it's sending protobuf encoded messages to the device. Since this lib is in Python but the app was in Java, I didn't really know how to invoke the compiled protobuf encoding code in the Java (via Xposed framework maybe, looking into that).

So, the more plausible way (at the time from my last update) was to decode (reconstruct) the original protobuf definition and compile it again into Python, which it seemed harder than I thought in my later experiment. Any suggestion here?

will give this guy a try: https://github.com/marin-m/pbtk

I've recently acquired an Anova Pro, and have followed the steps in this issue, as it appears to be the exact same problem. Did you manage to get the binary/hex of the protobuf messages being sent? Presumably if the above doesn't work, we could use something like Online Protobuf Decoder and determine the field names via process of elimination / looking at the decompiled app code.

I've done some reverse engineering of the decompiled app and started the project https://github.com/dengelke/node-sous-vide which is still a WIP but is working with the Nano. Although not tested with the Pro there it shouldn't require much or any work to get running.

I've done some reverse engineering of the decompiled app and started the project https://github.com/dengelke/node-sous-vide which is still a WIP but is working with the Nano. Although not tested with the Pro there it shouldn't require much or any work to get running.

Thanks @dengelke ! Are 544.js and 563.js decompiled codes from the Android app and then later translated into JS? I had the similar code from Java but was struggling to reverse engineer that into Python. Your decompiled JS code will help a lot, I will try to translate the JS into Py with something like https://github.com/PiotrDabkowski/Js2Py

Will write out a guide on how I decompiled it - the android app itself is written in JS using React Native so these are just modules from the app which were modified to work in a Node.js environment instead of React Native.

Currently refactoring the code to get rid of 544.js and 563.js using https://www.npmjs.com/package/protobufjs which will give the original protobuf definitions and a lot less code to worry about.

Not sure about converting the code from Js to Py, would suggest you play around with it and if you have a Nano or Pro try and use it. Big changes you need to look at for:

  • There are now two characteristics you need to use, one to write commands to, one to read responses from look at src/sendDeviceCommand.ts to see how this works compared to the original Anova which only had one bluetooth channel
  • If you have a look at src/Command.ts one of the device commands to see the current temperature is [1, 2, 4, 0] which just needs to be converted to a buffer and sent to the write characteristic by Buffer.from(commandArray) you will then get a response on the read characteristic, which needs to be converted to an array then decoded using the protobuf library

Good luck and let me know if you have any questions

Currently refactoring the code to get rid of 544.js and 563.js using https://www.npmjs.com/package/protobufjs which will give the original protobuf definitions and a lot less code to worry about.

https://github.com/dengelke/node-sous-vide/blob/master/src/proto/messages.proto

With this I can compile the Python codes directly :)

@c3V6a2Vy glad the proto definitions could be useful, these aren't the complete definitions - just the ones we required to get this package to work nicely.

Now having a better look at the decompiled code I have, seems the Pro although having a similar Bluetooth configuration has a completely different set of commands so although this package will let you connect to it, the commands won't work :/

For detailed decompilation instructions for the Android APK please give https://github.com/dengelke/node-sous-vide/blob/master/Decompilation.md a read

I would love to get my anova nano to work, any progress on this ? :-) Big thanks for the contribution so far to get anova working!

I think I managed to translate @dengelke's node-sous-vide to python: https://github.com/filmkorn/pyanova-nano

I've never touched any BLE client code nor asyncio code, so I'd love if someone could give this a review?

Sadly I could not get pyanova to run on Windows, so I basically started from scratch using bleak instead of pygatt/bluez. That doesn't mean the logic can't be integrated here.

I think I managed to translate @dengelke's node-sous-vide to python: https://github.com/filmkorn/pyanova-nano

I've never touched any BLE client code nor asyncio code, so I'd love if someone could give this a review?

Sadly I could not get pyanova to run on Windows, so I basically started from scratch using bleak instead of pygatt/bluez. That doesn't mean the logic can't be integrated here.

Nice work @filmkorn!

nice one! @filmkorn . taking a look. i borrowed friend's nano yesterday hopefully i can run some tests on it too.

works fine for me on windows 11 build 22621, Python 3.12.1

asyncio.run(print_device_sensors())
SensorValues(water_temp=23.2, water_temp_units='C', heater_temp=22.0, heater_temp_units='C', triac_temp=23.0, triac_temp_units='C', internal_temp=29.0, internal_temp_units='C', water_low=False, water_leak=False, motor_speed=0)

bleak seemed to be much more cross-platform friendly and easy to operate than bluez, the pygatt/bluez stuffs was used mainly because it's the only thing I can get it to compile on the SoC that I used to use, now that I lost that SoC too so I think it's time to move on to something nicer.