diff --git a/modules/commands/wx_command.py b/modules/commands/wx_command.py index 2af3ccd..69e884e 100644 --- a/modules/commands/wx_command.py +++ b/modules/commands/wx_command.py @@ -126,6 +126,9 @@ class WxCommand(BaseCommand): if last_part == "tomorrow": forecast_type = "tomorrow" location_parts = location_parts[:-1] + elif last_part == "hourly": + forecast_type = "hourly" + location_parts = location_parts[:-1] elif last_part.isdigit(): # Check if it's a number between 2-7 days = int(last_part) @@ -196,7 +199,7 @@ class WxCommand(BaseCommand): Args: location: The location (zipcode or city name) location_type: "zipcode" or "city" - forecast_type: "default", "tomorrow", or "multiday" + forecast_type: "default", "tomorrow", "multiday", or "hourly" num_days: Number of days for multiday forecast (2-7) """ try: @@ -275,15 +278,18 @@ class WxCommand(BaseCommand): if forecast_periods == self.ERROR_FETCHING_DATA: return self.translate('commands.wx.error_fetching') weather = self.format_multiday_forecast(forecast_periods, num_days) + elif forecast_type == "hourly": + hourly_periods, points_data = self.get_noaa_hourly_weather(lat, lon) + if hourly_periods == self.ERROR_FETCHING_DATA: + return self.translate('commands.wx.error_fetching') + weather = self.format_hourly_forecast(hourly_periods) else: # default weather, points_data = self.get_noaa_weather(lat, lon) if weather == self.ERROR_FETCHING_DATA: return self.translate('commands.wx.error_fetching') - # Try to get additional current conditions data - current_conditions = self.get_current_conditions(points_data) - if current_conditions and self._count_display_width(weather) < 120: - weather = f"{weather} {current_conditions}" + # Note: Current conditions are now integrated directly into the current period + # via _add_period_details() using observation station data # Get weather alerts (only for default forecast type to avoid cluttering) if forecast_type == "default": @@ -491,7 +497,6 @@ class WxCommand(BaseCommand): # Add wind info if available if wind_speed and wind_direction: - import re wind_match = re.search(r'(\d+)', wind_speed) if wind_match: wind_num = wind_match.group(1) @@ -499,53 +504,63 @@ class WxCommand(BaseCommand): if wind_dir: weather += f" {wind_dir}{wind_num}" - # Add humidity if available and space allows (using display width) - if humidity and self._count_display_width(weather) < 90: - weather += f" {humidity}%RH" + # PRIORITIZE: Add all available details to current period first + # Get observation station data for more accurate current conditions + observation_data = self.get_observation_data(weather_json) - # Add precipitation chance if available and space allows - if precip_chance and self._count_display_width(weather) < 100: + # Use most of the 130 char limit (120 chars) to ensure current period gets full details + # Additional periods will only be added if there's remaining space + # Pass observation_data to use real-time station data instead of parsing from text + weather = self._add_period_details(weather, detailed_forecast, 0, max_length=120, observation_data=observation_data) + + # Also add precipitation chance if available (not in helper function) + if precip_chance and self._count_display_width(weather) < 120: weather += f" 🌦️{precip_chance}%" - # Add UV index if available and space allows + # Also add UV index if available (not in helper function) uv_index = self.extract_uv_index(detailed_forecast) - if uv_index and self._count_display_width(weather) < 110: + if uv_index and self._count_display_width(weather) < 120: weather += f" UV{uv_index}" - # Add dew point if available and space allows - dew_point = self.extract_dew_point(detailed_forecast) - if dew_point and self._count_display_width(weather) < 120: - weather += f" 💧{dew_point}°" - - # Add visibility if available and space allows - visibility = self.extract_visibility(detailed_forecast) - if visibility and self._count_display_width(weather) < 130: - weather += f" 👁️{visibility}mi" - - # Add precipitation probability if available and space allows - precip_prob = self.extract_precip_probability(detailed_forecast) - if precip_prob and self._count_display_width(weather) < 140: - weather += f" 🌦️{precip_prob}%" - - # Add wind gusts if available and space allows - wind_gusts = self.extract_wind_gusts(detailed_forecast) - if wind_gusts and self._count_display_width(weather) < 140: - weather += f" 💨{wind_gusts}" - - # Add next period (Tonight) and Tomorrow if available - # First, find Tonight and Tomorrow periods + # Add next period (Today, Tonight) and Tomorrow if available + # First, find Today, Tonight, and Tomorrow periods + today_period = None tonight_period = None tomorrow_period = None current_period_name = current.get('name', '').lower() is_current_tonight = 'tonight' in current_period_name + is_current_night = any(word in current_period_name for word in ['tonight', 'overnight', 'night']) + # Check if current period is a night period (Overnight, Tonight, etc.) + # If so, we should prioritize showing the upcoming daytime period (Today) for i, period in enumerate(forecast): period_name = period.get('name', '').lower() - if 'tonight' in period_name and tonight_period is None: + # Look for "Today" period (daytime forecast) + if 'today' in period_name and today_period is None and i > 0: + # Make sure it's not a night period + if 'night' not in period_name and 'tonight' not in period_name: + today_period = (i, period) + elif 'tonight' in period_name and tonight_period is None: tonight_period = (i, period) elif 'tomorrow' in period_name and tomorrow_period is None: tomorrow_period = (i, period) + # If current is a night period and we haven't found Today yet, look for next daytime period + if is_current_night and not today_period: + # Look for the next period that's not a night period + for i, period in enumerate(forecast): + if i > 0: # Skip current period + period_name = period.get('name', '').lower() + # Look for daytime periods (Today, or day names without "night") + if 'today' in period_name and 'night' not in period_name: + today_period = (i, period) + break + # Also check for day names that aren't night periods + day_names = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] + if any(day in period_name for day in day_names) and 'night' not in period_name: + today_period = (i, period) + break + # If current is Tonight and we haven't found Tomorrow yet, look for next day's periods if is_current_tonight and not tomorrow_period: # Look for periods after Tonight (next day) @@ -557,10 +572,10 @@ class WxCommand(BaseCommand): tomorrow_period = (i, period) break - # Add Tonight if it's the immediate next period (and current is not already Tonight) - if tonight_period and tonight_period[0] == 1 and not is_current_tonight: - period = tonight_period[1] - period_name = self.abbreviate_noaa(period.get('name', 'Tonight')) + # If current is a night period, prioritize adding Today (the upcoming daytime) + if is_current_night and today_period: + period = today_period[1] + period_name = self.abbreviate_noaa(period.get('name', 'Today')) period_temp = period.get('temperature', '') period_short = period.get('shortForecast', '') period_detailed = period.get('detailedForecast', '') @@ -581,7 +596,6 @@ class WxCommand(BaseCommand): if period_wind_speed and period_wind_direction: test_str = weather + period_str if self._count_display_width(test_str) < 120: - import re wind_match = re.search(r'(\d+)', period_wind_speed) if wind_match: wind_num = wind_match.group(1) @@ -591,8 +605,75 @@ class WxCommand(BaseCommand): if self._count_display_width(test_str + wind_info) <= 130: period_str += wind_info + # Add additional details (humidity, dew point, visibility, etc.) + # But only if current period isn't too long - prioritize current period details + current_weather_len = self._count_display_width(weather) + # Only add details to additional periods if current period is under 110 chars + # This ensures we prioritize current period details first + if current_weather_len < 110: + period_str = self._add_period_details(period_str, period_detailed, current_weather_len, max_length=130) + # Only add if we have space (using display width) - if self._count_display_width(weather + period_str) <= 130: # Leave room for alerts + # Be more conservative - only add if current period is reasonable length + if current_weather_len < 110 and self._count_display_width(weather + period_str) <= 130: + weather += period_str + + # Add Tonight if it's the immediate next period (and current is not already Tonight) + # If we already added Today, we can still add Tonight if it's the next period after Today + if tonight_period and not is_current_tonight: + # Only add if it's the immediate next period, or if current is night and we haven't added Today yet + should_add_tonight = False + if is_current_night and today_period: + # If current is night and we added Today, check if Tonight comes after Today + if tonight_period[0] > today_period[0]: + should_add_tonight = True + elif tonight_period[0] == 1: + # If current is not night, Tonight should be the immediate next period + should_add_tonight = True + + if should_add_tonight: + period = tonight_period[1] + period_name = self.abbreviate_noaa(period.get('name', 'Tonight')) + period_temp = period.get('temperature', '') + period_short = period.get('shortForecast', '') + period_detailed = period.get('detailedForecast', '') + period_wind_speed = period.get('windSpeed', '') + period_wind_direction = period.get('windDirection', '') + + if period_temp and period_short: + # Try to get high/low + period_high_low = self.extract_high_low(period_detailed) + + period_emoji = self.get_weather_emoji(period_short) + if period_high_low: + period_str = f" | {period_name}: {period_emoji}{period_short} {period_high_low}" + else: + period_str = f" | {period_name}: {period_emoji}{period_short} {period_temp}°" + + # Add wind info if space allows (using display width) + if period_wind_speed and period_wind_direction: + test_str = weather + period_str + if self._count_display_width(test_str) < 120: + wind_match = re.search(r'(\d+)', period_wind_speed) + if wind_match: + wind_num = wind_match.group(1) + wind_dir = self.abbreviate_wind_direction(period_wind_direction) + if wind_dir: + wind_info = f" {wind_dir}{wind_num}" + if self._count_display_width(test_str + wind_info) <= 130: + period_str += wind_info + + # Add additional details (humidity, dew point, visibility, etc.) + # But only if current period isn't too long - prioritize current period details + current_weather_len = self._count_display_width(weather) + # Only add details to additional periods if current period is under 110 chars + # This ensures we prioritize current period details first + if current_weather_len < 110: + period_str = self._add_period_details(period_str, period_detailed, current_weather_len, max_length=130) + + # Only add if we have space (using display width) + # Be more conservative - only add if current period is reasonable length + if current_weather_len < 110 and self._count_display_width(weather + period_str) <= 130: weather += period_str # Always try to add Tomorrow if available (especially if current is Tonight) @@ -610,9 +691,9 @@ class WxCommand(BaseCommand): # Try to get high/low for tomorrow period_high_low = self.extract_high_low(period_detailed) - # Abbreviate forecast text if it's too long (especially when current is Tonight) + # Abbreviate forecast text if it's too long (especially when current is a night period) abbreviated_forecast = period_short - if is_current_tonight and len(period_short) > 20: + if (is_current_tonight or is_current_night) and len(period_short) > 20: # Try to shorten forecast text to fit more info # Remove transitional words and keep meaningful conditions words = period_short.split() @@ -654,12 +735,11 @@ class WxCommand(BaseCommand): period_str = f" | {period_name}: {period_emoji}{abbreviated_forecast} {period_temp}°" # Add wind info if space allows (using display width) - # Be more aggressive about adding wind when current is Tonight - wind_threshold = 115 if is_current_tonight else 120 + # Be more aggressive about adding wind when current is a night period + wind_threshold = 115 if (is_current_tonight or is_current_night) else 120 if period_wind_speed and period_wind_direction: test_str = weather + period_str if self._count_display_width(test_str) < wind_threshold: - import re wind_match = re.search(r'(\d+)', period_wind_speed) if wind_match: wind_num = wind_match.group(1) @@ -669,10 +749,19 @@ class WxCommand(BaseCommand): if self._count_display_width(test_str + wind_info) <= 130: period_str += wind_info - # Only add if we have space (using display width, prioritize tomorrow) - # Be more aggressive when current is Tonight - use up to 128 chars (leave 2 for alerts) - max_chars = 128 if is_current_tonight else 130 - if self._count_display_width(weather + period_str) <= max_chars: + # Add additional details (humidity, dew point, visibility, etc.) + # But only if current period isn't too long - prioritize current period details + current_weather_len = self._count_display_width(weather) + # Only add details to additional periods if current period is under 110 chars + # This ensures we prioritize current period details first + if current_weather_len < 110: + max_chars = 128 if (is_current_tonight or is_current_night) else 130 + period_str = self._add_period_details(period_str, period_detailed, current_weather_len, max_chars) + + # Only add if we have space (using display width, prioritize current period) + # Be more conservative - only add if current period is reasonable length + max_chars = 128 if (is_current_tonight or is_current_night) else 130 + if current_weather_len < 110 and self._count_display_width(weather + period_str) <= max_chars: weather += period_str return weather, weather_json @@ -681,6 +770,203 @@ class WxCommand(BaseCommand): self.logger.error(f"Error fetching NOAA weather: {e}") return self.ERROR_FETCHING_DATA, None + def get_noaa_hourly_weather(self, lat: float, lon: float) -> tuple: + """Get hourly weather forecast from NOAA + + Args: + lat: Latitude + lon: Longitude + + Returns: + Tuple of (hourly_periods_list, points_data) + """ + try: + # Get weather data from NOAA + weather_api = f"https://api.weather.gov/points/{lat},{lon}" + + # Get the forecast URL + weather_data = requests.get(weather_api, timeout=self.url_timeout) + if not weather_data.ok: + self.logger.warning("Error fetching weather data from NOAA") + return self.ERROR_FETCHING_DATA, None + + weather_json = weather_data.json() + hourly_forecast_url = weather_json['properties'].get('forecastHourly') + + if not hourly_forecast_url: + self.logger.warning("Hourly forecast not available for this location") + return self.ERROR_FETCHING_DATA, None + + # Get the hourly forecast + hourly_data = requests.get(hourly_forecast_url, timeout=self.url_timeout) + if not hourly_data.ok: + self.logger.warning("Error fetching hourly forecast from NOAA") + return self.ERROR_FETCHING_DATA, None + + hourly_json = hourly_data.json() + hourly_periods = hourly_json['properties']['periods'] + + if not hourly_periods: + self.logger.warning("No hourly periods returned from NOAA") + return self.ERROR_FETCHING_DATA, None + + return hourly_periods, weather_json + + except Exception as e: + self.logger.error(f"Error fetching NOAA hourly weather: {e}") + return self.ERROR_FETCHING_DATA, None + + def format_hourly_forecast(self, hourly_periods: list) -> str: + """Format hourly forecast to fit as many hours as possible in 130 chars + + Args: + hourly_periods: List of hourly forecast periods from NOAA + + Returns: + Formatted string with one hour per line + """ + try: + if not hourly_periods: + return self.translate('commands.wx.hourly_not_available') + + lines = [] + current_length = 0 + max_length = 130 + + # Filter to only future hours + now = datetime.now() + future_periods = [] + for period in hourly_periods: + start_time_str = period.get('startTime', '') + if start_time_str: + try: + # Parse ISO format with timezone + if 'Z' in start_time_str: + start_time = datetime.fromisoformat(start_time_str.replace('Z', '+00:00')) + else: + start_time = datetime.fromisoformat(start_time_str) + + # Convert to local timezone if needed + if start_time.tzinfo: + # Make naive for comparison + start_time = start_time.replace(tzinfo=None) + + if start_time > now: + future_periods.append(period) + except (ValueError, TypeError): + # If parsing fails, include it anyway + future_periods.append(period) + else: + # If no startTime, include it + future_periods.append(period) + + if not future_periods: + return "No future hourly periods available" + + # Format each hour + for period in future_periods: + start_time_str = period.get('startTime', '') + temp = period.get('temperature', '') + temp_unit = period.get('temperatureUnit', 'F') + short_forecast = period.get('shortForecast', '') + wind_speed = period.get('windSpeed', '') + wind_direction = period.get('windDirection', '') + precip_prob = period.get('probabilityOfPrecipitation', {}).get('value') + + # Format time (e.g., "2PM", "10AM") + time_str = "" + if start_time_str: + try: + # Parse ISO format - handle timezone + if 'Z' in start_time_str: + dt = datetime.fromisoformat(start_time_str.replace('Z', '+00:00')) + elif '+' in start_time_str or start_time_str.count('-') > 2: + # Has timezone info + dt = datetime.fromisoformat(start_time_str) + else: + # No timezone, parse as naive + dt = datetime.fromisoformat(start_time_str) + + # Extract hour (assume it's already in local time or close enough) + hour = dt.hour + + # Format as 12-hour time + if hour == 0: + time_str = "12AM" + elif hour < 12: + time_str = f"{hour}AM" + elif hour == 12: + time_str = "12PM" + else: + time_str = f"{hour-12}PM" + except (ValueError, TypeError): + time_str = "" + + # Build hour line: "10AM: 🌦️ 26% Chance Light Rain 49° SS5" + emoji = self.get_weather_emoji(short_forecast) + + # Abbreviate forecast if too long + forecast_short = short_forecast + if len(forecast_short) > 18: + # Take first 2-3 words + words = forecast_short.split() + if len(words) > 3: + forecast_short = ' '.join(words[:3]) + else: + forecast_short = forecast_short[:18] + + # Build the line - format: "10AM: 🌦️ 26% Chance Light Rain 49° SS5" + line_parts = [] + if time_str: + line_parts.append(f"{time_str}:") + + # Add emoji + line_parts.append(emoji) + + # Add precip probability if > 0% (before forecast text) + if precip_prob is not None and precip_prob > 0: + line_parts.append(f"{precip_prob}%") + + # Add forecast text + line_parts.append(forecast_short) + + # Add temperature + if temp: + line_parts.append(f"{temp}°") + + # Add wind if available (use compact format) + if wind_speed and wind_direction: + wind_match = re.search(r'(\d+)', wind_speed) + if wind_match: + wind_num = wind_match.group(1) + # Get direction abbreviation (first 1-2 chars) + wind_dir_abbrev = wind_direction[:2] if len(wind_direction) >= 2 else wind_direction + # Remove any spaces and make uppercase + wind_dir_abbrev = wind_dir_abbrev.replace(' ', '').upper() + line_parts.append(f"{wind_dir_abbrev}{wind_num}") + + line = " ".join(line_parts) + + # Check if adding this line would exceed limit + test_lines = lines + [line] + test_message = "\n".join(test_lines) + test_length = self._count_display_width(test_message) + + if test_length <= max_length: + lines.append(line) + else: + # This line would exceed limit, stop here + break + + if not lines: + return "Hourly forecast not available" + + return "\n".join(lines) + + except Exception as e: + self.logger.error(f"Error formatting hourly forecast: {e}") + return f"Error formatting hourly forecast: {str(e)}" + def format_tomorrow_forecast(self, forecast: list) -> str: """Format a detailed forecast for tomorrow""" try: @@ -749,7 +1035,6 @@ class WxCommand(BaseCommand): # Add wind info if wind_speed and wind_direction: - import re wind_match = re.search(r'(\d+)', wind_speed) if wind_match: wind_num = wind_match.group(1) @@ -909,9 +1194,97 @@ class WxCommand(BaseCommand): self.logger.error(f"Error formatting {num_days}-day forecast: {e}") return self.translate('commands.wx.multiday_error', num_days=num_days) + def _add_period_details(self, period_str: str, detailed_forecast: str, current_weather_length: int, max_length: int = 130, observation_data: dict = None) -> str: + """Add additional details (humidity, dew point, visibility, etc.) to a period string + + Args: + period_str: The base period string (e.g., " | Today: ☀️Sunny 75°") + detailed_forecast: The detailed forecast text to extract info from + current_weather_length: Current length of the weather string (to check total length) + max_length: Maximum total length allowed (default 130) + observation_data: Optional dict with observation station data (humidity, dew_point, visibility, wind_gusts, pressure) + + Returns: + Updated period string with additional details if space allows + """ + result = period_str + current_length = current_weather_length + self._count_display_width(result) + + # Extract additional details - prefer observation data if available (more accurate) + if observation_data: + humidity = observation_data.get('humidity') + dew_point = observation_data.get('dew_point') + visibility = observation_data.get('visibility') + wind_gusts = observation_data.get('wind_gusts') + pressure = observation_data.get('pressure') + else: + humidity = None + dew_point = None + visibility = None + wind_gusts = None + pressure = None + + # Fall back to parsing from detailed forecast if observation data not available + if not humidity: + humidity = self.extract_humidity(detailed_forecast) + if not dew_point: + dew_point = self.extract_dew_point(detailed_forecast) + if not visibility: + visibility = self.extract_visibility(detailed_forecast) + if not wind_gusts: + wind_gusts = self.extract_wind_gusts(detailed_forecast) + if not pressure: + pressure = self.extract_pressure(detailed_forecast) + + # Always try to get precip_prob from detailed forecast (not in observation data) + precip_prob = self.extract_precip_probability(detailed_forecast) + + # Add humidity if available and space allows + # Try to add all available details, only skip if they would exceed max_length + if humidity: + humidity_str = f" {humidity}%RH" + if self._count_display_width(result + humidity_str) + current_weather_length <= max_length: + result += humidity_str + current_length = current_weather_length + self._count_display_width(result) + + # Add dew point if available and space allows + if dew_point: + dew_str = f" 💧{dew_point}°" + if self._count_display_width(result + dew_str) + current_weather_length <= max_length: + result += dew_str + current_length = current_weather_length + self._count_display_width(result) + + # Add visibility if available and space allows + if visibility: + vis_str = f" 👁️{visibility}mi" + if self._count_display_width(result + vis_str) + current_weather_length <= max_length: + result += vis_str + current_length = current_weather_length + self._count_display_width(result) + + # Add precipitation probability if available and space allows + if precip_prob: + precip_str = f" 🌦️{precip_prob}%" + if self._count_display_width(result + precip_str) + current_weather_length <= max_length: + result += precip_str + current_length = current_weather_length + self._count_display_width(result) + + # Add wind gusts if available and space allows + if wind_gusts: + gust_str = f" 💨{wind_gusts}" + if self._count_display_width(result + gust_str) + current_weather_length <= max_length: + result += gust_str + current_length = current_weather_length + self._count_display_width(result) + + # Add pressure if available and space allows + if pressure: + pressure_str = f" 📊{pressure}hPa" + if self._count_display_width(result + pressure_str) + current_weather_length <= max_length: + result += pressure_str + + return result + def _count_display_width(self, text: str) -> int: """Count display width of text, accounting for emojis which may take 2 display units""" - import re # Count regular characters width = len(text) # Emojis typically take 2 display units in terminals/clients @@ -1127,7 +1500,6 @@ class WxCommand(BaseCommand): if not text: return "" - import re # Look for patterns like "humidity 45%" or "45% humidity" humidity_patterns = [ r'humidity\s+(\d+)%', @@ -1148,7 +1520,6 @@ class WxCommand(BaseCommand): if not text: return "" - import re # Look for patterns like "20% chance" or "chance of rain 30%" precip_patterns = [ r'(\d+)%\s+chance', @@ -1169,7 +1540,6 @@ class WxCommand(BaseCommand): if not text: return "" - import re # Look for more specific patterns to avoid false matches high_low_patterns = [ r'high\s+near\s+(\d+).*?low\s+around\s+(\d+)', @@ -1211,7 +1581,6 @@ class WxCommand(BaseCommand): if not text: return "" - import re # Look for UV index patterns uv_patterns = [ r'uv\s+index\s+(\d+)', @@ -1237,7 +1606,6 @@ class WxCommand(BaseCommand): if not text: return "" - import re # Look for dew point patterns dew_point_patterns = [ r'dew point\s+(\d+)', @@ -1263,7 +1631,6 @@ class WxCommand(BaseCommand): if not text: return "" - import re # Look for visibility patterns visibility_patterns = [ r'visibility\s+(\d+)\s+miles', @@ -1290,7 +1657,6 @@ class WxCommand(BaseCommand): if not text: return "" - import re # Look for precipitation probability patterns precip_prob_patterns = [ r'(\d+)%\s+chance\s+of\s+(?:rain|precipitation|showers)', @@ -1319,7 +1685,6 @@ class WxCommand(BaseCommand): if not text: return "" - import re # Look for wind gust patterns gust_patterns = [ r'gusts\s+to\s+(\d+)\s+mph', @@ -1342,26 +1707,61 @@ class WxCommand(BaseCommand): continue return "" + + def extract_pressure(self, text: str) -> str: + """Extract barometric pressure from forecast text""" + if not text: + return "" + + # Look for pressure patterns (hPa, mb, inches of mercury) + pressure_patterns = [ + r'pressure\s+(\d+)\s*hpa', + r'pressure\s+(\d+)\s*mb', + r'barometric\s+pressure\s+(\d+)\s*hpa', + r'barometric\s+pressure\s+(\d+)\s*mb', + r'(\d+)\s*hpa', + r'(\d+)\s*mb\s+pressure' + ] + + for pattern in pressure_patterns: + match = re.search(pattern, text.lower()) + if match: + pressure_val = match.group(1) + # Validate pressure (reasonable range 600-1100 hPa/mb) + # Normal sea level is ~1013 hPa, but high elevation locations can be lower + try: + pressure_int = int(pressure_val) + if 600 <= pressure_int <= 1100: + return pressure_val + except ValueError: + continue + + return "" - def get_current_conditions(self, points_data: dict) -> str: - """Get additional current conditions data from NOAA using existing points data""" + def get_observation_data(self, points_data: dict) -> dict: + """Get observation station data from NOAA and return as a dict + + Returns: + Dict with keys: humidity, dew_point, visibility, wind_gusts, pressure + Values are strings ready for display, or None if not available + """ try: if not points_data: - return "" + return {} weather_json = points_data station_url = weather_json['properties'].get('observationStations') if not station_url: - return "" + return {} # Get the nearest station stations_data = requests.get(station_url, timeout=self.url_timeout) if not stations_data.ok: - return "" + return {} stations_json = stations_data.json() if not stations_json.get('features'): - return "" + return {} # Get current observations from the nearest station station_id = stations_json['features'][0]['properties']['stationIdentifier'] @@ -1369,43 +1769,75 @@ class WxCommand(BaseCommand): obs_data = requests.get(obs_url, timeout=self.url_timeout) if not obs_data.ok: - return "" + return {} obs_json = obs_data.json() if not obs_json.get('properties'): - return "" + return {} props = obs_json['properties'] - conditions = [] + obs_data_dict = {} - # Extract useful current conditions with emojis - if props.get('relativeHumidity', {}).get('value'): - humidity = int(props['relativeHumidity']['value']) - conditions.append(f"{humidity}%RH") + # Extract useful current conditions + # Check for None explicitly to handle cases where value exists but is None + humidity_val = props.get('relativeHumidity', {}).get('value') + if humidity_val is not None: + humidity = int(humidity_val) + obs_data_dict['humidity'] = str(humidity) - if props.get('dewpoint', {}).get('value'): - dewpoint = int(props['dewpoint']['value'] * 9/5 + 32) # Convert C to F - conditions.append(f"💧{dewpoint}°") + dewpoint_val = props.get('dewpoint', {}).get('value') + if dewpoint_val is not None: + dewpoint = int(dewpoint_val * 9/5 + 32) # Convert C to F + obs_data_dict['dew_point'] = str(dewpoint) - if props.get('visibility', {}).get('value'): - visibility = int(props['visibility']['value'] * 0.000621371) # Convert m to miles + visibility_val = props.get('visibility', {}).get('value') + if visibility_val is not None: + visibility = int(visibility_val * 0.000621371) # Convert m to miles if visibility > 0: - conditions.append(f"👁️{visibility}mi") + obs_data_dict['visibility'] = str(visibility) - if props.get('windGust', {}).get('value'): - wind_gust = int(props['windGust']['value'] * 2.237) # Convert m/s to mph + wind_gust_val = props.get('windGust', {}).get('value') + if wind_gust_val is not None: + wind_gust = int(wind_gust_val * 2.237) # Convert m/s to mph if wind_gust > 10: - conditions.append(f"💨{wind_gust}") + obs_data_dict['wind_gusts'] = str(wind_gust) - if props.get('barometricPressure', {}).get('value'): - pressure = int(props['barometricPressure']['value'] / 100) # Convert Pa to hPa - conditions.append(f"📊{pressure}hPa") + pressure_val = props.get('barometricPressure', {}).get('value') + if pressure_val is not None: + pressure = int(pressure_val / 100) # Convert Pa to hPa + obs_data_dict['pressure'] = str(pressure) - return " ".join(conditions[:3]) # Limit to 3 conditions to avoid overflow + return obs_data_dict except Exception as e: - self.logger.debug(f"Error getting current conditions: {e}") + self.logger.debug(f"Error getting observation data: {e}") + return {} + + def get_current_conditions(self, points_data: dict) -> str: + """Get additional current conditions data from NOAA using existing points data (legacy method)""" + obs_data = self.get_observation_data(points_data) + if not obs_data: return "" + + conditions = [] + + # Build conditions list in priority order + if 'humidity' in obs_data: + conditions.append(f"{obs_data['humidity']}%RH") + + if 'dew_point' in obs_data: + conditions.append(f"💧{obs_data['dew_point']}°") + + if 'visibility' in obs_data: + conditions.append(f"👁️{obs_data['visibility']}mi") + + if 'wind_gusts' in obs_data: + conditions.append(f"💨{obs_data['wind_gusts']}") + + if 'pressure' in obs_data: + conditions.append(f"📊{obs_data['pressure']}hPa") + + return " ".join(conditions[:3]) # Limit to 3 conditions to avoid overflow def get_weather_emoji(self, condition: str) -> str: """Get emoji for weather condition"""