La Marzocco GS/3 & Linea Mini support

Thanks Rob. My apologies, I didn’t see your instructions. That got me onto the right path and I found this blog post showing detailed instructions if someone finds this helpful: https://den.dev/blog/intercepting-iphone-traffic-on-a-mac-a-how-to-guide/

So far I can’t get my Linea Mini to just change the state but I will keep playing and see if I can get it working.

No worries. If it seems like the APIs are succeeding and it’s just not controlling your machine, try power-cycling the machine from the button in the back.

I eventually got it working shortly after my reply. I think a mixture of rebooting HA and the machine got it working.

One thing I’ve noticed, when adding a machine it’s always showing as a GS/3.

Eg:

@property
    def device_info(self):
        """Device info."""
        return {
            "identifiers": {(DOMAIN,)},
            "name": "La Marzocco",
            "manufacturer": "La Marzocco",
            "model": "GS/3",
            "default_name": "lamarzocco",
            "entry_type": "None",
        }

Is there something returned in the API to set this correctly for Linea minis? Alternatively, if the Serial number first 2 characters are consistent, setting on that value.

Excellent, glad it’s working. Using the first two characters of the serial number is probably a good way to differentiate, probably even for the higher models. I’ll make that change when I get a chance, but for now, enjoy your upgrade :slight_smile:.

Sorry I forgot to say earlier. Thanks so much for your work! Bloody legend!

It’s been an excellent opportunity to learn Python and how to write a Home Assistant integration simultaneously :). It’s really not very functional without the local API, so I’m looking forward to figuring that out.

1 Like

Sigh, I figured it out. I just needed to encode with “latin-1”, rather than the default of “utf-8”. I’m getting reasonable commands as plonx described now and I’ll see if I can decode them.

Woohoo! I can now replicate the cloud functionality using the direct API, at least in a test app. I haven’t yet added it to the integration yet, but I don’t think that’ll be too difficult.

So far, these are the commands that I’ve found:

  • A command that I call “D8” that appears to request the same info that you get from the “status” cloud endpoint. This is a read starting with “R”, and has a preamble of 40000020.
  • A command that I call “E9” that appears to request the same info that you get from the “configuration” cloud endpoint. This is a read starting with “R”, and has a preamble of 0000001F
  • A command that I call “EB” that requests the auto on/off schedule. This is a read starting with “R”, and has a preamble of 0310001D
  • A command that writes a value, and it starts with “W” and has a preamble of 00000001. I’ve only used this to turn the machine on and off so far, so there may be other preambles.

The responses have a matching “R” or “W” and preamble based on the command and these are the types that I’ve found and decoded (more or less):

  • A periodic short status broadcast that gives the current brew/steam boiler temps. I don’t need to send a command to get this - it may just be sent to whoever has an active socket connection. Preamble is 401C0004
  • A response to the “D8” command with status info
  • A response to the “E9” command with config info
  • A response to the “EB” command with the auto on/off schedule and what’s enabled
  • A “confirmation” response to a “write” command from the app (message to the machine starting with “W”). This includes the string “OK” if it worked.

I’ll clean it up and get it into the integration so we can stop messing with the cloud API. The only downside is that the machine can only accept a single connection at a time, so if either the app or this integration is connected, the other will wait until it frees up.

Repo with library, test harness, and Wireshark JSON parser is here. The parser decodes JSON from the File->Export Packet Dissections->As JSON option in Wireshark.

I added some documentation at the repo above on how to install and run the test app, so feel free to give it a try and let me know how it goes. I only have a GS/3, so I don’t know if a Linea Mini will behave any differently. It only issues reads on its own, so I don’t think it’s likely to cause any harm if you stick to the “3” (“Status”) command.

Edit: Now converted to asyncio!

1 Like

Oh wow, sorry only just seen all the activity here since my last post.

Yes I had things working and integrated it with Siri via scriptable but it wasn’t 100% reliable so I started using the app again.

I have a break coming up so may have time to look in to this again.

Let me know if you need help with anything. What are the next steps to get it in to home assistant?

Also great work @rccoleman :tada:

edit:

now I’m getting “We’re sorry, but new users are temporarily limited to 3 replies in the same topic”

so you may not hear from me again :smile:

edit:

so I guess all my replies will have to go in to edits here

yes I think this is the issue I was having - but to be honest I think I also have this sometimes via the official app if I’m away from home and trying to turn the machine on (which the app does via the web api) - so I think this is an issue with the machine/api rather than our integrations.

awesome, I had given up trying to understand this, well done for sticking with it

Yes, I think the remote capability is just flaky and stops working sometimes. I always use it locally and hadn’t noticed until I did a sanity check.

I made quite a bit of progress on integrating the local API into the integration yesterday. All the status is now coming from the local API, and I added discovery (zeroconf) so it finds the machine automatically and grabs the IP and serial number from there. You still need to provide the client_id/client_secret/username/password because I need to get the encryption key from the cloud API, but it’s a lot more convenient than my first attempt.

I pushed the latest lmdirect package to the repo above and released v0.1, and the corresponding integration code is on the dev branch and not released yet. Feel free to check it out.

Things I still want to do:

  • Switch to PyCryptodome from PyCryptodomex because the former is already available in Home Assistant - Done!
  • Script mitmproxy to make it easier to find the client_id and client_secret. It looks like you can provide a python script to parse the incoming data and there are lots of examples. You still need to set up the phone manually, but I don’t think that can be helped.
  • Call the local on/off APIs from the integration rather than going through the cloud. It’s just replacing a function call with one from lmdirect, but I’m still hesitant about doing writes before fully understanding the protocol. It worked in testing, though. - Done!
  • Maybe pull the cloud stuff out of the HA integration and stick it in lmdirect to simplify the integration and make lmdirect a bit more friendly (can read the key for you). - Done!
  • Continue to reverse engineer the protocol. There’s an interesting string of ASCII numbers in the D8 response that don’t seem to correlate to anything, and it’s driving me crazy.
  • Release lmdirect to PyPI so that it can be added to the integration requirements the right way. - Done!

I’m sure I’ll think of a bunch of other stuff when I get back to it.

I was hoping that this would be a more useful “push” API, but the only things that I get unsolicited are the coffee/steam temps (every few secs) and on/off state (every 30s or so when it changes). It doesn’t look like config changes are reported automatically (like the coffee/temp set temp), so I still have to poll for that.

One big downside to using the local API is that it locks out the mobile app, and I want to improve on that. I may switch to opening/closing the connection on a polling interval to allow the app to get in. Unfortunately, it’s not like the Roomba where the app falls back to connecting remotely if it can’t connect locally - it just blocks until the port frees up, and I sometimes have to kill it to get it to try again.

I probably wouldn’t have done anything without your initial investigation, so many thanks for that. I’ve learned a ton about Python and how to build an integration.

1 Like

I just released a beta version of the integration that uses the local API as v0.5. It’s available in HACS, but you’ll need to turn on the “Show beta versions” switch to see it. I changed the config schema, so if you’ve installed an earlier version, you’ll need to remove the integration, restart, and you should see HA report that it discovered your machine when it starts up. After that, you configure it as before with the client_id, client_secret, username, and password. Please do give it a try and see if it works for you - I’m most interested in folks who have Linea Minis, as I can’t test that myself.

I was wrong and the app does appear to fall back to a remote connection if it can’t connect locally. You’re limited by what it can do via the gateway, which means you can’t change any of the config settings via the mobile app and you can’t yet change them in this integration, but you can still turn the machine on and off via the mobile app.

I’ve submitted a PR to have it included in the default HACS repo list, but it’s still waiting to be merged and you’ll need to add it manually for now.

1 Like

Lots of good updates.

  • Switch to PyCryptodome from PyCryptodomex because the former is already available in Home Assistant - Done!
  • Call the local on/off APIs from the integration rather than going through the cloud. It’s just replacing a function call with one from lmdirect, but I’m still hesitant about doing writes before fully understanding the protocol. It worked in testing, though. - Done!
  • Maybe pull the cloud stuff out of the HA integration and stick it in lmdirect to simplify the integration and make lmdirect a bit more friendly (can read the key for you). - Done!
  • Release lmdirect to PyPI so that it can be added to the integration requirements the right way. - Done!

v0.5.4 is available in HACS. Lots of internal refactoring, but also should be a lot more robust. Give it a try and let me know if it works.

1 Like

Even more goodness: I wrote a mitmproxy script that automatically spits out the credentials that you need! Script is here: https://github.com/rccoleman/lmdirect/blob/77c6f1084e3dfee95df1525b77f2a912030e1c80/find-auth.py

Here’s how to use it:

  • Install mitmproxy on the command line. Installation instructions for lots of platforms are on the website

  • Run mitmproxy to start a proxy server on port 8080 and display the sniffed traffic

    $ mitmproxy

  • On your phone, go into the Wifi settings and find the option to enable a proxy. Choose “manual” and enter the name/IP address of the machine running mitmproxy and port 8080

  • In a browser on the phone, go to mitm.it. That’s a special site served by the local proxy server where you can install certificates on your phone to allow the local mitmproxy server to sniff the traffic. Follow the instructions for your phone, possibly including the separate “trust certificate” step if you’re using an iPhone.

  • Generate some network traffic on your phone by browsing, launching apps, etc. and make sure that mitmproxy shows the requests on the screen. If not, consult their website to see what you or I may have missed and report back if I need to update the instructions.

  • Quit mitmproxy by hitting q and y

  • Download the find-auth.py script, either by cloning https://github.com/rccoleman/lmdirect, or by copying and pasting the raw content into a file on the machine where you installed mitmproxy

  • From the command line, run this and mitmproxy will start looking for token requests:
    mitmdump -q -s find-auth.py

  • On your phone, launch the La Marzocco mobile app and log out by hitting the small “person” icon in the upper right corner and hitting the “logout” button. As far as I can tell, you’ll only lose your temperature units selection in the app and nothing more.

  • Enter the username and password to log back into the app

  • If it worked, the client_id, client_secret, username, and password will all be printed on the screen like this, and mitmproxy will exit. Go disable the proxy on your phone to reestablish normal network access.

client_id: a_long_string
client_secret: another_long_string
username: [email protected]
password: password

Make a note of these, as you’ll need to copy/paste them into the integration. Both the client_id and client_secret appear to remain the same even when logging into and out of the app, so that’s convenient.

So that’s everything on my todo list other than decoding more of the protocol. I got a spontaneous and unexpected response just last night that I haven’t seen before, so there’s clearly more work to do :slight_smile:

1 Like

One step closer to understanding the protocol for those playing along at home. The last byte in each command/response is a checksum byte that’s a sum of all the ASCII bytes, including the R or W, modulo 256, represented as ASCII-encoded hex.

Typical example from one of the periodic unsolicited status messages from the machine:

R 401C0004 03BA 04DA BD

breaks down like this:

R: Read
401C0004: Response code (current temp report)
03BA: Coffee Temp (adjusted) * 10 (95.4C)
04DA: Steam Temp (adjusted) * 10 (124.2C)
BD: Check byte calculated against the ASCII string “R401C000403BA04DA” with the “Checksum8 modulo 256” algorithm described and calculated here (second algorithm), turned into ASCII-encoded hex and appended

I figured that it was something simple, but I kept trying to calculate the checksum based on the hex values represented and not the raw ASCII.

This isn’t critical for the existing functionality in the integration where I just parrot commands that the app sends, but it will be useful if I want to generate new commands based on values that the user enters. Phew, that was annoying.

1 Like

I’ve made a lot of progress on the integration and underlying library and I’m finally nearing the point where I’m out of ideas on what to do next. Since the last update, here’s what I’ve done:

  • Split the single entity into 3 separate switches (main, auto on/off, and prebrewing) and 2 sensors (coffee boiler temp and steam boiler temp). All the switches are functional, and I distributed the attributes to the appropriate entities.
  • Added several services - set_coffee_temp, set_boiler_temp, enable/disable auto on/off for each day of the week (in addition to the global switch), and set the on/off times for each day of the week. I’ve tested them all from dev->services, but not from automations/scripts yet. I don’t really feel the need to add them to automations, to be honest - I see them as more of a UI convenience.
  • Decoded nearly the entire protocol, at least what appears to be important. There are a few bytes here and there that I can’t figure out, some that change periodically and some that are static. I don’t feel like I’m missing anything, or that more info would enhance the integration at this point.
  • Refactor, refactor, refactor. So much refactoring.

There are a lot of new files and a lot of code changes, so I’m still testing prior to releasing to HACS.

For me, this is mainly a vehicle to learn Python and Home Assistant development and I don’t know if anyone is actively using it. Is anyone looking for specific functionality?

I think it would be cool to have a custom Lovelace card that showed the boiler temps, machine on/off, prebrew on/off, and auto on/off times for the week. Unfortunately, that’s well outside my area of expertise, and not something that I really care to delve into. Otherwise, it kinda feels “done” until any more inspiration strikes.

One fun fact: they use a single hex byte to represent the year in the time/date/report (currently 0x14/20), so 2256 may be the year 2000 for LM :slight_smile: . Second interesting fact: the app periodically requests the time and sends a correction:

App: R
03 00 00 07
DC 
Machine: R
03 00 00 07
00 27 08 04 18 0C 14 (12/24/2020, 8:27:00 AM)
B2 

App: W
03 00 00 07
10 26 08 04 18 0C 14 (12/24/2020 8:26:10AM)
B7 
Machine: W 03 00 00 07 OK 7B (all good)

I see the integration has been merged in to HACS but I’m not sure how to add it - it doesn’t seem to appear when I go to ‘Add Integration’ - any idea? Or do I just need to wait for a newer version to be released? (I think I have the latest…)

edit: I see I need to install HACS as per https://hacs.xyz/docs/installation/installation

Yes :slight_smile:

Anyone had an opportunity to try it? I’ve been working on tests and they’re helping me catch a few issues here and there. I’m really curious if anything doesn’t work or behaves strangely on machines other than my GS/3 AV. There are a lot of config options based on the keys on the front of my GS/3 AV, and I wonder whether those options are just inert for the LM with no buttons at all or on the GS/3 MP with no volumetric control.

I’m about to push out a new version and I’d like to fold in any changes related to other machines.

I’ve had it working for a couple of days.

After I had figured out installing HACS, setup of the La Marzocco integration for my Linea Mini worked flawlessly (thankfully I already had the client id etc. from my previous attempts).

I integrated it with HomeKit and have been able to operate it via Siri on various devices with no problems (just turning on and off so far).

I’ve been waiting to use it a little more before commenting on reliability etc. but it seems to be good other than my Home app on iOS sometimes showing that the Linea Mini is turned on when it is infact turned off. This doesn’t affect operation though as I can still request on/off commands and they work correctly. I am trying to figure out what causes it to show incorrectly - it’s just something I’ve noticed on a couple of occasions. I think it is when I use the app or the Linea Mini itself to turn on/off.

Good to hear that it’s working. The issue that you’re seeing may be due to the integration tying up the port from time to time as it polls. You could increase the polling interval to a higher number to see if that reduces possible collisions and makes the other apps more reliable:

const.py:

"""Set polling interval at 20s"""
POLLING_INTERVAL = 20

As I recall, the machine will automatically report via the local connection when it’s turned on after a while (it’s not immediate) and doesn’t report if it’s turned off. But that’s only if you have the connection open when it wants to send the status.

The integration will log any unidentified messages that it receives as errors, but that’s mainly so that I can try to figure out what they are. It just ignores them for now, but please do let me know if you see any log messages that look like this:

2020-12-31 12:53:56 ERROR (MainThread) [lmdirect.connection] Unexpected response: R0020002C0000014800000094000001AE00000020000005620000090C0000000F0000000A00000AD20000000F000000323E
2020-12-31 12:53:57 ERROR (MainThread) [lmdirect.connection] Unexpected response: R00500018000001F6000001820000953400001C3200009314000000414C

I’ve seen the first one once before and could never figure out what the values represent. The second one is entirely new to me and I need to dig into it.