diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bcbf671 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + ".", + "-p", + "*test.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/locationdata.py b/locationdata.py index 444ebf6..cfba686 100644 --- a/locationdata.py +++ b/locationdata.py @@ -1,6 +1,7 @@ # helper functions to use location data # K7MHI Kelly Keeton 2024 +import json from geopy.geocoders import Nominatim # pip install geopy import maidenhead as mh # pip install maidenhead import requests # pip install requests @@ -8,11 +9,14 @@ import bs4 as bs # pip install beautifulsoup4 URL_TIMEOUT = 10 # wait time for URL requests DAYS_OF_WEATHER = 4 # weather forecast days, the first two rows are today and tonight +# unified error messages to be able to test them from tests +NO_DATA_NOGPS = "no location data: does your device have GPS?" +ERROR_FETCHING_DATA = "error fetching data" def where_am_i(lat=0, lon=0): whereIam = "" if float(lat) == 0 and float(lon) == 0: - return "no location data: does your device have GPS?" + return NO_DATA_NOGPS # initialize Nominatim API geolocator = Nominatim(user_agent="mesh-bot") @@ -28,107 +32,119 @@ def where_am_i(lat=0, lon=0): def get_tide(lat=0, lon=0): station_id = "" if float(lat) == 0 and float(lon) == 0: - return "no location data: does your device have GPS?" + return NO_DATA_NOGPS station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50" - station_data = requests.get(station_lookup_url, timeout=URL_TIMEOUT) - if(station_data.ok): - station_json = station_data.json() - # get first station id in 50 mile radius - station_id = station_json['stationList'][0]['stationId'] - else: - return "error fetching station data" + try: + station_data = requests.get(station_lookup_url, timeout=URL_TIMEOUT) + if station_data.ok: + station_json = station_data.json() + else: + return ERROR_FETCHING_DATA + except (requests.exceptions.RequestException, json.JSONDecodeError): + return ERROR_FETCHING_DATA + + station_id = station_json['stationList'][0]['stationId'] station_url = "https://tidesandcurrents.noaa.gov/noaatidepredictions.html?id=" + station_id - station_data = requests.get(station_url, timeout=URL_TIMEOUT) - if(station_data.ok): - # extract table class="table table-condensed" - soup = bs.BeautifulSoup(station_data.text, 'html.parser') - table = soup.find('table', class_='table table-condensed') - # extract rows - rows = table.find_all('tr') - # extract data from rows - tide_data = [] - for row in rows: - row_text = "" - cols = row.find_all('td') - for col in cols: - row_text += col.text + " " - tide_data.append(row_text) - # format tide data into a string - tide_string = "" - for data in tide_data: - tide_string += data + "\n" - # trim off last newline - tide_string = tide_string[:-1] - return tide_string - - else: - return "error fetching tide data" + try: + station_data = requests.get(station_url, timeout=URL_TIMEOUT) + if not station_data.ok: + return ERROR_FETCHING_DATA + except (requests.exceptions.RequestException): + return ERROR_FETCHING_DATA + + # extract table class="table table-condensed" + soup = bs.BeautifulSoup(station_data.text, 'html.parser') + table = soup.find('table', class_='table table-condensed') + + # extract rows + rows = table.find_all('tr') + # extract data from rows + tide_data = [] + for row in rows: + row_text = "" + cols = row.find_all('td') + for col in cols: + row_text += col.text + " " + tide_data.append(row_text) + # format tide data into a string + tide_string = "" + for data in tide_data: + tide_string += data + "\n" + # trim off last newline + tide_string = tide_string[:-1] + return tide_string def get_weather(lat=0, lon=0, unit=0): weather = "" if float(lat) == 0 and float(lon) == 0: - return "no location data: does your device have GPS?" + return NO_DATA_NOGPS + weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon) + "&unit=" + str(unit) + try: + weather_data = requests.get(weather_url, timeout=URL_TIMEOUT) + if not weather_data.ok: + return ERROR_FETCHING_DATA + except (requests.exceptions.RequestException): + return ERROR_FETCHING_DATA - if unit == 0: - # default to imperial units - weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon) + + soup = bs.BeautifulSoup(weather_data.text, 'html.parser') + table = soup.find('div', id="detailed-forecast-body") + + if table is None: + return "no weather data found on NOAA for your location" else: - # metric units - weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&unit=1&lat=" + str(lat) + "&lon=" + str(lon) - - weather_data = requests.get(weather_url, timeout=URL_TIMEOUT) - if(weather_data.ok): - soup = bs.BeautifulSoup(weather_data.text, 'html.parser') - table = soup.find('div', id="detailed-forecast-body") - - if table is None: - return "no weather data found on NOAA for your location" - else: - # get rows - rows = table.find_all('div', class_="row") - - # extract data from rows - for row in rows: - # shrink the text - line = row.text.replace("Monday", "Mon ") \ - .replace("Tuesday", "Tue ") \ - .replace("Wednesday", "Wed ") \ - .replace("Thursday", "Thu ") \ - .replace("Friday", "Fri ") \ - .replace("Saturday", "Sat ") \ - .replace("Today", "Today ") \ - .replace("Tonight", "Tonight ") \ - .replace("Tomorrow", "Tomorrow ") \ - .replace("This Afternoon", "Afternoon ") \ - .replace("northwest", "NW") \ - .replace("northeast", "NE") \ - .replace("southwest", "SW") \ - .replace("southeast", "SE") \ - .replace("north", "N") \ - .replace("south", "S") \ - .replace("east", "E") \ - .replace("west", "W") \ - .replace("Northwest", "NW") \ - .replace("Northeast", "NE") \ - .replace("Southwest", "SW") \ - .replace("Southeast", "SE") \ - .replace("North", "N") \ - .replace("South", "S") \ - .replace("East", "E") \ - .replace("West", "W") \ - .replace("precipitation", "precip") \ - .replace("showers", "shwrs") \ - .replace("thunderstorms", "t-storms") - # only grab a few days of weather - if len(weather.split("\n")) < DAYS_OF_WEATHER: - weather += line + "\n" - # trim off last newline - weather = weather[:-1] + # get rows + rows = table.find_all('div', class_="row") - return weather + # extract data from rows + for row in rows: + # shrink the text + line = replace_weather(row.text) + # only grab a few days of weather + if len(weather.split("\n")) < DAYS_OF_WEATHER: + weather += line + "\n" + # trim off last newline + weather = weather[:-1] - else: - return "error fetching weather data" - + return weather + +def replace_weather(row): + replacements = { + "Monday": "Mon ", + "Tuesday": "Tue ", + "Wednesday": "Wed ", + "Thursday": "Thu ", + "Friday": "Fri ", + "Saturday": "Sat ", + "Today": "Today ", + "Tonight": "Tonight ", + "Tomorrow": "Tomorrow ", + "This Afternoon": "Afternoon ", + "northwest": "NW", + "northeast": "NE", + "southwest": "SW", + "southeast": "SE", + "north": "N", + "south": "S", + "east": "E", + "west": "W", + "Northwest": "NW", + "Northeast": "NE", + "Southwest": "SW", + "Southeast": "SE", + "North": "N", + "South": "S", + "East": "E", + "West": "W", + "precipitation": "precip", + "showers": "shwrs", + "thunderstorms": "t-storms" + } + + line = row + for key, value in replacements.items(): + line = line.replace(key, value) + + return line diff --git a/locationdata_test.py b/locationdata_test.py new file mode 100644 index 0000000..108372f --- /dev/null +++ b/locationdata_test.py @@ -0,0 +1,101 @@ +import unittest +from locationdata import * + +class TestGetWeather(unittest.TestCase): + + def test_get_weather_with_valid_coordinates(self): + # Test with valid coordinates + lat = "37.7749" + lon = "-122.4194" + weather = get_weather(lat, lon) + print(f"weather: {weather}") + self.assertNotEqual(weather, NO_DATA_NOGPS) + self.assertNotEqual(weather, ERROR_FETCHING_DATA) + # test replacement works + self.assertNotIn(weather, "Sunday") + self.assertNotIn(weather, "Monday") + self.assertNotIn(weather, "Tuesday") + self.assertNotIn(weather, "Wednesday") + self.assertNotIn(weather, "Thursday") + self.assertNotIn(weather, "Friday") + self.assertNotIn(weather, "Saturday") + self.assertNotIn(weather, "northwest") + self.assertNotIn(weather, "northeast") + self.assertNotIn(weather, "southwest") + self.assertNotIn(weather, "southeast") + self.assertNotIn(weather, "north") + self.assertNotIn(weather, "south") + self.assertNotIn(weather, "east") + self.assertNotIn(weather, "west") + self.assertNotIn(weather, "precipitation") + self.assertNotIn(weather, "showers") + self.assertNotIn(weather, "thunderstorms") + + def test_get_weather_with_invalid_coordinates(self): + # Test with invalid coordinates + lat = 0 + lon = 0 + weather = get_weather(lat, lon) + print(f"weather: {weather}") + self.assertEqual(weather, NO_DATA_NOGPS) + + def test_where_am_i_with_valid_coordinates(self): + # Test with invalid coordinates + lat = "37.7749" + lon = "-122.4194" + location = where_am_i(lat, lon) + print(f"location: {location}") + self.assertEqual(location, "South Van Ness Avenue San Francisco California 94103 United States Grid: CM87ss") + self.assertNotEqual(location, NO_DATA_NOGPS) + self.assertNotEqual(location, ERROR_FETCHING_DATA) + + def test_get_tide_with_valid_coordinates(self): + # Test with valid coordinates + lat = "37.7749" + lon = "-122.4194" + tide = get_tide(lat, lon) + print(f"tide: {tide}") + self.assertNotEqual(tide, NO_DATA_NOGPS) + self.assertNotEqual(tide, ERROR_FETCHING_DATA) + + def test_replace_weather(self): + # Test replacing days of the week + self.assertEqual(replace_weather("Monday"), "Mon ") + self.assertEqual(replace_weather("Tuesday"), "Tue ") + self.assertEqual(replace_weather("Wednesday"), "Wed ") + self.assertEqual(replace_weather("Thursday"), "Thu ") + self.assertEqual(replace_weather("Friday"), "Fri ") + self.assertEqual(replace_weather("Saturday"), "Sat ") + + # Test replacing time periods + self.assertEqual(replace_weather("Today"), "Today ") + self.assertEqual(replace_weather("Tonight"), "Tonight ") + self.assertEqual(replace_weather("Tomorrow"), "Tomorrow ") + self.assertEqual(replace_weather("This Afternoon"), "Afternoon ") + + # Test replacing directions + self.assertEqual(replace_weather("northwest"), "NW") + self.assertEqual(replace_weather("northeast"), "NE") + self.assertEqual(replace_weather("southwest"), "SW") + self.assertEqual(replace_weather("southeast"), "SE") + self.assertEqual(replace_weather("north"), "N") + self.assertEqual(replace_weather("south"), "S") + self.assertEqual(replace_weather("east"), "E") + self.assertEqual(replace_weather("west"), "W") + self.assertEqual(replace_weather("Northwest"), "NW") + self.assertEqual(replace_weather("Northeast"), "NE") + self.assertEqual(replace_weather("Southwest"), "SW") + self.assertEqual(replace_weather("Southeast"), "SE") + self.assertEqual(replace_weather("North"), "N") + self.assertEqual(replace_weather("South"), "S") + self.assertEqual(replace_weather("East"), "E") + self.assertEqual(replace_weather("West"), "W") + + # Test replacing weather terms + self.assertEqual(replace_weather("precipitation"), "precip") + self.assertEqual(replace_weather("showers"), "shwrs") + self.assertEqual(replace_weather("thunderstorms"), "t-storms") + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/mesh_bot.py b/mesh_bot.py old mode 100755 new mode 100644 diff --git a/mesh_bot.service b/mesh_bot.service index 6586de6..1c3229f 100644 --- a/mesh_bot.service +++ b/mesh_bot.service @@ -1,16 +1,20 @@ +# /etc/systemd/system/meshbot.service +# sudo systemctl daemon-reload +# sudo systemctl start meshbot.service [Unit] Description=MESH-BOT +After=network.target [Service] -ExecStart=/usr/bin/python /usr/local/meshing-around/mesh-bot.py +ExecStart=/usr/bin/python /usr/local/meshing-around/mesh_bot.py # Disable Python's buffering of STDOUT and STDERR, so that output from the # service shows up immediately in systemd's logs Environment=PYTHONUNBUFFERED=1 Restart=on-failure -Type=notify +Type=notify #try simple if any problems [Install] WantedBy=default.target diff --git a/pong_bot.py b/pong_bot.py old mode 100755 new mode 100644 diff --git a/requirements.txt b/requirements.txt index 370a21d..52fff42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ geopy maidenhead beautifulsoup4 dadjokes +mock \ No newline at end of file