def hw_config(string): sysinfo = Sysinfo() translate = {'is_vm': 'A Virtual Machine is required', 'cores': 'Number of CPU cores required is not met', 'installed_mem': 'Memory size required is not met'} new_string = '' try: match = re.findall(r'^@([_a-zA-Z]*)@(.*)', string)[0] hardware_config = sysinfo.get_hardware_config() new_string = str(hardware_config[match[0]]) + match[1] except: return (None, None) return (translate[match[0]], new_string)
def hw_config(string): sysinfo = Sysinfo() translate = { 'is_vm': 'A Virtual Machine is required', 'cores': 'Number of CPU cores required is not met', 'installed_mem': 'Memory size required is not met' } new_string = '' try: match = re.findall(r'^@([_a-zA-Z]*)@(.*)', string)[0] hardware_config = sysinfo.get_hardware_config() new_string = str(hardware_config[match[0]]) + match[1] except: return None, None return translate[match[0]], new_string
def av_config(string, encapsulate_str=False, escape=False): sysinfo = Sysinfo() new_string = string try: alienvault_config = sysinfo.get_alienvault_config() keys = re.findall(r'(@((?!and|or|not)[a-zA-Z_]+)@)', string) for delim_key, key in keys: new_key = alienvault_config.get(key, delim_key) if type(new_key) is list: new_key = ','.join(new_key) if (new_key != delim_key) and encapsulate_str: new_key = '"' + new_key + '"' if escape: new_key = re.escape(new_key) new_string = new_string.replace(delim_key, new_key) except Exception, e: return (string)
def av_config(string, encapsulate_str=False, escape=False): sysinfo = Sysinfo() new_string = string try: alienvault_config = sysinfo.get_alienvault_config() keys = re.findall(r'(@((?!and|or|not)[a-zA-Z_]+)@)', string) for delim_key, key in keys: new_key = alienvault_config.get(key, delim_key) if type(new_key) is list: new_key = ','.join(new_key) if (new_key != delim_key) and encapsulate_str: new_key = '"' + new_key + '"' if escape: new_key = re.escape(new_key) new_string = new_string.replace(delim_key, new_key) except Exception, e: return string
def run(self): # Parse command line options. parser = OptionParser(description='Save the world, one Alien at a time', version=default.version_string) parser.add_option("-v", "--verbose", dest="verbose", default=default.verbose, action="count", help="More meaningful warnings [default: %default]. Maximum verbosity depth is 2 (-vv)") parser.add_option("-l", "--plugin-list", dest="plugin_list", default=default.plugin_list, help="A list of plugins you want to run, separated by commas [default: run all plugins]") parser.add_option("-c", "--category_list", dest="category_list", default=default.category_list, help="A list of plugin categories you want to run [default: run all plugins]") parser.add_option("-s", "--severity_list", dest="severity_list", default=default.severity_list, help="A list of check severities you want to run [default: run all checks]") parser.add_option("-a", "--appliance_type", dest="appliance_type_list", default=default.appliance_type_list, help="Appliance whose checks you want to run [default: run checks for current appliance] ") parser.add_option("-P", "--plugin-dir", dest="plugin_dir", default=default.plugin_dir, help="Directory where plugins are stored [default: %default]") parser.add_option("-k", "--ko", dest="ko_only", default=default.ko, action="store_true", help=SUPPRESS_HELP) parser.add_option("-i", "--ignore-dummy", dest="ignore_dummy", default=default.ignore_dummy, action="store_true", help=SUPPRESS_HELP) output_group = OptionGroup(parser, 'Output options') output_group.add_option('-o', '--output-type', dest='output_type', type='choice', choices=default.valid_output_types, default=default.output_type, help='Output type [default: %default]') output_group.add_option('-p', '--output-path', dest='output_path', default=default.output_path, help='Output file path [default: %default]') output_group.add_option('-f', '--output-file-prefix', dest='output_file_prefix', default=default.output_file_prefix, help='Output file prefix [default: %default]') output_group.add_option('-r', '--output-raw', dest='output_raw', default=default.output_raw, action='store_true', help='Retrieve raw data [default: %default]') parser.add_option_group(output_group) (options, args) = parser.parse_args() # Disable normal output for 'ansible' and 'support' output options. if options.output_type in ['ansible']: Output.set_std_output(False) # Ignore dummy platform package self.__ignore_dummy_platform = options.ignore_dummy # Get basic system info. self.__sysinfo = Sysinfo() self.__alienvault_config = self.__sysinfo.get_alienvault_config() self.__successful_config = self.__sysinfo.get_successful_config() Output.emphasized('\nAlienVault Doctor version %s (%s)\n' % (default.version, default.nickname), ['AlienVault Doctor'], [GREEN]) # Parse Doctor configuration file options. if path.isfile(default.doctor_cfg_file): self.__parse_doctor_cfg__() else: Output.warning('Doctor configuration file does not exist, trying to continue...') # Parse plugin configuration files. if not path.isdir(options.plugin_dir): Output.error('"%s" is not a valid directory' % options.plugin_dir) sys.exit(default.error_codes['invalid_dir']) else: self.__plugin_dir = options.plugin_dir output_fd = None # Parse output options. if options.output_type in ['file', 'support']: mode = 'w+' # Support ticket ID has to be a 8 char long, all digit string. if options.output_type == 'support': if options.output_file_prefix == default.output_file_prefix or \ len(options.output_file_prefix) != 8 or not options.output_file_prefix.isdigit(): Output.error('For "support" output, a valid ticket number has to be specified as the file prefix (-f option)') sys.exit(default.error_codes['undef_support_prefix']) Output.set_std_output(False) if not path.exists(options.output_path): os.mkdir(options.output_path) Output.info('Output file directory "%s" created\n' % options.output_path) if path.isdir(options.output_path): try: self.__output_file = path.join(options.output_path, options.output_file_prefix + '-' + str(int(time.time())) + '.doctor') output_fd = open(self.__output_file, mode) except IOError as e: Output.warning('Cannot open file "%s" for writing: %s' % (self.__output_file, e)) else: Output.warning('"%s" is not a valid directory, messages will be shown in stdout only' % options.output_path) elif options.output_type == 'ansible': output_fd = sys.stdout elif options.output_type == 'none': pass else: Output.warning('"%s" is not a valid output type, messages will be shown in stdout only' % options.output_type) # Show some system info. self.__system_summary = self.__sysinfo.show_platform_info(extended=bool(options.verbose)) if self.__system_summary['Hardware profile'] != 'ossim-free': # Run a list of plugins or categories of plugins self.__plugin_list = options.plugin_list.split(',') if self.__plugin_list == [] or 'all' in self.__plugin_list: self.__plugin_list = os.listdir(options.plugin_dir) # Filter by category. self.__category_list = options.category_list.split(',') # Filter checks by severity. self.__severity_list = options.severity_list.split(',') # Filter checks by appliance_type. self.__appliance_type_list = options.appliance_type_list.split(',') # Run! Run! Run! Output.emphasized('\nHmmm, let the Doctor have a look at you%s' % ('...\n' if options.verbose > 0 else '\n'), ['Doctor'], [GREEN], False) for filename in self.__plugin_list: if filename.endswith('.plg'): if options.verbose < 1: Progress.dots() self.__run_plugin__(options.plugin_dir + '/' + filename, options.verbose, options.output_raw) # Separator print '' else: Output.emphasized('\nThe Doctor is not aimed to diagnose an ossim free version...', ['Doctor'], [GREEN]) # Show summary only for screen output. if options.output_type == default.output_type: if self.__system_summary['Hardware profile'] != 'ossim-free': if self.__summary != {}: Output.emphasized('\nHooray! The Doctor has diagnosed you, check out the results...', ['Doctor'], [GREEN]) else: Output.emphasized('\nThe Doctor has finished, nothing to see here though', ['Doctor'], [GREEN]) # Show if the system is in the 'Strike zone' if not self.__in_strike_zone: Output.emphasized('\n Be careful! Seems that you are not in the Strike Zone! Please check the output below.', ['Strike', 'Zone'], [RED]) # Show per plugin results. plugin_det = {} for x, y in self.__summary.iteritems(): if self.__plugin_dir in x: Output.emphasized('\n Plugin %s didn\'t run: %s' % (x, y['summary']), [x]) continue ident = int(y['id']) if 'id' in y.keys() else int(x.split(" ", 1)[0]) plugin_det[ident] = x plugin_ids = plugin_det.keys() plugin_ids.sort() failing_checks = [] for plugin_id in plugin_ids: plugin_name = plugin_det[plugin_id] result = self.__summary[plugin_name] plugin_description = result.get('description', None) plugin_strike_zone = result.get('strike_zone', None) if (not 'checks' in result.keys() or not result['checks']) and ('summary' in result.keys()): Output.emphasized('\n Plugin %s didn\'t run: %s' % (plugin_name, result['summary']), [plugin_name]) else: checks = result['checks'] header = '\n Plugin: %s' % plugin_name if plugin_description is not None: header += '\n %s' % plugin_description if plugin_strike_zone is not None: header += '\n In the Strike Zone?: %s' % str(plugin_strike_zone) Output.emphasized(header, [plugin_name, 'In the Strike Zone?']) for (check_name, check_result) in checks.items(): if check_result['result'] == 'failed': failing_checks.append({'check': check_name, 'severity': check_result['severity'], 'strike_zone': check_result['strike_zone'], 'detail': check_result['detail']}) if check_result['severity'] == 'Emerg': severity_color = RED elif check_result['severity'] == 'Alert': severity_color = RED elif check_result['severity'] == 'Critical': severity_color = RED elif check_result['severity'] == 'Error': severity_color = RED elif check_result['severity'] == 'Warning': severity_color = YELLOW elif check_result['severity'] == 'Notice': severity_color = GREEN elif check_result['severity'] == 'Info': severity_color = GREEN elif check_result['severity'] == 'Debug': severity_color = BLUE else: severity_color = YELLOW Output.emphasized('%s[*] %s: %s' % ((' '*9), check_name, check_result['summary']), ['*', check_name], [severity_color, EMPH]) if check_result['remediation'] != '': Output.emphasized('%sWord of advice: %s' % ((' '*13), check_result['remediation']), ['Word of advice']) else: Output.emphasized('%s[*] %s: All good' % ((' '*9), check_name), ['*', check_name], [GREEN, EMPH]) if options.ko_only: Output.emphasized('\n\n%s%s' % (' '*5, '*'*22), ['%s%s' % (' '*5, '*'*22)]) Output.emphasized(' Failing checks ', ['Failing checks']) Output.emphasized('%s%s' % (' '*5, '*'*22), ['%s%s' % (' '*5, '*'*22)]) if len(failing_checks) == 0: Output.emphasized('\n%sNone' % (' '*13)) else: for check_item in failing_checks: Output.emphasized('\n%sCheck: %s' % ((' '*13), check_item['check']), ['Check']) Output.emphasized('%sSeverity: %s' % ((' '*13), check_item['severity']), ['Severity']) Output.emphasized('%sStrike Zone: %s' % ((' '*13), check_item['strike_zone']), ['Strike Zone']) Output.emphasized('%sDetail: %s' % ((' '*13), check_item['detail']), ['Detail']) elif options.output_type in ['file', 'support']: if self.__system_summary['Hardware profile'] != 'ossim-free': full_summary = dict(self.__system_summary, **self.__summary) else: full_summary = dict(self.__system_summary) full_summary['strike_zone'] = self.__in_strike_zone output_data = plain_data = json.dumps(full_summary, sort_keys=True, indent=4, separators=(',', ': ')) + '\n' # 'file' output mode will store the results in a plain file. # 'support' output mode will try to upload the encrypted and compressed file to a FTP server. if options.output_type == 'file': output_fd.write(output_data) output_fd.close() Output.emphasized('\n\nResults are stored in %s' % self.__output_file, [self.__output_file]) elif options.output_type == 'support': output_data = plain_data if output_data is not None: output_data = self.__compress__(output_data) if output_data is not None: output_fd.write(self.__cipher__(output_data)) output_fd.close() # If the FTP upload fails, let the file stay in the directory for the web to take care of it. if output_data is not None: if self.__upload__(self.__output_file): unlink(self.__output_file) else: # Notify that there was a non fatal error. # Printing this on screen will notify the user. # The permissions are changed for the web UI to read it. uid = pwd.getpwnam("root").pw_uid gid = grp.getgrnam("alienvault").gr_gid os.chown(self.__output_file, uid, gid) os.chmod(self.__output_file, 0640) print '%s' % self.__output_file self.__rc = default.exit_codes['ftp_upload_failed'] elif options.output_type == 'ansible': if self.__system_summary['Hardware profile'] != 'ossim-free': full_summary = dict(self.__system_summary, **self.__summary) else: full_summary = dict(self.__system_summary) full_summary['strike_zone'] = self.__in_strike_zone output_fd.write(json.dumps(full_summary, sort_keys=True, indent=4, separators=(',', ': ')) + '\n') print '' sys.exit(self.__rc)
def run(self): # Parse command line options. parser = OptionParser( description='Save the world, one Alien at a time', version=default.version_string) parser.add_option( "-v", "--verbose", dest="verbose", default=default.verbose, action="count", help= "More meaningful warnings [default: %default]. Maximum verbosity depth is 2 (-vv)" ) parser.add_option( "-l", "--plugin-list", dest="plugin_list", default=default.plugin_list, help= "A list of plugins you want to run, separated by commas [default: run all plugins]" ) parser.add_option( "-c", "--category_list", dest="category_list", default=default.category_list, help= "A list of plugin categories you want to run [default: run all plugins]" ) parser.add_option( "-s", "--severity_list", dest="severity_list", default=default.severity_list, help= "A list of check severities you want to run [default: run all checks]" ) parser.add_option( "-a", "--appliance_type", dest="appliance_type_list", default=default.appliance_type_list, help= "Appliance whose checks you want to run [default: run checks for current appliance] " ) parser.add_option( "-P", "--plugin-dir", dest="plugin_dir", default=default.plugin_dir, help="Directory where plugins are stored [default: %default]") parser.add_option("-k", "--ko", dest="ko_only", default=default.ko, action="store_true", help=SUPPRESS_HELP) parser.add_option("-i", "--ignore-dummy", dest="ignore_dummy", default=default.ignore_dummy, action="store_true", help=SUPPRESS_HELP) output_group = OptionGroup(parser, 'Output options') output_group.add_option('-o', '--output-type', dest='output_type', type='choice', choices=default.valid_output_types, default=default.output_type, help='Output type [default: %default]') output_group.add_option('-p', '--output-path', dest='output_path', default=default.output_path, help='Output file path [default: %default]') output_group.add_option('-f', '--output-file-prefix', dest='output_file_prefix', default=default.output_file_prefix, help='Output file prefix [default: %default]') output_group.add_option('-r', '--output-raw', dest='output_raw', default=default.output_raw, action='store_true', help='Retrieve raw data [default: %default]') parser.add_option_group(output_group) (options, args) = parser.parse_args() # Disable normal output for 'ansible' and 'support' output options. if options.output_type in ['ansible']: Output.set_std_output(False) # Ignore dummy platform package self.__ignore_dummy_platform = options.ignore_dummy # Get basic system info. self.__sysinfo = Sysinfo() self.__alienvault_config = self.__sysinfo.get_alienvault_config() self.__successful_config = self.__sysinfo.get_successful_config() Output.emphasized( '\nAlienVault Doctor version %s (%s)\n' % (default.version, default.nickname), ['AlienVault Doctor'], [GREEN]) # Parse Doctor configuration file options. if path.isfile(default.doctor_cfg_file): self.__parse_doctor_cfg__() else: Output.warning( 'Doctor configuration file does not exist, trying to continue...' ) # Parse plugin configuration files. if not path.isdir(options.plugin_dir): Output.error('"%s" is not a valid directory' % options.plugin_dir) sys.exit(default.error_codes['invalid_dir']) else: self.__plugin_dir = options.plugin_dir output_fd = None # Parse output options. if options.output_type in ['file', 'support']: mode = 'w+' # Support ticket ID has to be a 8 char long, all digit string. if options.output_type == 'support': if options.output_file_prefix == default.output_file_prefix or \ len(options.output_file_prefix) != 8 or not options.output_file_prefix.isdigit(): Output.error( 'For "support" output, a valid ticket number has to be specified as the file prefix (-f option)' ) sys.exit(default.error_codes['undef_support_prefix']) Output.set_std_output(False) if not path.exists(options.output_path): os.mkdir(options.output_path) Output.info('Output file directory "%s" created\n' % options.output_path) if path.isdir(options.output_path): try: self.__output_file = path.join( options.output_path, options.output_file_prefix + '-' + str(int(time.time())) + '.doctor') output_fd = open(self.__output_file, mode) except IOError as e: Output.warning('Cannot open file "%s" for writing: %s' % (self.__output_file, e)) else: Output.warning( '"%s" is not a valid directory, messages will be shown in stdout only' % options.output_path) elif options.output_type == 'ansible': output_fd = sys.stdout elif options.output_type == 'none': pass else: Output.warning( '"%s" is not a valid output type, messages will be shown in stdout only' % options.output_type) # Show some system info. self.__system_summary = self.__sysinfo.show_platform_info( extended=bool(options.verbose)) if self.__system_summary['Hardware profile'] != 'ossim-free': # Run a list of plugins or categories of plugins self.__plugin_list = options.plugin_list.split(',') if self.__plugin_list == [] or 'all' in self.__plugin_list: self.__plugin_list = os.listdir(options.plugin_dir) # Filter by category. self.__category_list = options.category_list.split(',') # Filter checks by severity. self.__severity_list = options.severity_list.split(',') # Filter checks by appliance_type. self.__appliance_type_list = options.appliance_type_list.split(',') # Run! Run! Run! Output.emphasized( '\nHmmm, let the Doctor have a look at you%s' % ('...\n' if options.verbose > 0 else '\n'), ['Doctor'], [GREEN], False) for filename in self.__plugin_list: if filename.endswith('.plg'): if options.verbose < 1: Progress.dots() self.__run_plugin__(options.plugin_dir + '/' + filename, options.verbose, options.output_raw) # Separator print '' else: Output.emphasized( '\nThe Doctor is not aimed to diagnose an ossim free version...', ['Doctor'], [GREEN]) # Show summary only for screen output. if options.output_type == default.output_type: if self.__system_summary['Hardware profile'] != 'ossim-free': if self.__summary != {}: Output.emphasized( '\nHooray! The Doctor has diagnosed you, check out the results...', ['Doctor'], [GREEN]) else: Output.emphasized( '\nThe Doctor has finished, nothing to see here though', ['Doctor'], [GREEN]) # Show if the system is in the 'Strike zone' if not self.__in_strike_zone: Output.emphasized( '\n Be careful! Seems that you are not in the Strike Zone! Please check the output below.', ['Strike', 'Zone'], [RED]) # Show per plugin results. plugin_det = {} for x, y in self.__summary.iteritems(): if self.__plugin_dir in x: Output.emphasized( '\n Plugin %s didn\'t run: %s' % (x, y['summary']), [x]) continue ident = int(y['id']) if 'id' in y.keys() else int( x.split(" ", 1)[0]) plugin_det[ident] = x plugin_ids = plugin_det.keys() plugin_ids.sort() failing_checks = [] for plugin_id in plugin_ids: plugin_name = plugin_det[plugin_id] result = self.__summary[plugin_name] plugin_description = result.get('description', None) plugin_strike_zone = result.get('strike_zone', None) if (not 'checks' in result.keys() or not result['checks']) and ('summary' in result.keys()): Output.emphasized( '\n Plugin %s didn\'t run: %s' % (plugin_name, result['summary']), [plugin_name]) else: checks = result['checks'] header = '\n Plugin: %s' % plugin_name if plugin_description is not None: header += '\n %s' % plugin_description if plugin_strike_zone is not None: header += '\n In the Strike Zone?: %s' % str( plugin_strike_zone) Output.emphasized(header, [plugin_name, 'In the Strike Zone?']) for (check_name, check_result) in checks.items(): if check_result['result'] == 'failed': failing_checks.append({ 'check': check_name, 'severity': check_result['severity'], 'strike_zone': check_result['strike_zone'], 'detail': check_result['detail'] }) if check_result['severity'] == 'Emerg': severity_color = RED elif check_result['severity'] == 'Alert': severity_color = RED elif check_result['severity'] == 'Critical': severity_color = RED elif check_result['severity'] == 'Error': severity_color = RED elif check_result['severity'] == 'Warning': severity_color = YELLOW elif check_result['severity'] == 'Notice': severity_color = GREEN elif check_result['severity'] == 'Info': severity_color = GREEN elif check_result['severity'] == 'Debug': severity_color = BLUE else: severity_color = YELLOW Output.emphasized( '%s[*] %s: %s' % ((' ' * 9), check_name, check_result['summary']), ['*', check_name], [severity_color, EMPH]) if check_result['remediation'] != '': Output.emphasized( '%sWord of advice: %s' % ((' ' * 13), check_result['remediation']), ['Word of advice']) else: Output.emphasized( '%s[*] %s: All good' % ((' ' * 9), check_name), ['*', check_name], [GREEN, EMPH]) if options.ko_only: Output.emphasized('\n\n%s%s' % (' ' * 5, '*' * 22), ['%s%s' % (' ' * 5, '*' * 22)]) Output.emphasized(' Failing checks ', ['Failing checks']) Output.emphasized('%s%s' % (' ' * 5, '*' * 22), ['%s%s' % (' ' * 5, '*' * 22)]) if len(failing_checks) == 0: Output.emphasized('\n%sNone' % (' ' * 13)) else: for check_item in failing_checks: Output.emphasized( '\n%sCheck: %s' % ((' ' * 13), check_item['check']), ['Check']) Output.emphasized( '%sSeverity: %s' % ((' ' * 13), check_item['severity']), ['Severity']) Output.emphasized( '%sStrike Zone: %s' % ((' ' * 13), check_item['strike_zone']), ['Strike Zone']) Output.emphasized( '%sDetail: %s' % ((' ' * 13), check_item['detail']), ['Detail']) elif options.output_type in ['file', 'support']: if self.__system_summary['Hardware profile'] != 'ossim-free': full_summary = dict(self.__system_summary, **self.__summary) else: full_summary = dict(self.__system_summary) full_summary['strike_zone'] = self.__in_strike_zone output_data = plain_data = json.dumps( full_summary, sort_keys=True, indent=4, separators=(',', ': ')) + '\n' # 'file' output mode will store the results in a plain file. # 'support' output mode will try to upload the encrypted and compressed file to a FTP server. if options.output_type == 'file': output_fd.write(output_data) output_fd.close() Output.emphasized( '\n\nResults are stored in %s' % self.__output_file, [self.__output_file]) elif options.output_type == 'support': output_data = plain_data if output_data is not None: output_data = self.__compress__(output_data) if output_data is not None: output_fd.write(self.__cipher__(output_data)) output_fd.close() # If the FTP upload fails, let the file stay in the directory for the web to take care of it. if output_data is not None: if self.__upload__(self.__output_file): unlink(self.__output_file) else: # Notify that there was a non fatal error. # Printing this on screen will notify the user. # The permissions are changed for the web UI to read it. uid = pwd.getpwnam("root").pw_uid gid = grp.getgrnam("alienvault").gr_gid os.chown(self.__output_file, uid, gid) os.chmod(self.__output_file, 0640) print '%s' % self.__output_file self.__rc = default.exit_codes['ftp_upload_failed'] elif options.output_type == 'ansible': if self.__system_summary['Hardware profile'] != 'ossim-free': full_summary = dict(self.__system_summary, **self.__summary) else: full_summary = dict(self.__system_summary) full_summary['strike_zone'] = self.__in_strike_zone output_fd.write( json.dumps(full_summary, sort_keys=True, indent=4, separators=(',', ': ')) + '\n') print '' sys.exit(self.__rc)
def __init__(self): self.weather = Weather() self.info = Sysinfo()
class MyImage(): def __init__(self): self.weather = Weather() self.info = Sysinfo() def get(self, rotate: int = 180): image = Image.new("RGB", SIZE, "#74a8ce") draw = ImageDraw.Draw(image) sysinfo = self.info.get() draw.rectangle([(10, 140), (309, 229)], fill="#000000") draw.text((15, 145), f"> ip: {sysinfo['ip']}", fill="#00FF00", font=mono) draw.text((15, 165), f"> mem_total: {sysinfo['memory']['total']} MB", fill="#00FF00", font=mono) draw.text((15, 185), f"> mem_avail: {sysinfo['memory']['avail']} MB", fill="#00FF00", font=mono) draw.text((15, 205), f"> cpu_temp: {sysinfo['temp']}", fill="#00FF00", font=mono) name, weather = self.weather.get() if not weather: return image.rotate(rotate) now = weather["now"] forecast = weather["forecast"] with Image.open(f"/home/pi/workspace/LCD/icon/{now['icon']}.png") as f: f = f.resize((96, 96)) draw.bitmap((1, 0), f, fill="#FFFFFF") draw.text((100, 5), name, fill="#FFFFFF", font=sans_normal) draw.text((120, 25), f"{now['temp']}/{now['feelsLike']}°", fill="#FFFFFF", font=sans_big) draw.text((160, 5), f"{now['text']} Hum:{now['humidity']}%", font=sans_normal, fill="#FFFFFF") for i, info in enumerate(forecast): icon, tmin, tmax = info with Image.open(f"/home/pi/workspace/LCD/icon/{icon}.png") as f: f = f.resize((40, 40)) draw.bitmap((20 + i * 100, 90), f, fill="#FFFFFF") draw.text((60 + i * 100, 110), f"{tmin}~{tmax}", fill="#FFFFFF", font=sans_normal) return image.rotate(rotate) def update(self): self.weather.update() self.info.update()