/ scripts / libre.sh
libre.sh
  1  #!/usr/bin/env bash
  2  
  3  # TmuxLibre: Monitor Your Blood Glucose Levels in Tmux
  4  ################################################################################
  5  #
  6  # This script retrieves and displays continuous glucose monitoring (CGM) data
  7  # from the LibreView API. It caches the data to minimize API requests and
  8  # displays the latest glucose level along with a trend indicator in a Tmux
  9  # status bar.
 10  #
 11  # Features:
 12  # - Fetches glucose data from the LibreView API.
 13  # - Caches data to reduce the number of API requests.
 14  # - Displays glucose level and trend in a Tmux status bar.
 15  # - Rotates log files to manage log size.
 16  #
 17  # How It Works:
 18  # 1. The script loads configuration settings from ~/.tmuxlibre.conf.
 19  # 2. If necessary, it creates a default configuration file and exits.
 20  # 3. The script checks the cache for recent data.
 21  # 4. If the cache is outdated, it logs into the LibreView API and fetches new data.
 22  # 5. The script parses the data and determines the glucose trend.
 23  # 6. Finally, it outputs the glucose level and trend for display in Tmux.
 24  #
 25  # Configuration:
 26  # - Create or edit ~/.tmuxlibre.conf with the following settings:
 27  #   LIBREVIEW_API_URL="https://api-de.libreview.io"
 28  #   CACHE_FILE="/tmp/tmuxlibre_cache.json"
 29  #   CACHE_DURATION=180
 30  #   LOG_FILE="/tmp/tmuxlibre.log"
 31  #   LOG_MAX_SIZE=2000000
 32  #   LIBREVIEW_EMAIL="your_email"
 33  #   LIBREVIEW_PASSWORD="your_password"
 34  #
 35  # Dependencies:
 36  # - curl
 37  # - jq
 38  # - bc
 39  # - gzip
 40  ################################################################################
 41  
 42  # Setting the locale, some users have issues with different locales, this forces the correct one
 43  export LC_ALL=en_US.UTF-8
 44  
 45  # Load utils
 46  current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 47  source $current_dir/utils.sh
 48  
 49  # Constants (Default values, can be overridden by config)
 50  BASE_URL="${LIBREVIEW_API_URL:-https://api-de.libreview.io}"
 51  CACHE_FILE="/tmp/tmuxlibre_cache.json"
 52  CACHE_DURATION=180  # Cache duration in seconds (3 minutes)
 53  LOG_FILE="/tmp/tmuxlibre.log"
 54  LOG_MAX_SIZE=2000000 # 2MB
 55  CONFIG_FILE="$HOME/.tmuxlibre.conf"
 56  TOKEN_FILE="/tmp/tmuxlibre_token.json" # File to store the token and its expiry
 57  
 58  HEADERS=(
 59      -H "accept-encoding: gzip"
 60      -H "cache-control: no-cache"
 61      -H "connection: Keep-Alive"
 62      -H "content-type: application/json"
 63      -H "product: llu.android"
 64      -H "version: 4.7"
 65  )
 66  
 67  # Function to log messages to the log file and rotate the log if needed
 68  # Input: Log message as a string
 69  log() {
 70      echo "$(date +'%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
 71      rotate_log
 72  }
 73  
 74  # Function to get the file size in bytes
 75  # Input: File path as a string
 76  # Output: File size in bytes as a string
 77  get_file_size()
 78  {
 79      case $(uname -s) in
 80          Linux)
 81              du -b "$1" | cut -f1
 82              ;;
 83          Darwin|FreeBSD)
 84              du -k "$1" | cut -f1
 85              ;;
 86          *)
 87              log "Unsupported OS: $(uname -s)"
 88              echo 0
 89              ;;
 90      esac
 91  }
 92  
 93  # Function to rotate the log file if it exceeds the maximum size
 94  # No input, no output
 95  rotate_log() {
 96      if [ -f "$LOG_FILE" ]; then
 97          log_size=$(get_file_size "$LOG_FILE")
 98          if [ "$log_size" -gt $LOG_MAX_SIZE ]; then
 99              mv "$LOG_FILE" "$LOG_FILE.1"
100              gzip "$LOG_FILE.1"
101          fi
102      fi
103  }
104  
105  # Function to load configuration settings from the config file
106  # Creates a default config file if it doesn't exist
107  # No input, no output
108  load_config() {
109      if [ ! -f "$CONFIG_FILE" ]; then
110          cat <<EOL > "$CONFIG_FILE"
111  # Configuration file for tmuxlibre
112  # Uncomment and set the following variables as needed:
113  
114  # LIBREVIEW_API_URL="https://api-de.libreview.io"
115  # CACHE_FILE="/tmp/tmuxlibre_cache.json"
116  # CACHE_DURATION=180
117  # LOG_FILE="/tmp/tmuxlibre.log"
118  # LOG_MAX_SIZE=2000000
119  # LIBREVIEW_EMAIL=""
120  # LIBREVIEW_PASSWORD=""
121  EOL
122          log "Configuration file created: $CONFIG_FILE"
123          echo "🦋 CONFIG FILE CREATED, PLEASE CONFIGURE AND RE-RUN ❌"
124          exit 1
125      fi
126  
127      source "$CONFIG_FILE"
128  
129      : "${LIBREVIEW_API_URL:=$BASE_URL}"
130      : "${CACHE_FILE:=$CACHE_FILE}"
131      : "${CACHE_DURATION:=$CACHE_DURATION}"
132      : "${LOG_FILE:=$LOG_FILE}"
133      : "${LOG_MAX_SIZE:=$LOG_MAX_SIZE}"
134  
135      if [ -z "$LIBREVIEW_EMAIL" ] || [ -z "$LIBREVIEW_PASSWORD" ]; then
136          if grep -q '^[^#]' "$CONFIG_FILE"; then
137              log "Email and password must be set in the configuration file"
138              echo "🦋 CHECK CONFIG ❌"
139          fi
140          exit 1
141      fi
142  }
143  
144  # Function to log in to the LibreView API and retrieve an authentication token
145  # This function also checks if a cached token is available and still valid
146  # Input: Email and password as strings
147  # Output: Authentication token as a string
148  login() {
149      local email=$1
150      local password=$2
151  
152      # Check for a cached token
153      if [ -f "$TOKEN_FILE" ]; then
154          token_data=$(cat "$TOKEN_FILE")
155          token=$(echo "$token_data" | jq -r '.token')
156          expires=$(echo "$token_data" | jq -r '.expires')
157  
158          current_time=$(date +%s)
159          if (( current_time < expires )); then
160              echo "$token"
161              return
162          fi
163      fi
164  
165      # No valid cached token, perform login
166      local endpoint="/llu/auth/login"
167      local payload="{\"email\": \"$email\", \"password\": \"$password\"}"
168  
169      response=$(curl -s -X POST "$LIBREVIEW_API_URL$endpoint" "${HEADERS[@]}" -d "$payload" --compressed)
170      if [ $? -ne 0 ]; then
171          log "HTTP error occurred during login"
172          log "Response: $response"
173          echo "🦋 NO DATA ❌"
174          exit 1
175      fi
176  
177      log "Login response: $response"
178  
179      response=$(echo "$response" | tr -d '\0' | tr -cd '\11\12\15\40-\176')
180      token=$(echo "$response" | jq -r '.data.authTicket.token' 2>/dev/null)
181      expires=$(echo "$response" | jq -r '.data.authTicket.expires' 2>/dev/null)
182      if [ -z "$token" ] || [ "$token" == "null" ]; then
183          log "Failed to retrieve token from login response"
184          echo "🦋 NO DATA ❌"
185          exit 1
186      fi
187  
188      echo "{\"token\": \"$token\", \"expires\": $expires}" > "$TOKEN_FILE"
189  
190      echo "$token"
191  }
192  
193  # Function to get patient connections from the LibreView API
194  # Input: Authentication token as a string
195  # Output: Patient connections data as a JSON string
196  get_patient_connections() {
197      local token=$1
198      local endpoint="/llu/connections"
199      local auth_header="Authorization: Bearer $token"
200  
201      response=$(curl -s -X GET "$LIBREVIEW_API_URL$endpoint" "${HEADERS[@]}" -H "$auth_header" --compressed)
202      if [ $? -ne 0 ]; then
203          log "HTTP error occurred during fetching patient connections"
204          log "Response: $response"
205          echo "🦋 NO DATA ❌"
206          exit 1
207      fi
208  
209      log "Patient connections response: $response"
210  
211      data=$(echo "$response" | jq -c '.data' 2>/dev/null)
212      if [ -z "$data" ] || [ "$data" == "null" ]; then
213          log "No patient connections found"
214          echo "🦋 NO DATA ❌"
215          exit 1
216      fi
217  
218      echo "$data"
219  }
220  
221  # Function to get CGM data for a patient from the LibreView API
222  # Input: Authentication token and patient ID as strings
223  # Output: CGM data as a JSON string
224  get_cgm_data() {
225      local token=$1
226      local patient_id=$2
227      local endpoint="/llu/connections/$patient_id/graph"
228      local auth_header="Authorization: Bearer $token"
229  
230      response=$(curl -s -X GET "$LIBREVIEW_API_URL$endpoint" "${HEADERS[@]}" -H "$auth_header" --compressed)
231      if [ $? -ne 0 ]; then
232          log "HTTP error occurred during fetching CGM data"
233          log "Response: $response"
234          echo "🦋 NO DATA ❌"
235          exit 1
236      fi
237  
238      log "CGM data response: $response"
239  
240      echo "$response"
241  }
242  
243  # Function to get the emoji representing a trend based on a given input number
244  # Input: A number between 1 and 5 representing the trend
245  #        1 - SingleDown
246  #        2 - FortyFiveDown
247  #        3 - Flat
248  #        4 - FortyFiveUp
249  #        5 - SingleUp
250  # Output: The corresponding emoji for the trend
251  get_trend() {
252      case $1 in
253          1)
254              echo "⬇️" # SingleDown
255              ;;
256          2)
257              echo "↘️" # FortyFiveDown
258              ;;
259          3)
260              echo "➡️" # Flat
261              ;;
262          4)
263              echo "↗️" # FortyFiveUp
264              ;;
265          5)
266              echo "⬆️" # SingleUp
267              ;;
268          *)
269              echo "🤡"
270              ;;
271      esac
272  }
273  
274  # Function to cache CGM data to a file
275  # Input: CGM data as a JSON string
276  cache_data() {
277      local data=$1
278      echo "{\"timestamp\": $(date +%s), \"data\": $data}" > "$CACHE_FILE"
279  }
280  
281  # Function to load cached CGM data from a file
282  # Output: Cached CGM data as a JSON string, or an empty JSON object if the cache is outdated
283  load_cached_data() {
284      if [ ! -f "$CACHE_FILE" ]; then
285          echo "{}"
286          return
287      fi
288  
289      cached=$(cat "$CACHE_FILE")
290      cached_timestamp=$(echo "$cached" | jq -r '.timestamp' | awk '{printf "%d\n", $1}')
291      current_timestamp=$(date +%s)
292  
293      if (( current_timestamp - cached_timestamp > CACHE_DURATION )); then
294          echo "{}"
295          return
296      fi
297  
298      echo "$cached" | jq -c '.data'
299  }
300  
301  # Function to get CGM data, either from the cache or by fetching it from the API
302  # No input, outputs the glucose level and trend for display in Tmux
303  get_data() {
304      cached_data=$(load_cached_data)
305      if [ "$cached_data" != "{}" ]; then
306          cgm_data="$cached_data"
307      else
308          token=$(login "$LIBREVIEW_EMAIL" "$LIBREVIEW_PASSWORD")
309          if [ $? -ne 0 ]; then
310              exit 1
311          fi
312  
313          patient_data=$(get_patient_connections "$token")
314          if [ $? -ne 0 ]; then
315              exit 1
316          fi
317  
318          patient_id=$(echo "$patient_data" | jq -r '.[0].patientId' 2>/dev/null)
319          if [ -z "$patient_id" ];then
320              log "Patient ID not found in patient connections"
321              echo "🦋 NO DATA ❌"
322              exit 1
323          fi
324  
325          cgm_data=$(get_cgm_data "$token" "$patient_id")
326          if [ $? -ne 0 ]; then
327              exit 1
328          fi
329  
330          cache_data "$cgm_data"
331      fi
332  
333      value=$(echo "$cgm_data" | jq -r '.data.connection.glucoseMeasurement.Value' 2>/dev/null)
334      trendarrow=$(echo "$cgm_data" | jq -r '.data.connection.glucoseMeasurement.TrendArrow' 2>/dev/null)
335      trend=$(get_trend "$trendarrow")
336  
337      echo "🦋 $value $trend"
338  }
339  
340  # Load config and execute the main logic
341  load_config
342  get_data