Estaciones meteorológicas de baja calidad, WeeWX y modificaciones en la base de datos "a pelo"

Mi padre tiene una estación meteorológica pce-fws 20. Se trata de una estación bastante completa con un precio muy barato, ya que cuesta unos 112.53 € (gastos de envío e impuestos incluídos). Ese precio, no obstante, se compensa con unas deficiencias de calidad poco evidentes. Por ejemplo, que el panel solar no sea capaz de cargar la batería o, la causa de este artículo, que la comunicación inalámbrica entre la estación meteorológica y su pantalla no esté protegida.

Vuelvo a decirlo: el enlace inalámbrico entre la estación meteorológica y la pantalla de visualización no va protegido de ninguna forma contra errores de comunicación. ¿Qué puede ir mal? [1].

[1] Aunque aquí señalo lo de estación meteorológica barata pero de baja calidad, lo cierto es que estos problemas también afectan a estaciones caras. En general se trata un problema ignorado de forma sistemática por muchos fabricantes, caros y baratos.

Exacto: de vez en cuando, sobre todo cuando la batería de la estación meteorológica está baja, la comunicación se decodifica incorrectamente y se introducen errores en las medidas. Nótese que proteger esa comunicación es trivial añadiendo algo como un simple Código de Redundancia Cíclica (CRC) e ignorando los paquetes de información incorrectos. Los CRC son triviales de generar y verificar.

Mi padre utiliza WeeWX en una Raspberry PI para gestionar la estación meteorológica y enviar información del tiempo local cada cinco o diez minutos a innumerables webs y servicios como APRS (sí, mi padre es radioaficionado). WeeWX dispone de una funcionalidad que permite, básicamente, comprobar que las medidas que nos interesan estén en un rango adecuado (por ejemplo, que la temperatura exterior está por encima del cero absoluto y por debajo de los 6000 grados centígrados) y rechazar las medidas absurdas. Algo que no sería necesario si la comunicación se protegiese adecuadamente. Lamentablemente esta verificación no nos protege de medidas realistas pero incorrectas. Digamos, una velocidad del viento de 120Km/h cuando fuera no sopla ni una brisa.

A veces, como consecuencia de todo esto, se guardan valores realistas pero patentemente incorrectos para un ser humano. En estos casos es necesario, por tanto, modificar la base de datos para eliminar dichos valores incorrectos.

El primer paso es conocer la estructura de la base de datos de WeeWX. Afortunadamente existe algo de documentación que nos ayuda a dar los primeros pasos. También nos ayuda saber cosas como la forma de mostrar todas las tablas de una base de datos SQLite.

Resumiento: WeeWX utiliza dos bases de datos distintas: una que contiene TODAS las medidas en la historia de la instalación (típicamente una medida cada cinco o diez minutos) y otra que contiene resúmenes diarios, mensuales y anuales.

Tras experimentar un poco, compruebo que la opción más simple consiste en eliminar las medidas erróneas de la base de datos y, luego, pedir amablemente a WeeWX que revise todas las medidas para volver a generar los resúmenes.

Los pasos son los siguientes:

  1. Lo primero es parar WeeWX. Vamos a modificar la base de datos a lo bruto y no queremos interferencias.

    No se pierden datos porque la última semana de muestras se almacena en la memoria interna de la pantalla de la estación meteorológica. Cuando WeeWX arranca sabe por dónde se ha quedado y solicita a la pantalla las muestras que le faltan.

    root@MeteoPI:~# /etc/init.d/weewx stop
    Stopping weewx (via systemctl): weewx.service.
    
  2. En mi instalación Raspberry PI la base de datos de WeeWX se almacena en /home/weewx/archive. Entramos en ese directorio y hacemos lo siguiente:

    root@MeteoPI:/home/weewx/archive# sqlite3
    SQLite version 3.8.7.1 2014-10-29 13:59:56
    Enter ".help" for usage hints.
    Connected to a transient in-memory database.
    Use ".open FILENAME" to reopen on a persistent database.
    sqlite>
    sqlite> .open weewx.sdb
    sqlite> .tables
    archive
    sqlite> .schemas
    Error: unknown command or invalid arguments:  "schemas". Enter ".help" for help
    sqlite> .schema
    CREATE TABLE archive (`dateTime` INTEGER NOT NULL UNIQUE PRIMARY KEY, `usUnits` INTEGER NOT NULL, `interval` INTEGER NOT NULL, `barometer` REAL, `pressure` REAL, `altimeter` REAL, `inTemp` REAL, `outTemp` REAL, `inHumidity` REAL, `outHumidity` REAL, `windSpeed` REAL, `windDir` REAL, `windGust` REAL, `windGustDir` REAL, `rainRate` REAL, `rain` REAL, `dewpoint` REAL, `windchill` REAL, `heatindex` REAL, `ET` REAL, `radiation` REAL, `UV` REAL, `extraTemp1` REAL, `extraTemp2` REAL, `extraTemp3` REAL, `soilTemp1` REAL, `soilTemp2` REAL, `soilTemp3` REAL, `soilTemp4` REAL, `leafTemp1` REAL, `leafTemp2` REAL, `extraHumid1` REAL, `extraHumid2` REAL, `soilMoist1` REAL, `soilMoist2` REAL, `soilMoist3` REAL, `soilMoist4` REAL, `leafWet1` REAL, `leafWet2` REAL, `rxCheckPercent` REAL, `txBatteryStatus` REAL, `consBatteryVoltage` REAL, `hail` REAL, `hailRate` REAL, `heatingTemp` REAL, `heatingVoltage` REAL, `supplyVoltage` REAL, `referenceVoltage` REAL, `windBatteryStatus` REAL, `rainBatteryStatus` REAL, `outTempBatteryStatus` REAL, `inTempBatteryStatus` REAL);
    sqlite>
    

    Aquí podemos ver las tablas y los campos que existen.

  3. El error que tenemos aquí es una presión atmosférica muy baja. Buscamos el valor incorrecto:

    sqlite> select * from archive where pressure <881;
    1460872595|16|22|880.105988103702|874.4|880.516906741288||79.0|3.0|89.0|||7.92|495.0|||76.1781134684576|79.0|597.779885069778|||||||||||||||||||||||||||||||||
    

    Esta búsqueda necesita unos segundos porque ese campo no está indexado.

  4. En otra ventana Python vemos la fecha y hora de ese registro incorrecto y vemos que se ajusta a la fecha y hora que nos ocupa. Es decir, hemos dado con el registro correcto:

    >>> time.ctime(1460872595)
    'Sun Apr 17 07:56:35 2016'
    
  5. Borro ese registro:

    sqlite> delete from archive where dateTime=1460872595;
    
  6. Por curiosidad, vemos la estructura de la base de datos de resúmenes:

    sqlite> .open stats.sdb
    sqlite> .tables
    ET              dewpoint        inTemp          radiation       wind
    UV              extraTemp1      metadata        rain            windchill
    _stats_schema   heatindex       outHumidity     rainRate
    barometer       inHumidity      outTemp         rxCheckPercent
    sqlite> .schema
    CREATE TABLE barometer ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE inTemp ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE outTemp ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE inHumidity ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE outHumidity ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE rainRate ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE rain ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE dewpoint ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE windchill ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE heatindex ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE ET ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE radiation ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE UV ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE extraTemp1 ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE rxCheckPercent ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER);
    CREATE TABLE wind ( dateTime INTEGER NOT NULL UNIQUE PRIMARY KEY, min REAL, mintime INTEGER, max REAL, maxtime INTEGER, sum REAL, count INTEGER, gustdir REAL, xsum REAL, ysum REAL, squaresum REAL, squarecount INTEGER);
    CREATE TABLE metadata (name CHAR(20) NOT NULL UNIQUE PRIMARY KEY, value TEXT);
    CREATE TABLE _stats_schema (obs_name CHAR(20) NOT NULL UNIQUE, obs_type CHAR(12), reserve1 CHAR(20), reserve2 CHAR(20));
    
  7. En vez de liarnos con esa base de datos, que no es trivial, mejor la borramos y la regeneramos:

    root@MeteoPI:/home/weewx/archive# rm stats.sdb
    root@MeteoPI:/home/weewx/archive# cd /home/weewx/bin
    root@MeteoPI:/home/weewx/bin# time ./wee_config_database --backfill-stats
    Using configuration file /home/weewx/weewx.conf.
    Records processed: 2000; Last date: 2013-09-23 03:50:01 CEST (1379901001)
    ...
    Backfilled 258883 records from the archive database 'archive/weewx.sdb' into the statistical database 'archive/stats.sdb'
    
    real    23m23.299s
    user    18m11.000s
    sys     0m28.710s
    
  8. Lo que hemos visto en el punto anterior es la reconstrucción completa de los resúmenes históricos a partir de los datos almacenados desde septiembre de 2013, cuando pusimos a funcionar el servicio. No obstante, WeeWX permite usar una base de datos antigua y resumir simplemente lo nuevo. Para aprovecharme de ello y también protegerme de corrupciones y demás mala suerte, hago una copia de las bases de datos (esta Raspberry PI está además incluída en mi sistema de copias de seguridad).

    root@MeteoPI:/home/weewx/bin# cd /home/weewx/archive/
    root@MeteoPI:/home/weewx/archive# cp stats.sdb stats.sdb.2
    root@MeteoPI:/home/weewx/archive# cp weewx.sdb weewx.sdb.2
    root@MeteoPI:/home/weewx/archive# chown weewx:weewx stats.sdb
    root@MeteoPI:/home/weewx/archive# ls -la
    total 91132
    drwxr-xr-x  2 weewx weewx     4096 Apr 23 01:39 .
    drwxr-xr-x 14 weewx weewx     4096 Apr 23 01:27 ..
    -rw-r--r--  1 weewx weewx   862208 Apr 23 01:39 stats.sdb
    -rw-r--r--  1 weewx weewx   862208 Apr 23 01:40 stats.sdb.2
    -rw-r--r--  1 weewx weewx 45788160 Apr 23 01:13 weewx.sdb
    -rw-r--r--  1 weewx weewx 45788160 Apr 23 01:41 weewx.sdb.2
    
  9. Volvemos a lanzar WeeWX y, una vez que empiezan a entrar datos nuevos y WeeWX sube actualizaciones a los diferentes servicios, comprobamos que todo funciona con normalidad y que el dato problemático ha desaparecido.

    root@MeteoPI:/home/weewx/archive# /etc/init.d/weewx start
    Starting weewx (via systemctl): weewx.service.
    

Actualización 20161005: Vuelve a ocurrir un problema similar en septiembre de 2016. Se almacena una ráfaga de viento de más de 90Km/h. Se trata de un valor creíble en la costa gallega, pero incorrecto ese día concreto.

Volvemos a repetir el proceso:

  1. Tras parar WeeWX buscamos el error en la base de datos:

    root@MeteoPI:/home/weewx/archive# sqlite3
    SQLite version 3.8.7.1 2014-10-29 13:59:56
    Enter ".help" for usage hints.
    Connected to a transient in-memory database.
    Use ".open FILENAME" to reopen on a persistent database.
    sqlite> .open weewx.sdb
    sqlite> select * from archive where windGust>90;
    1387853565|16|5|1004.93095257578|997.2|1004.03708722839|17.4|14.6|73.0|88.0|47.88|0.0|91.8|0.0|1.8|0.15|12.6332079720433|14.6|14.6|||||||||||||||||||||||||||||||||
    1387858365|16|5|1002.61698747809|994.9|1001.72376028656|17.3|14.5|72.0|86.0|50.04|247.5|100.44|247.5|1.08000000000002|0.0900000000000016|12.184577372135|14.5|14.5|||||||||||||||||||||||||||||||||
    1387869165|16|5|1000.10654190419|992.4|999.209267812186|17.1|14.2|74.0|98.0|45.36|337.5|90.72|337.5|0.719999999999985|0.0599999999999987|13.8881064978996|14.2|14.2|||||||||||||||||||||||||||||||||
    1473085140|16|5||2256.2|2269.72040920224|26.3||40.0|22.0|||93.6|472.5|||623.451720902903|1024.3|1024.3|||||||||||||||||||||||||||||||||
    
  2. El error es el último registro (en invierno sí ha habido ráfagas de más de 90Km/h). Lo borramos:

    sqlite> delete from archive where dateTime=1473085140;
    
  3. Ahora en vez de recalcular los resúmenes completos, recuperamos la copia de la base de datos de hace unos meses y actualizamos la base de datos de resúmenes desde ahí. Es decir, una regeneración incremental:

    root@MeteoPI:/home/weewx/bin# time ./wee_config_database --backfill-stats
    Using configuration file /home/weewx/weewx.conf.
    Backfilled 19572 records from the archive database 'archive/weewx.sdb' into the statistical database 'archive/stats.sdb'
    
    real    1m55.138s
    user    1m27.010s
    sys     0m3.150s
    

    Obsérvese que ahora solo necesitamos dos minutos de proceso en vez de los 24 minutos de la vez anterior. Ventajas de la actualización incremental.

  4. Volvemos a hacer una copia de las base de datos por si tenemos que repetir el proceso de forma eficiente en el futuro y lanzamos WeeWX de nuevo:

    root@MeteoPI:/home/weewx/archive# cp stats.sdb stats.sdb.2
    root@MeteoPI:/home/weewx/archive# cp weewx.sdb weewx.sdb.2
    
    root@MeteoPI:/home/weewx/archive# /etc/init.d/weewx start
    Starting weewx (via systemctl): weewx.service.
    

Problema solucionado... por segunda vez. No será la última :-)

Actualización 20170813: En esta ocasión lo que tenemos es una temperatura exterior de 40 grados bajo cero. En Vigo. En agosto:

sqlite> select * from archive where outTemp <-30;
1502619241|16|10|1017.10012652575|1007.6|1014.49727523545|25.9|-40.0|50.0|63.0|1.08|292.5|3.6|292.5|0.0|0.0|-44.3033765096301|-40.0|-40.0|||||||||||||||||||||||||||||||||
sqlite> delete from archive where dateTime=1502619241;

Como las veces anteriores, reconstruímos la base de datos de estadísticas, hacemos copia de seguridad, etc.