Skip to content

Instantly share code, notes, and snippets.

@khskekec
Last active July 21, 2024 02:11
Show Gist options
  • Save khskekec/6c13ba01b10d3018d816706a32ae8ab2 to your computer and use it in GitHub Desktop.
Save khskekec/6c13ba01b10d3018d816706a32ae8ab2 to your computer and use it in GitHub Desktop.
HTTP dump of Libre Link Up used in combination with FreeStyle Libre 3
@jamescowie
Copy link

When using the login API I constantly get:

{"message":"Method Not Allowed"}

Any ideas on why?

@voljumet
Copy link

Hi, I only seem to be able to access a small amount of historical data. Is there a way to get all historical data going back to the first reading (e.g. a few years ago)?

I'm also looking for this

@Burak50
Copy link

Burak50 commented Jan 22, 2024

When using the login API I constantly get:

{"message":"Method Not Allowed"}

Any ideas on why?

I receive this message only when my link is structured like this: https://apiUrl/connections/{{patientId}}/

The correct version should be: https://apiUrl/connections/{{patientId}}/graph

edit: You wrote that you get it during login; I overlooked it. What does your link look like?

@ATouhou
Copy link

ATouhou commented Jan 25, 2024

Works like a charm! Thank you very much!

@BrandonGoodman
Copy link

This is great information. Thank you for sharing, @khskekec

Has anyone determined the possible values from the data.connection.glucoseMeasurement.TrendArrow field?
mostly just see 3 in my limited testing which seems to be ➡. My guess would be something like this:

  • 1 = ⬆
  • 2 = ↗️
  • 3 = ➡
  • 4 =↘️
  • 5 = ⬇️

Can anyone confirm? I could have it backward for all I know right now.

I also wonder what to expect to see populated in data.connection.glucoseMeasurement.TrendMessage. All I get there is null.

@maplerock
Copy link

I've got the below which I think is correct.

$trendArrow = match($freeStyleJson['data']['connection']['glucoseMeasurement']['TrendArrow']) {
            1 => '⬇️',
            2 => '↘️',
            3 => '➡️',
            4 => '↗️',
            5 => '⬆️',
        };

@BrandonGoodman
Copy link

so I did have them reversed. Thank you both for the corrections

@Burak50
Copy link

Burak50 commented Jan 28, 2024

FYI: for future users: https://api-eu.libreview.io does not work, for me anyway, the solution was https://api-de.....

The only question is still whether this is only the case in Germany or also in other European countries

My guess is that only the countries France, Germany, Italy, Netherlands, Spain, United Kingdom and United States of America work (because that's the countries listed on freestylelibre.com in the language selection)

wCsowDI 2

eu:
gq8gBh9 1

de:
G2hwAgy 1

@ATouhou
Copy link

ATouhou commented Jan 28, 2024

Denmark works with https://api-eu.libreview.io .
I can confirm the trendarrows :)

@ATouhou
Copy link

ATouhou commented Jan 28, 2024

LibreLinkUp is written in NativeScript. After beautifying bundle.js you can easily find out the enum definition:

function(e) {
    e[e.NotDetermined = 0] = "NotDetermined", e[e.FallingQuickly = 1] = "FallingQuickly", e[e.Falling = 2] = "Falling", e[e.Stable = 3] = "Stable", e[e.Rising = 4] = "Rising", e[e.RisingQuickly = 5] = "RisingQuickly"
}(a || (a = {}))

@gui-dos Where did you locate the librelinkup source file build.js? Could you provide instruction on where to find it and also share the file? :) Thank you

@mrniceboot
Copy link

I think you can do it something like this, however I'm certainly not an expert.
adb shell pm list packages #to figure out the package name
adb shell pm path [package name] #to list the apk paths
adb shell cp [path] /storage/emulated/0/Download #to copy the apk to the download folder
adb pull /storage/emulated/0/Download/[apk name] #to pull the apk to the local machine

Use something like jadx to open up the downloaded APK.
Find the bundle.js in Resources/assets/app/bundle.js
Then beautify the bundle.js with any reasonable tool (perhaps beautify.io) and go looking for whatever you want to find.

This is probably not the best way to do it, but it does work...

@OnizukaJS
Copy link

OnizukaJS commented Feb 6, 2024

Hi everyone,
I'm trying to retrieve data on my react/ts app but facing CORS issue.

The https://api-eu.libreview.io/llu/auth/login call is failing and I can see different errors on the console:

  • Refused to set unsafe header "accept-encoding"

  • Refused to set unsafe header "connection"

  • Access to XMLHttpRequest at 'https://api-eu.libreview.io/llu/auth/login' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

I followed this example:
https://github.com/DiaKEM/libre-link-up-api-client

import axios from 'axios'
import { LibreCgmData } from './types/client'
import { ActiveSensor, Connection, GlucoseItem } from './types/connection'
import { ConnectionsResponse, Datum } from './types/connections'
import { LoginRedirectResponse, LoginResponse } from './types/login'
import { AE, CountryResponse, RegionalMap } from './types/countries'
import { GraphData } from './types/graph'
import { mapData, trendMap } from './utils'

const LIBRE_LINK_SERVER = 'https://api-eu.libreview.io'

type ClientArgs = {
  username: string
  password: string
  connectionIdentifier?: string | ((connections: Datum[]) => string)
}

type ReadRawResponse = {
  connection: Connection
  activeSensors: ActiveSensor[]
  graphData: GlucoseItem[]
}

type ReadResponse = {
  current: LibreCgmData
  history: LibreCgmData[]
}

const urlMap = {
  login: '/llu/auth/login',
  connections: '/llu/connections',
  countries: '/llu/config/country?country=DE',
}
 
export const LibreLinkUpClient = ({
  username,
  password,
  connectionIdentifier
}: ClientArgs) => {
  let jwtToken: string | null = null
  let connectionId: string | null = null

  const instance = axios.create({
    baseURL: LIBRE_LINK_SERVER,
    headers: {
      'accept-encoding': 'gzip',
      'cache-control': 'no-cache',
      connection: 'Keep-Alive',
      'content-type': 'application/json',
      product: 'llu.android',
      version: '4.7.0',
    }
  })
  instance.interceptors.request.use(
    config => {
      if (jwtToken && config.headers) {
        // eslint-disable-next-line no-param-reassign
        config.headers.authorization = `Bearer ${jwtToken}`
      }

      return config
    },
    e => e,
    { synchronous: true }
  )

  const login = async (): Promise<LoginResponse> => {
    console.log("HELLO")
    const loginResponse = await instance.post<LoginResponse | LoginRedirectResponse>(urlMap.login, {
      email: username,
      password
    })
    console.log("response", loginResponse)

    if (loginResponse.data.status === 2) throw new Error('Bad credentials.')

    if ((loginResponse.data as LoginRedirectResponse).data.redirect) {
      const redirectResponse = loginResponse.data as LoginRedirectResponse
      const countryNodes = await instance.get<CountryResponse>(
        urlMap.countries
      )
      const targetRegion = redirectResponse.data.region as keyof RegionalMap
      const regionDefinition: AE | undefined = countryNodes.data.data.regionalMap[targetRegion]

      if (!regionDefinition) {
        throw new Error(
          `Unabkle to find region '${redirectResponse.data.region}'. Available nodes are ${Object.keys(countryNodes.data.data.regionalMap).join(', ')}`
        )
      }

      instance.defaults.baseURL = regionDefinition.lslApi
      return login()
    }
    jwtToken = (loginResponse.data as LoginResponse).data.authTicket.token

    return loginResponse.data as LoginResponse
  }

  const loginWrapper =
    <Return>(func: () => Promise<Return>) =>
    async (): Promise<Return> => {
      try {
        if (!jwtToken) await login()
        return func()
      } catch (e) {
        await login()
        return func()
      }
    }

  const getConnections = loginWrapper<ConnectionsResponse>(async () => {
    const response = await instance.get<ConnectionsResponse>(
      urlMap.connections
    )

    return response.data
  })

  const getConnection = (connections: Datum[]): string => {
    if (typeof connectionIdentifier === 'string') {
      const match = connections.find(
        ({ firstName, lastName }) =>
          `${firstName} ${lastName}`.toLowerCase() ===
          connectionIdentifier.toLowerCase()
      )

      if (!match) {
        throw new Error(
          `Unable to identify connection by given name '${connectionIdentifier}'.`
        )
      }

      return match.patientId
    }
    if (typeof connectionIdentifier === 'function') {
      const match = connectionIdentifier.call(null, connections)

      if (!match) {
        throw new Error(`Unable to identify connection by given name function`)
      }

      return match
    }

    return connections[0].patientId
  }

  const readRaw = loginWrapper<ReadRawResponse>(async () => {
    if (!connectionId) {
      const connections = await getConnections()

      connectionId = getConnection(connections.data)
    }

    const response = await instance.get<GraphData>(
      `${urlMap.connections}/${connectionId}/graph`
    )

    return response.data.data
  })

  const read = async (): Promise<ReadResponse> => {
    const response = await readRaw()

    return {
      current: mapData(response.connection.glucoseMeasurement),
      history: response.graphData.map(mapData),
    }
  }

  const observe = async () => {
    // @todo
  }

  let averageInterval: NodeJS.Timeout

  const readAveraged = async (
    amount: number,
    callback: (
      average: LibreCgmData,
      memory: LibreCgmData[],
      history: LibreCgmData[]
    ) => void,
    interval = 15000
  ) => {
    let mem: Map<string, LibreCgmData> = new Map()

    averageInterval = setInterval(async () => {
      const { current, history } = await read()
      mem.set(current.date.toString(), current)

      if (mem.size === amount) {
        const memValues = Array.from(mem.values())
        const averageValue = Math.round(
          memValues.reduce((acc, cur) => acc + cur.value, 0) / amount
        )
        const averageTrend =
          trendMap[
            parseInt(
              (
                Math.round(
                  (memValues.reduce(
                    (acc, cur) => acc + trendMap.indexOf(cur.trend),
                    0
                  ) /
                    amount) *
                    100
                ) / 100
              ).toFixed(0),
              10
            )
          ]

        mem = new Map()
        callback.apply(null, [
          {
            trend: averageTrend,
            value: averageValue,
            date: current.date,
            isHigh: current.isHigh,
            isLow: current.isLow,
          },
          memValues,
          history,
        ])
      }
    }, interval)

    return () => clearInterval(averageInterval)
  }

  return {
    observe,
    readRaw,
    read,
    readAveraged,
    login,
  }
}

Then, on my App.tsx I'm just calling the function like this:

(async function () {
    const username = import.meta.env.LIBRE_LINK_UP_USER_NAME
    const password = import.meta.env.LIBRE_LINK_UP_USER_PASSWORD
    const libreClient = LibreLinkUpClient({
      username,
      password,
      connectionIdentifier: 'IDENTIFIER'
    })
    const data = await libreClient.read()
    console.log("data", data)
  })()

@ATouhou
Copy link

ATouhou commented Feb 6, 2024

CORS error is local browser error. You can download an extension on your browser to disable CORS protection error. When you upload to remote server or host on a server the error should disappear

@faysal-ali
Copy link

Hi, I only seem to be able to access a small amount of historical data. Is there a way to get all historical data going back to the first reading (e.g. a few years ago)?

Any luck with this?

@ATouhou
Copy link

ATouhou commented Feb 10, 2024

@faysal-ali
I was able to access more data than described in this Gist, using the "/logbook" endpoint. Same headers and setup as "/graph" url.
The data available from that is the same as is shown in the app under "log book / Logbog (danish)".
My daughter is newly diagnosed and her data only goes back to 27/1, so I do not know if the data extends further than weeks/months.
Let me know if I can be of help :)

@ATouhou
Copy link

ATouhou commented Feb 10, 2024

@mrniceboot I was looking at the wrong APK file. Manually selecting the right one, worked wonders :-D !
I've uploaded it here: https://gist.github.com/ATouhou/deaf79038a6e2083b73be41c413e392f

@Burak50
Copy link

Burak50 commented Feb 11, 2024

@faysal-ali @ATouhou @voljumet @deelo55

I took a closer look at this, in general only 90* are possible backwards.
In addition to the actual header, two parameters must be passed in each case

"numPeriods": the return is divided into several blocks to make it clearer (default: 5)
"period" The days that are to be returned (default: 90)

*I only took the 90 days from libreview.com, but more is also possible, at least the API returns more than 90 days, in my case it is more than 400 days (if I set my period to 100)

API URL: https://api.libreview.io/glucoseHistory?numPeriods=5&period=90

@Burak50
Copy link

Burak50 commented Feb 11, 2024

FYI: for future users: https://api-eu.libreview.io does not work, for me anyway, the solution was https://api-de.....

The only question is still whether this is only the case in Germany or also in other European countries

My guess is that only the countries France, Germany, Italy, Netherlands, Spain, United Kingdom and United States of America work (because that's the countries listed on freestylelibre.com in the language selection)

wCsowDI 2

eu: gq8gBh9 1

de: G2hwAgy 1

I have also been able to solve this, the assumption that only specific lengths are available is taboo.
You can see: Countrie List

@ATouhou
Copy link

ATouhou commented Feb 12, 2024

That Seems to return data that is available on libreview and not librelinkup. So in my case, unless I use the patients login id / token, it will return no data . ( {
"status": 0,
"data": {
"lastUpload": 0,
"lastUploadCGM": 0,
"lastUploadPro": 0,
"reminderSent": 0,
"devices": [],
"periods": []
}, ) .

However, I will save it in my PostMan collection. Might come in handy in the future. Thanks @Burak50

@bruderjakob12
Copy link

This seems to be the most active thread concerning the LLU API, so I'd like to share some things I've noticed:

Regions

Get the region right

You don't have to get the region for log-in right the first time. If the credentials are correct, the endpoint e.g., https://api-la.libreview.io/llu/auth/login will reply with

{"status":0,"data":{"redirect":true,"region":"de"}}

So you know that the right endpoint for these credentials is https://api-de.libreview.io/llu/auth/login.

New Region!

Since a few weeks there is a new la region (Brazil, Mexico, ??) and it seems that users have been migrated. Tokens are still valid, you just get the the FollowerNotConnectedToPatient message. For a while the status-code 911 was sent for those requests, but that subsided.

LLU 4.11 - new Header-Item Account-Id

With LLU version 4.11 a new header is required - not an issue right now, as you can set the version to 4.0 and even lower, but it might become relevant. The header-key is Account-Id and the value is the SHA256-hash of the LLU-account-id.
r is the request-object from the login.

HEADER["Account-Id"] = hashlib.sha256(r.json()['data']['user']['id'].encode()).hexdigest()

@ATouhou
Copy link

ATouhou commented Jul 8, 2024

Thanks for sharing! ^^

@m-mansoor-ali
Copy link

P.S This was in response to a comment that has been deleted

If you have no patient ID returned from the get_patient_connections(token) function, then the first possibilities that jump to my mind are:

  1. You aren't using a LibreLinkUp account, this is a different from the FreeStyle LibreLink account. The linkup account is only used to view the freestyle account.

    • The fix, download the LibreLinkUp app, sign up an account (with different email from FreeStyle LibreLink account) and from the FreeStyle LibreLink account invite the LibreLinkUp account. Then retry with the python script using LibreLinkUp account details.
  2. You are using a LibreLinkUp account, but you haven't been connected to anyone's FreeStyle LibreLink account to be able to view them.

    • The fix, get whoever you would like to view to connect your LibreLinkUp account email from their FreeStyle LibreLink app, under Connections in the sidebar (if I recall).

The patient ID should be in the format of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx a mix of numbers and lower case letters.

Just checked the endpoint using https://github.com/smpurkis/libre-api-pg/tree/old and it ran successfully for me.

I used same email id in LibreLinkUp and FreeStyle LibreLink account and it worked for me i.e. I invited same account.

@dakyskye
Copy link

Hi, beautiful dump! So as I understand LibreLinkUp application makes those HTTP calls every second or what? It's able to update display with fetched data in real-time so I was expecting some kind of continuous streaming.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment