Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import requests
- import json
- from datetime import datetime
- from threading import Thread
- from kivy.metrics import dp
- from kivy.clock import Clock
- from kivy.lang import Builder
- from kivy.utils import platform
- from kivy.uix.scrollview import ScrollView
- from dateutil import parser
- from kivymd.app import MDApp
- from kivymd.uix.screen import MDScreen
- from kivymd.uix.boxlayout import MDBoxLayout
- from kivymd.uix.label import MDLabel
- from kivymd.uix.toolbar import MDTopAppBar
- from kivymd.uix.card import MDCard
- from kivymd.uix.progressbar import MDProgressBar
- from kivymd.uix.refreshlayout import MDScrollViewRefreshLayout
- Builder.load_string('''
- <WeatherScreen>:
- MDScrollViewRefreshLayout:
- id: refresh_layout
- refresh_callback: root.refresh
- root_layout: root
- MDScrollView:
- MDBoxLayout:
- orientation: 'vertical'
- spacing: dp(20)
- padding: dp(20)
- adaptive_height: True
- MDTopAppBar:
- title: "Weather Forecast"
- elevation: 0
- md_bg_color: app.theme_cls.primary_color
- specific_text_color: 1, 1, 1, 1
- left_action_items: [["arrow-left", lambda x: app.go_to_main_menu()]]
- right_action_items: [["refresh", lambda x: root.refresh()]]
- MDCard:
- id: current_weather_card
- orientation: 'vertical'
- size_hint: 1, None
- height: dp(200)
- padding: dp(20)
- spacing: dp(10)
- elevation: 2
- MDLabel:
- id: update_label
- text: "Updating..."
- theme_text_color: "Secondary"
- font_style: "Caption"
- halign: "center"
- MDBoxLayout:
- spacing: dp(20)
- adaptive_size: True
- center_x: self.parent.center_x
- MDLabel:
- id: weather_icon
- text: ""
- font_style: "Icon"
- size_hint: None, None
- size: dp(48), dp(48)
- theme_text_color: "Custom"
- text_color: 1, 0.8, 0, 1
- MDLabel:
- id: temperature_label
- text: "--°C"
- font_style: "H2"
- halign: "center"
- bold: True
- MDBoxLayout:
- id: loading_indicator
- size_hint_y: None
- height: dp(20)
- spacing: dp(5)
- pos_hint: {'center_x': 0.5}
- MDSpinner:
- size_hint: None, None
- size: dp(20), dp(20)
- active: False
- MDCard:
- id: location_card
- size_hint: 1, None
- height: dp(100)
- padding: dp(20)
- elevation: 2
- MDBoxLayout:
- MDLabel:
- text: "map-marker"
- font_style: "Icon"
- size_hint: None, None
- size: dp(32), dp(32)
- theme_text_color: "Primary"
- MDLabel:
- id: location_label
- text: "Locating..."
- font_style: "Subtitle1"
- halign: "left"
- valign: "center"
- MDCard:
- id: hourly_forecast_card
- orientation: 'vertical'
- size_hint: 1, None
- height: dp(200)
- padding: dp(20)
- spacing: dp(10)
- elevation: 2
- MDLabel:
- text: "12h Forecast"
- font_style: "H6"
- bold: True
- theme_text_color: "Primary"
- ScrollView:
- MDBoxLayout:
- id: hourly_forecast_container
- orientation: 'horizontal'
- spacing: dp(15)
- adaptive_width: True
- height: dp(100)
- MDCard:
- id: details_card
- size_hint: 1, None
- height: dp(120)
- padding: dp(20)
- elevation: 2
- GridLayout:
- cols: 3
- spacing: dp(20)
- adaptive_height: True
- WeatherDetailItem:
- icon: "weather-windy"
- value: "-- km/h"
- label: "Wind"
- WeatherDetailItem:
- icon: "water-percent"
- value: "--%"
- label: "Humidity"
- WeatherDetailItem:
- icon: "weather-rainy"
- value: "-- mm"
- label: "Rain"
- <WeatherDetailItem@MDBoxLayout>:
- orientation: 'vertical'
- spacing: dp(5)
- adaptive_size: True
- center_x: self.parent.center_x
- MDLabel:
- text: root.icon
- font_style: "Icon"
- size_hint: None, None
- size: dp(24), dp(24)
- theme_text_color: "Primary"
- halign: "center"
- MDLabel:
- text: root.value
- font_style: "H6"
- halign: "center"
- bold: True
- MDLabel:
- text: root.label
- font_style: "Caption"
- theme_text_color: "Secondary"
- halign: "center"
- ''')
- class WeatherScreen(MDScreen):
- def on_enter(self):
- self.refresh()
- def refresh(self):
- self.ids.update_label.text = "Updating..."
- self.ids.temperature_label.text = "--°C"
- self.ids.location_label.text = "Locating..."
- self.show_loading(True)
- Thread(target=self.get_location_and_fetch_weather, daemon=True).start()
- def show_loading(self, show):
- spinner = self.ids.loading_indicator.children[0]
- spinner.active = show
- for widget_id in ['current_weather_card', 'location_card', 'hourly_forecast_card', 'details_card']:
- self.ids[widget_id].opacity = 0.3 if show else 1
- self.ids.refresh_layout.refreshing = show
- def get_location_and_fetch_weather(self):
- try:
- lat, lon = 48.4, 10.0
- used_gps = False
- if platform == 'android':
- from android.permissions import Permission, request_permissions
- request_permissions([Permission.ACCESS_FINE_LOCATION, Permission.INTERNET])
- from jnius import autoclass
- Context = autoclass('android.content.Context')
- PythonActivity = autoclass('org.kivy.android.PythonActivity')
- location_service = PythonActivity.mActivity.getSystemService(Context.LOCATION_SERVICE)
- location = location_service.getLastKnownLocation(location_service.GPS_PROVIDER)
- if location:
- lat = location.getLatitude()
- lon = location.getLongitude()
- used_gps = True
- self.fetch_weather(lat, lon, used_gps)
- except Exception as e:
- print(f"Location error: {e}")
- self.fetch_weather(48.4, 10.0, False)
- def fetch_weather(self, lat, lon, used_gps):
- try:
- url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true&hourly=temperature_2m&timezone=auto"
- response = requests.get(url, timeout=15)
- data = response.json()
- current = data.get("current_weather", {})
- weather_data = {
- 'temp': current.get("temperature", "--"),
- 'wind': current.get("windspeed", "--"),
- 'code': current.get("weathercode", 0),
- 'time': current.get("time", ""),
- 'hourly_temps': data.get("hourly", {}).get("temperature_2m", [])[:12],
- 'hourly_times': data.get("hourly", {}).get("time", [])[:12]
- }
- Clock.schedule_once(lambda dt: self.update_ui(weather_data))
- self.update_hourly_forecast(weather_data['hourly_times'], weather_data['hourly_temps'])
- Thread(target=self.reverse_geocode, args=(lat, lon, used_gps), daemon=True).start()
- except Exception as e:
- Clock.schedule_once(lambda dt: self.show_error(str(e)))
- def update_ui(self, data):
- self.ids.temperature_label.text = f"{data['temp']}°C"
- self.ids.weather_icon.text = self.weather_code_to_icon(data['code'])
- details = self.ids.details_card.children[0].children
- details[0].value = f"{data['wind']} km/h"
- details[1].value = "--%"
- details[2].value = "--mm"
- try:
- update_time = parser.isoparse(data['time']).strftime("%H:%M")
- self.ids.update_label.text = f"Updated: {update_time}"
- except:
- self.ids.update_label.text = "Updated: Now"
- self.show_loading(False)
- def update_hourly_forecast(self, times, temps):
- container = self.ids.hourly_forecast_container
- container.clear_widgets()
- try:
- max_temp = max(t for t in temps if isinstance(t, (int, float))) if temps else 1
- min_temp = min(t for t in temps if isinstance(t, (int, float))) if temps else 0
- for time_str, temp in zip(times[:12], temps[:12]):
- box = MDBoxLayout(
- orientation='vertical',
- spacing=dp(5),
- size_hint=(None, None),
- size=(dp(60), dp(100))
- if isinstance(temp, (int, float)):
- bar_value = ((temp - min_temp) / (max_temp - min_temp)) * 100 if max_temp != min_temp else 50
- bar = MDProgressBar(
- value=bar_value,
- orientation='vertical',
- height=dp(50),
- size_hint_y=None
- )
- temp_text = f"{round(temp)}°"
- else:
- bar = MDProgressBar(value=0)
- temp_text = "--"
- box.add_widget(bar)
- box.add_widget(MDLabel(
- text=temp_text,
- font_style="Caption",
- halign="center"
- ))
- box.add_widget(MDLabel(
- text=time_str[-5:],
- font_style="Overline",
- theme_text_color="Secondary",
- halign="center"
- ))
- container.add_widget(box)
- except Exception as e:
- print(f"Forecast error: {e}")
- def reverse_geocode(self, lat, lon, used_gps):
- try:
- url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}"
- headers = {"User-Agent": "WeatherApp/1.0 ([email protected])"}
- response = requests.get(url, headers=headers, timeout=15)
- data = response.json()
- location_parts = []
- address = data.get('address', {})
- if city := address.get('city', address.get('town')):
- location_parts.append(city)
- if country := address.get('country'):
- location_parts.append(country)
- location_text = ", ".join(location_parts) or "Unknown Location"
- Clock.schedule_once(lambda dt: setattr(
- self.ids.location_label,
- 'text',
- f"{location_text}\n({lat:.2f}, {lon:.2f})"
- ))
- except Exception as e:
- print(f"Geocode error: {e}")
- Clock.schedule_once(lambda dt: setattr(
- self.ids.location_label,
- 'text',
- f"Approximate Location\n({lat:.2f}, {lon:.2f})"
- ))
- def weather_code_to_icon(self, code):
- icon_map = {
- 0: "weather-sunny",
- 1: "weather-sunny",
- 2: "weather-partly-cloudy",
- 3: "weather-cloudy",
- 45: "weather-fog",
- 51: "weather-pouring",
- 61: "weather-rainy",
- 71: "weather-snowy",
- 80: "weather-rainy",
- 95: "weather-lightning"
- }
- return icon_map.get(code, "weather-cloudy-alert")
- def show_error(self, message):
- self.ids.temperature_label.text = "Error"
- self.ids.update_label.text = message[:30]
- self.ids.location_label.text = "Check connection"
- self.show_loading(False)
- class WeatherApp(MDApp):
- def build(self):
- self.theme_cls.primary_palette = "Blue"
- self.theme_cls.theme_style = "Dark"
- return WeatherScreen()
- def go_to_main_menu(self):
- self.stop()
- if __name__ == "__main__":
- WeatherApp().run()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement