def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: station_id: WindGuru station identifier password: WindGuru password """ super(WindGuru, self).__init__(engine, config_dict) log.info("service version is %s", VERSION) try: site_dict = config_dict['StdRESTful']['WindGuru'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['station_id'] site_dict['password'] except KeyError as e: log.error("Data will not be posted: Missing option %s", e) return site_dict['manager_dict'] = weewx.manager.get_manager_dict( config_dict['DataBindings'], config_dict['Databases'], 'wx_binding') self.archive_queue = queue.Queue() self.archive_thread = WindGuruThread(self.archive_queue, **site_dict) self.archive_thread.start() self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record) log.info("Data will be uploaded for %s", site_dict['station_id'])
def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: stationid = INSERT_STATIONID_HERE password = INSERT_PASSWORD_HERE latitude: Station latitude in decimal degrees Default is station latitude longitude: Station longitude in decimal degrees Default is station longitude """ super(Weather365, self).__init__(engine, config_dict) log.info("service version is %s", VERSION) try: site_dict = config_dict['StdRESTful']['Weather365'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['stationid'] site_dict['password'] except KeyError as e: log.error("Data will not be posted: Missing option %s", e) return site_dict.setdefault('latitude', engine.stn_info.latitude_f) site_dict.setdefault('longitude', engine.stn_info.longitude_f) site_dict.setdefault('altitude', engine.stn_info.altitude_vt[0]) site_dict['manager_dict'] = weewx.manager.get_manager_dict( config_dict['DataBindings'], config_dict['Databases'], 'wx_binding') self.archive_queue = queue.Queue() self.archive_thread = Weather365Thread(self.archive_queue, **site_dict) self.archive_thread.start() self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record) log.info("Data will be uploaded for station id %s", site_dict['stationid'])
def check_enable(cfg_dict, service, *args): try: wdsupp_dict = accumulateLeaves(cfg_dict[service], max_level=1) except KeyError: log.debug("weewxwd3 check_enable: '%s' No config info. Skipped.", service) return None # Check to see whether all the needed options exist, and none of them have # been set to 'replace_me': try: for option in args: if wdsupp_dict[option] == 'replace_me': raise KeyError(option) except KeyError as e: log.debug("weewxwd3 check_enable: '%s' Missing option '%s'", service, e) return None return wdsupp_dict
def genImages(self, gen_ts): """Generate the images. The time scales will be chosen to include the given timestamp, with nice beginning and ending times. gen_ts: The time around which plots are to be generated. This will also be used as the bottom label in the plots. [optional. Default is to use the time of the last record in the database.] """ t1 = time.time() ngen = 0 # determine how much logging is desired log_success = to_bool(search_up(self.image_dict, 'log_success', True)) # Loop over each time span class (day, week, month, etc.): for timespan in self.image_dict.sections: # Now, loop over all plot names in this time span class: for plotname in self.image_dict[timespan].sections: # Accumulate all options from parent nodes: plot_options = accumulateLeaves( self.image_dict[timespan][plotname]) plotgen_ts = gen_ts if not plotgen_ts: binding = plot_options['data_binding'] db_manager = self.db_binder.get_manager(binding) plotgen_ts = db_manager.lastGoodStamp() if not plotgen_ts: plotgen_ts = time.time() image_root = os.path.join(self.config_dict['WEEWX_ROOT'], plot_options['HTML_ROOT']) # Get the path that the image is going to be saved to: img_file = os.path.join(image_root, '%s.png' % plotname) ai = to_int(plot_options.get('aggregate_interval')) # Check whether this plot needs to be done at all: if skipThisPlot(plotgen_ts, ai, img_file): continue # skip image files that are fresh, but only if staleness is defined stale = to_int(plot_options.get('stale_age')) if stale is not None: t_now = time.time() try: last_mod = os.path.getmtime(img_file) if t_now - last_mod < stale: log.debug("Skip '%s': last_mod=%s age=%s stale=%s", img_file, last_mod, t_now - last_mod, stale) continue except os.error: pass # Create the subdirectory that the image is to be put in. # Wrap in a try block in case it already exists. try: os.makedirs(os.path.dirname(img_file)) except OSError: pass # Create a new instance of a time plot and start adding to it plot = weeplot.genplot.TimePlot(plot_options) # Calculate a suitable min, max time for the requested time. (minstamp, maxstamp, timeinc) = weeplot.utilities.scaletime( plotgen_ts - int(plot_options.get('time_length', 86400)), plotgen_ts) # Override the x interval if the user has given an explicit interval: timeinc_user = to_int(plot_options.get('x_interval')) if timeinc_user is not None: timeinc = timeinc_user plot.setXScaling((minstamp, maxstamp, timeinc)) # Set the y-scaling, using any user-supplied hints: plot.setYScaling( weeutil.weeutil.convertToFloat( plot_options.get('yscale', ['None', 'None', 'None']))) # Get a suitable bottom label: bottom_label_format = plot_options.get('bottom_label_format', '%m/%d/%y %H:%M') bottom_label = time.strftime(bottom_label_format, time.localtime(plotgen_ts)) plot.setBottomLabel(bottom_label) # Set day/night display plot.setLocation(self.stn_info.latitude_f, self.stn_info.longitude_f) plot.setDayNight( to_bool(plot_options.get('show_daynight', False)), weeplot.utilities.tobgr( plot_options.get('daynight_day_color', '0xffffff')), weeplot.utilities.tobgr( plot_options.get('daynight_night_color', '0xf0f0f0')), weeplot.utilities.tobgr( plot_options.get('daynight_edge_color', '0xefefef'))) # Loop over each line to be added to the plot. for line_name in self.image_dict[timespan][plotname].sections: # Accumulate options from parent nodes. line_options = accumulateLeaves( self.image_dict[timespan][plotname][line_name]) # See what SQL variable type to use for this line. By # default, use the section name. var_type = line_options.get('data_type', line_name) # Look for aggregation type: aggregate_type = line_options.get('aggregate_type') if aggregate_type in (None, '', 'None', 'none'): # No aggregation specified. aggregate_type = aggregate_interval = None else: try: # Aggregation specified. Get the interval. aggregate_interval = line_options.as_int( 'aggregate_interval') except KeyError: log.error( "Aggregate interval required for aggregate type %s", aggregate_type) log.error("Line type %s skipped", var_type) continue # Now its time to find and hit the database: binding = line_options['data_binding'] db_manager = self.db_binder.get_manager(binding) start_vec_t, stop_vec_t, data_vec_t = weewx.xtypes.get_series( var_type, TimeSpan(minstamp, maxstamp), db_manager, aggregate_type=aggregate_type, aggregate_interval=aggregate_interval) # Get the type of plot ("bar', 'line', or 'vector') plot_type = line_options.get('plot_type', 'line') if aggregate_type and aggregate_type.lower() in ( 'avg', 'max', 'min') and plot_type != 'bar': # Put the point in the middle of the aggregate_interval for these aggregation types start_vec_t = ValueTuple([ x - aggregate_interval / 2.0 for x in start_vec_t[0] ], start_vec_t[1], start_vec_t[2]) stop_vec_t = ValueTuple([ x - aggregate_interval / 2.0 for x in stop_vec_t[0] ], stop_vec_t[1], stop_vec_t[2]) # Do any necessary unit conversions: new_start_vec_t = self.converter.convert(start_vec_t) new_stop_vec_t = self.converter.convert(stop_vec_t) new_data_vec_t = self.converter.convert(data_vec_t) # Add a unit label. NB: all will get overwritten except the # last. Get the label from the configuration dictionary. unit_label = line_options.get( 'y_label', weewx.units.get_label_string(self.formatter, self.converter, var_type)) # Strip off any leading and trailing whitespace so it's # easy to center plot.setUnitLabel(unit_label.strip()) # See if a line label has been explicitly requested: label = line_options.get('label') if not label: # No explicit label. Look up a generic one. NB: title_dict is a KeyDict which # will substitute the key if the value is not in the dictionary. label = self.title_dict[var_type] # See if a color has been explicitly requested. color = line_options.get('color') if color is not None: color = weeplot.utilities.tobgr(color) fill_color = line_options.get('fill_color') if fill_color is not None: fill_color = weeplot.utilities.tobgr(fill_color) # Get the line width, if explicitly requested. width = to_int(line_options.get('width')) interval_vec = None gap_fraction = None # Some plot types require special treatments: if plot_type == 'vector': vector_rotate_str = line_options.get('vector_rotate') vector_rotate = -float( vector_rotate_str ) if vector_rotate_str is not None else None else: vector_rotate = None if plot_type == 'bar': interval_vec = [ x[1] - x[0] for x in zip(new_start_vec_t.value, new_stop_vec_t.value) ] elif plot_type == 'line': gap_fraction = to_float( line_options.get('line_gap_fraction')) if gap_fraction is not None: if not 0 < gap_fraction < 1: log.error( "Gap fraction %5.3f outside range 0 to 1. Ignored.", gap_fraction) gap_fraction = None # Get the type of line (only 'solid' or 'none' for now) line_type = line_options.get('line_type', 'solid') if line_type.strip().lower() in ['', 'none']: line_type = None marker_type = line_options.get('marker_type') marker_size = to_int(line_options.get('marker_size', 8)) # Add the line to the emerging plot: plot.addLine( weeplot.genplot.PlotLine(new_stop_vec_t[0], new_data_vec_t[0], label=label, color=color, fill_color=fill_color, width=width, plot_type=plot_type, line_type=line_type, marker_type=marker_type, marker_size=marker_size, bar_width=interval_vec, vector_rotate=vector_rotate, gap_fraction=gap_fraction)) # OK, the plot is ready. Render it onto an image image = plot.render() try: # Now save the image image.save(img_file, optimize=True) ngen += 1 except IOError as e: log.error("Unable to save to file '%s' %s:", img_file, e) t2 = time.time() if log_success: log.info("Generated %d images for report %s in %.2f seconds", ngen, self.skin_dict['REPORT_NAME'], t2 - t1)
def generate(self, section, gen_ts): """Generate one or more reports for the indicated section. Each section in a period is a report. A report has one or more templates. section: A ConfigObj dictionary, holding the templates to be generated. Any subsections in the dictionary will be recursively processed as well. gen_ts: The report will be current to this time. """ ngen = 0 # Go through each subsection (if any) of this section, # generating from any templates they may contain for subsection in section.sections: # Sections 'SummaryByMonth' and 'SummaryByYear' imply summarize_by # certain time spans if 'summarize_by' not in section[subsection]: if subsection in CheetahGenerator.generator_dict: section[subsection]['summarize_by'] = subsection # Call recursively, to generate any templates in this subsection ngen += self.generate(section[subsection], gen_ts) # We have finished recursively processing any subsections in this # section. Time to do the section itself. If there is no option # 'template', then there isn't anything to do. Return. if 'template' not in section: return ngen # Change directory to the skin subdirectory. We use absolute paths # for cheetah, so the directory change is not necessary for generating # files. However, changing to the skin directory provides a known # location so that calls to os.getcwd() in any templates will return # a predictable result. os.chdir( os.path.join(self.config_dict['WEEWX_ROOT'], self.skin_dict['SKIN_ROOT'], self.skin_dict.get('skin', ''))) report_dict = accumulateLeaves(section) (template, dest_dir, encoding, default_binding) = self._prepGen(report_dict) # Get start and stop times default_archive = self.db_binder.get_manager(default_binding) start_ts = default_archive.firstGoodStamp() if not start_ts: log.info('Skipping template %s: cannot find start time', section['template']) return ngen if gen_ts: record = default_archive.getRecord( gen_ts, max_delta=to_int(report_dict.get('max_delta'))) if record: stop_ts = record['dateTime'] else: log.info( 'Skipping template %s: generate time %s not in database', section['template'], timestamp_to_string(gen_ts)) return ngen else: stop_ts = default_archive.lastGoodStamp() # Get an appropriate generator function summarize_by = report_dict['summarize_by'] if summarize_by in CheetahGenerator.generator_dict: _spangen = CheetahGenerator.generator_dict[summarize_by] else: # Just a single timespan to generate. Use a lambda expression. _spangen = lambda start_ts, stop_ts: [ weeutil.weeutil.TimeSpan(start_ts, stop_ts) ] # Use the generator function for timespan in _spangen(start_ts, stop_ts): start_tt = time.localtime(timespan.start) stop_tt = time.localtime(timespan.stop) if summarize_by in CheetahGenerator.format_dict: # This is a "SummaryBy" type generation. If it hasn't been done already, save the # date as a string, to be used inside the document date_str = time.strftime( CheetahGenerator.format_dict[summarize_by], start_tt) if date_str not in self.outputted_dict[summarize_by]: self.outputted_dict[summarize_by].append(date_str) # For these "SummaryBy" generations, the file name comes from the start of the timespan: _filename = self._getFileName(template, start_tt) else: # This is a "ToDate" generation. File name comes # from the stop (i.e., present) time: _filename = self._getFileName(template, stop_tt) # Get the absolute path for the target of this template _fullname = os.path.join(dest_dir, _filename) # Skip summary files outside the timespan if report_dict['summarize_by'] in CheetahGenerator.generator_dict \ and os.path.exists(_fullname) \ and not timespan.includesArchiveTime(stop_ts): continue # skip files that are fresh, but only if staleness is defined stale = to_int(report_dict.get('stale_age')) if stale is not None: t_now = time.time() try: last_mod = os.path.getmtime(_fullname) if t_now - last_mod < stale: log.debug("Skip '%s': last_mod=%s age=%s stale=%s", _filename, last_mod, t_now - last_mod, stale) continue except os.error: pass searchList = self._getSearchList(encoding, timespan, default_binding) tmpname = _fullname + '.tmp' try: # Cheetah V2 will crash if given a template file name in Unicode. So, # be prepared to catch the exception and convert to ascii: try: # TODO: Look into cacheing the compiled template. compiled_template = Cheetah.Template.Template( file=template, searchList=searchList, filter='assure_unicode', filtersLib=weewx.cheetahgenerator) except TypeError: compiled_template = Cheetah.Template.Template( file=template.encode('ascii', 'ignore'), searchList=searchList, filter='assure_unicode', filtersLib=weewx.cheetahgenerator) unicode_string = compiled_template.respond() if encoding == 'html_entities': byte_string = unicode_string.encode( 'ascii', 'xmlcharrefreplace') elif encoding == 'strict_ascii': byte_string = unicode_string.encode('ascii', 'ignore') else: byte_string = unicode_string.encode('utf8') # Open in binary mode. We are writing a byte-string, not a string with open(tmpname, mode='wb') as fd: fd.write(byte_string) os.rename(tmpname, _fullname) except Exception as e: # We would like to get better feedback when there are cheetah # compiler failures, but there seem to be no hooks for this. # For example, if we could get cheetah to emit the source # on which the compiler is working, one could compare that with # the template to figure out exactly where the problem is. # In Cheetah.Compile.ModuleCompiler the source is manipulated # a bit then handed off to parserClass. Unfortunately there # are no hooks to intercept the source and spit it out. So # the best we can do is indicate the template that was being # processed when the failure ocurred. log.error("Generate failed with exception '%s'", type(e)) log.error("**** Ignoring template %s", template) log.error("**** Reason: %s", e) weeutil.logger.log_traceback(log.error, "**** ") else: ngen += 1 finally: try: os.unlink(tmpname) except OSError: pass return ngen
def generate(self, section, section_name, gen_ts): """Generate one or more reports for the indicated section. Each section in a period is a report. A report has one or more templates. section: A ConfigObj dictionary, holding the templates to be generated. Any subsections in the dictionary will be recursively processed as well. gen_ts: The report will be current to this time. """ ngen = 0 # Go through each subsection (if any) of this section, # generating from any templates they may contain for subsection in section.sections: # Sections 'SummaryByMonth' and 'SummaryByYear' imply summarize_by # certain time spans if 'summarize_by' not in section[subsection]: if subsection in CheetahGenerator.generator_dict: section[subsection]['summarize_by'] = subsection # Call recursively, to generate any templates in this subsection ngen += self.generate(section[subsection], subsection, gen_ts) # We have finished recursively processing any subsections in this # section. Time to do the section itself. If there is no option # 'template', then there isn't anything to do. Return. if 'template' not in section: return ngen # Change directory to the skin subdirectory. We use absolute paths # for cheetah, so the directory change is not necessary for generating # files. However, changing to the skin directory provides a known # location so that calls to os.getcwd() in any templates will return # a predictable result. os.chdir( os.path.join(self.config_dict['WEEWX_ROOT'], self.skin_dict['SKIN_ROOT'], self.skin_dict.get('skin', ''))) report_dict = accumulateLeaves(section) (template, dest_dir, encoding, default_binding) = self._prepGen(report_dict) # Get start and stop times default_archive = self.db_binder.get_manager(default_binding) start_ts = default_archive.firstGoodStamp() if not start_ts: log.info('Skipping template %s: cannot find start time', section['template']) return ngen if gen_ts: record = default_archive.getRecord( gen_ts, max_delta=to_int(report_dict.get('max_delta'))) if record: stop_ts = record['dateTime'] else: log.info( 'Skipping template %s: generate time %s not in database', section['template'], timestamp_to_string(gen_ts)) return ngen else: stop_ts = default_archive.lastGoodStamp() # Get an appropriate generator function summarize_by = report_dict['summarize_by'] if summarize_by in CheetahGenerator.generator_dict: _spangen = CheetahGenerator.generator_dict[summarize_by] else: # Just a single timespan to generate. Use a lambda expression. _spangen = lambda start_ts, stop_ts: [ weeutil.weeutil.TimeSpan(start_ts, stop_ts) ] # Use the generator function for timespan in _spangen(start_ts, stop_ts): start_tt = time.localtime(timespan.start) stop_tt = time.localtime(timespan.stop) if summarize_by in CheetahGenerator.format_dict: # This is a "SummaryBy" type generation. If it hasn't been done already, save the # date as a string, to be used inside the document date_str = time.strftime( CheetahGenerator.format_dict[summarize_by], start_tt) if date_str not in self.outputted_dict[summarize_by]: self.outputted_dict[summarize_by].append(date_str) # For these "SummaryBy" generations, the file name comes from the start of the timespan: _filename = self._getFileName(template, start_tt) else: # This is a "ToDate" generation. File name comes # from the stop (i.e., present) time: _filename = self._getFileName(template, stop_tt) # Get the absolute path for the target of this template _fullname = os.path.join(dest_dir, _filename) # Skip summary files outside the timespan if report_dict['summarize_by'] in CheetahGenerator.generator_dict \ and os.path.exists(_fullname) \ and not timespan.includesArchiveTime(stop_ts): continue # skip files that are fresh, but only if staleness is defined stale = to_int(report_dict.get('stale_age')) if stale is not None: t_now = time.time() try: last_mod = os.path.getmtime(_fullname) if t_now - last_mod < stale: log.debug("Skip '%s': last_mod=%s age=%s stale=%s", _filename, last_mod, t_now - last_mod, stale) continue except os.error: pass searchList = self._getSearchList( encoding, timespan, default_binding, section_name, os.path.join(os.path.dirname(report_dict['template']), _filename)) # First, compile the template try: # TODO: Look into caching the compiled template. # Under Python 2, Cheetah V2 will crash if given a template file name in Unicode, # so make sure it's a string first, using six.ensure_str(). compiled_template = Cheetah.Template.Template( file=six.ensure_str(template), searchList=searchList, filter='AssureUnicode', filtersLib=weewx.cheetahgenerator) except Exception as e: log.error( "Compilation of template %s failed with exception '%s'", template, type(e)) log.error("**** Ignoring template %s", template) log.error("**** Reason: %s", e) weeutil.logger.log_traceback(log.error, "**** ") continue # Second, evaluate the compiled template try: # We have a compiled template in hand. Evaluate it. The result will be a long # Unicode string. unicode_string = compiled_template.respond() except Cheetah.Parser.ParseError as e: log.error("Parse error while evaluating file %s", template) log.error("**** Ignoring template %s", template) log.error("**** Reason: %s", e) continue except Cheetah.NameMapper.NotFound as e: log.error("Evaluation of template %s failed.", template) log.error("**** Ignoring template %s", template) log.error("**** Reason: %s", e) log.error( "**** To debug, try inserting '#errorCatcher Echo' at top of template" ) continue except Exception as e: log.error( "Evaluation of template %s failed with exception '%s'", template, type(e)) log.error("**** Ignoring template %s", template) log.error("**** Reason: %s", e) weeutil.logger.log_traceback(log.error, "**** ") continue # Third, convert the results to a byte string, using the strategy chosen by the user. if encoding == 'html_entities': byte_string = unicode_string.encode('ascii', 'xmlcharrefreplace') elif encoding == 'strict_ascii': byte_string = unicode_string.encode('ascii', 'ignore') elif encoding == 'normalized_ascii': # Normalize the string, replacing accented characters with non-accented # equivalents normalized = unicodedata.normalize('NFD', unicode_string) byte_string = normalized.encode('ascii', 'ignore') else: byte_string = unicode_string.encode(encoding) # Finally, write the byte string to the target file try: # Write to a temporary file first tmpname = _fullname + '.tmp' # Open it in binary mode. We are writing a byte-string, not a string with open(tmpname, mode='wb') as fd: fd.write(byte_string) # Now move the temporary file into place os.rename(tmpname, _fullname) ngen += 1 finally: try: os.unlink(tmpname) except OSError: pass return ngen
def gen_images(self, gen_ts): """Generate the images. The time scales will be chosen to include the given timestamp, with nice beginning and ending times. Args: gen_ts (int): The time around which plots are to be generated. This will also be used as the bottom label in the plots. [optional. Default is to use the time of the last record in the database.] """ t1 = time.time() ngen = 0 # determine how much logging is desired log_success = to_bool(search_up(self.image_dict, 'log_success', True)) # Loop over each time span class (day, week, month, etc.): for timespan in self.image_dict.sections: # Now, loop over all plot names in this time span class: for plotname in self.image_dict[timespan].sections: # Accumulate all options from parent nodes: plot_options = accumulateLeaves( self.image_dict[timespan][plotname]) plotgen_ts = gen_ts if not plotgen_ts: binding = plot_options['data_binding'] db_manager = self.db_binder.get_manager(binding) plotgen_ts = db_manager.lastGoodStamp() if not plotgen_ts: plotgen_ts = time.time() image_root = os.path.join(self.config_dict['WEEWX_ROOT'], plot_options['HTML_ROOT']) # Get the path that the image is going to be saved to: img_file = os.path.join(image_root, '%s.png' % plotname) # Check whether this plot needs to be done at all: if _skip_this_plot(plotgen_ts, plot_options, img_file): continue # Generate the plot. plot = self.gen_plot(plotgen_ts, plot_options, self.image_dict[timespan][plotname]) # 'plot' will be None if skip_if_empty was truthy, and the plot contains no data if plot: # We have a valid plot. Render it onto an image image = plot.render() # Create the subdirectory that the image is to be put in. Wrap in a try block # in case it already exists. try: os.makedirs(os.path.dirname(img_file)) except OSError: pass try: # Now save the image image.save(img_file) ngen += 1 except IOError as e: log.error("Unable to save to file '%s' %s:", img_file, e) t2 = time.time() if log_success: log.info("Generated %d images for report %s in %.2f seconds", ngen, self.skin_dict['REPORT_NAME'], t2 - t1)
def gen_plot(self, plotgen_ts, plot_options, plot_dict): """Generate a single plot image. Args: plotgen_ts: A timestamp for which the plot will be valid. This is generally the last datum to be plotted. plot_options: A dictionary of plot options. plot_dict: A section in a ConfigObj. Each subsection will contain data about plots to be generated Returns: An instance of weeplot.genplot.TimePlot or None. If the former, it will be ready to render. If None, then skip_if_empty was truthy and no valid data were found. """ # Create a new instance of a time plot and start adding to it plot = weeplot.genplot.TimePlot(plot_options) # Calculate a suitable min, max time for the requested time. minstamp, maxstamp, timeinc = weeplot.utilities.scaletime( plotgen_ts - int(plot_options.get('time_length', 86400)), plotgen_ts) x_domain = weeutil.weeutil.TimeSpan(minstamp, maxstamp) # Override the x interval if the user has given an explicit interval: timeinc_user = to_int(plot_options.get('x_interval')) if timeinc_user is not None: timeinc = timeinc_user plot.setXScaling((x_domain.start, x_domain.stop, timeinc)) # Set the y-scaling, using any user-supplied hints: yscale = plot_options.get('yscale', ['None', 'None', 'None']) plot.setYScaling(weeutil.weeutil.convertToFloat(yscale)) # Get a suitable bottom label: bottom_label_format = plot_options.get('bottom_label_format', '%m/%d/%y %H:%M') bottom_label = time.strftime(bottom_label_format, time.localtime(plotgen_ts)) plot.setBottomLabel(bottom_label) # Set day/night display plot.setLocation(self.stn_info.latitude_f, self.stn_info.longitude_f) plot.setDayNight( to_bool(plot_options.get('show_daynight', False)), weeplot.utilities.tobgr( plot_options.get('daynight_day_color', '0xffffff')), weeplot.utilities.tobgr( plot_options.get('daynight_night_color', '0xf0f0f0')), weeplot.utilities.tobgr( plot_options.get('daynight_edge_color', '0xefefef'))) # Calculate the domain over which we should check for non-null data. It will be # 'None' if we are not to do the check at all. check_domain = _get_check_domain( plot_options.get('skip_if_empty', False), x_domain) # Set to True if we have _any_ data for the plot have_data = False # Loop over each line to be added to the plot. for line_name in plot_dict.sections: # Accumulate options from parent nodes. line_options = accumulateLeaves(plot_dict[line_name]) # See what observation type to use for this line. By default, use the section # name. var_type = line_options.get('data_type', line_name) # Find the database binding = line_options['data_binding'] db_manager = self.db_binder.get_manager(binding) # If we were asked, see if there is any non-null data in the plot skip = _skip_if_empty(db_manager, var_type, check_domain) if skip: # Nothing but null data. Skip this line and keep going continue # Either we found some non-null data, or skip_if_empty was false, and we don't care. have_data = True # Look for aggregation type: aggregate_type = line_options.get('aggregate_type') if aggregate_type in (None, '', 'None', 'none'): # No aggregation specified. aggregate_type = aggregate_interval = None else: try: # Aggregation specified. Get the interval. aggregate_interval = weeutil.weeutil.nominal_spans( line_options['aggregate_interval']) except KeyError: log.error( "Aggregate interval required for aggregate type %s", aggregate_type) log.error("Line type %s skipped", var_type) continue # we need to pass the line options and plotgen_ts to our xtype # first get a copy of line_options option_dict = dict(line_options) # but we need to pop off aggregate_type and # aggregate_interval as they are used as explicit arguments # in our xtypes call option_dict.pop('aggregate_type', None) option_dict.pop('aggregate_interval', None) # then add plotgen_ts option_dict['plotgen_ts'] = plotgen_ts try: start_vec_t, stop_vec_t, data_vec_t = weewx.xtypes.get_series( var_type, x_domain, db_manager, aggregate_type=aggregate_type, aggregate_interval=aggregate_interval, **option_dict) except weewx.UnknownType: # If skip_if_empty is set, it's OK if a type is unknown. if not skip: raise # Get the type of plot ('bar', 'line', or 'vector') plot_type = line_options.get('plot_type', 'line').lower() if aggregate_type and plot_type != 'bar': # If aggregating, put the point in the middle of the interval start_vec_t = ValueTuple( [x - aggregate_interval / 2.0 for x in start_vec_t[0]], # Value start_vec_t[1], # Unit start_vec_t[2]) # Unit group stop_vec_t = ValueTuple( [x - aggregate_interval / 2.0 for x in stop_vec_t[0]], # Velue stop_vec_t[1], # Unit stop_vec_t[2]) # Unit group # Convert the data to the requested units new_data_vec_t = self.converter.convert(data_vec_t) # Add a unit label. NB: all will get overwritten except the last. Get the label # from the configuration dictionary. unit_label = line_options.get( 'y_label', self.formatter.get_label_string(new_data_vec_t[1])) # Strip off any leading and trailing whitespace so it's easy to center plot.setUnitLabel(unit_label.strip()) # See if a line label has been explicitly requested: label = line_options.get('label') if label: # Yes. Get the text translation. Use the untranslated version if no translation # is available. label = self.text_dict.get(label, label) else: # No explicit label. Look up a generic one. Use the variable type itself if # there is no generic label. label = self.generic_dict.get(var_type, var_type) # See if a color has been explicitly requested. color = line_options.get('color') if color is not None: color = weeplot.utilities.tobgr(color) fill_color = line_options.get('fill_color') if fill_color is not None: fill_color = weeplot.utilities.tobgr(fill_color) # Get the line width, if explicitly requested. width = to_int(line_options.get('width')) interval_vec = None gap_fraction = None vector_rotate = None # Some plot types require special treatments: if plot_type == 'vector': vector_rotate_str = line_options.get('vector_rotate') vector_rotate = -float(vector_rotate_str) \ if vector_rotate_str is not None else None elif plot_type == 'bar': interval_vec = [ x[1] - x[0] for x in zip(start_vec_t.value, stop_vec_t.value) ] elif plot_type == 'line': gap_fraction = to_float(line_options.get('line_gap_fraction')) if gap_fraction is not None and not 0 < gap_fraction < 1: log.error( "Gap fraction %5.3f outside range 0 to 1. Ignored.", gap_fraction) gap_fraction = None else: log.error("Unknown plot type '%s'. Ignored", plot_type) continue # Get the type of line (only 'solid' or 'none' for now) line_type = line_options.get('line_type', 'solid') if line_type.strip().lower() in ['', 'none']: line_type = None marker_type = line_options.get('marker_type') marker_size = to_int(line_options.get('marker_size', 8)) # Add the line to the emerging plot: plot.addLine( weeplot.genplot.PlotLine(stop_vec_t[0], new_data_vec_t[0], label=label, color=color, fill_color=fill_color, width=width, plot_type=plot_type, line_type=line_type, marker_type=marker_type, marker_size=marker_size, bar_width=interval_vec, vector_rotate=vector_rotate, gap_fraction=gap_fraction)) # Return the constructed plot if it has any non-null data, otherwise return None return plot if have_data else None