From d96c6a2b5331a8daa46bdcfcc601a06a89abdb10 Mon Sep 17 00:00:00 2001
From: Tom Keffer
Date: Thu, 30 Jan 2014 18:37:02 +0000
Subject: [PATCH] Added an option --string-check and --fix to the utility
wee_config_database to fix embedded strings found in the sqlite archive
database. Documented it.
---
bin/wee_config_database | 50 +++++++++++++++++++-------
bin/weedb/sqlite.py | 8 +++++
bin/weewx/archive.py | 5 +++
docs/changes.txt | 3 ++
docs/customizing.htm | 79 +++++++++++++++++++++++++++++++++++++++++
5 files changed, 132 insertions(+), 13 deletions(-)
diff --git a/bin/wee_config_database b/bin/wee_config_database
index c2d487b2..b2b49965 100755
--- a/bin/wee_config_database
+++ b/bin/wee_config_database
@@ -13,6 +13,7 @@ from __future__ import with_statement
import optparse
import syslog
+import sys
import user.extensions #@UnusedImport
import weedb
@@ -30,7 +31,7 @@ add or drop data types from the database schema or change unit systems."""
usage="""%prog: [config_path] [--help]
[--create-database] [--create-stats]
[--reconfigure] [--backfill-stats]
- [--string-check]"""
+ [--string-check] [--fix]"""
epilog="""If you are using the MySQL database it is assumed that you have the
appropriate permissions for the requested operation."""
@@ -59,7 +60,9 @@ def main():
parser.add_option("--backfill-stats", dest="backfill_stats", action='store_true',
help="Backfill the statistical database using the archive database")
parser.add_option("--string-check", dest="string_check", action="store_true",
- help="Check the archive database for strings in it.")
+ help="Check a sqlite version of the archive database for embedded strings in it.")
+ parser.add_option("--fix", dest="fix", action="store_true",
+ help="If a string is found, fix it.")
# Now we are ready to parse the command line:
(options, args) = parser.parse_args()
@@ -79,7 +82,7 @@ def main():
backfillStatsDatabase(config_dict)
if options.string_check:
- string_check(config_dict)
+ string_check(config_dict, options.fix)
def createMainDatabase(config_dict):
"""Create the main weewx archive database"""
@@ -187,7 +190,7 @@ def backfillStatsDatabase(config_dict):
print "Backfilled %d records from the archive database '%s' into the statistical database '%s'" % (nrecs, archive.database, statsDb.database)
-def string_check(config_dict):
+def string_check(config_dict, fix=False):
print "Checking archive database for strings..."
archive_db = config_dict['StdArchive']['archive_database']
archive_db_dict = config_dict['Databases'][archive_db]
@@ -195,23 +198,44 @@ def string_check(config_dict):
# Open up the main database archive
with weewx.archive.Archive.open(archive_db_dict) as archive:
- obs_type_list = []
+ obs_pytype_list = []
obs_list = []
+
+ # Get the schema and extract the Python type each observation type should be
for column in archive.connection.genSchemaOf('archive'):
schema_type = column[2]
- if column[2] == 'INTEGER':
+ if schema_type == 'INTEGER':
schema_type = int
- elif column[2] == 'REAL':
+ elif schema_type == 'REAL':
schema_type = float
- elif column[2] == 'STR':
+ elif schema_type == 'STR':
schema_type = str
+ # Save the observation type for this column (eg, 'outTemp'):
obs_list.append(column[1])
- obs_type_list.append(schema_type)
-
+ # Save the Python type for this column (eg, 'int'):
+ obs_pytype_list.append(schema_type)
+
+ # Cycle through each row in the database
for record in archive.genBatchRows():
+ # Now examine each column
for icol in range(len(record)):
- if record[icol] is not None and not isinstance(record[icol], obs_type_list[icol]):
- print weeutil.weeutil.timestamp_to_string(record['dateTime']), obs_list[icol], "; value=", record[icol]
-
+ # 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
+ sys.stdout.write("Timestamp = %s; record['%s']= %r; ... " % (record[0], obs_list[icol], record[icol]))
+
+ if fix:
+ # Cooerce 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
+ archive.updateValue(record[0], obs_list[icol], corrected_value)
+ # Inform the user
+ sys.stdout.write("changed to %r\n" % corrected_value)
+ else:
+ sys.stdout.write("ignored.\n")
+
if __name__=="__main__" :
main()
diff --git a/bin/weedb/sqlite.py b/bin/weedb/sqlite.py
index 681b946f..b9b7e838 100644
--- a/bin/weedb/sqlite.py
+++ b/bin/weedb/sqlite.py
@@ -9,6 +9,7 @@
#
"""Driver for sqlite"""
+from __future__ import with_statement
import os.path
# Import sqlite3. If it does not support the 'with' statement, then
@@ -78,6 +79,13 @@ class Connection(weedb.Connection):
"""Return a cursor object."""
return Cursor(self.connection)
+ def execute(self, sql_string, sql_tuple=() ):
+ """Execute a sql statement. This specialized version takes advantage
+ of sqlite's ability to do an execute without a cursor."""
+
+ with self.connection:
+ self.connection.execute(sql_string, sql_tuple)
+
def tables(self):
"""Returns a list of tables in the database."""
diff --git a/bin/weewx/archive.py b/bin/weewx/archive.py
index d04d4c76..d78589ac 100644
--- a/bin/weewx/archive.py
+++ b/bin/weewx/archive.py
@@ -254,6 +254,11 @@ class Archive(object):
finally:
_cursor.close()
+ def updateValue(self, timestamp, obs_type, new_value):
+ """Update (replace) a single value in the database."""
+
+ self.connection.execute("UPDATE %s SET %s=? WHERE dateTime=?" % (self.table, obs_type), (new_value, timestamp))
+
def getSql(self, sql, sqlargs=()):
"""Executes an arbitrary SQL statement on the database.
diff --git a/docs/changes.txt b/docs/changes.txt
index 6ae0f31b..2294dea9 100644
--- a/docs/changes.txt
+++ b/docs/changes.txt
@@ -36,6 +36,9 @@ When QC rejects values it now logs the rejection.
Introduced a new unit system, METRICWX. Similar to METRIC, it uses
mm for rain, mm/hr for rain rate, and m/s for speed.
+Added an option --string-check and --fix to the utility wee_config_database
+to fix embedded strings found in the sqlite archive database.
+
Font handles are now cached in order to work around a memory leak in PIL.
Image margins now scale with image and font sizes.
diff --git a/docs/customizing.htm b/docs/customizing.htm
index c81ebeb5..63a8b1ba 100644
--- a/docs/customizing.htm
+++ b/docs/customizing.htm
@@ -2439,6 +2439,7 @@ class MyAlarm(StdService):
low-battery alarm (lowBattery.py), which is
similar, except that it intercepts LOOP events (instead of
archiving events).
+
Customizing the archive database
For most users the default database will work just fine. It has the
added advantage of being compatible with the wview database.
@@ -2446,9 +2447,44 @@ class MyAlarm(StdService):
type to your database, or change its unit system. This section shows you
how to do this, using the utility $BIN_ROOT/wee_config_database.
+ This utility also has the ability to check a sqlite version of the archive
+ database for embedded strings (where a float is expected).
Before starting, it's worth running the utility with the --help
flag to see how it is used:
$BIN_ROOT/wee_config_database --help
+ This will result in an output that looks something like this:
+
+Usage: wee_config_database: [config_path] [--help]
+ [--create-database] [--create-stats]
+ [--reconfigure] [--backfill-stats]
+ [--string-check] [--fix]
+
+Configure the weewx databases. Most of these functions are handled
+automatically by weewx, but they may be useful as a utility in special cases.
+In particular, the 'reconfigure' option can be useful if you decide to add or
+drop data types from the database schema or change unit systems.
+
+Options:
+ -h, --help show this help message and exit
+ --config=FILE use configuration file FILE
+ --create-archive Create the archive database.
+ --create-stats Create the statistical database.
+ --reconfigure Create a new archive 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]. It will use the
+ schema found in './bin/user/schemas.py'. The new database
+ will have the same name as the old database, with a '_new'
+ on the end.
+ --backfill-stats Backfill the statistical database using the archive
+ database
+ --string-check Check a sqlite version of the archive database for
+ embedded strings in it.
+ --fix If a string is found, fix it.
+
+If you are using the MySQL database it is assumed that you have the
+appropriate permissions for the requested operation.
+
Adding a new observation type
Suppose you have installed an electric meter at your house and you wish
to correlate electrical usage with the weather. The meter has some sort
@@ -2622,6 +2658,7 @@ mv weewx.sdb_new weewx.sdb
+
Changing the unit system
Normally, data is stored in the databases using US Customary units and,
normally, you don't care --- data can always be displayed using any
@@ -2679,6 +2716,48 @@ class="symcode">$CONFIG_ROOT/weewx.conf
5. Recreate the stats database. Delete the stats
database, then let weewx regenerate it. It
will use the new unit system.
+
+ Checking for embedded strings
+ If you edit your sqlite archive database using an editing tool, occasionally
+ strings will get embedded in it, causing weewx to raise an exception.
+ This is only a problem with sqlite. There is no analogous problem
+ with MySQL databases. The symptom will look something like this:
+
+Dec 31 16:55:09 arm weewx[18141]: wxengine: Record generation will be attempted in 'hardware'
+Dec 31 16:55:09 arm weewx[18141]: wxengine: Using archive database: archive_sqlite
+Dec 31 16:55:10 arm weewx[18141]: stats: Created schema for statistical database
+Dec 31 17:01:06 arm weewx[18141]: wxengine: Caught unrecoverable exception in wxengine:
+Dec 31 17:01:06 arm weewx[18141]: **** unsupported operand type(s) for +=: 'float' and 'unicode'
+Dec 31 17:01:06 arm weewx[18141]: **** Traceback (most recent call last):
+Dec 31 17:01:06 arm weewx[18141]: **** File "/usr/share/weewx/weewx/wxengine.py", line 886, in main
+Dec 31 17:01:06 arm weewx[18141]: **** engine = EngineClass(config_dict)
+Dec 31 17:01:06 arm weewx[18141]: **** File "/usr/share/weewx/weewx/wxengine.py", line 70, in __init__
+Dec 31 17:01:06 arm weewx[18141]: **** self.loadServices(config_dict)
+Dec 31 17:01:06 arm weewx[18141]: **** File "/usr/share/weewx/weewx/wxengine.py", line 124, in loadServices
+Dec 31 17:01:06 arm weewx[18141]: **** self.service_obj.append(weeutil.weeutil._get_object(svc)(self, config_dict))
+Dec 31 17:01:06 arm weewx[18141]: **** File "/usr/share/weewx/weewx/wxengine.py", line 432, in __init__
+Dec 31 17:01:06 arm weewx[18141]: **** self.setupStatsDatabase(config_dict)
+Dec 31 17:01:06 arm weewx[18141]: **** File "/usr/share/weewx/weewx/wxengine.py", line 543, in setupStatsDatabase
+Dec 31 17:01:06 arm weewx[18141]: **** self.statsDb.backfillFrom(self.archive)
+Dec 31 17:01:06 arm weewx[18141]: **** File "/usr/share/weewx/weewx/stats.py", line 461, in backfillFrom
+Dec 31 17:01:06 arm weewx[18141]: **** _statsDict.addRecord(_rec)
+Dec 31 17:01:06 arm weewx[18141]: **** File "/usr/share/weewx/weewx/accum.py", line 305, in addRecord
+Dec 31 17:01:06 arm weewx[18141]: **** self._add_value(record[obs_type], obs_type, record['dateTime'], add_hilo)
+Dec 31 17:01:06 arm weewx[18141]: **** File "/usr/share/weewx/weewx/accum.py", line 264, in _add_value
+Dec 31 17:01:06 arm weewx[18141]: **** self[obs_type].addSum(val)
+Dec 31 17:01:06 arm weewx[18141]: **** File "/usr/share/weewx/weewx/accum.py", line 81, in addSum
+Dec 31 17:01:06 arm weewx[18141]: **** self.sum += val
+Dec 31 17:01:06 arm weewx[18141]: **** TypeError: unsupported operand type(s) for +=: 'float' and 'unicode'
+Dec 31 17:01:06 arm weewx[18141]: **** Exiting.
+ The problem is that a unicode null string u''
+ got entered where a NULL should be. The
+ utility wee_config_database can fix this.
+ Run it with the option --string-check to
+ search for these embedded strings. Add the option --fix
+ to have the utility fix them:
+ $BIN_ROOT/wee_config_database $CONFIG_ROOT/weewx.conf --string-check --fix
+
Porting to new weather station hardware
Naturally, this is an advanced topic but, nevertheless, I'd really
like to encourage any Python wizards out there to give it a try. Of