From f179f0bcfcdd5597a57984c6e262d939db05f4da Mon Sep 17 00:00:00 2001 From: Leftos Aslanoglou Date: Sat, 13 Dec 2025 09:34:41 -0800 Subject: [PATCH 1/2] Add optional user configuration for airport conditions/NOTAMs modifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for an optional vATISLoadUserConfig.json file that allows users to: - Append custom text to airport conditions or NOTAMs - Remove specific text from airport conditions or NOTAMs This enables per-user customization of D-ATIS output, such as adding "NORCAL APPROACH ON 123.7" to RNO's NOTAMs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- vATISLoad.ipynb | 551 +----------------------------------------------- vATISLoad.pyw | 47 ++++- 2 files changed, 48 insertions(+), 550 deletions(-) diff --git a/vATISLoad.ipynb b/vATISLoad.ipynb index 24aa2b6..4fc00a6 100644 --- a/vATISLoad.ipynb +++ b/vATISLoad.ipynb @@ -47,554 +47,7 @@ "id": "cb02d716-5883-4228-a6fe-5dae01b904aa", "metadata": {}, "outputs": [], - "source": [ - "def update_vATISLoad():\n", - " online_file = ''\n", - " url = 'https://raw.githubusercontent.com/glott/vATISLoad/refs/heads/main/vATISLoad.pyw'\n", - " try:\n", - " online_file = requests.get(url).text.split('\\n')\n", - " except Exception as ignored:\n", - " return\n", - "\n", - " up_to_date = True\n", - " with open(sys.argv[0], 'r') as FileObj:\n", - " i = 0\n", - " for line in FileObj:\n", - " if ('DISABLE_AUTOUPDATES =' in line or 'RUN_UPDATE =' in line \n", - " or 'SHUTDOWN_LIMIT =' in line or 'AUTO_SELECT_FACILITY' in line) and i < 10:\n", - " pass\n", - " elif i > len(online_file) or len(line.strip()) != len(online_file[i].strip()):\n", - " up_to_date = False\n", - " break\n", - " i += 1\n", - "\n", - " if up_to_date:\n", - " return\n", - "\n", - " try:\n", - " os.rename(sys.argv[0], sys.argv[0] + '.bak')\n", - " with requests.get(url, stream=True) as r:\n", - " r.raise_for_status()\n", - " with open(sys.argv[0], 'wb') as f:\n", - " for chunk in r.iter_content(chunk_size=8192): \n", - " f.write(chunk)\n", - "\n", - " os.remove(sys.argv[0] + '.bak')\n", - " \n", - " except Exception as ignored:\n", - " if not os.path.isfile(sys.argv[0]) and os.path.isfile(sys.argv[0] + '.bak'):\n", - " os.rename(sys.argv[0] + '.bak', sys.argv[0])\n", - "\n", - " os.execv(sys.executable, ['python'] + sys.argv)\n", - "\n", - "def determine_active_callsign(return_artcc_only=False):\n", - " crc_path = ''\n", - " try:\n", - " key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'SOFTWARE\\\\CRC')\n", - " crc_path, value_type = winreg.QueryValueEx(key, 'Install_Dir')\n", - " except FileNotFoundError as ignored:\n", - " crc_path = os.path.join(os.getenv('LOCALAPPDATA'), 'CRC')\n", - " \n", - " crc_profiles = os.path.join(crc_path, 'Profiles')\n", - " crc_name = ''\n", - " crc_data = {}\n", - " crc_lastused_time = '2020-01-01T08:00:00'\n", - " try:\n", - " for filename in os.listdir(crc_profiles):\n", - " if filename.endswith('.json'): \n", - " file_path = os.path.join(crc_profiles, filename)\n", - " with open(file_path, 'r') as f:\n", - " data = json.load(f)\n", - " dt1 = datetime.strptime(crc_lastused_time, '%Y-%m-%dT%H:%M:%S')\n", - " if 'LastUsedAt' not in data or data['LastUsedAt'] == None:\n", - " continue\n", - " dt2 = datetime.strptime(data['LastUsedAt'].split('.')[0].replace('Z',''), '%Y-%m-%dT%H:%M:%S')\n", - " if dt2 > dt1:\n", - " crc_lastused_time = data['LastUsedAt'].split('.')[0].replace('Z','')\n", - " crc_name = data['Name']\n", - " crc_data = data\n", - " except Exception as ignored:\n", - " return None\n", - "\n", - " if return_artcc_only:\n", - " return crc_data['ArtccId']\n", - "\n", - " try:\n", - " lastPos = crc_data['LastUsedPositionId']\n", - " crc_ARTCC = os.path.join(crc_path, 'ARTCCs') + os.sep + crc_data['ArtccId'] + '.json'\n", - " with open(crc_ARTCC, 'r') as f:\n", - " data = json.load(f)\n", - "\n", - " pos = determine_position_from_id(data['facility']['positions'], lastPos)\n", - " if pos is not None:\n", - " return pos\n", - "\n", - " for child1 in data['facility']['childFacilities']:\n", - " pos = determine_position_from_id(child1['positions'], lastPos)\n", - " if pos is not None:\n", - " return pos\n", - " \n", - " for child2 in child1['childFacilities']:\n", - " pos = determine_position_from_id(child2['positions'], lastPos)\n", - " if pos is not None:\n", - " return pos\n", - " \n", - " except Exception as ignored:\n", - " pass\n", - "\n", - " return None\n", - "\n", - "async def auto_select_facility():\n", - " artcc = determine_active_callsign(return_artcc_only=True)\n", - " if artcc is None:\n", - " return\n", - " \n", - " if not AUTO_SELECT_FACILITY and not artcc in ['ZOA', 'ZMA', 'ZDC']:\n", - " return\n", - "\n", - " # Determine if CRC is open and a profile is loaded\n", - " crc_found = False\n", - " for win in [w.title for w in pygetwindow.getAllWindows()]:\n", - " if 'CRC : 1' in win:\n", - " crc_found = True\n", - "\n", - " if not crc_found:\n", - " return\n", - " \n", - " try:\n", - " async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n", - " # Determine if any vATIS profile matches ARTCC\n", - " await websocket.send(json.dumps({'type': 'getProfiles'}))\n", - " m = json.loads(await asyncio.wait_for(websocket.recv(), timeout=0.25))['profiles']\n", - " \n", - " match_id = ''\n", - " for p in m:\n", - " if artcc in p['name']:\n", - " match_id = p['id']\n", - " \n", - " if len(match_id) < 0:\n", - " return\n", - " \n", - " # Determine if current profile is already the desired profile\n", - " await websocket.send(json.dumps({'type': 'getActiveProfile'}))\n", - " m = json.loads(await asyncio.wait_for(websocket.recv(), timeout=0.25))\n", - " \n", - " if 'id' in m:\n", - " # Do not select a profile if current profile is already selected\n", - " if m['id'] == match_id:\n", - " return\n", - " \n", - " # Load new profile\n", - " await websocket.send(json.dumps({'type': 'loadProfile', 'value': {'id': match_id}}))\n", - " await asyncio.sleep(1)\n", - " \n", - " except Exception as ignored:\n", - " pass\n", - "\n", - "async def try_websocket(shutdown=RUN_UPDATE, limit=SHUTDOWN_LIMIT, initial=False):\n", - " t0 = time.time()\n", - " for i in range(0, 250):\n", - " if initial and i < 30:\n", - " await auto_select_facility()\n", - " \n", - " t1 = time.time()\n", - " if t1 - t0 > limit:\n", - " if shutdown:\n", - " sys.exit()\n", - " return\n", - " try:\n", - " async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n", - " await websocket.send(json.dumps({'type': 'getStations'}))\n", - " try:\n", - " m = json.loads(await asyncio.wait_for(websocket.recv(), timeout=1))\n", - " if time.time() - t0 > 5:\n", - " await asyncio.sleep(1)\n", - " if m['type'] != 'stations':\n", - " await asyncio.sleep(0.5)\n", - " continue\n", - " return\n", - " except Exception as ignored:\n", - " pass\n", - " except Exception as ignored:\n", - " dt = time.time() - t1\n", - " if dt < 1:\n", - " await asyncio.sleep(1 - dt)\n", - " pass\n", - "\n", - "async def get_datis_stations(initial=False):\n", - " await try_websocket(initial=initial)\n", - " \n", - " data = {}\n", - " async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n", - " await websocket.send(json.dumps({'type': 'getStations'}))\n", - " m = json.loads(await websocket.recv())\n", - " \n", - " not_stations = False\n", - " while m['type'] != 'stations':\n", - " not_stations = True\n", - " await asyncio.sleep(0.1)\n", - " await websocket.send(json.dumps({'type': 'getStations'}))\n", - " m = json.loads(await websocket.recv())\n", - " \n", - " if not_stations:\n", - " await asyncio.sleep(0.5)\n", - " await websocket.send(json.dumps({'type': 'getStations'}))\n", - " m = json.loads(await websocket.recv())\n", - "\n", - " for s in m['stations']:\n", - " name = s['name']\n", - "\n", - " if s['atisType'] == 'Arrival':\n", - " name += '_A'\n", - " elif s['atisType'] == 'Departure':\n", - " name += '_D'\n", - " \n", - " if 'D-ATIS' in s['presets']:\n", - " data[name] = s['id']\n", - " \n", - " return data\n", - "\n", - "def get_atis_replacements(stations):\n", - " stations = list(set(value.replace('_A', '').replace('_D', '') for value in stations))\n", - "\n", - " config = {}\n", - " try:\n", - " url = 'https://raw.githubusercontent.com/glott/vATISLoad/refs/heads/main/vATISLoadConfig.json'\n", - " config = json.loads(requests.get(url).text)\n", - " except Exception as ignored:\n", - " pass\n", - "\n", - " if 'replacements' not in config:\n", - " return {}\n", - "\n", - " replacements = {}\n", - " for a in config['replacements']:\n", - " if a in stations:\n", - " replacements[a] = config['replacements'][a]\n", - "\n", - " return replacements\n", - " \n", - "async def get_contractions(station):\n", - " try:\n", - " async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n", - " if '_D' in station:\n", - " payload = {'station': station[0:4], 'atisType': 'Departure'}\n", - " elif '_A' in station:\n", - " payload = {'station': station[0:4], 'atisType': 'Arrival'}\n", - " else:\n", - " payload = {'station': station[0:4]}\n", - " await websocket.send(json.dumps({'type': 'getContractions', 'value': payload}))\n", - " m = json.loads(await asyncio.wait_for(websocket.recv(), timeout=0.25))\n", - " \n", - "\n", - " c = {}\n", - " contractions = m['stations'][0]['contractions']\n", - " for cont in contractions:\n", - " c[contractions[cont]['text']] = '@' + cont\n", - " \n", - " c = dict(sorted(c.items(), key=lambda item: len(item[0])))\n", - " c = {key: c[key] for key in reversed(c)}\n", - "\n", - " return c\n", - " except asyncio.TimeoutError:\n", - " pass\n", - "\n", - " return {}\n", - "\n", - "def get_datis_data():\n", - " data = {}\n", - " try:\n", - " url = 'https://atis.info/api/all'\n", - " data = json.loads(requests.get(url, timeout=2.5).text)\n", - " except Exception as ignored:\n", - " os.system('cmd /K \\\"cls & echo Unable to fetch D-ATIS data. & timeout 5 & exit\\\"')\n", - " \n", - " return data\n", - "\n", - "async def get_datis(station, atis_data, replacements):\n", - " atis_type = 'combined'\n", - " if '_A' in station:\n", - " atis_type = 'arr'\n", - " elif '_D' in station:\n", - " atis_type = 'dep'\n", - "\n", - " atis_info = ['D-ATIS NOT AVBL.', '']\n", - " if 'error' in atis_data:\n", - " return atis_info\n", - "\n", - " datis = ''\n", - " for a in atis_data:\n", - " if a['airport'] != station[0:4] or a['type'] != atis_type:\n", - " continue\n", - " datis = a['datis']\n", - "\n", - " # Ignore D-ATIS more than 1.75 hours old\n", - " try: \n", - " t_updated = datetime.strptime(a['updatedAt'][:26], \"%Y-%m-%dT%H:%M:%S.%f\")\n", - " t_updated = t_updated.replace(tzinfo=timezone.utc)\n", - " t_now = datetime.now(timezone.utc)\n", - "\n", - " if (t_now - t_updated).total_seconds() / 3600 > 1.75:\n", - " return atis_info\n", - " except Exception as ignored:\n", - " pass\n", - "\n", - " if len(datis) == 0:\n", - " return atis_info\n", - "\n", - " # Strip beginning and ending D-ATIS text\n", - " datis = '. '.join(datis.split('. ')[2:])\n", - " datis = re.sub(' ...ADVS YOU HAVE.*', '', datis)\n", - " datis = datis.replace('NOTICE TO AIR MISSIONS, NOTAMS. ', 'NOTAMS... ') \\\n", - " .replace('NOTICE TO AIR MISSIONS. ', 'NOTAMS... ') \\\n", - " .replace('NOTICE TO AIR MEN. ', 'NOTAMS... ') \\\n", - " .replace('NOTICE TO AIRMEN. ', 'NOTAMS... ') \\\n", - " .replace('NOTAMS. ', 'NOTAMS... ') \\\n", - " .replace('NOTAM. ', 'NOTAMS... ')\n", - "\n", - " # Replace defined replacements\n", - " for r in replacements:\n", - " if '%r' in replacements[r]:\n", - " datis = re.sub(r + '[,.;]{0,2}', replacements[r].replace('%r', ''), datis)\n", - " else:\n", - " datis = re.sub(r + '[,.;]{0,2}', replacements[r], datis)\n", - " datis = re.sub(r'\\s+', ' ', datis).strip()\n", - "\n", - " # Clean up D-ATIS\n", - " datis = datis.replace('...', '/./').replace('..', '.') \\\n", - " .replace('/./', '...').replace(' ', ' ').replace(' . ', '. ') \\\n", - " .replace(', ,', ',').replace(' ; ', '; ').replace(' .,', ' ,') \\\n", - " .replace(' , ', ', ').replace('., ', ', ').replace('&', '&') \\\n", - " .replace(' ;.', '.').replace(' ;,', ',')\n", - "\n", - " # Replace contractions\n", - " contractions = await get_contractions(station)\n", - " for c, v in contractions.items():\n", - " if not c.isdigit():\n", - " datis = re.sub(r'(?= 4:\n", - " break\n", - " \n", - " if s not in disconnected_atises:\n", - " continue\n", - " \n", - " payload = {'type': 'connectAtis', 'value': {'id': i}}\n", - " async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n", - " await websocket.send(json.dumps(payload))\n", - "\n", - " try:\n", - " m = await asyncio.wait_for(websocket.recv(), timeout=0.1)\n", - " n += 1\n", - " except Exception as ignored:\n", - " pass\n", - "\n", - "def kill_open_instances():\n", - " prev_instances = {}\n", - "\n", - " for q in psutil.process_iter():\n", - " if 'python' in q.name():\n", - " for parameter in q.cmdline():\n", - " if 'vATISLoad' in parameter and parameter.endswith('.pyw'):\n", - " q_create_time = q.create_time()\n", - " q_create_datetime = datetime.fromtimestamp(q_create_time)\n", - " prev_instances[q.pid] = {'process': q, 'start': q_create_datetime}\n", - " \n", - " prev_instances = dict(sorted(prev_instances.items(), key=lambda item: item[1]['start']))\n", - " \n", - " for i in range(0, len(prev_instances) - 1):\n", - " k = list(prev_instances.keys())[i]\n", - " prev_instances[k]['process'].terminate()\n", - "\n", - "def open_vATIS():\n", - " # Set 'autoFetchAtisLetter' to True\n", - " config_path = os.getenv('LOCALAPPDATA') + '\\\\org.vatsim.vatis\\\\AppConfig.json'\n", - " try:\n", - " with open(config_path, 'r') as f:\n", - " data = json.load(f)\n", - " if 'autoFetchAtisLetter' in data:\n", - " data['autoFetchAtisLetter'] = True\n", - " with open(config_path, 'w') as f:\n", - " json.dump(data, f, indent=2)\n", - " except Exception as ignored:\n", - " pass\n", - "\n", - " # Check if vATIS is open\n", - " for process in psutil.process_iter(['name']):\n", - " if process.info['name'] == 'vATIS.exe':\n", - " return\n", - "\n", - " exe = os.getenv('LOCALAPPDATA') + '\\\\org.vatsim.vatis\\\\current\\\\vATIS.exe'\n", - " subprocess.Popen(exe);\n", - "\n", - "async def get_connected_atis_data():\n", - " stations = await get_datis_stations()\n", - " atis_statuses = await get_atis_statuses()\n", - "\n", - " connected_atis_data = {}\n", - " \n", - " for station in [k for k, v in atis_statuses.items() if v == 'Connected']:\n", - " payload = {'type': 'getAtis', 'value': {'id': stations[station]}}\n", - " async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n", - " await websocket.send(json.dumps(payload))\n", - "\n", - " m = json.loads(await websocket.recv())['value']\n", - " connected_atis_data[station] = [m['airportConditions'], m['notams']]\n", - "\n", - " return connected_atis_data\n", - "\n", - "async def disconnect_over_connection_limit(delay=True):\n", - " if True:\n", - " time.sleep(5)\n", - " \n", - " stations = await get_datis_stations()\n", - " atis_statuses = await get_atis_statuses()\n", - " connected_atises = [k for k, v in atis_statuses.items() if v == 'Connected']\n", - "\n", - " if len(connected_atises) <= 4 or SHUTDOWN_LIMIT == 346:\n", - " return\n", - "\n", - " for i in range(4, len(connected_atises)):\n", - " s, i = connected_atises[i], stations[connected_atises[i]]\n", - " payload = {'type': 'disconnectAtis', 'value': {'id': i}}\n", - " async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n", - " await websocket.send(json.dumps(payload))\n", - "\n", - "def find_deleted_portions(original, modified):\n", - " sequence_matcher = difflib.SequenceMatcher(None, original, modified)\n", - " \n", - " deleted_portions = []\n", - " for tag, i1, i2, j1, j2 in sequence_matcher.get_opcodes():\n", - " if tag == 'delete': \n", - " deleted_portions.append(original[i1:i2])\n", - " \n", - " return deleted_portions\n", - "\n", - "def compare_atis_data(prev_data, new_data):\n", - " compared_output = {}\n", - "\n", - " for station in prev_data:\n", - " if station not in new_data:\n", - " continue\n", - " \n", - " conditionDiff = find_deleted_portions(prev_data[station][0], new_data[station][0])\n", - " notamDiff = find_deleted_portions(prev_data[station][1], new_data[station][1])\n", - "\n", - " if len(conditionDiff) > 0 or len(notamDiff) > 0:\n", - " compared_output[station] = conditionDiff + notamDiff\n", - "\n", - " return compared_output" - ] + "source": "def update_vATISLoad():\n online_file = ''\n url = 'https://raw.githubusercontent.com/glott/vATISLoad/refs/heads/main/vATISLoad.pyw'\n try:\n online_file = requests.get(url).text.split('\\n')\n except Exception as ignored:\n return\n\n up_to_date = True\n with open(sys.argv[0], 'r') as FileObj:\n i = 0\n for line in FileObj:\n if ('DISABLE_AUTOUPDATES =' in line or 'RUN_UPDATE =' in line \n or 'SHUTDOWN_LIMIT =' in line or 'AUTO_SELECT_FACILITY' in line) and i < 10:\n pass\n elif i > len(online_file) or len(line.strip()) != len(online_file[i].strip()):\n up_to_date = False\n break\n i += 1\n\n if up_to_date:\n return\n\n try:\n os.rename(sys.argv[0], sys.argv[0] + '.bak')\n with requests.get(url, stream=True) as r:\n r.raise_for_status()\n with open(sys.argv[0], 'wb') as f:\n for chunk in r.iter_content(chunk_size=8192): \n f.write(chunk)\n\n os.remove(sys.argv[0] + '.bak')\n \n except Exception as ignored:\n if not os.path.isfile(sys.argv[0]) and os.path.isfile(sys.argv[0] + '.bak'):\n os.rename(sys.argv[0] + '.bak', sys.argv[0])\n\n os.execv(sys.executable, ['python'] + sys.argv)\n\ndef determine_active_callsign(return_artcc_only=False):\n crc_path = ''\n try:\n key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'SOFTWARE\\\\CRC')\n crc_path, value_type = winreg.QueryValueEx(key, 'Install_Dir')\n except FileNotFoundError as ignored:\n crc_path = os.path.join(os.getenv('LOCALAPPDATA'), 'CRC')\n \n crc_profiles = os.path.join(crc_path, 'Profiles')\n crc_name = ''\n crc_data = {}\n crc_lastused_time = '2020-01-01T08:00:00'\n try:\n for filename in os.listdir(crc_profiles):\n if filename.endswith('.json'): \n file_path = os.path.join(crc_profiles, filename)\n with open(file_path, 'r') as f:\n data = json.load(f)\n dt1 = datetime.strptime(crc_lastused_time, '%Y-%m-%dT%H:%M:%S')\n if 'LastUsedAt' not in data or data['LastUsedAt'] == None:\n continue\n dt2 = datetime.strptime(data['LastUsedAt'].split('.')[0].replace('Z',''), '%Y-%m-%dT%H:%M:%S')\n if dt2 > dt1:\n crc_lastused_time = data['LastUsedAt'].split('.')[0].replace('Z','')\n crc_name = data['Name']\n crc_data = data\n except Exception as ignored:\n return None\n\n if return_artcc_only:\n return crc_data['ArtccId']\n\n try:\n lastPos = crc_data['LastUsedPositionId']\n crc_ARTCC = os.path.join(crc_path, 'ARTCCs') + os.sep + crc_data['ArtccId'] + '.json'\n with open(crc_ARTCC, 'r') as f:\n data = json.load(f)\n\n pos = determine_position_from_id(data['facility']['positions'], lastPos)\n if pos is not None:\n return pos\n\n for child1 in data['facility']['childFacilities']:\n pos = determine_position_from_id(child1['positions'], lastPos)\n if pos is not None:\n return pos\n \n for child2 in child1['childFacilities']:\n pos = determine_position_from_id(child2['positions'], lastPos)\n if pos is not None:\n return pos\n \n except Exception as ignored:\n pass\n\n return None\n\nasync def auto_select_facility():\n artcc = determine_active_callsign(return_artcc_only=True)\n if artcc is None:\n return\n \n if not AUTO_SELECT_FACILITY and not artcc in ['ZOA', 'ZMA', 'ZDC']:\n return\n\n # Determine if CRC is open and a profile is loaded\n crc_found = False\n for win in [w.title for w in pygetwindow.getAllWindows()]:\n if 'CRC : 1' in win:\n crc_found = True\n\n if not crc_found:\n return\n \n try:\n async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n # Determine if any vATIS profile matches ARTCC\n await websocket.send(json.dumps({'type': 'getProfiles'}))\n m = json.loads(await asyncio.wait_for(websocket.recv(), timeout=0.25))['profiles']\n \n match_id = ''\n for p in m:\n if artcc in p['name']:\n match_id = p['id']\n \n if len(match_id) < 0:\n return\n \n # Determine if current profile is already the desired profile\n await websocket.send(json.dumps({'type': 'getActiveProfile'}))\n m = json.loads(await asyncio.wait_for(websocket.recv(), timeout=0.25))\n \n if 'id' in m:\n # Do not select a profile if current profile is already selected\n if m['id'] == match_id:\n return\n \n # Load new profile\n await websocket.send(json.dumps({'type': 'loadProfile', 'value': {'id': match_id}}))\n await asyncio.sleep(1)\n \n except Exception as ignored:\n pass\n\nasync def try_websocket(shutdown=RUN_UPDATE, limit=SHUTDOWN_LIMIT, initial=False):\n t0 = time.time()\n for i in range(0, 250):\n if initial and i < 30:\n await auto_select_facility()\n \n t1 = time.time()\n if t1 - t0 > limit:\n if shutdown:\n sys.exit()\n return\n try:\n async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n await websocket.send(json.dumps({'type': 'getStations'}))\n try:\n m = json.loads(await asyncio.wait_for(websocket.recv(), timeout=1))\n if time.time() - t0 > 5:\n await asyncio.sleep(1)\n if m['type'] != 'stations':\n await asyncio.sleep(0.5)\n continue\n return\n except Exception as ignored:\n pass\n except Exception as ignored:\n dt = time.time() - t1\n if dt < 1:\n await asyncio.sleep(1 - dt)\n pass\n\nasync def get_datis_stations(initial=False):\n await try_websocket(initial=initial)\n \n data = {}\n async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n await websocket.send(json.dumps({'type': 'getStations'}))\n m = json.loads(await websocket.recv())\n \n not_stations = False\n while m['type'] != 'stations':\n not_stations = True\n await asyncio.sleep(0.1)\n await websocket.send(json.dumps({'type': 'getStations'}))\n m = json.loads(await websocket.recv())\n \n if not_stations:\n await asyncio.sleep(0.5)\n await websocket.send(json.dumps({'type': 'getStations'}))\n m = json.loads(await websocket.recv())\n\n for s in m['stations']:\n name = s['name']\n\n if s['atisType'] == 'Arrival':\n name += '_A'\n elif s['atisType'] == 'Departure':\n name += '_D'\n \n if 'D-ATIS' in s['presets']:\n data[name] = s['id']\n \n return data\n\ndef get_atis_replacements(stations):\n stations = list(set(value.replace('_A', '').replace('_D', '') for value in stations))\n\n config = {}\n try:\n url = 'https://raw.githubusercontent.com/glott/vATISLoad/refs/heads/main/vATISLoadConfig.json'\n config = json.loads(requests.get(url).text)\n except Exception as ignored:\n pass\n\n if 'replacements' not in config:\n return {}\n\n replacements = {}\n for a in config['replacements']:\n if a in stations:\n replacements[a] = config['replacements'][a]\n\n return replacements\n\ndef get_user_config():\n config = {}\n config_path = os.path.join(os.path.dirname(sys.argv[0]), 'vATISLoadUserConfig.json')\n try:\n with open(config_path, 'r') as f:\n config = json.load(f)\n except Exception as ignored:\n pass\n return config\n\ndef apply_user_modifications(airport, conditions, notams, user_config):\n if airport not in user_config:\n return conditions, notams\n\n cfg = user_config[airport]\n\n # Apply conditions modifications\n if 'conditions' in cfg:\n for text in cfg['conditions'].get('remove', []):\n conditions = conditions.replace(text, '')\n append_text = cfg['conditions'].get('append', '')\n if append_text:\n if conditions and not conditions.endswith(' '):\n conditions += ' '\n conditions += append_text\n\n # Apply notams modifications\n if 'notams' in cfg:\n for text in cfg['notams'].get('remove', []):\n notams = notams.replace(text, '')\n append_text = cfg['notams'].get('append', '')\n if append_text:\n if notams and not notams.endswith(' '):\n notams += ' '\n notams += append_text\n\n # Clean up extra spaces\n conditions = re.sub(r'\\s+', ' ', conditions).strip()\n notams = re.sub(r'\\s+', ' ', notams).strip()\n\n return conditions, notams\n\nasync def get_contractions(station):\n try:\n async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n if '_D' in station:\n payload = {'station': station[0:4], 'atisType': 'Departure'}\n elif '_A' in station:\n payload = {'station': station[0:4], 'atisType': 'Arrival'}\n else:\n payload = {'station': station[0:4]}\n await websocket.send(json.dumps({'type': 'getContractions', 'value': payload}))\n m = json.loads(await asyncio.wait_for(websocket.recv(), timeout=0.25))\n \n\n c = {}\n contractions = m['stations'][0]['contractions']\n for cont in contractions:\n c[contractions[cont]['text']] = '@' + cont\n \n c = dict(sorted(c.items(), key=lambda item: len(item[0])))\n c = {key: c[key] for key in reversed(c)}\n\n return c\n except asyncio.TimeoutError:\n pass\n\n return {}\n\ndef get_datis_data():\n data = {}\n try:\n url = 'https://atis.info/api/all'\n data = json.loads(requests.get(url, timeout=2.5).text)\n except Exception as ignored:\n os.system('cmd /K \\\"cls & echo Unable to fetch D-ATIS data. & timeout 5 & exit\\\"')\n \n return data\n\nasync def get_datis(station, atis_data, replacements):\n atis_type = 'combined'\n if '_A' in station:\n atis_type = 'arr'\n elif '_D' in station:\n atis_type = 'dep'\n\n atis_info = ['D-ATIS NOT AVBL.', '']\n if 'error' in atis_data:\n return atis_info\n\n datis = ''\n for a in atis_data:\n if a['airport'] != station[0:4] or a['type'] != atis_type:\n continue\n datis = a['datis']\n\n # Ignore D-ATIS more than 1.75 hours old\n try: \n t_updated = datetime.strptime(a['updatedAt'][:26], \"%Y-%m-%dT%H:%M:%S.%f\")\n t_updated = t_updated.replace(tzinfo=timezone.utc)\n t_now = datetime.now(timezone.utc)\n\n if (t_now - t_updated).total_seconds() / 3600 > 1.75:\n return atis_info\n except Exception as ignored:\n pass\n\n if len(datis) == 0:\n return atis_info\n\n # Strip beginning and ending D-ATIS text\n datis = '. '.join(datis.split('. ')[2:])\n datis = re.sub(' ...ADVS YOU HAVE.*', '', datis)\n datis = datis.replace('NOTICE TO AIR MISSIONS, NOTAMS. ', 'NOTAMS... ') \\\n .replace('NOTICE TO AIR MISSIONS. ', 'NOTAMS... ') \\\n .replace('NOTICE TO AIR MEN. ', 'NOTAMS... ') \\\n .replace('NOTICE TO AIRMEN. ', 'NOTAMS... ') \\\n .replace('NOTAMS. ', 'NOTAMS... ') \\\n .replace('NOTAM. ', 'NOTAMS... ')\n\n # Replace defined replacements\n for r in replacements:\n if '%r' in replacements[r]:\n datis = re.sub(r + '[,.;]{0,2}', replacements[r].replace('%r', ''), datis)\n else:\n datis = re.sub(r + '[,.;]{0,2}', replacements[r], datis)\n datis = re.sub(r'\\s+', ' ', datis).strip()\n\n # Clean up D-ATIS\n datis = datis.replace('...', '/./').replace('..', '.') \\\n .replace('/./', '...').replace(' ', ' ').replace(' . ', '. ') \\\n .replace(', ,', ',').replace(' ; ', '; ').replace(' .,', ' ,') \\\n .replace(' , ', ', ').replace('., ', ', ').replace('&', '&') \\\n .replace(' ;.', '.').replace(' ;,', ',')\n\n # Replace contractions\n contractions = await get_contractions(station)\n for c, v in contractions.items():\n if not c.isdigit():\n datis = re.sub(r'(?= 4:\n break\n \n if s not in disconnected_atises:\n continue\n \n payload = {'type': 'connectAtis', 'value': {'id': i}}\n async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n await websocket.send(json.dumps(payload))\n\n try:\n m = await asyncio.wait_for(websocket.recv(), timeout=0.1)\n n += 1\n except Exception as ignored:\n pass\n\ndef kill_open_instances():\n prev_instances = {}\n\n for q in psutil.process_iter():\n if 'python' in q.name():\n for parameter in q.cmdline():\n if 'vATISLoad' in parameter and parameter.endswith('.pyw'):\n q_create_time = q.create_time()\n q_create_datetime = datetime.fromtimestamp(q_create_time)\n prev_instances[q.pid] = {'process': q, 'start': q_create_datetime}\n \n prev_instances = dict(sorted(prev_instances.items(), key=lambda item: item[1]['start']))\n \n for i in range(0, len(prev_instances) - 1):\n k = list(prev_instances.keys())[i]\n prev_instances[k]['process'].terminate()\n\ndef open_vATIS():\n # Set 'autoFetchAtisLetter' to True\n config_path = os.getenv('LOCALAPPDATA') + '\\\\org.vatsim.vatis\\\\AppConfig.json'\n try:\n with open(config_path, 'r') as f:\n data = json.load(f)\n if 'autoFetchAtisLetter' in data:\n data['autoFetchAtisLetter'] = True\n with open(config_path, 'w') as f:\n json.dump(data, f, indent=2)\n except Exception as ignored:\n pass\n\n # Check if vATIS is open\n for process in psutil.process_iter(['name']):\n if process.info['name'] == 'vATIS.exe':\n return\n\n exe = os.getenv('LOCALAPPDATA') + '\\\\org.vatsim.vatis\\\\current\\\\vATIS.exe'\n subprocess.Popen(exe);\n\nasync def get_connected_atis_data():\n stations = await get_datis_stations()\n atis_statuses = await get_atis_statuses()\n\n connected_atis_data = {}\n \n for station in [k for k, v in atis_statuses.items() if v == 'Connected']:\n payload = {'type': 'getAtis', 'value': {'id': stations[station]}}\n async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n await websocket.send(json.dumps(payload))\n\n m = json.loads(await websocket.recv())['value']\n connected_atis_data[station] = [m['airportConditions'], m['notams']]\n\n return connected_atis_data\n\nasync def disconnect_over_connection_limit(delay=True):\n if True:\n time.sleep(5)\n \n stations = await get_datis_stations()\n atis_statuses = await get_atis_statuses()\n connected_atises = [k for k, v in atis_statuses.items() if v == 'Connected']\n\n if len(connected_atises) <= 4 or SHUTDOWN_LIMIT == 346:\n return\n\n for i in range(4, len(connected_atises)):\n s, i = connected_atises[i], stations[connected_atises[i]]\n payload = {'type': 'disconnectAtis', 'value': {'id': i}}\n async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket:\n await websocket.send(json.dumps(payload))\n\ndef find_deleted_portions(original, modified):\n sequence_matcher = difflib.SequenceMatcher(None, original, modified)\n \n deleted_portions = []\n for tag, i1, i2, j1, j2 in sequence_matcher.get_opcodes():\n if tag == 'delete': \n deleted_portions.append(original[i1:i2])\n \n return deleted_portions\n\ndef compare_atis_data(prev_data, new_data):\n compared_output = {}\n\n for station in prev_data:\n if station not in new_data:\n continue\n \n conditionDiff = find_deleted_portions(prev_data[station][0], new_data[station][0])\n notamDiff = find_deleted_portions(prev_data[station][1], new_data[station][1])\n\n if len(conditionDiff) > 0 or len(notamDiff) > 0:\n compared_output[station] = conditionDiff + notamDiff\n\n return compared_output" }, { "cell_type": "code", @@ -664,4 +117,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/vATISLoad.pyw b/vATISLoad.pyw index cec4558..f45d501 100644 --- a/vATISLoad.pyw +++ b/vATISLoad.pyw @@ -256,7 +256,49 @@ def get_atis_replacements(stations): replacements[a] = config['replacements'][a] return replacements - + +def get_user_config(): + config = {} + config_path = os.path.join(os.path.dirname(sys.argv[0]), 'vATISLoadUserConfig.json') + try: + with open(config_path, 'r') as f: + config = json.load(f) + except Exception as ignored: + pass + return config + +def apply_user_modifications(airport, conditions, notams, user_config): + if airport not in user_config: + return conditions, notams + + cfg = user_config[airport] + + # Apply conditions modifications + if 'conditions' in cfg: + for text in cfg['conditions'].get('remove', []): + conditions = conditions.replace(text, '') + append_text = cfg['conditions'].get('append', '') + if append_text: + if conditions and not conditions.endswith(' '): + conditions += ' ' + conditions += append_text + + # Apply notams modifications + if 'notams' in cfg: + for text in cfg['notams'].get('remove', []): + notams = notams.replace(text, '') + append_text = cfg['notams'].get('append', '') + if append_text: + if notams and not notams.endswith(' '): + notams += ' ' + notams += append_text + + # Clean up extra spaces + conditions = re.sub(r'\s+', ' ', conditions).strip() + notams = re.sub(r'\s+', ' ', notams).strip() + + return conditions, notams + async def get_contractions(station): try: async with websockets.connect('ws://127.0.0.1:49082/', close_timeout=0.01) as websocket: @@ -392,6 +434,7 @@ async def configure_atises(connected_only=False, initial=False, temp_rep={}): stations = await get_datis_stations(initial=initial) replacements = get_atis_replacements(stations) atis_data = get_datis_data() + user_config = get_user_config() atis_statuses = await get_atis_statuses() @@ -412,6 +455,8 @@ async def configure_atises(connected_only=False, initial=False, temp_rep={}): v = {'id': i, 'preset': 'D-ATIS', 'syncAtisLetter': True} v['airportConditionsFreeText'], v['notamsFreeText'] = await get_datis(s, atis_data, rep) + v['airportConditionsFreeText'], v['notamsFreeText'] = apply_user_modifications( + s[0:4], v['airportConditionsFreeText'], v['notamsFreeText'], user_config) if connected_only and v['airportConditionsFreeText'] == 'D-ATIS NOT AVBL.': continue From c602ac192ece67d6ab6a9783d091d0a1388eda4f Mon Sep 17 00:00:00 2001 From: Leftos Aslanoglou Date: Sat, 13 Dec 2025 09:45:33 -0800 Subject: [PATCH 2/2] Add user configuration and LLM-related files to .gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 68bc17f..769521c 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,9 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# User config +vATISLoadUserConfig.json + +# LLM stuff +.claude/