def _get_site_dict(config_dict, service, *args): """Obtain the site options, with defaults from the StdRESTful section. If the service is not enabled, or if one or more required parameters is not specified, then return None.""" try: site_dict = accumulateLeaves(config_dict['StdRESTful'][service], max_level=1) except KeyError: log_err("restx: %s: no config info. Skipped." % service) return None try: if not to_bool(site_dict['enabled']): log_inf("restx: %s: service not enabled." % service) except KeyError: pass try: for option in args: if site_dict[option] == 'replace_me': raise KeyError(option) except KeyError, e: log_dbg("restx: %s. Data will not be posted: missing option %s" % (service, e)) return None
def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: username: OpenWeatherMap username password: OpenWeatherMap password station_name: station name latitude: Station latitude in decimal degrees Default is station latitude longitude: Station longitude in decimal degrees Default is station longitude altitude: Station altitude in meters Default is station altitude """ super(OpenWeatherMap, self).__init__(engine, config_dict) loginf("service version is %s" % VERSION) try: site_dict = config_dict["StdRESTful"]["OpenWeatherMap"] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict["username"] site_dict["password"] site_dict["station_name"] except KeyError, e: logerr("Data will not be posted: Missing option %s" % e) return
def __init__(self, engine, config_dict): super(OpsGenieHeartbeat, self).__init__(engine, config_dict) # Extract the required parameters. If one of them is missing, # a KeyError exception will occur. Be prepared to catch it. try: # Extract a copy of the dictionary with the registry options: _opsgenie_dict = accumulateLeaves( config_dict['OpsGenie']['Heartbeat'], max_level=1) # Should the service be run? if not to_bool(_opsgenie_dict.pop('send_heartbeat', False)): syslog.syslog( syslog.LOG_INFO, "restx: OpsGenieHeartbeat: " "Send Heartbeat not requested.") return if _opsgenie_dict['apiKey'] is None: raise KeyError("apiKey") if _opsgenie_dict['heartbeatName'] is None: raise KeyError("heartbeatName") except KeyError, e: syslog.syslog( syslog.LOG_DEBUG, "restx: OpsGenieHeartbeat: " "Heartbeats will not be sent. Missing option {}".format(e)) return
def __init__(self, engine, cfg_dict): """This service recognizes standard restful options plus the following: Required parameters: database: name of the database at the server Optional parameters: host: server hostname Default is localhost port: server port Default is 8086 server_url: full restful endpoint of the server Default is None measurement: name of the measurement Default is 'record' tags: comma-delimited list of name=value pairs to identify the measurement. tags cannot contain spaces. Default is None create_database: should the upload attempt to create database first Default is True line_format: which line protocol format to use. Possible values are single-line or multi-line. Default is single-line append_units_label: should units label be appended to name Default is True obs_to_upload: Which observations to upload. Possible values are most, none, or all. When none is specified, only items in the inputs list will be uploaded. When all is specified, all observations will be uploaded, subject to overrides in the inputs list. Default is most inputs: dictionary of weewx observation names with optional upload name, format, and units Default is None binding: options include "loop", "archive", or "loop,archive" Default is archive """ super(Influx, self).__init__(engine, cfg_dict) loginf("service version is %s" % VERSION) try: site_dict = cfg_dict['StdRESTful']['Influx'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['database'] except KeyError, e: logerr("Data will not be uploaded: Missing option %s" % e) return
def __init__(self, engine, config_dict): super(EveryAware, self).__init__(engine, config_dict) loginf("service version is %s" % VERSION) try: site_dict = config_dict['StdRESTful']['EveryAware'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['feeds'] site_dict['geoLatitude'] = config_dict['Station']['latitude'] site_dict['geoLongitude'] = config_dict['Station']['longitude'] site_dict['location'] = config_dict['Station']['location'] site_dict['altitude'] = config_dict['Station']['altitude'] site_dict['stationType'] = config_dict['Station']['station_type'] except KeyError, e: logerr("Data will not be posted: Missing option %s" % e) return
def __init__(self, engine, config_dict): super(StdRainlog, self).__init__(engine, config_dict) self.protocol_name = 'Rainlog' try: site_dict = accumulateLeaves(config_dict['StdRESTful']['Rainlog'], max_level=1) site_dict['username'] site_dict['password'] site_dict['WEEWX_ROOT'] = config_dict['WEEWX_ROOT'] except KeyError, e: logdbg("rainlog: %s: " "Data will not be posted: Missing option %s" % (self.protocol_name, e)) return
def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: appid: APPID from OpenWeatherMap station_id: station identifier latitude: Station latitude in decimal degrees Default is station latitude longitude: Station longitude in decimal degrees Default is station longitude altitude: Station altitude in meters Default is station altitude """ super(OpenWeatherMap, self).__init__(engine, config_dict) loginf('service version is %s' % VERSION) # Check to make sure this version of weewx supports JSON posts. # To do this, look for function weewx.restx.RESTThread.get_post_body try: getattr(weewx.restx.RESTThread, 'get_post_body') except AttributeError: loginf('WeeWX needs to be upgraded to V3.8 in order to post to OWM') loginf('**** OWM upload skipped') return try: site_dict = config_dict['StdRESTful']['OpenWeatherMap'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['appid'] site_dict['station_id'] except KeyError as e: logerr("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 = OpenWeatherMapThread(self.archive_queue, **site_dict) self.archive_thread.start() self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record) loginf("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: station_id: WindGuru station identifier password: WindGuru password """ super(WindGuru, self).__init__(engine, config_dict) loginf("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, e: logerr("Data will not be posted: Missing option %s" % e) return
def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: id: WeatherCloud identifier key: WeatherCloud key """ super(WeatherCloud, self).__init__(engine, config_dict) loginf("service version is %s" % VERSION) try: site_dict = config_dict['StdRESTful']['WeatherCloud'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['id'] site_dict['key'] except KeyError, e: logerr("Data will not be posted: Missing option %s" % e) return
def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: username: OpenWeatherMap username password: OpenWeatherMap password station_name: station name latitude: Station latitude in decimal degrees Default is station latitude longitude: Station longitude in decimal degrees Default is station longitude altitude: Station altitude in meters Default is station altitude """ super(OpenWeatherMap, self).__init__(engine, config_dict) loginf('service version is %s' % VERSION) try: site_dict = config_dict['StdRESTful']['OpenWeatherMap'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['username'] site_dict['password'] site_dict['station_name'] except KeyError as e: logerr("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 = OpenWeatherMapThread(self.archive_queue, **site_dict) self.archive_thread.start() self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record) loginf("Data will be uploaded for %s" % site_dict['station_name'])
def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: Required parameters: server_url: URL of the broker, e.g., something of the form mqtt://username:password@localhost:1883/ Default is None Optional parameters: unit_system: one of US, METRIC, or METRICWX Default is None; units will be those of data in the database topic: the MQTT topic under which to post Default is 'weather' append_units_label: should units label be appended to name Default is True obs_to_upload: Which observations to upload. Possible values are none or all. When none is specified, only items in the inputs list will be uploaded. When all is specified, all observations will be uploaded, subject to overrides in the inputs list. Default is all inputs: dictionary of weewx observation names with optional upload name, format, and units Default is None tls: dictionary of TLS parameters used by the Paho client to establish a secure connection with the broker. Default is None """ super(MQTT, self).__init__(engine, config_dict) loginf("service version is %s" % VERSION) try: site_dict = config_dict['StdRESTful']['MQTT'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['server_url'] except KeyError, e: logerr("Data will not be uploaded: Missing option %s" % e) return
def __init__(self, engine, config_dict): super(LaMetric, self).__init__(engine, config_dict) loginf('service version is %s' % VERSION) try: site_dict = config_dict['StdRESTful']['LaMetric'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['server_ip'] site_dict['device_key'] site_dict['icon'] site_dict['sound'] except KeyError as e: logerr("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 = LaMetricThread(self.archive_queue, **site_dict) self.archive_thread.start() self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record) loginf("Data will be sent to %s" % site_dict['server_ip'])
def __init__(self, engine, config_dict): """This service recognizes standard restful options plus the following: Required parameters: server_url: URL of the broker, e.g., something of the form mqtt://username:password@localhost:1883/ Default is None Optional parameters: unit_system: one of US, METRIC, or METRICWX Default is None; units will be those of data in the database topic: the MQTT topic under which to post Default is 'weather' append_units_label: should units label be appended to name Default is True obs_to_upload: Which observations to upload. Possible values are none or all. When none is specified, only items in the inputs list will be uploaded. When all is specified, all observations will be uploaded, subject to overrides in the inputs list. Default is all inputs: dictionary of weewx observation names with optional upload name, format, and units Default is None tls: dictionary of TLS parameters used by the Paho client to establish a secure connection with the broker. Default is None agg_topic_loop: Allows to set the destination sub-topic for aggregated loop packets (only used if aggregation contains aggregate) Default is loop agg_topic_archive: Allows to set the destination sub-topic for aggregated archive packets (only used if aggregation contains aggregate) Default is loop """ super(MQTT, self).__init__(engine, config_dict) loginf("service version is %s" % VERSION) try: site_dict = config_dict['StdRESTful']['MQTT'] site_dict = accumulateLeaves(site_dict, max_level=1) site_dict['server_url'] except KeyError as e: logerr("Data will not be uploaded: Missing option %s" % e) return # for backward compatibility: 'units' is now 'unit_system' _compat(site_dict, 'units', 'unit_system') site_dict.setdefault('client_id', '') site_dict.setdefault('topic', 'weather') site_dict.setdefault('append_units_label', True) site_dict.setdefault('augment_record', True) site_dict.setdefault('obs_to_upload', 'all') site_dict.setdefault('retain', False) site_dict.setdefault('qos', 0) site_dict.setdefault('aggregation', 'individual,aggregate') site_dict.setdefault('agg_topic_loop', 'loop') site_dict.setdefault('agg_topic_archive', 'loop') usn = site_dict.get('unit_system', None) if usn is not None: site_dict['unit_system'] = weewx.units.unit_constants[usn] if 'tls' in config_dict['StdRESTful']['MQTT']: site_dict['tls'] = dict(config_dict['StdRESTful']['MQTT']['tls']) if 'inputs' in config_dict['StdRESTful']['MQTT']: site_dict['inputs'] = dict( config_dict['StdRESTful']['MQTT']['inputs']) site_dict['append_units_label'] = to_bool( site_dict.get('append_units_label')) site_dict['augment_record'] = to_bool(site_dict.get('augment_record')) site_dict['retain'] = to_bool(site_dict.get('retain')) site_dict['qos'] = to_int(site_dict.get('qos')) binding = site_dict.pop('binding', 'archive') loginf("binding to %s" % binding) # if we are supposed to augment the record with data from weather # tables, then get the manager dict to do it. there may be no weather # tables, so be prepared to fail. try: if site_dict.get('augment_record'): _manager_dict = weewx.manager.get_manager_dict_from_config( config_dict, 'wx_binding') site_dict['manager_dict'] = _manager_dict except weewx.UnknownBinding: pass self.archive_queue = Queue.Queue() self.archive_thread = MQTTThread(self.archive_queue, **site_dict) self.archive_thread.start() if 'archive' in binding: self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record) if 'loop' in binding: self.bind(weewx.NEW_LOOP_PACKET, self.new_loop_packet) if 'topic' in site_dict: loginf("topic is %s" % site_dict['topic']) if usn is not None: loginf("desired unit system is %s" % usn) loginf("data will be uploaded to %s" % _obfuscate_password(site_dict['server_url'])) if 'tls' in site_dict: loginf("network encryption/authentication will be attempted")
def gen_windrose_plots(self): """Generate the windrose plots. Loop through each 2nd level section (ie [[]]) under [ImageStackedWindRoseGenerator] and generate the plot defined by each 2nd level section. """ # Time period taken to generate plots, set plot count to 0 t1 = time.time() ngen = 0 # Loop over each time span class (day, week, month, etc.): for span in self.image_dict.sections: # Now, loop over all plot names in this time span class: for plot in self.image_dict[span].sections: # Accumulate all options from parent nodes: p_options = accumulateLeaves(self.image_dict[span][plot]) # Get end time for plot. In order try self.gen_ts, last known # good archive time stamp and then current time self.p_gen_ts = self.gen_ts if not self.p_gen_ts: self.p_gen_ts = self.archive.lastGoodStamp() if not self.p_gen_ts: self.p_gen_ts = time.time() # Get the period for the plot, default to 24 hours if no # period set self.period = p_options.as_int('period') if 'period' in p_options else 86400 # Get the path of the image file we will save image_root = os.path.join(self.config_dict['WEEWX_ROOT'], p_options['HTML_ROOT']) # Get image file format. Can use any format PIL can write # Default to png if 'format' in p_options: file_format = p_options['format'] else: file_format = "png" # Get full file name and path for plot img_file = os.path.join(image_root, '%s.%s' % (plot, file_format)) # Check whether this plot needs to be done at all: if self.skip_this_plot(img_file, plot): continue # 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 os.error: pass # Loop over each line to be added to the plot. for line_name in self.image_dict[span][plot].sections: # Accumulate options from parent nodes. line_options = accumulateLeaves(self.image_dict[span][plot][line_name]) # See if a plot title has been explicitly requested. # 'label' used for consistency in skin.conf with # ImageGenerator sections label = line_options.get('label') if label: self.label = unicode(label, 'utf8') else: # No explicit label so set label to nothing self.label = label # See if a time_stamp has been explicitly requested. self.t_stamp = line_options.get('time_stamp') # See if time_stamp location has been explicitly set _location = line_options.get('time_stamp_location') if _location: self.t_stamp_loc = [x.upper() for x in _location] else: self.t_stamp_loc = None # See what SQL variable type to use for this plot and get # corresponding 'direction' type. Can really only plot # windSpeed and windGust, if anything else default to # windSpeed. self.obName = line_options.get('data_type', line_name) if self.obName == 'windSpeed': self.dirName = 'windDir' elif self.obName == 'windGust': self.dirName = 'windGustDir' else: self.obName == 'windSpeed' self.dirName = 'windDir' # Get our data tuples for speed and direction. vector_tspan = TimeSpan(self.p_gen_ts - self.period + 1, self.p_gen_ts) (_, time_vec_t_ws_stop, data_speed) = self.archive.getSqlVectors(vector_tspan, self.obName) (_, time_vec_t_wd_stop, dir_vec) = self.archive.getSqlVectors(vector_tspan, self.dirName) # Convert our speed values to the units we are going to # use in our plot speed_vec = self.converter.convert(data_speed) # Get units for display on legend self.units = self.skin_dict['Units']['Labels'][speed_vec[1]].strip() # Find maximum speed from our data max_speed = max(speed_vec[0]) # Set upper speed range for our plot, set to a multiple of # 10 for a neater display max_speed_range = (int(max_speed / 10.0) + 1) * 10 # Setup 2D list with speed range boundaries in speed_list[0] # petal colours in speed_list[1] speed_list = [[0 for x in range(7)] for x in range(2)] # Store petal colours speed_list[1] = self.petal_colors # Loop though each speed range boundary and store in # speed_list[0] i = 1 while i < 7: speed_list[0][i] = self.speedFactor[i] * max_speed_range i += 1 # Setup 2D list for wind direction # wind_bin[0] represents each of 16 compass directions # ([0] is N, [1] is ENE etc). # wind_bin[1] holds count of obs in a particular speed range # for given direction wind_bin = [[0 for x in range(7)] for x in range(17)] # Setup list to hold obs counts for each speed range speed_bin = [0 for x in range(7)] # How many obs do we have? samples = len(time_vec_t_ws_stop[0]) # Loop through each sample and increment direction counts # and speed ranges for each direction as necessary. 'None' # direction is counted as 'calm' (or 0 speed) and # (by definition) no direction and are plotted in the # 'bullseye' on the plot i = 0 while i < samples: if (speed_vec[0][i] is None) or (dir_vec[0][i] is None): wind_bin[16][6] += 1 else: bin = int((dir_vec[0][i] + 11.25) / 22.5) % 16 if speed_vec[0][i] > speed_list[0][5]: wind_bin[bin][6] += 1 elif speed_vec[0][i] > speed_list[0][4]: wind_bin[bin][5] += 1 elif speed_vec[0][i] > speed_list[0][3]: wind_bin[bin][4] += 1 elif speed_vec[0][i] > speed_list[0][2]: wind_bin[bin][3] += 1 elif speed_vec[0][i] > speed_list[0][1]: wind_bin[bin][2] += 1 elif speed_vec[0][i] > 0: wind_bin[bin][1] += 1 else: wind_bin[bin][0] += 1 i += 1 # Add 'None' obs to 0 speed count speed_bin[0] += wind_bin[16][6] # Don't need the 'None' counts so we can delete them del wind_bin[-1] # Now set total (direction independent) speed counts. Loop # through each petal speed range and increment direction # independent speed ranges as necessary j = 0 while j < 7: i = 0 while i < 16: speed_bin[j] += wind_bin[i][j] i += 1 j += 1 # Calc the value to represented by outer ring # (range 0 to 1). Value to rounded up to next multiple of # 0.05 (ie next 5%) self.max_ring_value = (int(max(sum(b) for b in wind_bin) / (0.05 * samples)) + 1) * 0.05 # Find which wind rose arm to use to display ring range # labels - look for one that is relatively clear. Only # consider NE, SE, SW and NW preference in order is # SE, SW, NE and NW # Default to SE label_dir = 6 # Is SE clear? if sum(wind_bin[6]) / float(samples) <= 0.3 * self.max_ring_value: # If so take it label_dir = 6 else: # If not lets loop through the others for i in [10, 2, 14]: # Is SW, NE or NW clear if sum(wind_bin[i])/float(samples) <= 0.3 * self.max_ring_value: # If so let's take it and exit the for loop label_dir = i break else: # If none are free then let's take the smallest of # the four # Set max possible number of readings + 1 label_count = samples + 1 for i in [2, 6, 10, 14]: # Loop through directions # If this direction has fewer obs than previous # best (least) if sum(wind_bin[i]) < label_count: # Set min count so far to this bin label_count = sum(wind_bin[i]) # Set label_dir to this direction label_dir = i self.label_dir = label_dir # Set up an Image object to hold our windrose plot self.windrose_image_setup() # Get a Draw object to draw on self.draw = ImageDraw.Draw(self.image) # Set fonts to be used self.plotFont = get_font_handle(self.font_path, self.plot_font_size) self.legendFont = get_font_handle(self.font_path, self.legend_font_size) self.labelFont = get_font_handle(self.font_path, self.label_font_size) # Estimate space required for the legend text_width, text_height = self.draw.textsize("0 (100%)", font=self.legendFont) legend_width = int(text_width + 2 * self.legend_bar_width + 1.5 * self.plot_border) # Estimate space required for label (if required) text_width, text_height = self.draw.textsize("Wind Rose", font=self.labelFont) if self.label: label_height = int(text_width+self.plot_border) else: label_height = 0 # Calculate the diameter of the circular plot space in # pixels. Two diameters are calculated, one based on image # height and one based on image width. We will take the # smallest one. To prevent optical distortion for small # plots diameter will be divisible by 22 self.roseMaxDiameter = min(int((self.image_height - 2 * self.plot_border - label_height / 2) / 22.0) * 22, int((self.image_width - (2 * self.plot_border + legend_width)) / 22.0) * 22) if self.image_width > self.image_height: # If wider than height text_width, text_height = self.draw.textsize("W", font=self.plotFont) # x coord of windrose circle origin(0,0) top left corner self.originX = self.plot_border + text_width + 2 + self.roseMaxDiameter / 2 # y coord of windrose circle origin(0,0) is top left corner self.originY = int(self.image_height / 2) else: # x coord of windrose circle origin(0,0) top left corner self.originX = 2 * self.plot_border + self.roseMaxDiameter / 2 # y coord of windrose circle origin(0,0) is top left corner self.originY = 2 * self.plot_border + self.roseMaxDiameter / 2 # Setup windrose plot. Plot circles, range rings, range # labels, N-S and E-W centre lines and compass pont labels self.windrose_plot_setup() # Plot wind rose petals # Each petal is constructed from overlapping pieslices # starting from outside (biggest) and working in (smallest) # Start at 'North' windrose petal a = 0 # Loop through each wind rose arm while a < len(wind_bin): s = len(speed_list[0]) - 1 cum_radius = sum(wind_bin[a]) if cum_radius > 0: arm_radius = int((10 * self.roseMaxDiameter * sum(wind_bin[a])) / (11 * 2.0 * self.max_ring_value * samples)) while s > 0: # Calc radius of current arm pie_radius = int(round(arm_radius * cum_radius/sum(wind_bin[a]) + self.roseMaxDiameter / 22, 0)) # Set bound box for pie slice bbox = (self.originX-pie_radius, self.originY-pie_radius, self.originX+pie_radius, self.originY+pie_radius) # Draw pie slice self.draw.pieslice(bbox, int(a * 22.5 - 90 - self.petal_width / 2), int(a * 22.5 - 90 + self.petal_width / 2), fill=speed_list[1][s], outline='black') cum_radius -= wind_bin[a][s] s -= 1 # Move 'in' for next pieslice a += 1 # Next arm # Draw 'bullseye' to represent windSpeed=0 or calm # Produce the label label0 = str(int(round(100.0 * speed_bin[0] / sum(speed_bin), 0))) + '%' # Work out its size, particularly its width text_width, text_height = self.draw.textsize(label0, font=self.plotFont) # Size the bound box bbox = (int(self.originX - self.roseMaxDiameter / 22), int(self.originY - self.roseMaxDiameter / 22), int(self.originX + self.roseMaxDiameter / 22), int(self.originY + self.roseMaxDiameter / 22)) self.draw.ellipse(bbox, outline='black', fill=speed_list[1][0]) # Draw the circle self.draw.text((int(self.originX-text_width / 2), int(self.originY - text_height / 2)), label0, fill=self.plot_font_color, font=self.plotFont) # Display the value # Setup the legend. Draw label/title (if set), stacked bar, # bar labels and units self.legend_setup(speed_list, speed_bin) # Save the file. self.image.save(img_file) ngen += 1 if self.log_success: syslog.syslog(syslog.LOG_INFO, "imageStackedWindRose: Generated %d images for %s in %.2f seconds" % (ngen, self.skin_dict['REPORT_NAME'], time.time() - t1))
def __init__(self, engine, config_dict): super(OpsGenieAlerts, self).__init__(engine, config_dict) if not config_dict['OpsGenie'].has_key('Alerts'): syslog.syslog( syslog.LOG_INFO, "restx: OpsGenieAlerts: no Alerts section! No alerts will be set" ) return if not config_dict['OpsGenie']['Alerts'].has_key('apiKey'): syslog.syslog( syslog.LOG_INFO, "restx: OpsGenieAlerts: no apiKey! No alerts will be set") return if config_dict['OpsGenie']['Alerts'].has_key('ExpressionUnits'): if config_dict['OpsGenie']['Alerts']['ExpressionUnits'] == "US": self.unit_system = weewx.US if config_dict['OpsGenie']['Alerts'][ 'ExpressionUnits'] == "METRIC": self.unit_system = weewx.METRIC if config_dict['OpsGenie']['Alerts'][ 'ExpressionUnits'] == "METRICWX": self.unit_system = weewx.METRICWX else: self.unit_system = None self.alerts = dict() for subsection in config_dict['OpsGenie']['Alerts'].sections: alert_config = accumulateLeaves( config_dict['OpsGenie']['Alerts'][subsection], max_level=1) if not alert_config.has_key('Alias'): syslog.syslog( syslog.LOG_INFO, "restx: OpsGenieAlerts: Missing Alias in config section: ". format(subsection)) continue self.alerts[subsection] = dict() self.alerts[subsection]['Alias'] = alert_config['Alias'] try: if alert_config['Expression'] is None: raise KeyError("Expression") if alert_config['Message'] is None: raise KeyError("Message") except KeyError, e: syslog.syslog( syslog.LOG_INFO, "restx: OpsGenieAlerts: " "Missing option {} for Alert: {}".format( e, self.alerts[subsection]['Alias'])) return self.alerts[subsection]['Expression'] = alert_config['Expression'] self.alerts[subsection]['AlertActive'] = False self.alerts[subsection]['my_queues'] = Queue.Queue() self.alerts[subsection]['my_threads'] = OpsGenieAlertsThread( self.alerts[subsection]['my_queues'], alert_config) self.alerts[subsection]['my_threads'].start() syslog.syslog( syslog.LOG_INFO, "restx: OpsGenieAlerts: " "Alerts will be created for {}.".format( self.alerts[subsection]['Alias']))
def genWindRosePlots(self): """Generate the windrose plots. Loop through each 2nd level section (ie [[]]) under [ImageStackedWindRoseGenerator] and generate the plot defined by each 2nd level section. """ # Time period taken to generate plots, set plot count to 0 t1 = time.time() ngen = 0 # Loop over each time span class (day, week, month, etc.): for span in self.image_dict.sections: # Now, loop over all plot names in this time span class: for plot in self.image_dict[span].sections: # Accumulate all options from parent nodes: p_options = accumulateLeaves(self.image_dict[span][plot]) # Get end time for plot. In order try self.gen_ts, last known # good archive time stamp and then current time self.p_gen_ts = self.gen_ts if not self.p_gen_ts: self.p_gen_ts = self.archive.lastGoodStamp() if not self.p_gen_ts: self.p_gen_ts = time.time() # Get the period for the plot, default to 24 hours if no # period set self.period = p_options.as_int('period') if p_options.has_key('period') else 86400 # Get the path of the image file we will save image_root = os.path.join(self.config_dict['WEEWX_ROOT'], p_options['HTML_ROOT']) # Get image file format. Can use any format PIL can write # Default to png if p_options.has_key('format'): format = p_options['format'] else: format = "png" # Get full file name and path for plot img_file = os.path.join(image_root, '%s.%s' % (plot, format)) # Check whether this plot needs to be done at all: if self.skipThisPlot(img_file, plot): continue # 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: pass # Loop over each line to be added to the plot. for line_name in self.image_dict[span][plot].sections: # Accumulate options from parent nodes. line_options = accumulateLeaves(self.image_dict[span][plot][line_name]) # See if a plot title has been explicitly requested. # 'label' used for consistency in skin.conf with # ImageGenerator sections label = line_options.get('label') if label: self.label = unicode(label, 'utf8') else: # No explicit label so set label to nothing self.label = label # See if a time_stamp has been explicitly requested. self.t_stamp = line_options.get('time_stamp') # See if time_stamp location has been explicitly set _location = line_options.get('time_stamp_location') if _location: self.t_stamp_loc = [x.upper() for x in _location] else: self.t_stamp_loc = None # See what SQL variable type to use for this plot and get # corresponding 'direction' type. Can really only plot # windSpeed and windGust, if anything else default to # windSpeed. self.obName = line_options.get('data_type', line_name) if self.obName == 'windSpeed': self.dirName = 'windDir' elif self.obName == 'windGust': self.dirName = 'windGustDir' else: self.obName == 'windSpeed' self.dirName = 'windDir' # Get our data tuples for speed and direction. getSqlVectors_TS = TimeSpan(self.p_gen_ts - self.period + 1, self.p_gen_ts) (_, time_vec_t_ws_stop, data_speed) = self.archive.getSqlVectors(getSqlVectors_TS, self.obName) (_, time_vec_t_wd_stop, dir_vec) = self.archive.getSqlVectors(getSqlVectors_TS, self.dirName) # Convert our speed values to the units we are going to # use in our plot speed_vec = self.converter.convert(data_speed) # Get units for display on legend self.units = self.skin_dict['Units']['Labels'][speed_vec[1]].strip() # Find maximum speed from our data maxSpeed = max(speed_vec[0]) # Set upper speed range for our plot, set to a multiple of # 10 for a neater display maxSpeedRange = (int(maxSpeed / 10.0) + 1) * 10 # Setup 2D list with speed range boundaries in speedList[0] # petal colours in speedList[1] speedList = [[0 for x in range(7)] for x in range(2)] # Store petal colours speedList[1] = self.petal_colors # Loop though each speed range boundary and store in # speedList[0] i = 1 while i < 7: speedList[0][i] = self.speedFactor[i] * maxSpeedRange i += 1 # Setup 2D list for wind direction # windBin[0] represents each of 16 compass directions # ([0] is N, [1] is ENE etc). # windBin[1] holds count of obs in a partiuclr speed range # for given direction windBin = [[0 for x in range(7)] for x in range(17)] # Setup list to hold obs counts for each speed range speedBin = [0 for x in range(7)] # How many obs do we have? samples = len(time_vec_t_ws_stop[0]) # Loop through each sample and increment direction counts # and speed ranges for each direction as necessary. 'None' # direction is counted as 'calm' (or 0 speed) and # (by definition) no direction and are plotted in the # 'bullseye' on the plot i = 0 while i < samples: if (speed_vec[0][i] is None) or (dir_vec[0][i] is None): windBin[16][6] += 1 else: bin = int((dir_vec[0][i] + 11.25) / 22.5) % 16 if speed_vec[0][i] > speedList[0][5]: windBin[bin][6] += 1 elif speed_vec[0][i] > speedList[0][4]: windBin[bin][5] += 1 elif speed_vec[0][i] > speedList[0][3]: windBin[bin][4] += 1 elif speed_vec[0][i] > speedList[0][2]: windBin[bin][3] += 1 elif speed_vec[0][i] > speedList[0][1]: windBin[bin][2] += 1 elif speed_vec[0][i] > 0: windBin[bin][1] += 1 else: windBin[bin][0] += 1 i += 1 # Add 'None' obs to 0 speed count speedBin[0] += windBin[16][6] # Don't need the 'None' counts so we can delete them del windBin[-1] # Now set total (direction independent) speed counts. Loop # through each petal speed range and increment direction # independent speed ranges as necessary j = 0 while j < 7: i = 0 while i < 16: speedBin[j] += windBin[i][j] i += 1 j += 1 # Calc the value to represented by outer ring # (range 0 to 1). Value to rounded up to next multiple of # 0.05 (ie next 5%) self.maxRingValue = (int(max(sum(b) for b in windBin)/(0.05 * samples)) + 1) * 0.05 # Find which wind rose arm to use to display ring range # labels - look for one that is relatively clear. Only # consider NE, SE, SW and NW preference in order is # SE, SW, NE and NW # Is SE clear? if sum(windBin[6]) / float(samples) <= 0.3 * self.maxRingValue: labelDir = 6 # If so take it else: # If not lets loop through the others for i in [10, 2, 14]: # Is SW, NE or NW clear if sum(windBin[i])/float(samples) <= 0.3 * self.maxRingValue: labelDir = i # If so let's take it break # And exit for loop else: # If none are free then let's # take the smallest of the four labelCount = samples + 1 # Set max possible number of # readings+1 i = 2 # Start at NE for i in [2, 6, 10, 14]: # Loop through directions # If this direction has fewer obs than previous # best (least) if sum(windBin[i]) < labelCount: # Set min count so far to this bin labelCount = sum(windBin[i]) # Set labelDir to this direction labelDir = i self.labelDir = labelDir # Set up an Image object to hold our windrose plot self.windRoseImageSetup() # Get a Draw object to draw on self.draw = ImageDraw.Draw(self.image) # Set fonts to be used self.plotFont = get_font_handle(self.font_path, self.plot_font_size) self.legendFont = get_font_handle(self.font_path, self.legend_font_size) self.labelFont = get_font_handle(self.font_path, self.label_font_size) # Estimate space requried for the legend textWidth, textHeight = self.draw.textsize("0 (100%)", font=self.legendFont) legendWidth = int(textWidth + 2 * self.legend_bar_width + 1.5 * self.plot_border) # Estimate space required for label (if required) textWidth, textHeight = self.draw.textsize("Wind Rose", font=self.labelFont) if self.label: labelHeight = int(textWidth+self.plot_border) else: labelHeight=0 # Calculate the diameter of the circular plot space in # pixels. Two diameters are calculated, one based on image # height and one based on image width. We will take the # smallest one. To prevent optical distortion for small # plots diameter will be divisible by 22 self.roseMaxDiameter = min(int((self.image_height - 2 * self.plot_border - labelHeight / 2) / 22.0) * 22, int((self.image_width - (2 * self.plot_border + legendWidth)) / 22.0) * 22) if self.image_width > self.image_height: # If wider than height textWidth, textHeight = self.draw.textsize("W", font=self.plotFont) # x coord of windrose circle origin(0,0) top left corner self.originX = self.plot_border + textWidth + 2 + self.roseMaxDiameter / 2 # y coord of windrose circle origin(0,0) is top left corner self.originY = int(self.image_height / 2) else: # x coord of windrose circle origin(0,0) top left corner self.originX = 2 * self.plot_border + self.roseMaxDiameter / 2 # y coord of windrose circle origin(0,0) is top left corner self.originY = 2 * self.plot_border + self.roseMaxDiameter / 2 # Setup windrose plot. Plot circles, range rings, range # labels, N-S and E-W centre lines and compass pont labels self.windRosePlotSetup() # Plot wind rose petals # Each petal is constructed from overlapping pieslices # starting from outside (biggest) and working in (smallest) a = 0 #start at 'North' windrose petal while a < len(windBin): #loop through each wind rose arm s = len(speedList[0]) - 1 cumRadius = sum(windBin[a]) if cumRadius > 0: armRadius = int((10 * self.roseMaxDiameter * sum(windBin[a])) / (11 * 2.0 * self.maxRingValue * samples)) while s > 0: # Calc radius of current arm pieRadius = int(round(armRadius * cumRadius/sum(windBin[a]) + self.roseMaxDiameter / 22,0)) # Set bound box for pie slice bbox = (self.originX-pieRadius, self.originY-pieRadius, self.originX+pieRadius, self.originY+pieRadius) # Draw pie slice self.draw.pieslice(bbox, int(a * 22.5 - 90 - self.petal_width / 2), int(a * 22.5 - 90 + self.petal_width / 2), fill=speedList[1][s], outline='black') cumRadius -= windBin[a][s] s -= 1 # Move 'in' for next pieslice a += 1 # Next arm # Draw 'bullseye' to represent windSpeed=0 or calm # Produce the label label0 = str(int(round(100.0 * speedBin[0] / sum(speedBin), 0))) + '%' # Work out its size, particularly its width textWidth, textHeight = self.draw.textsize(label0, font=self.plotFont) # Size the bound box bbox = (int(self.originX - self.roseMaxDiameter / 22), int(self.originY - self.roseMaxDiameter / 22), int(self.originX + self.roseMaxDiameter / 22), int(self.originY + self.roseMaxDiameter / 22)) self.draw.ellipse(bbox, outline='black', fill=speedList[1][0]) # Draw the circle self.draw.text((int(self.originX-textWidth / 2), int(self.originY - textHeight / 2)), label0, fill=self.plot_font_color, font=self.plotFont) # Display the value # Setup the legend. Draw label/title (if set), stacked bar, # bar labels and units self.legendSetup(speedList, speedBin) #Save the file. self.image.save(img_file) ngen += 1 syslog.syslog(syslog.LOG_INFO, "imageStackedWindRose: Generated %d images for %s in %.2f seconds" % (ngen, self.skin_dict['REPORT_NAME'], time.time() - t1))