La Marzocco added mobile app support to the GS/3 and Linea Mini espresso machines earlier this year and it adds some nice features to what are already great machines - shot timer for the Linea Mini, the ability to turn the machine on/off, change a large subset of the settings, control the on/off schedule, adjust the volumetric buttons, etc. The machines and the app connect to their cloud over Wifi and I would love to see integration with Home Assistant, but I don’t see any open source efforts to plug into or reverse engineer their API. Is anyone aware of an ongoing project?
There are two APIs:
-
[cms]
: https://cms.lamarzocco.io (for authentication and user account details) -
[gw]
: https://gw.lamarzocco.io (for communicating with the Linea Mini, firmware updates)
To sign in to the API Gateway
POST [cms]/oauth/v2/token
- This requires client_id, client_secret, grant_type:password, password and username.
- If successful, will return an oauth bearer token. Use this bearer token in the header for all other API requests.
To get the serial number of your machine(s)
GET [cms]/api/customer
This will return a JSON object, the serial number will be at data.fleet[0].machine.serialNumber
.
To turn a Linea Mini on and off via the API Gateway
POST [gw]/v1/home/machines/serialNumber/status
- replacing serialNumber with the serial number displayed in the app under “my machines” e.g. LM123456
- With the token received in (1) as the bearer token
- the request being
{"status": "STANDBY"}
or{"status": "ON"}
To get the current configuration:
GET [gw]/v1/home/machines/serialNumber/configuration
This will return something like this:
{
"status": true,
"data": {
"received": "2020-01-01T20:00:00.000Z",
"ENABLE_PREBREWING": true,
"STBY_TIMER": 30,
"TON_PREBREWING_K1": 1.1,
"TON_PREBREWING_K2": 0,
"TON_PREBREWING_K3": 0,
"TON_PREBREWING_K4": 0,
"TOFF_PREBREWING_K1": 2.2,
"TOFF_PREBREWING_K2": 0,
"TOFF_PREBREWING_K3": 0,
"TOFF_PREBREWING_K4": 0,
"TSET_STEAM": 0,
"TSET_COFFEE": 95.2,
"DOSE_K1": 0,
"DOSE_K2": 0,
"DOSE_K3": 0,
"DOSE_K4": 0,
"DOSE_K5": 0,
"DOSE_TEA": 0
}
}
To get the current status, e.g. the current boiler temperature
GET [gw]/v1/home/machines/serialNumber/status
This will return something like:
{
"status": true,
"data": {
"received": "2020-01-01T01:00:00.000Z",
"MACHINE_STATUS": "STANDBY",
"TEMP_COFFEE": 59,
"TEMP_STEAM": 0
}
}
Ports
There are some details on the ports used here
TCP 53 (DNS)
TCP 80 (HTTP, for firmware updates)
TCP 443 (HTTPS)
TCP 8883 (MQTTS)
UDP 53 (DNS)
Wow, awesome! I have some work to do over the weekend!
I’ve not worked on adding functionality to home assistant before but I should be able to figure out the Linea Mini API. If you stick something on GitHub I should have some time to collaborate on it, if you like.
I’ve just fixed a few bugs in Core or integrations and haven’t done any original development for HA yet. It seems pretty straightforward and I imagine that I could start with an existing integration. I’ll reach back out when I have something to contribute.
Thanks for the research! Did you just snoop the net traffic from the app or do you have a secret source?
Let me know how you get on. You probably won’t get far without knowing a client_id and client_secret you can use to connect to the oauth API, which you may be able to get by looking at your network traffic.
The API Gateway only allows you to control a few settings such as turning the machine on and off.
To control other settings such as temperature you need to use a local network connection.
You can send messages to the Linea Mini on the local network but I think the messages are a bespoke, low level/binary API. I think it would be worth trying to convince La Marzocco to release the details for this API rather than risk bricking expensive equipment. I would hope that some basic 3rd party integrations using the API Gateway (e.g. a voice assistant integration) would be seen as a positive thing by them and could result in them being receptive to releasing further details on their local network API, but that’s just a guess.
For what it’s worth, the messages are of the format RXXXXXX
WXXXXXX
for read/writes (X’s are bytes and the number of them can vary, I think it’s 10 bytes for outgoing read requests and 12 bytes for outgoing write requests, and larger for incoming responses). These messages are then AES encrypted using the 256bit key you can get from the API, with a zero initialisation vector. They are then Base64 encoded and wrapped with an @
at the start of the message and a %
at the end of the message. This is then sent as a TCP packet to port 1774 on the Linea Mini.
To get the 256bit AES key, send a GET to [cms]/api/customer and it will be in the response at data.fleet[0].communicationKey
.
, i’ve also been interested in looking into this for my Linea Mini. I started digging into this and was able to get an auth token back and make a subsequent request to tell the machine to turn on but it doesn’t actually turn on even though the response returns a 200. Have either of you been able to successfully turn the machine on/off through the /v1/home/machines/serialNumber/status
endpoint?
I’ve also been able to successfully get auth token back, but not get the machine to turn on/off using the /status endpoint.
Has anyone else had any luck?
i have a feeling it might just use bluetooth if you’re on the local network. I’m going to investigate the next time I’m not home and can analyze the packets when it’s “remote connected”
I’m just getting around to playing with this and I can get my GS3 to turn on/off via the gateway via the instructions above. I was just composing a reply saying that I was having the same problem as the 2 folks above with a “200” response and no change to the machine, but it just started working. I was also having trouble getting the iOS app to connect remotely as well, so there might have been an issue with the gateway while I was testing. As soon as I could connect and control the machine through the iOS app, my POST started working too.
It’s odd because while the on/off POST wasn’t working, I was able to get the configuration data through a GET to the same gateway. In any case, it’s working now and I’ll continue to play with it.
I grabbed my client_id and client_secret using mitmproxy, installing the cert on my iPhone, getting the app to request a new token, and examining the resulting “token” request. Is there an easier/more friendly approach for folks who might use an HA integration?
I have a working prototype of an Integration and a better understanding of both Python and integration development :).
I implemented a Config Flow that takes the following:
- Machine serial number (used in the API endpoints)
- Username
- Password
- Client ID
- Client Secret
As I mentioned above, the client ID and secret are not trivial to retrieve and I’d like to know if anyone has a better way to get them than what I described. As it is, I think it’s out of reach for the vast majority of folks. This only uses the gateway and doesn’t connect directly, so commands take a few seconds and I can only turn the machine on and off right now. That does seem to be reliable, though.
I’ll clean it up a bit and stick it up on github to see if it works for you.
I spent some more time on it today and it’s working pretty well for me. Please check it out and let me know how it works for you:
I added support for HACS via custom repository, so it’s easier to install now.
I had to do exactly the same thing as you to get the client and secret key. It was incredibly tedious
I’m away from my machine for now but will pull your repo and try it out when I’m back. I wrote my controller in Go and am curious how our implementations differ
I noticed this morning that the machine was on, but my integration still said it was off. The mobile app, when connected directly over Wifi, reported the correct “on” state.
From the logs, it looks like the API is still connecting and getting data back, but is repeatedly returning stale data from 8 hours ago when the machine was off. When I turned wifi off on my phone the mobile app attempted to connect remotely and repeatedly failed with “No connection”, so it seems like it may actually be connecting to the gateway, but just doesn’t like the data it’s getting back. I power-cycled my GS/3 from the rear physical switch and now both the app and my integration are reporting current, correct data.
I think this is what was happening to all of us earlier, where the machine sometimes loses connectivity with the gateway and the gateway acts like everything is fine. I don’t have all the raw data received from the API anymore from when it was failing, but what I do have looks fine except for the “received” data/time being in the past and the MACHINE_STATUS being wrong.
In summary, if the gateway isn’t reporting correct status or controlling your machine through your manual API calls, check whether the mobile app can connect remotely (without wifi). If not, try hard power-cycling your espresso machine and see if that fixes it.
Just got this right now:
Hi,
on December 2nd 2020, La Marzocco Home Mobile App will be down due to server maintenance.
The maintenance is expected to last from 21:30 CET to 22:00 CET. During that time, you will see the red bar “no connection” on top of your screen but your machine will remain fully functional with the parameters previously set.
I’ve been trying to make the direct interface work today and I’m finding it quite vexing. Based on plonx’s comments, I grabbed some traffic from the app to the machine, pulled out the base64-encoded portion between @
and %
, decoded it (so far so good), then ran into trouble decrypting it using the key from the [cms]/api/customer.
The communicationKey
that I get back is a unicode string with 32 unicode characters, but is represented by 52 bytes when fully encoded. I can’t use it directly (AES256 wants a 32 byte key), so I ran it through sha256 to produce the required length. I can run through the decryption process just fine with a zero-filled iv, but nothing that I get back starts with “R” or “W”. I’ve been playing around with it for quite some time and getting nowhere, so I thought I’d post here in case someone has some thoughts on how to proceed.
The AES256 decryption code that I’m using is quite simple:
import base64
import hashlib
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes
import logging
class AESCipher:
def __init__(self, key):
self.key = hashlib.sha256(key.encode()).digest()
def encrypt(self, text):
cipher = AES.new(self.key, AES.MODE_CBC, iv=bytearray(16))
ciphertext = cipher.encrypt(text.encode())
return base64.b64encode(ciphertext)
def decrypt(self, ciphertext):
ciphertext = base64.b64decode(ciphertext)
cipher = AES.new(self.key, AES.MODE_CBC, iv=bytearray(16))
return cipher.decrypt(ciphertext)
My test app, key excluded is here:
ciphertext1 = "AnavVSCyu5U7ZnDsXPoB4A=="
ciphertext2 = "ZDK1xN6WMoAbQT1Z9u/HtQ=="
cipher = AESCipher(key)
ciphertext = cipher.encrypt('This is a string')
print(cipher.decrypt(ciphertext).decode())
print(cipher.decrypt(ciphertext1))
print(cipher.decrypt(ciphertext2))
Output:
This is a string
b'r\x98"\n*[\xc9\xc1\xa5\xd4-RuB\x10C'
b"\xda'\x9d{\xf7B\xe7\xaa\x13\xe6\xba{\x137$h"
I also tried just truncating the key to 32 bytes, leading to different resulting values, but the same bad output.
I’m initializing the class with the unicode string that I get back from the API, and then passing in some of the base64 commands the app was sending to my machine (GS/3). I’ll keep poking around, plus please do point out anything that looks wrong.
@rccoleman I know you said you don’t want to help people sniff the Client ID/Secret but I’ve tried everything and can’t capture them. Any tips? I’ve love to get this going and help you out.
I described most of what I did in the quoted text, but this is what I can recall in steps:
- Install mitmproxy on my Macbook Pro
- Install the cert from their site on my iPhone
- “Trust” the cert (instructions are on their site)
- Configure a “manual” proxy on my iPhone for the Wifi connection to point to my Macbook Pro
- Launch the LM app on my phone and play around with it until I see a request to the ‘token’ endpoint
- Expand that request and the client_id and client_secret will be right there
What part are you having trouble with?