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