"""
sunback.py
A program that downloads the most current images of the sun from the SDO satellite,
then finds the most likely temperature in each pixel.
Then it sets each of the images to the desktop background in series.
Handles the primary functions
"""
from time import localtime, altzone, timezone, strftime, sleep, time, asctime
from urllib.request import urlretrieve
from os import getcwd, makedirs
from os.path import normpath, abspath, join, dirname
from PIL import Image, ImageFont, ImageDraw
import pytesseract as pt
from sys import exit
debug = False
[docs]class Parameters:
"""
A container class for the run parameters of the program
"""
seconds = 1
minutes = 60 * seconds
hours = 60 * minutes
[docs] def __init__(self):
"""Sets all the attributes to None"""
# Initialize Variables
self.background_update_delay_seconds = None
self.time_multiplier_for_long_display = None
self.local_directory = None
self.use_wavelengths = None
self.resolution = None
self.web_image_frame = None
self.web_image_location = None
self.web_paths = None
self.file_ending = None
self.run_time_offset = None
self.start_time = time()
self.is_first_run = True
self.set_default_values()
def check_real_number(self, number):
assert type(number) in [float, int]
assert number > 0
def set_default_values(self):
"""Sets the Defaults for all the Parameters"""
# Set Delay Time for Background Rotation
self.set_update_delay_seconds(30 * self.seconds)
self.set_time_multiplier(3)
# Set File Paths
self.set_local_directory()
# Set Wavelengths
self.set_wavelengths(['0171', '0193', '0211', '0304', '0131', '0335', '0094', 'HMIBC', 'HMIIF'])
# Set Resolution
self.set_download_resolution(2048)
# Set Web Location
self.set_web_image_frame("https://sdo.gsfc.nasa.gov/assets/img/latest/latest_{}_{}")
# Add extra images
new_web_path_1 = "https://sdo.gsfc.nasa.gov/assets/img/latest/f_211_193_171pfss_{}.jpg".format(self.resolution)
self.append_to_web_paths(new_web_path_1, 'PFSS')
# Select File Ending
self.set_file_ending("{}_Now.jpg")
return 0
# Methods that Set Parameters
def set_update_delay_seconds(self, delay):
self.check_real_number(delay)
self.background_update_delay_seconds = delay
return 0
def set_time_multiplier(self, multiplier):
self.check_real_number(multiplier)
self.time_multiplier_for_long_display = multiplier
return 0
def set_local_directory(self, path=None):
if path is not None:
self.local_directory = path
else:
self.local_directory = self.discover_best_default_directory()
makedirs(self.local_directory, exist_ok=True)
def set_wavelengths(self, waves):
# [self.check_real_number(int(num)) for num in waves]
self.use_wavelengths = waves
self.use_wavelengths.sort()
if self.has_all_necessary_data():
self.make_web_paths()
return 0
def set_download_resolution(self, resolution):
self.check_real_number(resolution)
self.resolution = min([170, 256, 512, 1024, 2048, 3072, 4096], key=lambda x: abs(x - resolution))
if self.has_all_necessary_data():
self.make_web_paths()
def set_web_image_frame(self, path):
self.web_image_frame = path
if self.has_all_necessary_data():
self.make_web_paths()
def set_file_ending(self, string):
self.file_ending = string
# Methods that create something
def make_web_paths(self):
self.web_image_location = self.web_image_frame.format(self.resolution, "{}.jpg")
self.web_paths = [self.web_image_location.format(wave) for wave in self.use_wavelengths]
def append_to_web_paths(self, path, wave=' '):
self.web_paths.append(path)
self.use_wavelengths.append(wave)
# Methods that return information or do something
def has_all_necessary_data(self):
if self.web_image_frame is not None:
if self.use_wavelengths is not None:
if self.resolution is not None:
return True
return False
def get_local_path(self, wave):
return normpath(join(self.local_directory, self.file_ending.format(wave)))
@staticmethod
def discover_best_default_directory():
"""Determine where to store the images"""
subdirectory_name = r"data\images"
if __file__ in globals():
directory = join(dirname(abspath(__file__)), subdirectory_name)
else:
directory = join(abspath(getcwd()), subdirectory_name)
return directory
def determine_delay(self, wave):
""" Determine how long to wait """
delay = self.background_update_delay_seconds + 0
if 'temp' in wave:
delay *= self.time_multiplier_for_long_display
self.run_time_offset = time() - self.start_time
delay -= self.run_time_offset
delay = max(delay, 0)
return delay
def wait_if_required(self, delay):
""" Wait if Required """
if self.is_first_run:
self.is_first_run = False
elif delay <= 0:
pass
else:
# print("Took {:0.1f} seconds. ".format(self.run_time_offset), end='')
print("Waiting for {:0.0f} seconds ({} total)... ".format(delay, self.background_update_delay_seconds),
end='', flush=True)
fps = 10
for ii in (range(int(fps * delay))):
sleep(1 / fps)
print("Done")
def sleep_for_time(self, wave):
""" Make sure that the loop takes the right amount of time """
self.wait_if_required(self.determine_delay(wave))
[docs]class Sunback:
"""
The Primary Class that Does Everything
Parameters
----------
parameters : Parameters (optional)
a class specifying run options
"""
[docs] def __init__(self, parameters=None):
"""Initialize a new parameter object or use the provided one"""
if parameters:
self.params = parameters
else:
self.params = Parameters()
def download_image(self, local_path, web_path):
"""
Download an image and save it to file
Go to the internet and download an image
Parameters
----------
web_path : str
The web location of the image
local_path : str
The local save location of the image
"""
tries = 3
for ii in range(tries):
try:
print("Downloading Image...", end='', flush=True)
urlretrieve(web_path, local_path)
print("Success", flush=True)
return 0
except KeyboardInterrupt:
raise
except Exception:
print("Failed {} Time(s).".format(ii + 1), flush=True)
raise Exception
@staticmethod
def update_background(local_path):
"""
Update the System Background
Parameters
----------
local_path : str
The local save location of the image
"""
print("Updating Background...", end='', flush=True)
assert isinstance(local_path, str)
import platform
this_system = platform.system()
try:
if this_system == "Windows":
import ctypes
ctypes.windll.user32.SystemParametersInfoW(20, 0, local_path, 0)
elif this_system == "Darwin":
from appscript import app, mactypes
app('Finder').desktop_picture.set(mactypes.File(local_path))
elif this_system == "Linux":
import os
os.system(
"/usr/bin/gsettings set org.gnome.desktop.background picture-uri {}".format(local_path))
else:
raise OSError("Operating System Not Supported")
print("Success")
except:
print("Failed")
raise
return 0
@staticmethod
def modify_image(local_path, wave, resolution):
"""
Modify the Image with some Annotations
Parameters
----------
local_path : str
The local save location of the image
wave : str
The name of the desired wavelength
resolution: int
The resolution of the images
"""
print('Modifying Image...', end='', flush=True)
# Open the image for modification
img = Image.open(local_path)
img_raw = img
try:
# Are we working with the HMI image?
is_hmi = wave[0] == 'H'
# Shrink the HMI images to be the same size
if is_hmi:
small_size = int(0.84*resolution) # 1725
old_img = img.resize((small_size, small_size))
old_size = old_img.size
new_size = (resolution, resolution)
new_im = Image.new("RGB", new_size)
x = int((new_size[0] - old_size[0]) / 2)
y = int((new_size[1] - old_size[1]) / 2)
new_im.paste(old_img, (x, y))
img = new_im
# Read the time and reprint it
if localtime().tm_isdst:
offset = altzone / 3600
else:
offset = timezone / 3600
cropped = img_raw.crop((0, 1950, 1024, 2048))
results = pt.image_to_string(cropped)
if is_hmi: # HMI Data
image_time = results[-6:]
image_hour = int(image_time[:2])
image_minute = int(image_time[2:4])
else: # AIA Data
image_time = results[-11:-6]
image_hour = int(image_time[:2])
image_minute = int(image_time[-2:])
image_hour = int(image_hour - offset) % 12
pre = ''
except:
image_hour = localtime().tm_hour % 12
image_minute = localtime().tm_min
pre = 'x'
if image_hour == 0:
image_hour = 12
# Draw on the image and save
draw = ImageDraw.Draw(img)
# Draw the wavelength
font = ImageFont.truetype(normpath(r"C:\Windows\Fonts\Arial.ttf"), 42)
towrite = wave[1:] if wave[0] == '0' else wave
draw.text((1510, 300), towrite, (200, 200, 200), font=font)
# Draw a scale Earth
corner_x = 1580
corner_y = 350
width_x = 15
width_y = width_x
draw.ellipse((corner_x, corner_y, corner_x + width_x, corner_y + width_y), fill='white', outline='green')
# Draw the Current Time
draw.rectangle([(450, 150), (560, 200)], fill=(0, 0, 0))
draw.text((450, 150), strftime("%I:%M"), (200, 200, 200), font=font)
# Draw the Image Time
draw.text((450, 300), "{:0>2}:{:0>2}{}".format(image_hour, image_minute, pre), (200, 200, 200), font=font)
img.save(local_path)
print("Success")
# except:
# print("Failed");
# return 1
return 0
def loop(self, wave, web_path):
"""The Main Loop"""
self.params.start_time = time()
print("Image: {}, at {}".format(wave, asctime()))
# Define the Image
local_path = self.params.get_local_path(wave)
# Download the Image
self.download_image(local_path, web_path)
# Modify the Image
self.modify_image(local_path, wave, self.params.resolution)
# Wait for a bit
self.params.sleep_for_time(wave)
# Update the Background
self.update_background(local_path)
print('')
# print("\n----->>>>>Cycle {:0.1f} seconds\n".format(time()-self.params.start_time))
def print_header(self):
print("\nSunback: Live SDO Background Updater \nWritten by Chris R. Gilly")
print("Check out my website: http://gilly.space\n")
print("Delay: {} Seconds".format(self.params.background_update_delay_seconds))
print("Resolution: {}\n".format(self.params.resolution))
def run(self):
"""Run the program in a way that won't break"""
self.print_header()
fail_count = 0
fail_max = 10
while True:
for wave, web_path in zip(self.params.use_wavelengths, self.params.web_paths):
try:
self.loop(wave, web_path)
except (KeyboardInterrupt, SystemExit):
print("\n\nOk, I'll Stop.\n")
exit(0)
except Exception as error:
print("Failure!")
fail_count += 1
if fail_count < fail_max:
print("I failed, but I'm ignoring it. Count: {}/{}".format(fail_count, fail_max))
continue
else:
print("Too Many Failures, I Quit!")
exit(1)
def debug(self):
"""Run the program in a way that will break"""
self.print_header()
while True:
for wave, web_path in zip(self.params.use_wavelengths, self.params.web_paths):
self.loop(wave, web_path)
def run(delay=30, resolution=2048, debug=False):
p = Parameters()
p.set_update_delay_seconds(delay)
p.set_download_resolution(resolution)
if debug:
Sunback(p).debug()
else:
Sunback(p).run()
if __name__ == "__main__":
# Do something if this file is invoked on its own
run(30, debug=debug)