Last active
March 11, 2026 12:44
-
-
Save tsohr/b5277db45f58e97049d69946c1244002 to your computer and use it in GitHub Desktop.
CGS2 de-cloud weather station service py file fork version of https://github.com/ea/cgs2_decloud/blob/main/weather_server/weather_server.py changing weather data source to openweathermap.org
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 192.168.123.234 your server ip | |
| 192.168.123.234 mqtt.bj.cleargrass.com | |
| 192.168.123.234 gateway.cleargrass.com | |
| 192.168.123.234 qing.cleargrass.com | |
| 192.168.123.234 cn.ots.io.mi.com | |
| 192.168.123.234 cn.ot.io.mi.com | |
| 192.168.123.234 baidu.com | |
| 192.168.123.234 www.baidu.com | |
| 192.168.123.234 qingosapi.dev.cleargrass.com | |
| 192.168.123.234 vip3.alidns.com | |
| 192.168.123.234 vip4.alidns.com |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [root@Qingping-Air-Monitor:/etc]# touch a | |
| touch: cannot touch 'a': Read-only file system | |
| [root@Qingping-Air-Monitor:/]# uname -a | |
| Linux Qingping-Air-Monitor 4.19.232 #10 SMP Sun Jan 4 17:50:17 CST 2026 aarch64 GNU/Linux | |
| cat > /oem/etc/hosts <<EOF | |
| 127.0.0.1 localhost | |
| 127.0.1.1 Qingping-Air-Monitor | |
| 192.168.123.234 mqtt.bj.cleargrass.com | |
| 192.168.123.234 gateway.cleargrass.com | |
| 192.168.123.234 qing.cleargrass.com | |
| 192.168.123.234 cn.ots.io.mi.com | |
| 192.168.123.234 cn.ot.io.mi.com | |
| 192.168.123.234 baidu.com | |
| 192.168.123.234 www.baidu.com | |
| 192.168.123.234 qingosapi.dev.cleargrass.com | |
| 192.168.123.234 vip3.alidns.com | |
| 192.168.123.234 vip4.alidns.com | |
| EOF | |
| echo "mount --bind /oem/etc/hosts /etc/hosts" >> /oem/bin/start.sh | |
| cat > /data/etc/setting.ini <<EOF | |
| [192.168.123.234] | |
| save_history_interval=60 | |
| sync_history_interval=60 | |
| [application] | |
| current_main_page_index=0 | |
| is_initialized=true | |
| is_show_qingping_app_view=false | |
| language=4096 | |
| poweroff_timestamp=1501888431 | |
| use_http=true | |
| [battery] | |
| is_show_battery_precentage=false | |
| [datetime] | |
| is_automatically=true | |
| is_initialized=false | |
| timezone=Asia/Shanghai | |
| timezone_auto=Asia/Shanghai | |
| [developer] | |
| is_adbd_enabled=true | |
| is_serial_enabled=false | |
| [device] | |
| miio_did=222222222 | |
| miio_key=1111111111111111 | |
| miio_mac=FF:FF:FF:FF:FF:FF | |
| wifi_mac=58:FF:FF:FF:FF:FF | |
| [host] | |
| client_id="58FFFFFFFFFF|securemode=2,signmethod=hmacsha1|" | |
| host=192.168.123.234 | |
| password=ffffffffffffffffffffffffffffffffffffffff | |
| port=1883 | |
| pub_topic=/fffffffff/58FFFFFFFFFF/user/update | |
| sub_topic=/fffffffff/58FFFFFFFFFF/user/get | |
| tls=0 | |
| username=58FFFFFFFFFF&zTxDnQAGg | |
| [location] | |
| is_auto=true | |
| [qingping] | |
| is_iot_paired=false | |
| is_paired=false | |
| [screen] | |
| auto_brightness=true | |
| brightness_value=350 | |
| screen_off_charging_interval=99999 | |
| screen_off_discharge_interval=5 | |
| screen_saver=2 | |
| screen_saver_interval=5 | |
| [sensors] | |
| co2_autocalib_enabled=true | |
| co2_type=3 | |
| EOF | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import ssl | |
| import time | |
| import threading | |
| import json | |
| import socketserver | |
| import requests | |
| from http.server import BaseHTTPRequestHandler, HTTPServer | |
| from http import HTTPStatus | |
| from socketserver import ThreadingMixIn | |
| # --- Configuration --- | |
| HOST = "0.0.0.0" | |
| PORT = 80 | |
| LAT = 0#<deleted> | |
| LON = 0#<deleted> | |
| weather_data = None | |
| city_id_placeholder = "<deleted>" | |
| station_name = "<deleted>" | |
| location_data = { | |
| "city_id":"<deleted>","name":station_name,"name_cn":station_name, | |
| "name_en":station_name,"name_cn_tw":station_name,"country":"<deleted>", | |
| "country_cn":"<deleted>","country_en":"<deleted>","country_cn_tw":"<deleted>", | |
| "area_cn_first":"<deleted>","area_cn_second":"<deleted>", | |
| "area_first":"<deleted>","timezone":"<deleted>", | |
| "timezone_gmt":"<deleted>","coordinate":{"longitude":"<deleted>","latitude":"<deleted>"} | |
| } | |
| pollution_data = None | |
| openweather_api_key = "<deleted>" | |
| # --------------------- | |
| #source_json = current_data | |
| def transform_weather_data(source_json, pollution_data): | |
| """ | |
| Transforms NWS-style weather observation JSON into a custom target format. | |
| Args: | |
| source_json (dict): The input dictionary in the NWS GeoJSON/LD format. | |
| Returns: | |
| dict: The transformed dictionary in the custom target format. | |
| """ | |
| # 1. Location Data | |
| # Coordinate extraction: [longitude, latitude] | |
| longitude = str(LON) | |
| latitude = str(LAT) | |
| # 2. Weather Data | |
| # Temperature: value is in degC, convert to float | |
| temperature_c = source_json["current"]['temp'] | |
| temperature = float(temperature_c) if temperature_c is not None else 0.0 | |
| # Relative Humidity: value is in percent | |
| relative_humidity = source_json["current"]['humidity'] | |
| humidity = int(round(relative_humidity)) if relative_humidity is not None else 0 | |
| # Wind Speed: value is in km/h, convert to float | |
| wind_speed_kmh = source_json["current"]['wind_speed'] | |
| wind_speed = float(wind_speed_kmh) if wind_speed_kmh is not None else 0.0 | |
| uv_index = source_json["current"]['uvi'] | |
| uv_index = float(uv_index) if uv_index is not None else 0.0 | |
| aqi = pollution_data["list"][0]["main"]["aqi"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0 | |
| co = pollution_data["list"][0]["components"]["co"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0 | |
| no = pollution_data["list"][0]["components"]["no"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0 | |
| no2 = pollution_data["list"][0]["components"]["no2"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0 | |
| o3 = pollution_data["list"][0]["components"]["o3"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0 | |
| so2 = pollution_data["list"][0]["components"]["so2"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0 | |
| pm10 = pollution_data["list"][0]["components"]["pm10"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0 | |
| pm25 = pollution_data["list"][0]["components"]["pm2_5"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0 | |
| nh3 = pollution_data["list"][0]["components"]["nh3"] if pollution_data and "list" in pollution_data and len(pollution_data["list"]) > 0 else 0.0 | |
| hourly_forecast = source_json["hourly"][0] | |
| pop = hourly_forecast['pop'] if 'pop' in hourly_forecast else 0.0 | |
| today_forecast = source_json["daily"][0] | |
| temp_max = today_forecast['temp']['max'] | |
| temp_min = today_forecast['temp']['min'] | |
| # Wind Direction: value is in degrees, convert to a basic cardinal direction | |
| wind_direction_deg = source_json["current"]['wind_deg'] | |
| wind_dir_text = "N/A" | |
| if wind_direction_deg is not None: | |
| # Simple cardinal direction logic (e.g., 0-22.5 is N, 22.5-67.5 is NE, etc.) | |
| dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"] | |
| index = int((wind_direction_deg % 360) / 22.5) | |
| wind_dir_text = dirs[index] | |
| text_description = source_json["current"]['weather'][0]['main'].lower() | |
| skycon_map = { | |
| "clear": "CLEAR_DAY", "sunny": "CLEAR_DAY", "partly cloudy": "PARTLY_CLOUDY_DAY", | |
| "cloudy": "CLOUDY", "overcast": "CLOUDY", "rain": "RAIN", | |
| "snow": "SNOW", "fog": "FOG" | |
| } | |
| def map_description(text_description,skycon_map): | |
| for k in skycon_map.keys(): | |
| if k in text_description: | |
| return skycon_map[k] | |
| return skycon_map["clear"] | |
| skycon = map_description(text_description,skycon_map) | |
| pub_time = source_json["current"]['dt'] if 'dt' in source_json["current"] else int(time.time()) | |
| # Placeholder for the arbitrary city ID | |
| timezone_offset_hours = source_json["timezone_offset"] // 3600 | |
| timezoneFmt = "UTC" + ("+" if timezone_offset_hours >= 0 else "") + str(timezone_offset_hours) | |
| transformed_data = { | |
| "city": { | |
| "city": station_name, | |
| "cityId": city_id_placeholder, | |
| "cnAddress": { | |
| "city": "", | |
| "cityId": city_id_placeholder, | |
| "country": "KR", | |
| "province": "" | |
| }, | |
| "cnCity": "", | |
| "country": "KR", # Assumption based on weather.gov data | |
| "enAddress": { | |
| "city": station_name, | |
| "cityId": city_id_placeholder, | |
| "country": "KR", | |
| "province": "" # Province/State is not directly mapped here | |
| }, | |
| "latitude": latitude, | |
| "longitude": longitude, | |
| "name": station_name, | |
| "name_cn": "", | |
| "name_cn_tw": "", | |
| "name_en": station_name, | |
| "province": "", | |
| # Placeholder: Timezone information is not provided in source. | |
| "timezone": source_json["timezone"], | |
| "timezoneFmt": timezoneFmt # Convert offset seconds to hours | |
| }, | |
| "city_id": city_id_placeholder, | |
| "weather": { | |
| # --- AQI/Pollution (Placeholders, as this is NOT in the source data) --- | |
| "aqi": aqi, | |
| "aqi_day_max_cn": aqi, | |
| "aqi_day_max_en": aqi, | |
| "aqi_day_min_cn": aqi, | |
| "aqi_day_min_en": aqi, | |
| "aqi_us": aqi, | |
| "co": co, | |
| "co_us": co, | |
| "no2": no2, | |
| "no2_us": no2, | |
| "noAqi": True, # Indicates no AQI data | |
| "o3": o3, | |
| "o3_us": o3, | |
| "pm10": pm10, | |
| "pm25": pm25, | |
| "so2": so2, | |
| "so2_us": so2, | |
| # --- Available Weather Data --- | |
| "humidity": humidity, | |
| "probability": pop, # NWS observation doesn't directly provide rain probability | |
| "pub_time": pub_time, | |
| "skycon": skycon, | |
| # --- Temperature (Temp max/min must be placeholders) --- | |
| "temp_max": temp_max, # Using current temp as a simple placeholder for the day's max | |
| "temp_min": temp_min, # Using current temp as a simple placeholder for the day's min | |
| "temperature": temperature, | |
| "ultraviolet": uv_index, # Placeholder, UV index not in source data | |
| "vehicle_limit": { | |
| "type": "city_unlimited" # Placeholder/default value | |
| }, | |
| # --- Wind Data --- | |
| "wind": { | |
| "speed": round(wind_speed, 2), # km/h | |
| "wind_dir": wind_dir_text, | |
| "wind_level": int(wind_speed / 5), # Crude wind level based on speed | |
| } | |
| } | |
| } | |
| # {"city_id":"n980610","name":"Test","name_cn":"\u6ce2\u7279\u5170","name_en":"Test1","name_cn_tw":"\u6ce2\u7279\u5170","country":"U.S.A.","country_cn":"U.S.A.","country_en":"U.S.A.","country_cn_tw":"U.S.A.","area_cn_first":"","area_cn_second":"<h1>Moonbase 1</h1><script>alert(1);</alert>","area_first":"","timezone":"America/Los_Angeles","timezone_gmt":"GMT-7:00","coordinate":{"longitude":"-122.67621","latitude":"45.52345"} | |
| return transformed_data | |
| USER_AGENT = 'airqmonitor/1.0 (https://github.com/ea)' | |
| def get_current_observation(): | |
| observation_url = f"https://api.openweathermap.org/data/3.0/onecall?lat={LAT}&lon={LON}&exclude=minutely&appid={openweather_api_key}&units=metric&lang=kr" | |
| headers = {'User-Agent': USER_AGENT} | |
| try: | |
| response = requests.get(observation_url, headers=headers) | |
| response.raise_for_status() | |
| return response.json() | |
| except requests.exceptions.RequestException as e: | |
| return None | |
| def get_current_air_pollution(): | |
| pollution_url = f"https://api.openweathermap.org/data/2.5/air_pollution?lat={LAT}&lon={LON}&appid={openweather_api_key}" | |
| headers = {'User-Agent': USER_AGENT} | |
| try: | |
| response = requests.get(pollution_url, headers=headers) | |
| response.raise_for_status() | |
| return response.json() | |
| except requests.exceptions.RequestException as e: | |
| return None | |
| def scheduled_weather_update(): | |
| # 1. fetch data from NWS | |
| # 2. transform data to suitable json | |
| # 3. sleep 10 minutes | |
| global weather_data,pollution_data | |
| current_data = get_current_observation() | |
| pollution_data = get_current_air_pollution() | |
| weather_data = transform_weather_data(current_data, pollution_data) | |
| t = threading.Timer(600, scheduled_weather_update) | |
| t.daemon = True | |
| t.start() | |
| class ThreadingTLSServer(ThreadingMixIn, HTTPServer): | |
| pass | |
| class HttpsRequestHandler(BaseHTTPRequestHandler): | |
| """ | |
| A custom handler to process HTTP requests. | |
| """ | |
| def _set_headers(self, status_code=200, content_type='application/json'): | |
| """Helper function to set common response headers.""" | |
| self.send_response(status_code) | |
| self.send_header('Content-type', content_type) | |
| self.end_headers() | |
| def _send_json(self,payload): | |
| body = payload.encode("utf-8") | |
| self.send_response(HTTPStatus.OK) | |
| self.send_header("Content-Type", "application/json; charset=utf-8") | |
| self.send_header("Connection", "keep-alive") | |
| self.send_header("Vary", "Accept-Encoding") | |
| self.send_header("Access-Control-Allow-Origin", "*") | |
| self.send_header("Access-Control-Allow-Headers", "*") | |
| self.send_header("Content-Length", str(len(body))) | |
| self.end_headers() | |
| self.wfile.write(body) | |
| def do_GET(self): | |
| """Handle GET requests.""" | |
| global weather_data,location_data | |
| if self.path == '/daily/locate': | |
| payload = '{"data":'+json.dumps(location_data)+',"code":0}' | |
| self._send_json(payload) | |
| return | |
| elif self.path.startswith("/daily/weatherNow"): | |
| payload = '{"code" : 0,"data" : ' + json.dumps(weather_data) + '}' | |
| print(payload) | |
| self._send_json(payload) | |
| return | |
| elif self.path.startswith("/daily/now"): | |
| payload = '{"data":{"timestamp":'+str(int(time.time()))+',"time":"'+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+'"},"code":0}' | |
| # {"data":{"timestamp":1771079693,"time":"2026-02-14 22:34:53"},"code":0} | |
| print(payload) | |
| self._send_json(payload) | |
| return | |
| elif self.path.startswith("/device/pairStatus"): | |
| payload = """ | |
| {"desc":"ok","code":10503} | |
| """ | |
| self._send_json(payload) | |
| return | |
| elif self.path.startswith("/cooperation/companies"): | |
| payload = """ | |
| {"data":{"cooperation":[]},"code":0} | |
| """ | |
| self._send_json(payload) | |
| return | |
| elif self.path.startswith("/cooperation/getJDKey"): | |
| payload = """{"data":false,"code":0}""" | |
| self._send_json(payload) | |
| return | |
| elif self.path.startswith("/firmware/checkUpdate"): | |
| payload = """{"data":{"upgrade_sign": 0 } , "code" : 0 }""" | |
| self._send_json(payload) | |
| return | |
| else: | |
| # --- Handle other paths (404 Not Found) --- | |
| self._set_headers(404) | |
| error_message = json.dumps({"error": "Not Found", "path": self.path}) | |
| self.wfile.write(error_message.encode('utf-8')) | |
| print(f"[{self.client_address[0]}:{self.client_address[1]}] 404 Not Found for {self.path}") | |
| def run_server(): | |
| server_address = (HOST, PORT) | |
| scheduled_weather_update() | |
| httpd = HTTPServer(server_address, HttpsRequestHandler) | |
| print("Press Ctrl+C to stop.") | |
| try: | |
| httpd.serve_forever() | |
| except KeyboardInterrupt: | |
| pass | |
| httpd.server_close() | |
| print("Server stopped.") | |
| if __name__ == '__main__': | |
| run_server() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment