diff --git a/.gitignore b/.gitignore index e9cd469..68c03b9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ matrix_patterns build dist .env -weather.py +config-local.yaml __pycache__/ \ No newline at end of file diff --git a/config.yaml b/config.yaml index c27eba6..11cd958 100644 --- a/config.yaml +++ b/config.yaml @@ -29,7 +29,7 @@ quadrants: scope: panel args: file: zigzag.json - path: snapshot_files/left + path: snapshot_files panel: left top-right: - app: @@ -67,7 +67,7 @@ quadrants: scope: panel args: file: every-third-row.json - path: snapshot_files/right + path: snapshot_files panel: right bottom-right: - app: diff --git a/led_system_monitor.py b/led_system_monitor.py index 3103f6c..cbb9c59 100644 --- a/led_system_monitor.py +++ b/led_system_monitor.py @@ -69,10 +69,16 @@ def find_keyboard_device(): log.warning(f"Warning: Could not auto-detect keyboard device: {e}") return None -def get_config(config_file): - config_file = os.environ.get("CONFIG_FILE", None) or config_file +def get_config(): current_dir = os.path.dirname(os.path.abspath(__file__)) - config_file = os.path.join(current_dir, config_file) + config_file_name = 'config-local.yaml' + config_file = os.path.join(current_dir, config_file_name) + if os.path.exists(config_file): + log.debug(f"using local config file {config_file}") + else: + config_file_name = 'config.yaml' + config_file = os.path.join(current_dir, config_file_name) + log.debug(f"Using default config file {config_file}") with open(config_file, 'r') as f: return safe_load(f) @@ -134,7 +140,7 @@ def app(args, base_apps, plugin_apps): ################################################################################ ### Parse config file to enable control of apps by quadrant and by time slice ## ################################################################################ - config = get_config(args.config_file) + config = get_config() duration = config['duration'] #Default config to be applied if not set in an app quads = config['quadrants'] top_left, bottom_left, top_right, bottom_right, = \ @@ -469,14 +475,12 @@ def main(args): mode_group.add_argument("--help", "-h", action="help", help="Show this help message and exit") - mode_group.add_argument("-config-file", "-cf", type=str, default="config.yaml", help="File that specifies which apps to run in each panel quadrant") mode_group.add_argument("--no-key-listener", "-nkl", action="store_true", help="Do not listen for key presses") mode_group.add_argument("--disable-plugins", "-dp", action="store_true", help="Do not load any plugin code") mode_group.add_argument("--list-apps", "-la", action="store_true", help="List the installed apps, and exit") args = parser.parse_args() if args.no_key_listener: print("Key listener disabled") - log.info(f"Using config file {args.config_file}") app(args, base_apps, plugin_apps) if __name__ == "__main__": @@ -491,6 +495,6 @@ def main(args): logging.basicConfig( level=level, format="%(asctime)s %(levelname)s %(name)s: %(message)s", -) + ) main(sys.argv) diff --git a/plugins/time_weather_plugin.py b/plugins/time_weather_plugin.py index 460349f..6e05fc3 100644 --- a/plugins/time_weather_plugin.py +++ b/plugins/time_weather_plugin.py @@ -29,6 +29,7 @@ @cache # Cache results so we avoid exceeding the API rate limit +# No need to invalidate cache since location per given zip is fixed def get_location_by_zip(zip_info, weather_api_key): zip_code, country = zip_info result = requests.get(f"{OPENWEATHER_HOST}/geo/1.0/zip?zip={zip_code},{country}&appid={weather_api_key}").json() @@ -38,6 +39,8 @@ def get_location_by_zip(zip_info, weather_api_key): return loc @cache +# Cache results so we avoid exceeding the API rate limit +# No need to invalidate cache since location per given IP address is generally fixed def get_location_by_ip(ip_api_key, ip): client = IPLocateClient(api_key=ip_api_key) result = client.lookup(ip) @@ -173,7 +176,8 @@ def wrapper(): Timer(interval, wrapper).start() -# Get fresh weather data every 30 secs +# Get fresh weather data every 30 secs. Two calls per minute will be well within the openweather API +# free tierlimit of 60 calls/minute and 1,000,000 calls/month repeat_function(30, weather_monitor.get.cache_clear) draw_chars = getattr(drawing, 'draw_chars') diff --git a/requirements.txt b/requirements.txt index 453f1c3..9f5dc2e 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -pyserial -numpy -psutil -evdev -pynput -pyyaml +pyserial >= 3.5 +numpy >= 2.4.1 +psutil >= 7.2.1 +evdev >= 1.9.2 +pynput >= 1.8.1 +pyyaml >= 6.0.3 pyinstaller requests >= 2.32.5 python-iplocate >= 1.0.0 -dotenv >= 0.9.9 +python-dotenv >= 1.2.1 \ No newline at end of file diff --git a/weather.py b/weather.py new file mode 100755 index 0000000..ba7f4d2 --- /dev/null +++ b/weather.py @@ -0,0 +1,115 @@ +import os, sys +import requests +from zoneinfo import ZoneInfo +from iplocate import IPLocateClient +from datetime import datetime, timedelta + +OPENWEATHER_HOST = 'https://api.openweathermap.org' +IPIFY_HOST = 'https://api.ipify.org' + +TEST_CONFIG = { + 'zip_info': ('10001', 'US'), # New York, NY + 'lat_lon': (40.7128, -74.0060), # New York, NY + 'units': 'metric', + 'forecast_day': 1, # 1=tomorrow, 2=day after tomorrow, etc. + 'forecast_hour': 12, # Hour of the day for forecast (0-23) +} + +def get_weather(forecast): + zip_info = TEST_CONFIG['zip_info'] + lat_lon =TEST_CONFIG['lat_lon'] + units =TEST_CONFIG['units'] + forecast_day = TEST_CONFIG['forecast_day'] + forecast_hour =TEST_CONFIG['forecast_hour'] + mist_like = ['Mist', 'Fog', 'Dust', 'Haze', 'Smoke', 'Squall', 'Ash', 'Sand', 'Tornado'] + + ip = requests.get(IPIFY_HOST).text + ip_api_key = os.environ.get("IP_LOCATE_API_KEY", None) + weather_api_key = os.environ.get("OPENWEATHER_API_KEY", None) + + try: + if lat_lon: + loc = lat_lon + elif zip_info: + loc = get_location_by_zip(zip_info, weather_api_key) + elif ip_api_key: + loc = get_location_by_ip(ip_api_key, ip) + else: + raise Exception("No location method configured") + + temp_symbol = 'degC'if units == 'metric' else 'degF' if units == 'imperial' else 'degK' + + if forecast: + forecast = requests.get(f"{OPENWEATHER_HOST}/data/2.5/forecast?lat={loc[0]}&lon={loc[1]}&appid={weather_api_key}&units={units}").json() + fc = forecast['list'][0] + temp = fc['main']['temp'] + cond = fc['weather'][0]['main'] + target_date = (datetime.now(ZoneInfo('GMT')).date() + timedelta(days=forecast_day)) + for fc in forecast['list']: + dt = datetime.strptime(fc['dt_txt'], '%Y-%m-%d %H:%M:%S') + if dt.date() == target_date and dt.hour >= forecast_hour: + temp = fc['main']['temp'] + cond = fc['weather'][0]['main'] + if cond in mist_like: cond = 'mist-like' + _forecast = [temp, temp_symbol, cond] + print(f"Forecast weather for time {fc['dt_txt']}") + return _forecast + temp = forecast['list'][-1]['main']['temp'] + cond = forecast['list'][-1]['weather'][0]['main'] + if cond in mist_like: cond = 'mist-like' + _forecast = [temp, temp_symbol, cond] + print(f"Forecast weather for time {fc['dt_txt']}") + return _forecast + else: + current = requests.get(f"{OPENWEATHER_HOST}/data/2.5/weather?lat={loc[0]}&lon={loc[1]}&appid={weather_api_key}&units={units}").json() + + _current = [current['main']['temp'], temp_symbol, current['weather'][0]['main']] + if _current[2] in mist_like: _current[2] = 'mist-like' + return _current + except Exception as e: + print(f"Error getting weather: {e}") + return None + + + +def get_time(): + """ + Return the current time as a tuple (HHMM, is_pm). is_pm is False if 24-hour format is used. + Represent in local time or GMT, and in 24-hour or 12-hour format, based on configuration. + """ + from datetime import datetime + # TODOD get from config file + format_24_hour = False + use_gmt = False + now = datetime.now(ZoneInfo("GMT")) if use_gmt else datetime.now().astimezone() + if format_24_hour : + return (now.strftime("%H%M"), False) + else: + return (now.strftime("%I%M"),now.strftime("%p") == 'PM' ) + +def get_location_by_zip(zip_info, weather_api_key): + zip_code, country = zip_info + result = requests.get(f"http://api.openweathermap.org/geo/1.0/zip?zip={zip_code},{country}&appid={weather_api_key}").json() + lat = result['lat'] + lon = result['lon'] + loc = lat, lon + return loc + +def get_location_by_ip(ip_api_key, ip): + client = IPLocateClient(api_key=ip_api_key) + result = client.lookup(ip) + if result.country: print(f"Country: {result.country}") + if result.city: print(f"City: {result.city}") + if result.privacy.is_vpn: print(f"VPN: {result.privacy.is_vpn}") + if result.privacy.is_proxy: print(f"Proxy: {result.privacy.is_proxy}") + loc = result.latitude, result.longitude + return loc + +if __name__ == "__main__": + from dotenv import load_dotenv + load_dotenv() + current_time = get_time() + print(f"Time: {current_time[0]} {'PM' if current_time[1] else 'AM/24-hour'}") + fc = get_weather(forecast=True) + current = get_weather(forecast=False) + print(f"Weather: Current: {current}, Forecast: {fc}") \ No newline at end of file