diff --git a/THE FOLDER NAME SHOULD BE mgconky NOT mgconky-master.txt b/THE FOLDER NAME SHOULD BE mgconky NOT mgconky-master.txt index 98d3021..6a98f07 100644 --- a/THE FOLDER NAME SHOULD BE mgconky NOT mgconky-master.txt +++ b/THE FOLDER NAME SHOULD BE mgconky NOT mgconky-master.txt @@ -4,4 +4,4 @@ This should be the folder: ~/.conky/mgconky/ -Thank you! +Thank you. diff --git a/conf b/conf index a214e35..a5417d8 100644 --- a/conf +++ b/conf @@ -15,11 +15,10 @@ -- !!!IMPORTANT!!! THE FOLLOWING ARE REQUIREMENTS FOR THIS SCRIPT TO WORK PROPERLY: -- (1) DEPENDENCIES. -- Install the following software, if not already installed: --- "Conky" sudo apt-get install conky-all --- "Jq" sudo apt-get install jq --- "Curl" sudo apt-get install curl --- "Wget" sudo apt-get install wget --- "python3" sudo apt-get install python3 +-- "Conky" sudo apt-get install conky-all (or use software manager) +-- "Jq" sudo apt-get install jq (or use software manager) +-- "Curl" sudo apt-get install curl (or use software manager) +-- "Wget" sudo apt-get install wget (or use software manager) -- (2) FONTS. -- Install the following custom fonts: -- "Neuropolitical" Place .ttf file in ~/.fonts/ https://www.dafont.com/font-comment.php?file=neuropolitical @@ -29,9 +28,8 @@ -- chmod +x ~/.conky/mgconky/weather/get_weather.sh -- chmod +x ~/.conky/mgconky/weather/parse_weather.sh -- chmod +x ~/.conky/mgconky/weather/parse_forecast.sh --- chmod +x ~/.conky/mgconky/stocks/get_stocks_alphavantage.py --- chmod +x ~/.conky/mgconky/stocks/process_stocks_alphavantage.py --- chmod +x ~/.conky/mgconky/stocks/get_stocks_finnhub.py +-- chmod +x ~/.conky/mgconky/weather/parse_forecast.sh +-- chmod +x ~/.conky/mgconky/stocks/get_stocks.py (Requires python3, which is usually pre-installed on your OS) -- (4) WEATHER. -- Make a free account at https://openweathermap.org/ -- Write down your API key, which is found on the "API keys" tab after you log in. (https://home.openweathermap.org/api_key @@ -93,16 +91,16 @@ conky.config = { color7 = "#FF0000", -- Bad values (red) -- Weather variables - template0 = "", -- OpenWeatherMap API key (https://home.openweathermap.org/api_keys) - template1 = "4467657", -- OpenWeatherMap City ID (the number in the URL of your city, for example: https://openweathermap.org/city/5128581) + template0 = "YOUR_OPENWEATHERMAP_API_KEY_HERE", -- OpenWeatherMap API key (https://home.openweathermap.org/api_keys) + template1 = "YOUR_OPENWEATHERMAP_CITY_ID_HERE", -- OpenWeatherMap City ID (the number in the URL of your city, for example: https://openweathermap.org/city/5128581) template2 = "imperial", -- Temp unit ("default" for Kelvin, "metric" for Celsius, "imperial" for Fahrenheit) template3 = "", -- Locale (e.g., "es_ES.UTF-8") # Leave empty for default -- Stock variables - template4 = "", -- FinnHub API key (https://finnhub.io/) - template5 = " 0 then output = output .. '${voffset 0}${color0}${font Neuropolitical:size=8:bold}DRIVE${font Courier:size=9} /dev/' .. drive .. '${color} ${color1}${hr 2}${color}\n' for _, mount in ipairs(data.mountpoints) do - - -- Skip entries that are not real mountpoints like [SWAP] - if mount:sub(1, 1) ~= "/" then - goto continue - end - - -- Skip common temporary mounts - if mount:match("^/sys/") then goto continue end - if mount:match("^/proc/") then goto continue end - if mount:match("^/dev/") then goto continue end - - -- Skip mounts under /var/, but allow a mount at /var itself - if mount ~= "/var" and mount:match("^/var/") then goto continue end - - -- Skip runtime mounts except removable media - if mount:match("^/run/") and not mount:match("^/run/media/") then goto continue end - -- Use df to get the size and used space for the mount local handle = io.popen("df -h --output=target,size,used " .. mount .. " | tail -n 1") local df_result = handle:read("*a") @@ -195,19 +173,10 @@ function conky_get_drives_and_volumes() -- Parse df output local target, size, used = df_result:match("(%S+)%s+(%S+)%s+(%S+)") - - -- Ensure mount still exists before emitting fs_bar - local test = io.open(target, "r") - if not test then goto continue end - test:close() - - -- Create output lines for this mountpoint. if target and size and used then output = output .. '${voffset 2}' .. target .. ': ${alignr}${color3}' .. used .. 'B${color} of ${color3}' .. size .. 'B${color}\n' output = output .. '${voffset -2}${color5}${fs_bar ' .. target .. '}${color}\n' end - - ::continue:: end output = output .. '\n' -- Add a blank line after the last mount point of the current drive end @@ -387,64 +356,3 @@ print(" CHECK SWAP STATUS = " .. conky_check_swap_status()) print(" GET MEMORY USAGE = " .. conky_get_memory_usage("mem")) print(" GET SWAP USAGE = " .. conky_get_memory_usage("swap")) - --- ####################################################### GET ACTIVE CONNECTIONS ############################################################## - -function conky_get_key_with_highest_value(t) - local max_key = nil - local max_value = -math.huge -- Initialize with the smallest possible number - - for key, value in pairs(t) do - if type(value) == "number" and value > max_value then - max_value = value - max_key = key - end - end - - return max_key -end - -function conky_get_connections() - local handle = io.popen("ss -eptH | grep ESTAB 2>/dev/null") - if not handle then return "Error: Unable to execute ss command" end - - local process_names = {} - local process_counts = {} - - -- Read each line of output - for line in handle:lines() do - -- Extract process name and PID using pattern matching - local pname, pid = line:match('users:%(%("([^"]+)",pid=(%d+)') - pid = tonumber(pid) -- Ensure pid is a number - if pname and pid then - -- Store process name by PID and count occurrences - process_names[pid] = pname - process_counts[pid] = (process_counts[pid] or 0) + 1 - end - end - handle:close() - - -- Header line - local tab1_offset = "${goto 145}" - local line_prefix = "${voffset 0}${color}${font StyleBats:size=10}h${font Courier:size=9}${voffset -1}${color3}" - local result = string.format("${color}Process%sPID${alignr}Num \n", tab1_offset) - - -- Sort and format output - while true do - local max_key = conky_get_key_with_highest_value(process_counts) - - if max_key == nil then - break -- Stop when the table is empty - end - - result = result .. string.format("%s %s%s%d${alignr}%d \n", line_prefix, string.sub(process_names[max_key], 1, 16), tab1_offset, max_key, process_counts[max_key]) - - -- Remove the highest key from both tables - process_counts[max_key] = nil - process_names[max_key] = nil - end - - return result -end - - diff --git a/stocks/get_stocks_alphavantage.py b/stocks/get_stocks_alphavantage.py index cb255e8..af2fb49 100755 --- a/stocks/get_stocks_alphavantage.py +++ b/stocks/get_stocks_alphavantage.py @@ -1,31 +1,11 @@ #!/usr/bin/env python3 -import os -import sys -import time -import json -import argparse import requests from datetime import datetime, timedelta - -# Directory where per-symbol cache JSON files are stored -CACHE_DIR = os.path.expanduser("~/.cache/mgconky/stocks_alphavantage/") - -# Delay between API calls to respect AlphaVantage rate limits -API_DELAY_SECONDS = 1 - -# Minimum age before re-querying a symbol to avoid API thrashing -NO_THRASH_SECONDS = 60 * 60 # 1 hour - - -def log_error(message): - """Print timestamped error message to stderr and flush immediately.""" - ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - print(f"[{ts}] {message}", file=sys.stderr, flush=True) - +import argparse def fetch_intraday_data(api_key, symbol, interval="1min"): - # Query AlphaVantage intraday endpoint for the given symbol + """Fetch current and historical intraday price using TIME_SERIES_INTRADAY.""" url = "https://www.alphavantage.co/query" params = { "function": "TIME_SERIES_INTRADAY", @@ -39,31 +19,36 @@ def fetch_intraday_data(api_key, symbol, interval="1min"): data = response.json() time_series = data.get(f"Time Series ({interval})") + # Debug: Log the raw response + #print(f"DEBUG: Response data for {symbol}: {data}") + if time_series: - # Use earliest bar as open price and latest bar as current price + # Sort the timestamps to get the first (open) and latest price sorted_timestamps = sorted(time_series.keys()) - open_time = sorted_timestamps[0] - latest_time = sorted_timestamps[-1] + open_time = sorted_timestamps[0] # Earliest timestamp of the day + latest_time = sorted_timestamps[-1] # Most recent timestamp + + # Extract open price and latest price + open_price = float(time_series[open_time]["1. open"]) + latest_price = float(time_series[latest_time]["4. close"]) return { - "current_price": float(time_series[latest_time]["4. close"]), - "compare_price": float(time_series[open_time]["1. open"]), + "current_price": latest_price, + "compare_price": open_price } else: - # API responded but did not include expected time series data - log_error(f"No 'Time Series ({interval})' data found for {symbol}.") + print(f"Error: No 'Time Series ({interval})' data found for {symbol}.") + return None else: - # HTTP error from AlphaVantage - log_error(f"Failed to fetch intraday data for {symbol} (HTTP {response.status_code})") + print(f"Error: Failed to fetch intraday data for {symbol} (HTTP {response.status_code})") + return None except requests.RequestException as e: - # Network or request failure - log_error(f"Failed to fetch intraday data for {symbol} - {e}") - - return None + print(f"Error: Failed to fetch intraday data for {symbol} - {e}") + return None def fetch_historical_data(api_key, symbol, range_in_days): - # Query AlphaVantage daily endpoint for historical comparison + """Fetch current and historical price using TIME_SERIES_DAILY, allowing fallback to nearby dates.""" url = "https://www.alphavantage.co/query" params = { "function": "TIME_SERIES_DAILY", @@ -76,109 +61,96 @@ def fetch_historical_data(api_key, symbol, range_in_days): data = response.json() time_series = data.get("Time Series (Daily)") + # Debug: Log the raw response + #print(f"DEBUG: Response data for {symbol}: {data}") + if time_series: - # Most recent trading day's close is the current price + # Current price is the latest closing price latest_date = max(time_series.keys()) - current_price = float(time_series[latest_date]["4. close"]) + latest_data = time_series[latest_date] + current_price = float(latest_data["4. close"]) - # Target calendar date for comparison - target = datetime.now() - timedelta(days=range_in_days) + # Dates to check: exact, one day before, one day after + target_date = (datetime.now() - timedelta(days=range_in_days)).strftime("%Y-%m-%d") + fallback_dates = [ + target_date, + (datetime.now() - timedelta(days=range_in_days + 1)).strftime("%Y-%m-%d"), + (datetime.now() - timedelta(days=range_in_days - 1)).strftime("%Y-%m-%d") + ] - # Walk backward up to 10 days to find a valid trading day + # Find the first available date in the fallback list historical_price = None - for offset in range(10): - d = (target - timedelta(days=offset)).strftime("%Y-%m-%d") - if d in time_series: - historical_price = float(time_series[d]["4. close"]) + for date in fallback_dates: + if date in time_series: + historical_price = float(time_series[date]["4. close"]) break return { "current_price": current_price, - "compare_price": historical_price, + "compare_price": historical_price } else: - # API responded but did not include expected daily series data - log_error(f"No 'Time Series (Daily)' data found for {symbol}.") + print(f"Error: No 'Time Series (Daily)' data found for {symbol}.") + return None else: - # HTTP error from AlphaVantage - log_error(f"Failed to fetch historical data for {symbol} (HTTP {response.status_code})") - + print(f"Error: Failed to fetch historical data for {symbol} (HTTP {response.status_code})") + return None except requests.RequestException as e: - # Network or request failure - log_error(f"Failed to fetch historical data for {symbol} - {e}") - - return None - + print(f"Error: Failed to fetch historical data for {symbol} - {e}") + return None def main(): # Parse command-line arguments - parser = argparse.ArgumentParser() - parser.add_argument("--api_key", required=True) - parser.add_argument("--symbols", required=True) - parser.add_argument("--range_in_days", type=int, default=0) + parser = argparse.ArgumentParser(description="Fetch stock data from Alpha Vantage.") + parser.add_argument("--api_key", required=True, help="Your Alpha Vantage API key") + parser.add_argument("--symbols", required=True, help="Comma-separated list of stock symbols") + parser.add_argument("--range_in_days", type=int, default=0, help="Number of days for historical comparison (0 for none)") + parser.add_argument("--price_dec_places", type=int, default=0, help="Decimal places for prices") + parser.add_argument("--percent_dec_places", type=int, default=1, help="Decimal places for percentages") args = parser.parse_args() - # Normalize and split ticker symbols + # Split symbols symbols = args.symbols.strip().upper().split(",") - # Ensure cache directory exists - os.makedirs(CACHE_DIR, exist_ok=True) + # Use a list to build the final output string + output = [] - for i, symbol in enumerate(symbols): - cache_path = os.path.join(CACHE_DIR, f"{symbol}.json") + # Prepare for printing output + color_header = "${color}" + color_label = "${color3}" + color_value = "${color3}" + color_good = "${color6}" + color_bad = "${color7}" + line_tab1_offset = "${goto 25}" + line_tab2_offset = "${goto 90}" + line_tab3_offset = "${alignr}" # Could also replace this with goto 120 if you don't like the right alignment - # --- No-thrash protection: skip API call if cache is still fresh --- - if os.path.exists(cache_path): - try: - with open(cache_path, "r") as f: - payload = json.load(f) - - if ( - isinstance(payload, dict) - and "timestamp" in payload - and isinstance(payload["timestamp"], (int, float)) - and (time.time() - payload["timestamp"]) < NO_THRASH_SECONDS - ): - # Cached data is recent enough -> skip querying AlphaVantage - continue - except Exception: - # Any read/parse error -> ignore cache and refetch - pass - - # Fetch either intraday or historical data depending on range - fetched_data = ( - fetch_intraday_data(args.api_key, symbol) - if args.range_in_days < 1 - else fetch_historical_data(args.api_key, symbol, args.range_in_days) - ) - - # Only write cache if fetched data is structurally valid - if ( - isinstance(fetched_data, dict) - and "current_price" in fetched_data - and isinstance(fetched_data["current_price"], (int, float)) - ): - tmp = os.path.join(CACHE_DIR, f"{symbol}.json.tmp") - final = os.path.join(CACHE_DIR, f"{symbol}.json") - - # Payload includes metadata for staleness and debugging - payload = { - "symbol": symbol, - "range_in_days": args.range_in_days, - "timestamp": int(time.time()), - "data": fetched_data, - } - - # Atomic write to avoid corrupting existing cache - with open(tmp, "w") as f: - json.dump(payload, f) - os.replace(tmp, final) - - # Rate-limit delay between symbols - if i < len(symbols) - 1: - time.sleep(API_DELAY_SECONDS) + # Iterate symbols + for symbol in symbols: + fetched_data = fetch_intraday_data(args.api_key, symbol) if args.range_in_days < 1 else fetch_historical_data(args.api_key, symbol, args.range_in_days) + if fetched_data: + current_price = fetched_data["current_price"] + compare_price = fetched_data["compare_price"] + price_difference = current_price - compare_price + percent_change = (price_difference / current_price) * 100 + color_dynamic = ( + color_good if round(price_difference, args.price_dec_places) > 0 + else color_bad if round(price_difference, args.price_dec_places) < 0 + else color_value + ) + output.append( + f"{line_tab1_offset}{color_label}{symbol}: {line_tab2_offset}{color_value}{round(current_price, args.price_dec_places):.{args.price_dec_places}f} " + f"{line_tab3_offset}{color_dynamic}{round(price_difference, args.price_dec_places):+.{args.price_dec_places}f} " + f"({round(percent_change, args.percent_dec_places):+.{args.percent_dec_places}f}%)" + ) + else: + output.append(f"{symbol}: Error fetching data") + # Join all parts of the output and print it + header_label = f"Intraday" if args.range_in_days < 1 else f"{args.range_in_days} Day" + header_line = f"{line_tab1_offset}{color_header}Ticker{line_tab2_offset}Price ($$){line_tab3_offset}{header_label}{color_label}" + return header_line + "\n" + f"{line_tab1_offset}{color_header}${{voffset -5}}${{hr 1}}" + "\n" + "\n".join(output) if __name__ == "__main__": - main() + print(main()) diff --git a/stocks/process_stocks_alphavantage.py b/stocks/process_stocks_alphavantage.py deleted file mode 100755 index 547a074..0000000 --- a/stocks/process_stocks_alphavantage.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 - -import os -import sys -import time -import json -import argparse -from datetime import datetime, timedelta - -# Directory containing cached per-symbol JSON files -CACHE_DIR = os.path.expanduser("~/.cache/mgconky/stocks_alphavantage/") - -def main(): - # Parse command-line arguments (reader does not need api_key) - parser = argparse.ArgumentParser(description="Process cached Alpha Vantage stock data.") - parser.add_argument("--symbols", required=True, help="Comma-separated list of stock symbols") - parser.add_argument("--range_in_days", type=int, default=0, help="Number of days for historical comparison") - parser.add_argument("--price_dec_places", type=int, default=0, help="Decimal places for prices") - parser.add_argument("--percent_dec_places", type=int, default=1, help="Decimal places for percentages") - parser.add_argument("--stale_seconds", type=int, default=13 * 3600, help="Seconds before cached data is considered stale") - args = parser.parse_args() - - # Split ticker symbols by comma - symbols = args.symbols.strip().upper().split(",") - - # Collect formatted output lines for Conky - output = [] - - # Conky color and layout formatting - color_header = "${color}" - color_label = "${color3}" - color_value = "${color3}" - color_good = "${color6}" - color_bad = "${color7}" - line_tab1_offset = "${goto 25}" - line_tab2_offset = "${goto 90}" - line_tab3_offset = "${alignr}" - - # Process each symbol - for symbol in symbols: - cache_path = os.path.join(CACHE_DIR, f"{symbol}.json") - - # Missing cache file -- display formatted placeholder - if not os.path.exists(cache_path): - output.append( - f"{line_tab1_offset}{color_bad}{symbol}: " - f"{line_tab2_offset}{color_value}-- " - f"{line_tab3_offset}{color_value}-- (--%)" - ) - continue - - # Determine cache age using file modification time - age_seconds = time.time() - os.stat(cache_path).st_mtime - - # Symbol turns red (color_bad) if cached data is stale - symbol_color = color_bad if age_seconds > args.stale_seconds else color_label - - # Load cached JSON payload - try: - with open(cache_path, "r") as f: - payload = json.load(f) - - # Validate payload structure before use - if not isinstance(payload, dict) or "data" not in payload: - raise ValueError("Invalid cache payload") - - fetched_data = payload["data"] - - except Exception: - # Corrupt or unreadable cache file -- display formatted placeholder - output.append( - f"{line_tab1_offset}{color_bad}{symbol}: " - f"{line_tab2_offset}{color_value}-- " - f"{line_tab3_offset}{color_value}-- (--%)" - ) - continue - - # ------------------------------ - # VALIDATION - # ------------------------------ - if ( - isinstance(fetched_data, dict) - and "current_price" in fetched_data - and isinstance(fetched_data["current_price"], (int, float)) - ): - current_price = fetched_data["current_price"] - compare_price = fetched_data["compare_price"] - - # Comparison price may be missing if no trading day was found - if not isinstance(compare_price, (int, float)): - symbol_color = color_bad - output.append( - f"{line_tab1_offset}{symbol_color}{symbol}: " - f"{line_tab2_offset}{color_value}-- " - f"{line_tab3_offset}{color_value}-- (--%)" - ) - continue - - # Compute absolute and percentage change - price_difference = current_price - compare_price - percent_change = (price_difference / current_price) * 100 - - # Color change based on price movement direction - color_dynamic = ( - color_good if round(price_difference, args.price_dec_places) > 0 - else color_bad if round(price_difference, args.price_dec_places) < 0 - else color_value - ) - - # The actual formatted line with valid stock data - output.append( - f"{line_tab1_offset}{symbol_color}{symbol}: " - f"{line_tab2_offset}{color_value}{round(current_price, args.price_dec_places):.{args.price_dec_places}f} " - f"{line_tab3_offset}{color_dynamic}{round(price_difference, args.price_dec_places):+.{args.price_dec_places}f} " - f"({round(percent_change, args.percent_dec_places):+.{args.percent_dec_places}f}%)" - ) - else: - # Cached data missing required fields -- display formatted placeholder - output.append( - f"{line_tab1_offset}{color_bad}{symbol}: " - f"{line_tab2_offset}{color_value}-- " - f"{line_tab3_offset}{color_value}-- (--%)" - ) - - # Header label depends on intraday vs historical mode - header_label = "Intraday" if args.range_in_days < 1 else f"{args.range_in_days} Day" - - # Formatted header line - header_line = ( - f"{line_tab1_offset}{color_header}Ticker" - f"{line_tab2_offset}Price ($$)" - f"{line_tab3_offset}{header_label}{color_label}" - ) - - # Print final output for Conky to render - print( - header_line - + "\n" - + f"{line_tab1_offset}{color_header}${{voffset -5}}${{hr 1}}" - + "\n" - + "\n".join(output) - ) - -if __name__ == "__main__": - main() -