Using Indego Robot Mowers from Homey

Calling an HTTP API with HomeyScript

My brother-in-law has a Bosch Indego robot mower that he wants to control via Homey. There was no app readily available, but there is an API available that has several open source clients. I helped him create some blocks of HomeyScript to call the API and integrate the mower into the rest of his smart home. The code here leans heavily on the work already done in the Java Controller Application and in the pyIndego Python Library, especially the documentation of the protocol.

Authentication

Most requests in the API require a contextId value, and to get one of those we first need to make a POST request to the /authenticate endpoint providing some details about the client as well as a Basic authentication token.

The authentication token is built by base64 encoding a string comprised of your username and password separated by a colon. This can be done conveniently in most programming languages, such as javascript:

return btoa("username@email.com:secret-password");

As is, this will output the following (but don't forget to use your actual username and password if you want to connect to the API for real):

dXNlcm5hbWVAZW1haWwuY29tOnNlY3JldC1wYXNzd29yZA==

(Note that the above code sample won't actually work in HomeyScript, so you'll need to use a different approach if you want to generate the token as part of that)

With the token generated we can get a contextId from the /authenticate endpoint:

POST https://api.indego.iot.bosch-si.com/api/v1/authenticate
Authorization: Basic dXNlcm5hbWVAZW1haWwuY29tOnNlY3JldC1wYXNzd29yZA==
Content-Type: application/json

{
    "accept_tc_id":"202012",
    "device": "",
    "os_type": "Android",
    "os_version": "4.0",
    "dvc_manuf": "unknown",
    "dvc_type": "unknown"
}

The token generated previously is provided in the Authorization header, while we can use sample data to populate most of the fields in the request. It is worth noting that the accept_tc_id field value will likely need to be updated in the future if a new revision of the terms and conditions for the API are released.

HTTP/1.1 200 
Content-Type: application/json

{
  "contextId" : "'3f2a9e8c-93cb-402e-a200-e325859f3ffe",
  "userId" : "0a86dc31-7136-4009-9ef6-61ac4cab696e",
  "alm_sn" : "000000000"
}

This provides us with the contextId that is needed to make other requests.

Before making other requests we can make this in a HomeyScript function:

// pull values from the flow editor
const user = args[0];
const pwd = args[1];

// btoa isn't available :(
const buffer = Buffer.from(user + ':' + pwd);
const headerData = buffer.toString('base64');
const authHeader = 'Basic ' + headerData;

const authRequestBody = {
    accept_tc_id: "202012",
    device: "",
    os_type: "Android",
    os_version: "4.0",
    dvc_manuf: "unknown",
    dvc_type: "unknown"
};

const result = await fetch('https://api.indego.iot.bosch-si.com/api/v1/authenticate', {
    method: 'POST',
    body: JSON.stringify(authRequestBody),
    headers: {
        'Authorization': authHeader,
        'Content-Type': 'application/json'
    }
});

// fail for any error and return any error message we were given
if (!result.ok) {
    throw new Error(result.statusText);
}

const body = await result.json();

// return just the context id because we don't care about the other values
return body.contextId;

The two const values for user and pwd should be provided from the flow editor, and this will output the contextId as a text value, that can then be passed to another function to do something.

Get Available Devices

If you already know the serial number for your mower, you can skip this step and just use that to work with it directly. If you don't know the serial number or you have multiple mowers that you want to work with, there is an API endpoint that will list all the available devices:

GET https://api.indego.iot.bosch-si.com/api/v1/alms/
x-im-context-id: 3f2a9e8c-93cb-402e-a200-e325859f3ffe

Which will give a list containing the serial number and status code for the mowers connected to the account:

HTTP/1.1 200
Content-Type: application/json

[ {
  "alm_sn" : "000000000",
  "alm_status" : 258
} ]

The serial number is then used to get more detailed information or to control the mower.

Get Information About The Mower

Now we have the contextId and the serial number of the mower we want to work with, we can make two different calls to get information about the mower.

State

First /state will give information about the current state of the mower, we need to include the serial number of the mower we want to get information about in the url and the context id is provided as a header:

GET https://api.indego.iot.bosch-si.com/api/v1/alms/{serial-number}/state
x-im-context-id: 3f2a9e8c-93cb-402e-a200-e325859f3ffe

Which gives a response like this:

HTTP/1.1 200
Content-Type: application/json

{
  "state" : 258,
  "enabled" : true,
  "map_update_available" : true,
  "mowed" : 98,
  "mowmode" : 1,
  "xPos" : 12,
  "yPos" : 15,
  "runtime" : {
    "total" : {
      "operate" : 100000,
      "charge" : 30000
    },
    "session" : {
      "operate" : 2,
      "charge" : 0
    }
  },
  "mapsvgcache_ts" : 1582506399367,
  "svg_xPos" : 131,
  "svg_yPos" : 111,
  "config_change" : false,
  "mow_trig" : false
}

The status code can be looked up in the following table that is a combination of data found in both the projects that I linked at the start of this post and some extra details that my brother-in-law figured out:

Status Code Description
0 Reading Status
101 Docked
257 Charging
258 Docked
259 Docked - Software Update
260 Charging (Ran out of power)
261 Docked (Not 258 State)
262 Docked - Loading Map
263 Docked -Saving Map
266 Leaving Dock
512 Mowing
513 Mowing
514 Relocalising
515 Loading map
516 Learning lawn
517 Paused
518 Border cut
519 Idle in lawn
520 Mowing
521 Mowing
522 Mowing
523 Spot Mow
524 Mow without Docking Station
525 Mowing
768 Mowing
769 Returning to Dock
770 Returning to Dock
771 Returning to Dock - Battery low
772 Returning to dock - Calendar timeslot ended
773 Returning to dock - Battery temp range
774 Returning to dock
775 Returning to dock - Lawn complete
776 Returning to dock - Relocalising
1005 Mowing
1025 Diagnostic mode
1026 End of life
1027 Service Requesting Status
1038 Mower immobilized
1281 Software update
1537 Stuck
64513 Sleeping (Docked)
99999 Offline

Operating Data

And then /operatingData which can provide more detailed information for some properties, again including the serial number in the url and the context id as a header:

GET https://api.indego.iot.bosch-si.com/api/v1/alms/{serial number}/operatingData
x-im-context-id: 3f2a9e8c-93cb-402e-a200-e325859f3ffe

In a response that looks like this:

HTTP/1.1 200 
Content-Type: application/json


{
  "runtime" : {
    "total" : {
      "operate" : 100000,
      "charge" : 35002
    },
    "session" : {
      "operate" : 0,
      "charge" : 0
    }
  },
  "battery" : {
    "voltage" : 7.0,
    "cycles" : 0,
    "discharge" : -0.1,
    "ambient_temp" : 23,
    "battery_temp" : 23,
    "percent" : 70
  },
  "garden" : {
    "id" : 1,
    "name" : 1,
    "signal_id" : 3,
    "size" : 157,
    "inner_bounds" : 0,
    "cuts" : 0,
    "runtime" : 100000,
    "charge" : 35002,
    "bumps" : 281,
    "stops" : 90,
    "last_mow" : 3,
    "map_cell_size" : 120
  },
  "hmiKeys" : 12019
}

HomeyScript

Knowing how these requests and responses look, we can make useful HomeyScript functions so we could display the information somewhere or include it as part of a flow.

Get Status

Query the state endpoint and return the status converted to a human readable string:

// pull values from the flow editor
const contextId = args[0];
const serialNumber = args[1];

// get the current state
const result = await fetch('https://api.indego.iot.bosch-si.com/api/v1/alms/' + serialNumber + '/state', {
    method: 'GET',
    headers: { 'x-im-context-id': contextId }
});

if (!result.ok) {
    throw new Error(result.statusText);
}

const body = await result.json();

// convert the status code to human readable text
switch(body.state) {
    case 0: return "Reading status";
    case 257: return "Charging";
    case 258: return "Docked";
    case 259: return "Docked - Software update";
    case 260: return "Docked (Ran out of Power)";
    case 261: return "Docked (not 258 State)";
    case 262: return "Docked - Loading map";
    case 263: return "Docked - Saving map";
    case 266: return "Leaving dock";
    case 513: return "Mowing";
    case 514: return "Relocalising";
    case 515: return "Loading map";
    case 516: return "Learning lawn";
    case 517: return "Paused";
    case 518: return "Border cut";
    case 519: return "Idle in lawn";
    case 523: return "Spot Mow";
    case 524: return "Mow without Docking Station";
    case 769: return "Returning to Dock";
    case 770: return "Returning to Dock";
    case 771: return "Returning to Dock - Battery low";
    case 772: return "Returning to dock - Calendar timeslot ended";
    case 773: return "Returning to dock - Battery temp range";
    case 774: return "Returning to dock";
    case 775: return "Returning to dock - Lawn complete";
    case 776: return "Returning to dock - Relocalising";
    case 1005: return "Mowing";
    case 1025: return "Diagnostic mode";
    case 1026: return "End of life";
    case 1027: return "Service Requesting Status";
    case 1038: return "Mower immobilized";
    case 1281: return "Software update";
    case 1537: return "Stuck";
    case 64513: return "Sleeping (Docked)";
    case 99999: return "Offline";
    default: throw new Error("Unknown state" + body.state);
}

Get Battery Percentage

Query the operating data and return only the battery percentage value, this can easily be modified to return other values instead:

// pull values from the flow editor
const contextId = args[0];
const serialNumber = args[1];

// get operating data
const result = await fetch('https://api.indego.iot.bosch-si.com/api/v1/alms/' + serialNumber + '/operatingData', {
    method: 'GET',
    headers: { 'x-im-context-id': contextId }
});

if (!result.ok) {
    throw new Error(result.statusText);
}

const body = await result.json();

//return battery percentage
return body.battery.percent;

Control the Mower

With code in place to authenticate with the API and retrieve information about the mower it is quite straightforward to control the mower. We just need to make a PUT request to the /state endpoint with the desired state command:

PUT https://api.indego.iot.bosch-si.com/api/v1/alms/{serial number}/state
x-im-context-id: 3f2a9e8c-93cb-402e-a200-e325859f3ffe
content-type: application/json

{
  "state": "mow"
}

We can issue a mow command to start the mower, and a returnToDock command to stop it and have it go back to the dock.

In HomeyScript this can be done like so:

// pull values from the flow editor
const contextId = args[0];
const serialNumber = args[1];

// send the request to the api
const body = {
    state: "mow"
};

const result = await fetch('https://api.indego.iot.bosch-si.com/api/v1/alms/' + serialNumber + '/state', {
    method: 'PUT',
    body: JSON.stringify(body),
    headers: {
        'x-im-context-id': contextId,
        'Content-Type': 'application/json'
    }
});

if (!result.ok) {
    throw new Error(result.statusText);
}

// this doesn't return a body, so as long as it didn't fail it should be good

Putting it together

The code snippets in this post can be added to HomeyScript cards in the flow editor, and you can link up your robot mower to anything else controlled by Homey.

The screenshot below shows examples of controlling the mower with a virtual device, and monitoring the battery percentage with a virtual sensor.

A screenshot of the Homey flow editor showing some ways of integrating the code from this post

Happy Automating!