diff --git a/weather_forecast/models/weather_api.py b/weather_forecast/models/weather_api.py index ff2f37e..9ffee41 100644 --- a/weather_forecast/models/weather_api.py +++ b/weather_forecast/models/weather_api.py @@ -1,15 +1,30 @@ from odoo import fields, models, api, _ -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError import requests from collections import defaultdict from datetime import datetime +import logging + +_logger = logging.getLogger(__name__) class WeatherAPI(models.Model): _name = 'weather.api' - _description = 'Weather API' + _description = 'Weather API Integration' _rec_name = 'company_id' - company_id = fields.Many2one('res.company', string='Công ty', required=True, default=lambda self: self.env.company) + # Thêm trường mới để cấu hình timeout + api_timeout = fields.Integer( + string="API Timeout (seconds)", + default=10, + help="Timeout for API requests in seconds" + ) + + company_id = fields.Many2one( + 'res.company', + string='Công ty', + required=True, + default=lambda self: self.env.company + ) current_temperature = fields.Float(string="Nhiệt độ hiện tại", compute="_compute_weather", store=False) current_humidity = fields.Float(string="Độ ẩm hiện tại", compute="_compute_weather", store=False) @@ -21,172 +36,259 @@ class WeatherAPI(models.Model): today_hourly_third = fields.Html(string="17h - 24h", compute="_compute_weather", store=False) next_3_days_weather = fields.Html(string="Dự báo 3 ngày", compute="_compute_weather", store=False) - + @api.depends() def _compute_weather(self): for rec in self: - current = rec.get_weather_current() - rec.current_temperature = current.get('temperature') - rec.current_humidity = current.get('humidity') - rec.current_wind_speed = current.get('wind_speed') - rec.current_description = current.get('description') + try: + current = rec.get_weather_current() + rec.current_temperature = current.get('temperature') + rec.current_humidity = current.get('humidity') + rec.current_wind_speed = current.get('wind_speed') + rec.current_description = current.get('description') - # Weather by time blocks - today = rec._get_today_weather() - rec.today_hourly_first = rec._format_html(today['first']) - rec.today_hourly_second = rec._format_html(today['second']) - rec.today_hourly_third = rec._format_html(today['third']) + # Weather by time blocks + today = rec._get_today_weather() + rec.today_hourly_first = rec._format_html(today['first']) + rec.today_hourly_second = rec._format_html(today['second']) + rec.today_hourly_third = rec._format_html(today['third']) - # Next 3 days - forecast = rec._get_next_3days_forecast() - html = "" - rec.next_3_days_weather = html + # Next 3 days + forecast = rec._get_next_3days_forecast() + rec.next_3_days_weather = rec._format_3days_html(forecast) + + except Exception as e: + _logger.error("Error computing weather data: %s", str(e)) + # Set default values when error occurs + rec._set_default_weather_values() + + def _set_default_weather_values(self): + """Set default values when API fails""" + self.update({ + 'current_temperature': 0.0, + 'current_humidity': 0.0, + 'current_wind_speed': 0.0, + 'current_description': _("Không thể lấy dữ liệu thời tiết"), + 'today_hourly_first': "

Không có dữ liệu

", + 'today_hourly_second': "

Không có dữ liệu

", + 'today_hourly_third': "

Không có dữ liệu

", + 'next_3_days_weather': "

Không có dữ liệu

", + }) def _format_html(self, data): if not data: return "

Không có dữ liệu.

" html = "" return html + def _format_3days_html(self, forecast_data): + if not forecast_data: + return "

Không có dữ liệu.

" + + html = """ + +
+ """ + + for day in forecast_data: + html += f""" +
+

{day['date']}

+

Tình trạng: {day['main_description']}

+

Nhiệt độ: {day['temp_min']}°C - {day['temp_max']}°C

+

Gió: {day['wind_speed']} m/s

+

Độ ẩm: {day['humidity']}%

+

+ """ + html += "
" + return html - # Lấy thông tin API key từ cấu hình @api.model def get_api_key(self): - """Lấy thông tin API key từ cấu hình""" + """Lấy thông tin API key từ cấu hình với kiểm tra bảo mật""" api_key = self.env['ir.config_parameter'].sudo().get_param('weather_infor.api_key') if not api_key: - raise UserError("API Key chưa được cấu hình. Vui lòng vào Settings > Weather API Settings để nhập API Key.") + raise ValidationError(_("API Key chưa được cấu hình. Vui lòng vào Settings > Weather API Settings để nhập API Key.")) return api_key - - # Lấy thông tin thành phố từ cấu hình công ty @api.model def get_city(self): - """Lấy thông tin thành phố từ cấu hình công ty""" + """Lấy thông tin thành phố từ cấu hình công ty với validation""" city = self.env.company.city if not city: - raise UserError(_("Thành phố chưa được cấu hình. Vui lòng vào Settings > Công ty để nhập thành phố.")) - return city - + raise ValidationError(_("Thành phố chưa được cấu hình. Vui lòng vào Settings > Công ty để nhập thành phố.")) + return city.strip() + + def _make_api_request(self, url): + """Helper method to make API requests with proper error handling""" + try: + response = requests.get( + url, + timeout=self.api_timeout, + headers={'User-Agent': 'Odoo/1.0'} + ) + response.raise_for_status() # Raises HTTPError for bad responses + return response.json() + except requests.exceptions.Timeout: + _logger.error("Weather API timeout after %s seconds", self.api_timeout) + raise UserError(_("Dịch vụ thời tiết không phản hồi. Vui lòng thử lại sau.")) + except requests.exceptions.RequestException as e: + _logger.error("Weather API request failed: %s", str(e)) + raise UserError(_("Không thể kết nối đến dịch vụ thời tiết. Vui lòng kiểm tra kết nối mạng.")) + except ValueError as e: # Includes JSON decode errors + _logger.error("Invalid JSON response from weather API: %s", str(e)) + raise UserError(_("Dữ liệu thời tiết nhận được không hợp lệ.")) - # Lấy thông tin thời tiết hiện tại @api.model def get_weather_current(self): - """Lấy dữ liệu thời tiết hiện tại""" - api_key = self.get_api_key() - city = self.get_city() - url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric" - - response = requests.get(url) - if response.status_code == 200: - data = response.json() + """Lấy dữ liệu thời tiết hiện tại với xử lý lỗi tốt hơn""" + try: + api_key = self.get_api_key() + city = self.get_city() + url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric&lang=vi" + + data = self._make_api_request(url) + return { 'temperature': data['main']['temp'], 'humidity': data['main']['humidity'], 'wind_speed': data['wind']['speed'], - 'description': data['weather'][0]['description'] + 'description': data['weather'][0]['description'].capitalize() } - else: - raise UserError(_("Không thể lấy dữ liệu thời tiết. Vui lòng kiểm tra lại API key hoặc tên thành phố.")) - + except KeyError as e: + _logger.error("Missing expected data in weather API response: %s", str(e)) + raise UserError(_("Dữ liệu thời tiết không đầy đủ. Vui lòng thử lại sau.")) - # Lấy thông tin thời tiết 3 ngày @api.model def _get_next_3days_forecast(self): - """Lấy dữ liệu thời tiết cho 3 ngày tiếp theo""" - api_key = self.get_api_key() - city = self.get_city() - url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric" + """Lấy dữ liệu thời tiết cho 3 ngày tiếp theo với xử lý dữ liệu tốt hơn""" + try: + api_key = self.get_api_key() + city = self.get_city() + url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric&lang=vi" + + data = self._make_api_request(url) + forecast_data = data['list'] - response = requests.get(url) - if response.status_code != 200: - raise UserError(_("Không thể lấy dữ liệu thời tiết. Vui lòng kiểm tra lại API key hoặc tên thành phố.")) - - data = response.json() - forecast_data = data['list'] + # Gom dữ liệu theo ngày + weather_data = defaultdict(list) + for forecast in forecast_data: + try: + dt_txt = forecast['dt_txt'] + date_str = dt_txt.split(' ')[0] + weather_data[date_str].append(forecast) + except (KeyError, AttributeError) as e: + _logger.warning("Invalid forecast data structure: %s", str(e)) + continue - # Gom dữ liệu theo ngày - weather_data = defaultdict(list) - for forecast in forecast_data: - dt_txt = forecast['dt_txt'] - date_str = dt_txt.split(' ')[0] - weather_data[date_str].append(forecast) + # Chọn 3 ngày tiếp theo (bỏ qua hôm nay) + today = datetime.now().date().strftime('%Y-%m-%d') + sorted_dates = sorted([d for d in weather_data.keys() if d != today]) + + if not sorted_dates: + return [] + + next_3_days = sorted_dates[:3] # Lấy 3 ngày đầu tiên sau hôm nay + + result = [] + for date in next_3_days: + daily_forecasts = weather_data[date] + if not daily_forecasts: + continue + + temps = [f['main']['temp'] for f in daily_forecasts] + humidities = [f['main']['humidity'] for f in daily_forecasts] + descriptions = [f['weather'][0]['description'] for f in daily_forecasts] + wind_speeds = [f['wind']['speed'] for f in daily_forecasts] - # Chọn 3 ngày tiếp theo - sorted_dates = sorted(weather_data.keys()) - today = datetime.now().date() - if today in sorted_dates: - sorted_dates.remove(today) - next_3_days = sorted_dates[1:4] + result.append({ + 'date': date, + 'temp_min': min(temps), + 'temp_max': max(temps), + 'humidity': round(sum(humidities) / len(humidities), 2), + 'wind_speed': round(sum(wind_speeds) / len(wind_speeds), 2), + 'main_description': max(set(descriptions), key=descriptions.count).capitalize(), + }) + return result - result = [] - for date in next_3_days: - daily_forecasts = weather_data[date] - temps = [f['main']['temp'] for f in daily_forecasts] - humidities = [f['main']['humidity'] for f in daily_forecasts] - descriptions = [f['weather'][0]['description'] for f in daily_forecasts] - wind_speeds = [f['wind']['speed'] for f in daily_forecasts] + except Exception as e: + _logger.error("Error getting 3-day forecast: %s", str(e)) + raise UserError(_("Không thể lấy dự báo thời tiết. Vui lòng thử lại sau.")) - result.append({ - 'date': date, - 'temp_min': min(temps), - 'temp_max': max(temps), - 'humidity': round(sum(humidities) / len(humidities), 2), - 'wind_speed': round(sum(wind_speeds) / len(wind_speeds), 2), - 'main_description': max(set(descriptions), key=descriptions.count), # mô tả xuất hiện nhiều nhất - }) - return result - - - # Lấy thông tin thời tiết ngày hôm nay, chia làm 3 khung giờ @api.model def _get_today_weather(self): - """Lấy dữ liệu thời tiết ngày hôm nay chia làm 3 khung giờ: 1-8h, 9-16h, 17-24h""" - api_key = self.get_api_key() - city = self.env.company.city - url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric" + """Lấy dữ liệu thời tiết ngày hôm nay chia làm 3 khung giờ""" + try: + api_key = self.get_api_key() + city = self.get_city() + url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric&lang=vi" + + data = self._make_api_request(url) + forecast_data = data['list'] - response = requests.get(url) - if response.status_code != 200: - raise UserError(_("Không thể lấy dữ liệu thời tiết. Vui lòng kiểm tra lại API key hoặc tên thành phố.")) - - data = response.json() - forecast_data = data['list'] - - today_str = datetime.now().strftime('%Y-%m-%d') - hourly_data = { - 'first': [], # 1-8h - 'second': [], # 9-16h - 'third': [] # 17-24h - } - - for forecast in forecast_data: - dt_txt = forecast['dt_txt'] - date_part, time_part = dt_txt.split(' ') - if date_part != today_str: - continue # chỉ lấy dữ liệu của ngày hôm nay - - hour = int(time_part.split(':')[0]) - weather_info = { - 'hour': hour, - 'date': date_part, - 'temp_min': forecast['main']['temp_min'], - 'temp_max': forecast['main']['temp_max'], - 'humidity': forecast['main']['humidity'], - 'wind_speed': forecast['wind']['speed'], - 'description': forecast['weather'][0]['description'] + today_str = datetime.now().strftime('%Y-%m-%d') + hourly_data = { + 'first': [], # 1-8h + 'second': [], # 9-16h + 'third': [] # 17-24h } - if 0 <= hour <= 8: - hourly_data['first'].append(weather_info) - elif 9 <= hour <= 16: - hourly_data['second'].append(weather_info) - else: - hourly_data['third'].append(weather_info) - return hourly_data + for forecast in forecast_data: + try: + dt_txt = forecast['dt_txt'] + date_part, time_part = dt_txt.split(' ') + if date_part != today_str: + continue + + hour = int(time_part.split(':')[0]) + weather_info = { + 'hour': hour, + 'date': date_part, + 'temp_min': forecast['main']['temp_min'], + 'temp_max': forecast['main']['temp_max'], + 'humidity': forecast['main']['humidity'], + 'wind_speed': forecast['wind']['speed'], + 'description': forecast['weather'][0]['description'].capitalize() + } + + if 0 <= hour <= 8: + hourly_data['first'].append(weather_info) + elif 9 <= hour <= 16: + hourly_data['second'].append(weather_info) + else: + hourly_data['third'].append(weather_info) + except (KeyError, ValueError) as e: + _logger.warning("Invalid hourly forecast data: %s", str(e)) + continue + + return hourly_data + + except Exception as e: + _logger.error("Error getting today's weather: %s", str(e)) + return { + 'first': [], + 'second': [], + 'third': [] + } \ No newline at end of file diff --git a/weather_forecast/static/description/icon.png b/weather_forecast/static/description/icon.png new file mode 100644 index 0000000..0cc7518 Binary files /dev/null and b/weather_forecast/static/description/icon.png differ diff --git a/weather_forecast/static/description/icon.svg b/weather_forecast/static/description/icon.svg new file mode 100644 index 0000000..5168cab --- /dev/null +++ b/weather_forecast/static/description/icon.svg @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/weather_forecast/views/menu.xml b/weather_forecast/views/menu.xml index 63cb1a1..6259109 100644 --- a/weather_forecast/views/menu.xml +++ b/weather_forecast/views/menu.xml @@ -1,7 +1,7 @@ -