mirror of
https://github.com/weewx/weewx.git
synced 2026-04-18 08:36:54 -04:00
* Got test suites working again. * Updated TODO * indicate specific firmware for cc3000 * clarify rainwise firmware * Further refinement - max and maxtime now updated - reworked the progress function, now a method in the class Have left __main__ code in that has been used for testing windSpeed recalculation fix * indicate when calibrations are ignored * Now emits the barometer trend in LOOP packets as field 'trendIcon' Vantage only. * Updated TODO guide, reflecting this morning's phone call. * Presses on, despite database error * Simplified patching by moving metadata code to DaySummaryManager * Got rid of _getVersion() Can now get it from _read_metadata * Remove interval weighting fix from weewx startup Refer TODO.txt * Update TODO.txt * added write timeout to ultimeter driver * better logging when cc3000 loses contact with sensors * document some of the channel, sensor, and logger specifics for the wmr100/wmr200 hardware * Removed the p word, patch.py is now database.py, rejigged wee_database as per skype and followup emails - progress function for fixes included as a method in base class that can be overridden - much changing of logging to give consistent results/output - believe I retained Tom's recent changes to patch.py (commit4acf752) * Picks daily summary weights on the basis of database version * Rewrote the backfill routine (again) * wee_database now uses new version of backfill_day_summary * Fixed problem that prevented cold startup * Revised to reflect latest wee_database incantation * Modified weighting fix Should not mess with lessUpdate Delete lastWeightPatch after successful patch * Check to make sure the daily summaries have not been partially updated. * Revised to reflect latest wee_database incantation * Revised wee_database and interval weighting paras, added windSpeed recalc para * Removed vacuum * Updated what has been done and committed * Better diagnostics with partial update of the daily summaries. * MySQL now uses transaction isolation level READ COMMITTED * Add .config (#204) * Changed semantics of "$last_xxx" tags. * Template test updates including fixing issue #201 (#205) * Add .config * Standardise test skin for index.html.tml to remove "%x %X" locale dependent formatting. Note: Used 24 hour time as AM/PM can also be locale dependent. Also include fixes for line formatting in some other test files and expected results. * sysctlbyname is not available on every platform, so catch AttributeError too, otherwise cheetah fails with a name error * Got rid of tabs * update usb mode info for acurite models * Removed intervalgenRoundTS and archiveDaysAgoSpan. * Simplified tags. Got template test working again. * Somehow, style "indent" got lost * Documented new "$ago" tags. * Documented $ago options, as well as .start and .end * Update changes doc * Stupid typo * use markdown instead of html for README * indenting seems to confuse markdown's handling of urls * Left over $last_day in $spans example (#206) * Add .config * Standardise test skin for index.html.tml to remove "%x %X" locale dependent formatting. Note: Used 24 hour time as AM/PM can also be locale dependent. Also include fixes for line formatting in some other test files and expected results. * Left over $last_day in $spans example. * a user-specified sensor_map will update, not replace, the default sensor_map * Updated the upgrade guide. * ensure that weewx-multi works on deb and rpm as well as setup.py * document the sensor mapping changes * Added comprehensive example to customizing guide. * Updated TODO * Got the weighting update to work on MySQL * added crude caching of pressure in wmr300 * Fixed typo * Fixed little errors. Consistently use "tag", instead of "dot code." * no altimeter, just pressure and barometer * Fixed little HTML problems. * Reformatted, in anticipation of a refactoring of some sections. * Fixed location of 'Version' So it works with automatic replacement of 'Version' * initial experiment with different fonts * override jquery-ui hijacking of toc font family * bring header highlighting to h2 to correlate with toc. reduce post-header margins. * fix some table header cruft. prevent indent sections from overflowing right margin. let the table cells breath. * fix broken tty formatting * Checkpoint Work in progress. * Refined examples of creating new units and groups * Polishing. Or, maybe fiddling? * minor css fixes. rearrange troubleshooting sections about pressure. * minor cleanup to readme * Fixed test suites Add MySQL back to template tests. * Reworked the iteration examples. * Corrected and clarified the units used in the "electricity" example. * remove write_timeout since naming is inconsistent between pyserial versions and there is no backward compatibility * remove write_timeout from ultimeter * Minor changes to users guide * increase body size to 100%. background for code. weeWX in titles. true bold for monospace. prep for direct font comparisons. * Clarified the role of encoding * update utilities guide with suggested wee_database descriptions and args * more compaction * increase margins on html examples. use droid serif for html examples. * Clarified a few things * decode weewx into weeWX * include transaction limit defaults * eliminate transaction-limit * weeWX fixes in install pages. more fixes to utilities. * missed a few code spans * fix version label alignment * use only major.minor for docs * Fix error where import fields that are None can cause Source.mapRawData() to crash in some cases * Updated TODO and NEW_FEATURES * There will be no daykeys if the daily summaries have been dropped. * Restructure usage string, hard code transaction days * dry-run goes with fix-strings, not check-strings * Log daily summary version * No need to check for weewx.debug * include examples in manifest * added examples and extensions to data_files * do not install sample extensions * Can now specify date field separator for Cumulus imports, weewx -> weeWX * Clarified option strings * Rudimentary test of selective daily summary rebuild * No longer allows selective rebuild of the daily summaries if the summaries are not complete * Hardwired UTF-8 encoding, but with a warning comment. * Hardwired UTF-8 encoding, but with a warning comment. * Documented Cumulus import separator config option * Added comments about Tools. * Changed to execute in user environment * Ported to PyMySQL as an alternative to python-mysqldb See https://github.com/PyMySQL/PyMySQL for a description of PyMySQL * Recognize additional MySQL "Can't connect" error * Fixed error in test suites Subsequent tests depended on ordering of a dictionary. * pymysql seems to have problems connecting via file socket unless it is told explicitly about it. * Workaround for pypy compiler * Defaults now support MySQLdb over pymysql * Fixed bug in record augmentation. The augmentation was happening without giving StdConvert a chance to do its thing. * More clear msg when encountering an ImportError * Clarified the relationship between archive period and report_timing option, aded note regarding primacy of the report cycle * Reverted back to MySQLdb only version. * Changed config option names but never changed the code! * One transaction for updating daily high/lows and archive record Formerly, these were done in two transactions. * v3.7.0a2 * Adjustable value for how long to wait after a bad uploader login. Option retry_login. Fixes issue #212. * Fixed Cumulus import rain field issue wee_import will try to use field 26(AA) - midnight reset daily rainfall but if not available due it will revert to field 9(J) or 11(L) * Switched back to __str__ when extracting string out of template. .respond() doesn't seem to encode Unicode characters properly * Fix errors in wee_import WU step-by-step, remove Cumulus version caveat on rain * Now uses dedicated test users 'weewx1' and 'weewx2' * Formal check of the various MySQLdb exceptions. * Added sqlite3 exceptions. * Reworked check_strings() output (#213) Reworked check_strings() screen and syslog output: - now gives progress ala --rebuild-daily - syslog is silent for --check-strings and --fix-strings with --dry-run - left 'Preparing' (rather than 'Starting') but added 'this may take a while' as there is a significant delay in dbmanager.genBatchRows() initialising at line 619 (well there was for 400k records) * reduce debug log spewage in wmr300 driver * Finished formal test of errors * First cut at V3.7 exception hierarchy * Ported the weedb sqlite driver to the new exception hierarchy. * Ported MySQLdb to the new database exception hierarchy. * windSpeed fix now gracefully handles no windSpeed summary table, tweaked --update output/logging * Now picks up absence of windSpeed daily summary * The weedb Connection object can now be used in a "with" clause. * The weedb Cursor object can now be used in a "with" clause. * V3.7.0a3 * more code removal * code formatting only * use apt instructions for debian installs * Reworked --wee-database section of Utilities Guide to reflect current wee-database operation - revised usage - reword --rebuild-daily - reword --check - reword --update * More details on upgrading * Moved start time to just before applying the patch * Accumulator is now initialized with override values from weewx.conf * Added sentences about wee_import/interval and weight patching multiple dbs * remove extensions from rpm and deb packages * v3.7.0b1 * Cleaned up some HTML warnings. * must do a try loop at the read level so we can skip the no data 'errors' and return empty buffer so that a subsequent write will get the station to talk again * Slightly more robust mechanism for decoding last time a file was FTP'd. * adjust wording of weighting description * added examples * simplify * fixed typo * fixed typo * read /etc/default before bailing out * make init script work properly with /etc/default/weewx * make output consistent * more simplification * keep PEP happy * avoid resource consumption from slow reports by extending the StdReport.max_wait. provide log messages when it happens. * new features have move to the roadmap * no more todo items left * do not emit default sensor_map to the config * aborted attempt to get additional battery status * bump to 3.7.0b2 * do recipe using wget instead of curl * forgot the O option to wget * do not warn when calibrations are ignored - the implementation resulted in too many log entries * get rid of tabs * added notes about wmr300 rain counter and logger * added norwegian 'no data available' as 'Ingen data er tilgjengelige' * simplify. eliminate more passive voice and gerunds. * avoid run-on * provide better feedback for operational errors. make manager logging more consistent. * fix typo in wmr300 ConfEditor * decode heatindex and windchill from wmr300 sensor outputs * fixed bad extract_signed invocation * fixed wmr* partial packets note * added mysqldb install instructions to userguide * minor html fixes * added link to wee_extension * clarify acurite sensor transmission periods. * added battery status for all wmr100 remote t/h sensors * added battery status for all wmr100 remote t sensors * document changes to wmr100, wmr200, wmr9x8 drivers. fix 'Calculatios' typo. * fixed inverted wmr200 battery status * rename fault_out to out_fault to match pattern of other faults * make battery status labels consistent across all wmr drivers * wmr300 driver moves from rc to 0.18 * bump to 3.7.0b3 * css fixes: neutralize the glaring yellow; brighten the note green to more closely match the tone of warning red; @media tweaks to match font changes. * fix column title * distinguish selection color from code color * fixed shift bug in weewx-multi * Fixed (I think) issue #219 * update logwatch script to properly handle revised generator log messages refer commit03c3e4ef57 (diff-3cefdd7265f340e9683b0a2d0417b70f)* normalize the quick-start * Merge branch 'development', remote branch 'origin' * fix layout table width on installation pages * v3.7.0b4 * use released_versions instead of previous_versions * Merge branch 'development', remote branch 'origin' * parameterize release rule. make release rule idempotent. * replace cheetahtemplate.org with pythonhosted.org * wee_database --help output was slightly different to reality * Removed BOM at beginning of customizing.htm * wee_database --help output was slightly different to reality * Reworded comment on whether to --update daily summaries. * cater for change in manager log output * remove misleading windGustDir info * bump to 3.7.0 * Added date to change log * escape the dollars in release target * fixed log syntax * adjust log level for wmr100 bad usb report * emit rapidfire cache info only when debug >= 3 * enable post_interval overrides for WOW uploader * Fixes issue #230, exception when using Rapidfire with metric units * Added StdRESTbase back in. It seems that some uploaders still depend on it. * Fixed problem that prevented a MySQL port from being specified. * Added antialias GIF to list of files to be installed. * Make sure GIF files get uploaded * distribute examples in a single directory * distinguish docs/examples vs examples * Fixed bug that prevented a port from being specified for MySQL installations. * Removed redundant change log entry * Add MySQL Error 2003 to exceptions (#234) * Added PR #234 to change log * Documented change in location of the examples * update examples paths in remaining guides. explicitly list all path changes for examples. * By default, autocommit is now enabled for the MySQL driver. Fixes issue #237. Included regression test.
794 lines
36 KiB
Python
Executable File
794 lines
36 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Copyright (c) 2009-2015 Tom Keffer <tkeffer@gmail.com>
|
|
#
|
|
# See the file LICENSE.txt for your full rights.
|
|
#
|
|
"""Configure databases used by weeWX"""
|
|
from __future__ import with_statement
|
|
|
|
# python imports
|
|
import datetime
|
|
import optparse
|
|
import syslog
|
|
import sys
|
|
import time
|
|
|
|
# weewx imports
|
|
import user.extensions #@UnusedImport
|
|
import weecfg.database
|
|
import weedb
|
|
import weewx.manager
|
|
import weewx.units
|
|
|
|
from weeutil.weeutil import TimeSpan, timestamp_to_string
|
|
|
|
usage = """wee_database --help
|
|
wee_database --create
|
|
wee_database --reconfigure
|
|
wee_database --transfer --dest-binding=BINDING_NAME [--dry-run]
|
|
wee_database --check
|
|
wee_database --update [--dry-run]
|
|
wee_database --check-strings
|
|
wee_database --fix-strings [--dry-run]
|
|
wee_database --drop-daily
|
|
wee_database --rebuild-daily [--date=YYYY-mm-dd |
|
|
--from=YYYY-mm-dd --to=YYYY-mm-dd]
|
|
|
|
Description:
|
|
|
|
Manipulate the weeWX database. Most of these operations are handled
|
|
automatically by weeWX, but they may be useful in special cases."""
|
|
|
|
# List of 'dest' settings used by our 'verbs', note 'dest' may be explicit or
|
|
# implicit. If adding more 'verbs' need to add corresponding 'dest' here.
|
|
dest_list = ['create', 'drop_daily', 'rebuild_daily', 'reconfigure', 'transfer',
|
|
'check', 'update', 'check_strings', 'fix']
|
|
|
|
def main():
|
|
|
|
# Set defaults for the system logger:
|
|
syslog.openlog('wee_database', syslog.LOG_PID|syslog.LOG_CONS)
|
|
|
|
# Create a command line parser:
|
|
parser = optparse.OptionParser(usage=usage)
|
|
|
|
# Add the various options:
|
|
parser.add_option("--config", dest="config_path", type=str,
|
|
metavar="CONFIG_FILE",
|
|
help="Use configuration file CONFIG_FILE.")
|
|
parser.add_option("--create", dest="create_database", action='store_true',
|
|
help="Create the weeWX database and initialize it with the"
|
|
" schema.")
|
|
parser.add_option("--drop-daily", dest="drop_daily", action='store_true',
|
|
help="Drop the daily summary tables from a database.")
|
|
parser.add_option("--rebuild-daily", dest="rebuild_daily",
|
|
action='store_true',
|
|
help="Rebuild the daily summaries from data in the archive"
|
|
" table.")
|
|
parser.add_option("--date", dest="date", type=str, metavar="YYYY-mm-dd",
|
|
help="This date only (option --rebuild-daily only).")
|
|
parser.add_option("--from", dest="from_date", type=str, metavar="YYYY-mm-dd",
|
|
help="Start with this date (option --rebuild-daily only).")
|
|
parser.add_option("--to", dest="to_date", type=str, metavar="YYYY-mm-dd",
|
|
help="End with this date (option --rebuild-daily only).")
|
|
parser.add_option("--reconfigure", action='store_true',
|
|
help="Create a new database using configuration"
|
|
" information found in the configuration file. In"
|
|
" particular, the new database will use the unit system"
|
|
" found in option [StdConvert][target_unit]."
|
|
" The new database will have the same name as the old"
|
|
" database, with a '_new' on the end.")
|
|
parser.add_option("--transfer", dest="transfer", action='store_true',
|
|
help="Transfer the weeWX archive from source database to"
|
|
" destination database.")
|
|
parser.add_option("--check", dest="check", action="store_true",
|
|
help="Check the calculations in the daily summary tables.")
|
|
parser.add_option("--update", dest="update", action="store_true",
|
|
help="Update the daily summary tables if required and"
|
|
" recalculate the daily summary maximum windSpeed values.")
|
|
parser.add_option("--check-strings", dest="check_strings", action="store_true",
|
|
help="Check the archive table for null strings that may"
|
|
" have been introduced by a SQL editing program.")
|
|
parser.add_option("--fix-strings", dest="fix", action='store_true',
|
|
help="Fix any null strings in a SQLite database.")
|
|
parser.add_option("--binding", dest="binding", metavar="BINDING_NAME",
|
|
default='wx_binding',
|
|
help="The data binding to use. Default is 'wx_binding'.")
|
|
parser.add_option("--dest-binding", dest="dest_binding",
|
|
metavar="BINDING_NAME",
|
|
help="The destination data binding (option --transfer only).")
|
|
parser.add_option('--dry-run', dest="dry_run", action='store_true',
|
|
default=False,
|
|
help='Print what would happen but do not do it. Default'
|
|
' is False.')
|
|
|
|
# Now we are ready to parse the command line:
|
|
(options, args) = parser.parse_args()
|
|
|
|
# Do a check to see if the user used more than 1 'verb' . More than 1 verb
|
|
# should be avoided - what did the user really want to do?
|
|
# first get a list of options actually used
|
|
options_list = [opt for opt, val in options.__dict__.items() if val is not None]
|
|
# now a list of the 'dest' settings used
|
|
dest_used = [opt for opt in options_list if opt in dest_list]
|
|
# If we have more than 1 entry in dest_used then we have more than 1 'verb'
|
|
# used. Then inform the user and exit. If only 1 'verb' then carry on.
|
|
if len(dest_used) > 1:
|
|
# generate the message but we need to speak 'options' (aka verbs)
|
|
# not 'dest'
|
|
# get a dict of {option.dest: option.str} from our universe of options
|
|
opt_dict = dict((x.dest, x.get_opt_string()) for x in parser.option_list[1:])
|
|
# now get a list of 'verbs' used
|
|
verbs_used = [opt_dict[x] for x in dest_used]
|
|
# get rid of the [ and ]
|
|
verbs_str = ', '.join(verbs_used)
|
|
# our exit message
|
|
exit_str = ("Multiple verbs found in command line %s. Only one verb permitted.\nNothing done. Aborting." %
|
|
(verbs_str, ))
|
|
# now exit with our message
|
|
sys.exit(exit_str)
|
|
|
|
# get config_dict to use
|
|
config_path, config_dict = weecfg.read_config(options.config_path, args)
|
|
print "Using configuration file %s" % config_path
|
|
|
|
# set syslog level based
|
|
if int(config_dict.get('debug', 0)):
|
|
syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))
|
|
else:
|
|
syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_INFO))
|
|
|
|
db_binding = options.binding
|
|
database = config_dict['DataBindings'][db_binding]['database']
|
|
print "Using database binding '%s', which is bound to database '%s'" % (db_binding, database)
|
|
|
|
if options.create_database:
|
|
createMainDatabase(config_dict, db_binding)
|
|
|
|
if options.drop_daily:
|
|
dropDaily(config_dict, db_binding)
|
|
|
|
if options.rebuild_daily:
|
|
rebuildDaily(config_dict, db_binding, options)
|
|
|
|
if options.reconfigure:
|
|
reconfigMainDatabase(config_dict, db_binding)
|
|
|
|
if options.transfer:
|
|
transferDatabase(config_dict, db_binding, options)
|
|
|
|
if options.check:
|
|
check(config_dict, db_binding, options)
|
|
|
|
if options.update:
|
|
update(config_dict, db_binding, options)
|
|
|
|
if options.check_strings:
|
|
check_strings(config_dict, db_binding, options, fix=False)
|
|
|
|
if options.fix:
|
|
check_strings(config_dict, db_binding, options, fix=True)
|
|
|
|
def createMainDatabase(config_dict, db_binding):
|
|
"""Create the weeWX database"""
|
|
|
|
# Try a simple open. If it succeeds, that means the database
|
|
# exists and is initialized. Otherwise, an exception will be thrown.
|
|
try:
|
|
with weewx.manager.open_manager_with_config(config_dict, db_binding) as dbmanager:
|
|
print "Database '%s' already exists. Nothing done." % (dbmanager.database_name,)
|
|
except weedb.OperationalError:
|
|
# Database does not exist. Try again, but allow initialization:
|
|
with weewx.manager.open_manager_with_config(config_dict, db_binding, initialize=True) as dbmanager:
|
|
print "Created database '%s'" % (dbmanager.database_name,)
|
|
|
|
def dropDaily(config_dict, db_binding):
|
|
"""Drop the daily summaries from a weeWX database"""
|
|
|
|
manager_dict = weewx.manager.get_manager_dict_from_config(config_dict,
|
|
db_binding)
|
|
database_name = manager_dict['database_dict']['database_name']
|
|
|
|
ans = None
|
|
while ans not in ['y', 'n']:
|
|
print "Proceeding will delete all your daily summaries from database '%s'" % database_name
|
|
ans = raw_input("Are you sure you want to proceed (y/n)? ")
|
|
if ans == 'y' :
|
|
t1 = time.time()
|
|
print "Dropping daily summary tables from '%s' ... " % database_name
|
|
try:
|
|
with weewx.manager.open_manager_with_config(config_dict, db_binding) as dbmanager:
|
|
try:
|
|
dbmanager.drop_daily()
|
|
except weedb.OperationalError, e:
|
|
print "Got error '%s'\nPerhaps there was no daily summary?" % e
|
|
else:
|
|
tdiff = time.time() - t1
|
|
print "Daily summary tables dropped from database '%s' in %.2f seconds" % (database_name, tdiff)
|
|
except weedb.OperationalError:
|
|
# No daily summaries. Nothing to be done.
|
|
print "No daily summaries found in database '%s'. Nothing done." % (database_name,)
|
|
|
|
def rebuildDaily(config_dict, db_binding, options):
|
|
"""Rebuild the daily summaries."""
|
|
|
|
manager_dict = weewx.manager.get_manager_dict_from_config(config_dict,
|
|
db_binding)
|
|
database_name = manager_dict['database_dict']['database_name']
|
|
|
|
# determine the period over which we are rebuilding from any command
|
|
# line date parameters
|
|
start_d, stop_d = _parse_dates(options)
|
|
# advise the user/log what we will do
|
|
if start_d is None and stop_d is None:
|
|
_msg = "All daily summaries will be rebuilt."
|
|
elif start_d and not stop_d:
|
|
_msg = "Daily summaries from %s through the end will be rebuilt." % start_d
|
|
elif not start_d and stop_d:
|
|
_msg = "Daily summaries from the begining through date %s will be rebuilt." % stop_d
|
|
elif start_d == stop_d:
|
|
_msg = "Daily summary for date %s will be rebuilt" % start_d
|
|
else:
|
|
_msg = "Daily summaries from %s through %s inclusive will be rebuilt." % (start_d, stop_d)
|
|
|
|
syslog.syslog(syslog.LOG_INFO, _msg)
|
|
print _msg
|
|
|
|
ans = None
|
|
while ans not in ['y', 'n']:
|
|
ans = raw_input("Proceed (y/n)? ")
|
|
if ans == 'y':
|
|
break
|
|
elif ans == 'n':
|
|
print "Nothing done."
|
|
return
|
|
|
|
t1 = time.time()
|
|
|
|
# Open up the database. This will create the tables necessary for the daily
|
|
# summaries if they don't already exist:
|
|
with weewx.manager.open_manager_with_config(config_dict, db_binding, initialize=True) as dbmanager:
|
|
|
|
syslog.syslog(syslog.LOG_INFO, "Rebuilding daily summaries in database '%s' ..." % database_name)
|
|
print "Rebuilding daily summaries in database '%s' ..." % database_name
|
|
if options.dry_run:
|
|
print "Dry run. Nothing done."
|
|
return
|
|
else:
|
|
# now do the actual rebuild
|
|
nrecs, ndays = dbmanager.backfill_day_summary(start_d=start_d,
|
|
stop_d=stop_d,
|
|
trans_days=20)
|
|
tdiff = time.time() - t1
|
|
# advise the user/log what we did
|
|
syslog.syslog(syslog.LOG_INFO, "Rebuild of daily summaries in database '%s' complete" % database_name)
|
|
if nrecs:
|
|
sys.stdout.flush()
|
|
print
|
|
print "Processed %d records to rebuild %d day summaries in %.2f seconds " % (nrecs,
|
|
ndays,
|
|
tdiff)
|
|
print "Rebuild of daily summaries in database '%s' complete" % database_name
|
|
else:
|
|
print "Daily summaries up to date in '%s'" % database_name
|
|
|
|
def reconfigMainDatabase(config_dict, db_binding):
|
|
"""Create a new database, then populate it with the contents of an old database"""
|
|
|
|
manager_dict = weewx.manager.get_manager_dict_from_config(config_dict,
|
|
db_binding)
|
|
# Make a copy for the new database (we will be modifying it)
|
|
new_database_dict = dict(manager_dict['database_dict'])
|
|
|
|
# Now modify the database name
|
|
new_database_dict['database_name'] = manager_dict['database_dict']['database_name'] + '_new'
|
|
|
|
# First check and see if the new database already exists. If it does, check
|
|
# with the user whether it's ok to delete it.
|
|
try:
|
|
weedb.create(new_database_dict)
|
|
except weedb.DatabaseExists:
|
|
ans = None
|
|
while ans not in ['y', 'n']:
|
|
ans = raw_input("New database '%s' already exists. Delete it first (y/n)? " % (new_database_dict['database_name'],))
|
|
if ans == 'y':
|
|
weedb.drop(new_database_dict)
|
|
elif ans == 'n':
|
|
print "Nothing done."
|
|
return
|
|
|
|
# Get the unit system of the old archive:
|
|
with weewx.manager.Manager.open(manager_dict['database_dict']) as old_dbmanager:
|
|
old_unit_system = old_dbmanager.std_unit_system
|
|
|
|
if old_unit_system is None:
|
|
print "Old database has not been initialized. Nothing to be done."
|
|
return
|
|
|
|
# Get the unit system of the new archive:
|
|
try:
|
|
target_unit_nickname = config_dict['StdConvert']['target_unit']
|
|
except KeyError:
|
|
target_unit_system = None
|
|
else:
|
|
target_unit_system = weewx.units.unit_constants[target_unit_nickname.upper()]
|
|
|
|
|
|
ans = None
|
|
while ans not in ['y', 'n']:
|
|
print "Copying database '%s' to '%s'" % (manager_dict['database_dict']['database_name'],
|
|
new_database_dict['database_name'])
|
|
if target_unit_system is None or old_unit_system==target_unit_system:
|
|
print ("The new database will use the same unit system as the old ('%s')." %
|
|
(weewx.units.unit_nicknames[old_unit_system],))
|
|
else:
|
|
print ("Units will be converted from the '%s' system to the '%s' system." %
|
|
(weewx.units.unit_nicknames[old_unit_system],
|
|
weewx.units.unit_nicknames[target_unit_system]))
|
|
ans = raw_input("Are you sure you wish to proceed (y/n)? ")
|
|
if ans == 'y':
|
|
t1 = time.time()
|
|
weewx.manager.reconfig(manager_dict['database_dict'],
|
|
new_database_dict,
|
|
new_unit_system=target_unit_system,
|
|
new_schema=manager_dict['schema'])
|
|
tdiff = time.time() - t1
|
|
print "Database '%s' copied to '%s' in %.2f seconds." % (manager_dict['database_dict']['database_name'],
|
|
new_database_dict['database_name'],
|
|
tdiff)
|
|
elif ans == 'n':
|
|
print "Nothing done."
|
|
|
|
def transferDatabase(config_dict, db_binding, options):
|
|
"""Transfer 'archive' data from one database to another"""
|
|
|
|
# do we have enough to go on, must have a dest binding
|
|
if options.dest_binding is None:
|
|
print "Destination binding not specified. Nothing Done. Aborting."
|
|
return
|
|
# get manager dict for our source binding
|
|
src_manager_dict = weewx.manager.get_manager_dict_from_config(config_dict,
|
|
db_binding)
|
|
# get manager dict for our dest binding
|
|
try:
|
|
dest_manager_dict = weewx.manager.get_manager_dict_from_config(config_dict,
|
|
options.dest_binding)
|
|
except weewx.UnknownBinding:
|
|
# if we can't find the binding display a message then return
|
|
print ("Unknown destination binding '%s', confirm destination binding." %
|
|
(options.dest_binding, ))
|
|
print "Nothing Done. Aborting."
|
|
return
|
|
except weewx.UnknownDatabase:
|
|
# if we can't find the database display a message then return
|
|
print "Error accessing destination database, confirm destination binding and/or database."
|
|
print "Nothing Done. Aborting."
|
|
return
|
|
except (ValueError, AttributeError):
|
|
# maybe a schema issue
|
|
print "Error accessing destination database."
|
|
print ("Maybe the destination schema is incorrectly specified in binding '%s' in weewx.conf?" %
|
|
(options.dest_binding, ))
|
|
print "Nothing Done. Aborting."
|
|
return
|
|
except (weewx.UnknownDatabaseType):
|
|
# maybe a [Databases] issue
|
|
print "Error accessing destination database."
|
|
print "Maybe the destination database is incorrectly defined in weewx.conf?"
|
|
print "Nothing Done. Aborting."
|
|
return
|
|
# get a manager for our source
|
|
with weewx.manager.Manager.open(src_manager_dict['database_dict']) as src_manager:
|
|
# get first and last timestamps from the source so we can count the
|
|
# records to transfer and display an appropriate message
|
|
first_ts = src_manager.firstGoodStamp()
|
|
last_ts = src_manager.lastGoodStamp()
|
|
if first_ts is not None and last_ts is not None:
|
|
# we have source records
|
|
num_recs = src_manager.getAggregate(TimeSpan(first_ts, last_ts),
|
|
'dateTime', 'count')[0]
|
|
else:
|
|
# we have no source records to transfer so abort with a message
|
|
print ("No records found in source database '%s' for transfer." %
|
|
(src_manager.database_name, ))
|
|
print "Nothing done. Aborting."
|
|
exit()
|
|
ans = None
|
|
if not options.dry_run: # is it a dry run ?
|
|
# not a dry run, actually do the transfer
|
|
while ans not in ['y', 'n']:
|
|
ans = raw_input("Transfer %s records from source database '%s' to destination database '%s' (y/n)? " %
|
|
(num_recs, src_manager.database_name, dest_manager_dict['database_dict']['database_name']))
|
|
if ans == 'y':
|
|
t1 = time.time()
|
|
# wrap in a try..except in case we have an error
|
|
try:
|
|
with weewx.manager.Manager.open_with_create(dest_manager_dict['database_dict'],
|
|
table_name=dest_manager_dict['table_name'],
|
|
schema=dest_manager_dict['schema']) as dest_manager:
|
|
sys.stdout.write("transferring, this may take a while.... ")
|
|
sys.stdout.flush()
|
|
# do the transfer, should be quick as it's done as a
|
|
# single transaction
|
|
dest_manager.addRecord(src_manager.genBatchRecords())
|
|
print "complete"
|
|
# get first and last timestamps from the dest so we can
|
|
# count the records transferred and display a message
|
|
first_ts = dest_manager.firstGoodStamp()
|
|
last_ts = dest_manager.lastGoodStamp()
|
|
tdiff = time.time() - t1
|
|
if first_ts is not None and last_ts is not None:
|
|
num_recs = dest_manager.getAggregate(TimeSpan(first_ts, last_ts),
|
|
'dateTime', 'count')[0]
|
|
print ("%s records transferred from source database '%s' to" %
|
|
(num_recs, src_manager.database_name))
|
|
print ("destination database '%s' in %.2f seconds." %
|
|
(dest_manager.database_name, tdiff))
|
|
else:
|
|
print ("Error. No records were transferred from source database '%s' to destination database '%s'." %
|
|
(src_manager.database_name, dest_manager.database_name))
|
|
except ImportError:
|
|
# Probably when trying to load db driver
|
|
print ("Error accessing destination database '%s'." %
|
|
(dest_manager_dict['database_dict']['database_name'], ))
|
|
print "Nothing done. Aborting."
|
|
raise
|
|
except (OSError, weedb.OperationalError):
|
|
# probably a weewx.conf typo or MySQL db not created
|
|
print ("Error accessing destination database '%s'." %
|
|
(dest_manager_dict['database_dict']['database_name'], ))
|
|
print "Maybe it does not exist (MySQL) or is incorrectly defined in weewx.conf?"
|
|
print "Nothing done. Aborting."
|
|
return
|
|
|
|
elif ans == 'n':
|
|
# we decided not to do the transfer
|
|
print "Nothing done."
|
|
return
|
|
else:
|
|
# it's a dry run so say what we would have done then return
|
|
print ("Transfer %s records from source database '%s' to destination database '%s'." %
|
|
(num_recs, src_manager.database_name, dest_manager_dict['database_dict']['database_name']))
|
|
print "Dry run, nothing done."
|
|
|
|
def check(config_dict, db_binding, options):
|
|
"""Check database and report outstanding fixes/issues.
|
|
|
|
Performs the following checks:
|
|
- checks database version
|
|
- checks for null strings in SQLite database
|
|
"""
|
|
|
|
t1 = time.time()
|
|
|
|
# Check interval weighting
|
|
print "Checking daily summary tables version..."
|
|
|
|
# Get a database manager object
|
|
dbm = weewx.manager.open_manager_with_config(config_dict, db_binding)
|
|
|
|
# check the daily summary version
|
|
_daily_summary_version = dbm._read_metadata('Version')
|
|
msg = "Daily summary tables are at version %s" % _daily_summary_version
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
|
|
if _daily_summary_version is not None and _daily_summary_version >= '2.0':
|
|
# interval weighting fix has been applied
|
|
msg = "Interval Weighting Fix is not required."
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
else:
|
|
print "Recommend running --update to recalculate interval weightings."
|
|
print "Daily summary tables version check completed in %0.2f seconds." % (time.time() - t1)
|
|
|
|
# now check for null strings
|
|
check_strings(config_dict, db_binding, options, fix=False)
|
|
|
|
def update(config_dict, db_binding, options):
|
|
"""Apply any required database fixes.
|
|
|
|
Applies the following fixes:
|
|
- checks if database version is 2.0, if not interval weighting fix is
|
|
applied
|
|
- recalculates windSpeed daily summary max and maxtime fields from
|
|
archive
|
|
"""
|
|
|
|
# prompt for confirmation if it is not a dry run
|
|
ans = 'y' if options.dry_run else None
|
|
while ans not in ['y', 'n']:
|
|
ans = raw_input("The update process does not affect archive data, but does alter the database.\nContinue (y/n)? ")
|
|
if ans == 'n':
|
|
# we decided not to update the summary tables
|
|
msg = "Update cancelled"
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
return
|
|
|
|
if options.dry_run:
|
|
_log_level = syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_WARNING))
|
|
|
|
msg = "Preparing Interval Weighting Fix..."
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
|
|
# notify if this is a dry run
|
|
if options.dry_run:
|
|
print "This is a dry run: weighted intervals will be calculated but not saved."
|
|
|
|
# Get a database manager object
|
|
dbm = weewx.manager.open_manager_with_config(config_dict, db_binding)
|
|
database_name = dbm.database_name
|
|
|
|
# Interval weighting
|
|
# first construct an interval weighting config dict
|
|
weighting_config_dict = {'name': 'Interval Weighting Fix',
|
|
'binding': db_binding,
|
|
'trans_days': 100,
|
|
'dry_run': options.dry_run}
|
|
|
|
# create an interval weighting fix object
|
|
weight_obj = weecfg.database.IntervalWeighting(config_dict,
|
|
weighting_config_dict)
|
|
# check the daily summary version
|
|
_daily_summary_version = dbm._read_metadata('Version')
|
|
msg = "Daily summary tables are at version %s" % _daily_summary_version
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
|
|
if _daily_summary_version is not None and _daily_summary_version >= '2.0':
|
|
# interval weighting fix has been applied
|
|
msg = "Interval Weighting Fix is not required."
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
else:
|
|
# apply the interval weighting
|
|
msg = "Calculating interval weights..."
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
t1 = time.time()
|
|
weight_obj.run()
|
|
msg = "Interval Weighting Fix completed in %0.2f seconds." % (time.time() - t1)
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
|
|
# recalc the max/maxtime windSpeed values
|
|
_fix_wind(config_dict, db_binding, options)
|
|
# just in case, set the syslog level back where we found it
|
|
if options.dry_run:
|
|
syslog.setlogmask(_log_level)
|
|
|
|
def _fix_wind(config_dict, db_binding, options):
|
|
"""Recalculate the windSpeed daily summary max and maxtime fields.
|
|
|
|
Create a WindSpeedRecalculation object and call its run() method to
|
|
recalculate the max and maxtime fields from archive data. This process is
|
|
idempotent so it can be called repeatedly with no ill effect.
|
|
"""
|
|
|
|
t1 = time.time()
|
|
msg = "Preparing Maximum windSpeed Fix..."
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
|
|
# notify if this is a dry run
|
|
if options.dry_run:
|
|
print "This is a dry run: maximum windSpeed will be calculated but not saved."
|
|
|
|
# construct a windSpeed recalculation config dict
|
|
wind_config_dict = {'name': 'Maximum windSpeed Fix',
|
|
'binding': db_binding,
|
|
'trans_days': 100,
|
|
'dry_run': options.dry_run}
|
|
|
|
# create a windSpeedRecalculation object
|
|
wind_obj = weecfg.database.WindSpeedRecalculation(config_dict,
|
|
wind_config_dict)
|
|
# perform the recalculation, wrap in a try..except to catch any db errors
|
|
try:
|
|
wind_obj.run()
|
|
except weedb.NoTableError, e:
|
|
msg = "Maximum windSpeed Fix applied: no windSpeed found"
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
else:
|
|
msg = "Maximum windSpeed Fix completed in %0.2f seconds" % (time.time() - t1)
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
print msg
|
|
|
|
def check_strings(config_dict, db_binding, options, fix=False):
|
|
"""Scan the archive table for null strings.
|
|
|
|
Identifies and lists any null string occurrences in the archive table. If
|
|
fix is True then any null strings that are found are fixed.
|
|
"""
|
|
|
|
t1 = time.time()
|
|
if options.dry_run or not fix:
|
|
_log_level = syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_WARNING))
|
|
if fix:
|
|
syslog.syslog(syslog.LOG_INFO, "Preparing Null String Fix")
|
|
print "Preparing Null String Fix, this may take a while..."
|
|
# notify if this is a dry run
|
|
if options.dry_run:
|
|
print "This is a dry run: null strings will be detected but not fixed"
|
|
else:
|
|
print "Preparing Null String Check, this may take awhile..."
|
|
|
|
# open up the main database archive table
|
|
with weewx.manager.open_manager_with_config(config_dict, db_binding) as dbmanager:
|
|
|
|
obs_pytype_list = []
|
|
obs_list = []
|
|
|
|
# get the schema and extract the Python type each observation type should be
|
|
for column in dbmanager.connection.genSchemaOf('archive'):
|
|
schema_type = column[2]
|
|
if schema_type == 'INTEGER':
|
|
schema_type = int
|
|
elif schema_type == 'REAL':
|
|
schema_type = float
|
|
elif schema_type == 'STR':
|
|
schema_type = str
|
|
# save the observation type for this column (eg, 'outTemp'):
|
|
obs_list.append(column[1])
|
|
# save the Python type for this column (eg, 'int'):
|
|
obs_pytype_list.append(schema_type)
|
|
|
|
records = 0
|
|
_found = []
|
|
# cycle through each row in the database
|
|
for record in dbmanager.genBatchRows():
|
|
records += 1
|
|
# now examine each column
|
|
for icol in range(len(record)):
|
|
# check to see if this column is an instance of the correct
|
|
# Python type
|
|
if record[icol] is not None and not isinstance(record[icol], obs_pytype_list[icol]):
|
|
# Oops. Found a bad one. Print it out.
|
|
if fix:
|
|
syslog.syslog(syslog.LOG_INFO,
|
|
"Timestamp = %s; record['%s']= %r; ... "
|
|
% (record[0],
|
|
obs_list[icol],
|
|
record[icol]))
|
|
|
|
if fix:
|
|
# coerce to the correct type. If it can't be done, then
|
|
# set it to None.
|
|
try:
|
|
corrected_value = obs_pytype_list[icol](record[icol])
|
|
except ValueError:
|
|
corrected_value = None
|
|
# update the database with the new value but only if
|
|
# it's not a dry run
|
|
if not options.dry_run:
|
|
dbmanager.updateValue(record[0], obs_list[icol], corrected_value)
|
|
_found.append((record[0], obs_list[icol], record[icol], corrected_value))
|
|
# log it
|
|
syslog.syslog(syslog.LOG_INFO, " changed to %r\n" % corrected_value)
|
|
else:
|
|
_found.append((record[0], obs_list[icol], record[icol]))
|
|
# notify the user of progress
|
|
if records % 1000 == 0:
|
|
print >>sys.stdout, "Checking record: %d; Timestamp: %s\r" % \
|
|
(records, timestamp_to_string(record[0])),
|
|
sys.stdout.flush()
|
|
# update our final count now that we have finished
|
|
print >>sys.stdout, "Checking record: %d; Timestamp: %s\r" % \
|
|
(records, timestamp_to_string(record[0])),
|
|
print
|
|
tdiff = time.time() - t1
|
|
# now display details of what we found if we found any null strings
|
|
if len(_found) > 0:
|
|
print "The following null strings were found:"
|
|
for item in _found:
|
|
if len(item) == 4:
|
|
print "Timestamp = %s; record['%s'] = %r; ... changed to %r" % item
|
|
else:
|
|
print "Timestamp = %s; record['%s'] = %r; ... ignored" % item
|
|
# how many did we fix?
|
|
fixed = len([a for a in _found if len(a)==4])
|
|
# summarise our results
|
|
if len(_found) == 0:
|
|
# found no null strings, log it and display on screen
|
|
syslog.syslog(syslog.LOG_INFO, "No null strings found.")
|
|
print "No null strings found."
|
|
elif fixed == len(_found):
|
|
# fixed all we found
|
|
if options.dry_run:
|
|
# its a dry run so display to screen but not to log
|
|
print "%d of %d null strings found would have been fixed." % (fixed,
|
|
len(_found))
|
|
else:
|
|
# really did fix so log and display to screen
|
|
syslog.syslog(syslog.LOG_INFO,
|
|
"%d of %d null strings found were fixed." % (fixed,
|
|
len(_found)))
|
|
print "%d of %d null strings found were fixed." % (fixed, len(_found))
|
|
elif fix:
|
|
# this should never occur - found some but didn't fix them all when we
|
|
# should have
|
|
if options.dry_run:
|
|
# its a dry run so say what would have happened
|
|
print "Could not fix all null strings. %d of %d null strings found would have been fixed." % (fixed,
|
|
len(_found))
|
|
else:
|
|
# really did fix so log and display to screen
|
|
syslog.syslog(syslog.LOG_INFO,
|
|
"Could not fix all null strings. %d of %d null strings found were fixed." % (fixed,
|
|
len(_found)))
|
|
print "Could not fix all null strings. %d of %d null strings found were fixed." % (fixed,
|
|
len(_found))
|
|
else:
|
|
# found some null string but it was only a check not a fix, just
|
|
# display to screen
|
|
print ("%d null strings were found.\r\n"
|
|
"Recommend running --fix-strings to fix these strings." % len(_found))
|
|
|
|
# and finally details on time taken
|
|
if fix:
|
|
syslog.syslog(syslog.LOG_INFO, "Applied Null String Fix in %0.2f seconds." % tdiff)
|
|
print "Applied Null String Fix in %0.2f seconds." % tdiff
|
|
else:
|
|
# it was a check not a fix so just display to screen
|
|
print "Completed Null String Check in %0.2f seconds." % tdiff
|
|
# just in case, set the syslog level back where we found it
|
|
if options.dry_run or not fix:
|
|
syslog.setlogmask(_log_level)
|
|
|
|
def _parse_dates(options):
|
|
"""Parse --date, --from and --to command line options.
|
|
|
|
Inputs:
|
|
options: the optparse options
|
|
|
|
Returns: A two-way tuple (start_d, stop_d), where
|
|
start_d: A datetime.date object holding the first date. May be None
|
|
stop_d: A dateimte.date object holding the last date. May be None
|
|
"""
|
|
|
|
# Default is None, unless user has specified an option
|
|
_first_d = None
|
|
_last_d = None
|
|
|
|
if options.date:
|
|
# Make sure we are not overspecified
|
|
if options.from_date or options.to_date:
|
|
raise ValueError("Specify either --date or a --from and --to combination; not both")
|
|
|
|
# there is a --date but is it valid
|
|
try:
|
|
_first_d = datetime.datetime.strptime(options.date, "%Y-%m-%d").date()
|
|
except ValueError:
|
|
raise ValueError("Invalid --date option specified.")
|
|
else:
|
|
_last_d = _first_d
|
|
return (_first_d, _last_d)
|
|
|
|
if options.from_date:
|
|
try:
|
|
_first_d = datetime.datetime.strptime(options.from_date, "%Y-%m-%d").date()
|
|
except ValueError:
|
|
raise ValueError("Invalid --from option specified.")
|
|
|
|
if options.to_date:
|
|
try:
|
|
_last_d = datetime.datetime.strptime(options.to_date, "%Y-%m-%d").date()
|
|
except ValueError:
|
|
raise ValueError("Invalid --to option specified.")
|
|
|
|
if _first_d and _last_d:
|
|
# If both --from and --to have been specified, make sure the latter is greater
|
|
# than the former
|
|
if _last_d < _first_d:
|
|
raise weewx.ViolatedPrecondition("--from value is later than --to value.")
|
|
|
|
return (_first_d, _last_d)
|
|
|
|
|
|
if __name__=="__main__" :
|
|
main()
|