Simple UV Monitoring: Utilizing the SI1145 Sensor with a Raspberry Pi and Weewx


Software • by Sven Reifschneider • 01 August 2023 • 2 comments
#software #howto #weewx #weather #raspberrypi

In a world increasingly driven by data, precise environmental monitoring is more crucial than ever. This is especially true when it comes to weather monitoring, where UV index readings are not just critical for scientific research, but also for public health and safety. This article delves into the integration of an SI1145 UV Sensor with a Raspberry Pi and Weewx to create an advanced weather monitoring station.

The Raspberry Pi is an invaluable asset in the IoT landscape, offering both high performance and cost-effectiveness. When coupled with the tried-and-true SI1145 UV Sensor, it forms a robust solution for UV monitoring. We'll guide you through the technical steps from setting up the Raspberry Pi to integrating the data with Weewx.

The Raspberry Pi + SI1145 Combo

Strategically positioned on a rooftop and equipped with an array of sensors, a Raspberry Pi serves as the cornerstone of our advanced weather station. Although various sensors can be used, our focus is on the SI1145 UV Sensor. While not the most modern sensor on the market, it has proven to be reliable and reasonably accurate. It may not rival the expensive modules used in professional weather stations, but it is quite suitable for obtaining a general UV index reading.

Technically, the sensor operates similarly to other sensors, but the Python script addresses it differently. Most sensors come with compatible libraries, simplifying their usage. From a hardware perspective, the sensor connects to the Raspberry Pi via an I2C interface, which should be the case for similar systems, as the script directly communicates with the I2C interface.

On the software side, Python3 (usually pre-installed on most Linux systems) is required, along with a web server (we recommend nginx, easily installable with sudo apt install nginx-full). Python also needs the Adafruit-GPIO library, which can be installed with sudo pip3 install Adafruit-GPIO.

The first task is to place a Python script on your Raspberry Pi that reads the SI1145 and provides the current reading in a JSON file. The script uses the Python_SI1145 class by Joe Gutting on GitHub (MIT License), which we have adapted for Python3.

This class makes up the most part of the script. The sensor reading and JSON saving is found at the end of the script.

#!/usr/bin/python
# Fetching the UV index from the SI1145 module
import datetime

# SI1145 class
# Source: https://github.com/THP-JOE/Python_SI1145
# Author: Joe Gutting, adjusted for Python3 by Neoground GmbH
import logging
import time
import json

import Adafruit_GPIO.I2C as I2C

# COMMANDS
SI1145_PARAM_QUERY = 0x80
SI1145_PARAM_SET = 0xA0
SI1145_NOP = 0x0
SI1145_RESET = 0x01
SI1145_BUSADDR = 0x02
SI1145_PS_FORCE = 0x05
SI1145_ALS_FORCE = 0x06
SI1145_PSALS_FORCE = 0x07
SI1145_PS_PAUSE = 0x09
SI1145_ALS_PAUSE = 0x0A
SI1145_PSALS_PAUSE = 0xB
SI1145_PS_AUTO = 0x0D
SI1145_ALS_AUTO = 0x0E
SI1145_PSALS_AUTO = 0x0F
SI1145_GET_CAL = 0x12

# Parameters
SI1145_PARAM_I2CADDR = 0x00
SI1145_PARAM_CHLIST = 0x01
SI1145_PARAM_CHLIST_ENUV = 0x80
SI1145_PARAM_CHLIST_ENAUX = 0x40
SI1145_PARAM_CHLIST_ENALSIR = 0x20
SI1145_PARAM_CHLIST_ENALSVIS = 0x10
SI1145_PARAM_CHLIST_ENPS1 = 0x01
SI1145_PARAM_CHLIST_ENPS2 = 0x02
SI1145_PARAM_CHLIST_ENPS3 = 0x04

SI1145_PARAM_PSLED12SEL = 0x02
SI1145_PARAM_PSLED12SEL_PS2NONE = 0x00
SI1145_PARAM_PSLED12SEL_PS2LED1 = 0x10
SI1145_PARAM_PSLED12SEL_PS2LED2 = 0x20
SI1145_PARAM_PSLED12SEL_PS2LED3 = 0x40
SI1145_PARAM_PSLED12SEL_PS1NONE = 0x00
SI1145_PARAM_PSLED12SEL_PS1LED1 = 0x01
SI1145_PARAM_PSLED12SEL_PS1LED2 = 0x02
SI1145_PARAM_PSLED12SEL_PS1LED3 = 0x04

SI1145_PARAM_PSLED3SEL = 0x03
SI1145_PARAM_PSENCODE = 0x05
SI1145_PARAM_ALSENCODE = 0x06

SI1145_PARAM_PS1ADCMUX = 0x07
SI1145_PARAM_PS2ADCMUX = 0x08
SI1145_PARAM_PS3ADCMUX = 0x09
SI1145_PARAM_PSADCOUNTER = 0x0A
SI1145_PARAM_PSADCGAIN = 0x0B
SI1145_PARAM_PSADCMISC = 0x0C
SI1145_PARAM_PSADCMISC_RANGE = 0x20
SI1145_PARAM_PSADCMISC_PSMODE = 0x04

SI1145_PARAM_ALSIRADCMUX = 0x0E
SI1145_PARAM_AUXADCMUX = 0x0F

SI1145_PARAM_ALSVISADCOUNTER = 0x10
SI1145_PARAM_ALSVISADCGAIN = 0x11
SI1145_PARAM_ALSVISADCMISC = 0x12
SI1145_PARAM_ALSVISADCMISC_VISRANGE = 0x20

SI1145_PARAM_ALSIRADCOUNTER = 0x1D
SI1145_PARAM_ALSIRADCGAIN = 0x1E
SI1145_PARAM_ALSIRADCMISC = 0x1F
SI1145_PARAM_ALSIRADCMISC_RANGE = 0x20

SI1145_PARAM_ADCCOUNTER_511CLK = 0x70

SI1145_PARAM_ADCMUX_SMALLIR = 0x00
SI1145_PARAM_ADCMUX_LARGEIR = 0x03

# REGISTERS
SI1145_REG_PARTID = 0x00
SI1145_REG_REVID = 0x01
SI1145_REG_SEQID = 0x02

SI1145_REG_INTCFG = 0x03
SI1145_REG_INTCFG_INTOE = 0x01
SI1145_REG_INTCFG_INTMODE = 0x02

SI1145_REG_IRQEN = 0x04
SI1145_REG_IRQEN_ALSEVERYSAMPLE = 0x01
SI1145_REG_IRQEN_PS1EVERYSAMPLE = 0x04
SI1145_REG_IRQEN_PS2EVERYSAMPLE = 0x08
SI1145_REG_IRQEN_PS3EVERYSAMPLE = 0x10

SI1145_REG_IRQMODE1 = 0x05
SI1145_REG_IRQMODE2 = 0x06

SI1145_REG_HWKEY = 0x07
SI1145_REG_MEASRATE0 = 0x08
SI1145_REG_MEASRATE1 = 0x09
SI1145_REG_PSRATE = 0x0A
SI1145_REG_PSLED21 = 0x0F
SI1145_REG_PSLED3 = 0x10
SI1145_REG_UCOEFF0 = 0x13
SI1145_REG_UCOEFF1 = 0x14
SI1145_REG_UCOEFF2 = 0x15
SI1145_REG_UCOEFF3 = 0x16
SI1145_REG_PARAMWR = 0x17
SI1145_REG_COMMAND = 0x18
SI1145_REG_RESPONSE = 0x20
SI1145_REG_IRQSTAT = 0x21
SI1145_REG_IRQSTAT_ALS = 0x01

SI1145_REG_ALSVISDATA0 = 0x22
SI1145_REG_ALSVISDATA1 = 0x23
SI1145_REG_ALSIRDATA0 = 0x24
SI1145_REG_ALSIRDATA1 = 0x25
SI1145_REG_PS1DATA0 = 0x26
SI1145_REG_PS1DATA1 = 0x27
SI1145_REG_PS2DATA0 = 0x28
SI1145_REG_PS2DATA1 = 0x29
SI1145_REG_PS3DATA0 = 0x2A
SI1145_REG_PS3DATA1 = 0x2B
SI1145_REG_UVINDEX0 = 0x2C
SI1145_REG_UVINDEX1 = 0x2D
SI1145_REG_PARAMRD = 0x2E
SI1145_REG_CHIPSTAT = 0x30

# I2C Address
SI1145_ADDR = 0x60

class SI1145(object):
    def __init__(self, address=SI1145_ADDR, busnum=I2C.get_default_bus()):
        self._logger = logging.getLogger('SI1145')

        # Create I2C device.
        self._device = I2C.Device(address, busnum)

        # reset device
        self._reset()

        # Load calibration values.
        self._load_calibration()

    # device reset
    def _reset(self):
        self._device.write8(SI1145_REG_MEASRATE0, 0)
        self._device.write8(SI1145_REG_MEASRATE1, 0)
        self._device.write8(SI1145_REG_IRQEN, 0)
        self._device.write8(SI1145_REG_IRQMODE1, 0)
        self._device.write8(SI1145_REG_IRQMODE2, 0)
        self._device.write8(SI1145_REG_INTCFG, 0)
        self._device.write8(SI1145_REG_IRQSTAT, 0xFF)

        self._device.write8(SI1145_REG_COMMAND, SI1145_RESET)
        time.sleep(.01)
        self._device.write8(SI1145_REG_HWKEY, 0x17)
        time.sleep(.01)

    # write Param
    def writeParam(self, p, v):
        self._device.write8(SI1145_REG_PARAMWR, v)
        self._device.write8(SI1145_REG_COMMAND, p | SI1145_PARAM_SET)
        paramVal = self._device.readU8(SI1145_REG_PARAMRD)
        return paramVal

    # load calibration to sensor
    def _load_calibration(self):
        # /***********************************/
        # Enable UVindex measurement coefficients!
        self._device.write8(SI1145_REG_UCOEFF0, 0x29)
        self._device.write8(SI1145_REG_UCOEFF1, 0x89)
        self._device.write8(SI1145_REG_UCOEFF2, 0x02)
        self._device.write8(SI1145_REG_UCOEFF3, 0x00)

        # Enable UV sensor
        self.writeParam(SI1145_PARAM_CHLIST,
                        SI1145_PARAM_CHLIST_ENUV | SI1145_PARAM_CHLIST_ENALSIR | SI1145_PARAM_CHLIST_ENALSVIS | SI1145_PARAM_CHLIST_ENPS1)

        # Enable interrupt on every sample
        self._device.write8(SI1145_REG_INTCFG, SI1145_REG_INTCFG_INTOE)
        self._device.write8(SI1145_REG_IRQEN, SI1145_REG_IRQEN_ALSEVERYSAMPLE)

        # /****************************** Prox Sense 1 */

        # Program LED current
        self._device.write8(SI1145_REG_PSLED21, 0x03)  # 20mA for LED 1 only
        self.writeParam(SI1145_PARAM_PS1ADCMUX, SI1145_PARAM_ADCMUX_LARGEIR)

        # Prox sensor #1 uses LED #1
        self.writeParam(SI1145_PARAM_PSLED12SEL, SI1145_PARAM_PSLED12SEL_PS1LED1)

        # Fastest clocks, clock div 1
        self.writeParam(SI1145_PARAM_PSADCGAIN, 0)

        # Take 511 clocks to measure
        self.writeParam(SI1145_PARAM_PSADCOUNTER, SI1145_PARAM_ADCCOUNTER_511CLK)

        # in prox mode, high range
        self.writeParam(SI1145_PARAM_PSADCMISC, SI1145_PARAM_PSADCMISC_RANGE | SI1145_PARAM_PSADCMISC_PSMODE)
        self.writeParam(SI1145_PARAM_ALSIRADCMUX, SI1145_PARAM_ADCMUX_SMALLIR)

        # Fastest clocks, clock div 1
        self.writeParam(SI1145_PARAM_ALSIRADCGAIN, 0)

        # Take 511 clocks to measure
        self.writeParam(SI1145_PARAM_ALSIRADCOUNTER, SI1145_PARAM_ADCCOUNTER_511CLK)

        # in high range mode
        self.writeParam(SI1145_PARAM_ALSIRADCMISC, SI1145_PARAM_ALSIRADCMISC_RANGE)

        # fastest clocks, clock div 1
        self.writeParam(SI1145_PARAM_ALSVISADCGAIN, 0)

        # Take 511 clocks to measure
        self.writeParam(SI1145_PARAM_ALSVISADCOUNTER, SI1145_PARAM_ADCCOUNTER_511CLK)

        # in high range mode (not normal signal)
        self.writeParam(SI1145_PARAM_ALSVISADCMISC, SI1145_PARAM_ALSVISADCMISC_VISRANGE)

        # measurement rate for auto
        self._device.write8(SI1145_REG_MEASRATE0, 0xFF)  # 255 * 31.25uS = 8ms

        # auto run
        self._device.write8(SI1145_REG_COMMAND, SI1145_PSALS_AUTO)

    # returns the UV index * 100 (divide by 100 to get the index)
    def readUV(self):
        return self._device.readU16LE(0x2C)

    # returns visible + IR light levels
    def readVisible(self):
        return self._device.readU16LE(0x22)

    # returns IR light levels
    def readIR(self):
        return self._device.readU16LE(0x24)

    # Returns "Proximity" - assumes an IR LED is attached to LED
    def readProx(self):
        return self._device.readU16LE(0x26)

try:
    # Measure UV
    sensor = SI1145()
    uvIndex = sensor.readUV() / 100.0

    # SAVE
    data = {
        "time": datetime.datetime.now().isoformat(),
        "uv": uvIndex
    }
    json_data = json.dumps(data, indent=4)

    f = open('/var/www/sensors.json', 'w')
    f.write(json_data)
    f.close()

    print('Success')
except IOError as err:
    print('Error: ' + str(err))

After placing the script, perhaps under /opt/read_uv_index.py, modify the Raspberry Pi’s Crontab so that the script runs every minute. This ensures the JSON file containing the current UV index is continually updated.

To execute this, insert the following into /etc/crontab and make sure that you leave an empty line at the end:

*  *    * * *   username    /usr/bin/python3 /opt/read_uv_index.py > /dev/null 2>&1

Please use the default user (in our case username), which also owns the python script above.

The script should now run the next minute and create a JSON file at /var/www/sensors.json.

If errors occur you can simply call the script manually with the command from the crontab and evaluate any problems.

From outside the Raspberry Pi you can now easily fetch the data via any web browser: http://ip-of-raspberry-pi/sensors.json.

Integration into Weewx

The next step is to provide the UV index to Weewx. In my setup, this involves another Linux server running Weewx, where the main weather station is connected via USB.

Here, a Python script must be placed. This script is responsible for downloading the JSON file from the Raspberry Pi and storing the UV index in the Weewx database.

In the directory /usr/share/weewx/user place the file raspisensors.py as root. The script:

import json
import syslog
import weewx
import urllib.request
from weewx.wxengine import StdService

class RaspisensorsService(StdService):
    def __init__(self, engine, config_dict):
        super(RaspisensorsService, self).__init__(engine, config_dict)
        d = config_dict.get('RaspisensorsService', {})
        self.bind(weewx.NEW_ARCHIVE_RECORD, self.read_file)
    def read_file(self, event):
        try:
            with urllib.request.urlopen("http://ip-of-raspberry-pi/sensors.json") as url:
                value = url.read()
                sensors = json.loads(value)

                if "uv" in sensors:
                    event.record['UV'] = float(sensors['uv'])

        except Exception as e:
            syslog.syslog(syslog.LOG_ERR, "Cannot read sensors: " + str(e))

Adjust the IP address of the Raspberry Pi within the script. The script runs with each new archive record created by the weather station and supplements the new dataset with the UV index, if present in the JSON file.

You'll also need to modify the weewx.conf config file. Add the following to the [Engine] section which can be found at the end, under the [[Services]] subsection at data_services: user.raspisensors.RaspisensorsService.

In our case this section looks like this:

[Engine]
    # The following section specifies which services should be run and in what order.
    [[Services]]
        prep_services = weewx.engine.StdTimeSynch
        data_services = user.raspisensors.RaspisensorsService
        process_services = ...
        ...

Restart weewx to start capturing the UV index: systemctl restart weewx.

Conclusion

Integrating the SI1145 UV Sensor with a Raspberry Pi and Weewx is not only a compelling technical challenge but also a highly functional enhancement for any weather station. The setup improves data quality while offering a cost-effective solution for a rapidly growing area of application.

The scripts are designed to easily accommodate additional sensors, like a Lux sensor, for example. The SI1145 now provides us with minute-by-minute UV index updates, capturing spikes and peaks, and enabling us to assess direct sunlight exposure effectively.

You can see the UV index in action at our weather station, which consistently delivers satisfactory and realistic readings at an affordable cost using exactly this technique.

Do you have questions on this topic or need further assistance? We warmly invite you to share your thoughts and questions in the comments below.

This post was created with the support of artificial intelligence (GPT-4). Cover photo by Andrey Grinkevich on Unsplash.


Sven
About the author

Sven Reifschneider

Greetings! I am the founder and CEO of Neoground GmbH, an IT visionary and passionate photographer. On this blog, I share my expertise and enthusiasm for innovative IT solutions that propel companies forward in the digital age, intertwined with my passion for the visual, unveiling a universe where pixels and aesthetics coexist harmoniously.

Rooted in the picturesque Wetterau near Frankfurt with a perspective that reaches beyond the horizon, I invite you to join me in exploring the facets of digital transformation and the latest technologies. Are you ready to take the next step into the digital future? Follow the path of curiosity and let's shape innovations together.


2 comments

Reply to comment

You can use **Markdown** in your comment. Your email won't be published. Find out more about our data protection in the privacy policy.

18 Mar 2024, 08:01
W. Spitzner

Hallo,
super Anleitung, doch bei mir gibt es Probleme.
Import weewx gibt aus : no Module weewx
Import urllib.request: No module named request

können Sie mir sagen wo das Problem liegt?
Ich benutze Weewx 4.4.0
Danke

20 Mar 2024, 11:20
Sven Reifschneider

Hallo und vielen Dank für den Kommentar!

Wie es scheint, versuchen Sie das Skript raspisensors.py auszuführen. Diese Python-Datei ist jedoch als Plugin für weewx gedacht und läuft nur innerhalb von weewx, nachdem die Datei in der weewx.conf hinterlegt und weewx neu gestartet wurde. Jedes Mal, wenn ein neuer Eintrag mit den Werten der Wetterstation in die Datenbank geschrieben wird, wird das Skript ausgeführt, welches den Wert für das "UV" Feld hinterlegt.

Außerdem scheint es, dass in der Python-Umgebung urllib nicht verfügbar ist. Unter Debian müsste daher wahrscheinlich noch das Paket python3-urllib3 installiert werden.

You are replying to this comment.