Skip to content
dglcincPublic

About

Python library to vacuum data from Raspberry-Pi-based monitoring and control solutions (like HVAC)

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

pivac

Python package to pull data from Raspberry-Pi (RPi)-based inputs, and output JSON data. Supports the assembly and delivery of RPi-based monitoring and control solutions, like HVAC and home automation. Could also be used for other monitoring projects like home brewing and sous vide. These inputs include both RPi hardware inputs (like GPIO pins and GPIO-supported protocols like 1-Wire), as well as external sources useful for assembling a complete solution, such as HTTP-based data feeds and web sites (a.k.a. screen scraping.) This package is "input-only" - no provision is (currently) made to process updates.

Currently supported inputs include:

  • 1-WireTherm: 1-Wire-based Dallas temperature sensors (thermometers)
  • GPIO: Generic reads of RPi GPIO pins, pulled high or low using internal pullup resistors
  • RedLink: Screen scraping of Honeywell's website for RedLink thermostats, mytotalconnectcomfort.com (requires an account and installed RedLink equipment)
  • TED5000: Parsing of the live XML feed from the TED5000 home energy monitoring solution
  • ArduinoSensor: Pressure sensor data from an Arduino over HTTP (e.g. Fusch 100PSI, Fusch 200PSI) — a single implementation used by multiple config sections via the module: key
  • Emporia: Per-circuit power readings in Watts from an Emporia Vue Gen 2 energy monitor, via the PyEmVue cloud API
  • Sentry: Boiler operating data from an NTI Trinity Ti-200 boiler controller, read via computer vision from a Tapo C120 IP camera pointed at the Sentry 2100 display
  • FlirFX: Temperature and humidity from a FLIR camera

The package is extensible, so if the supported inputs don't meet your needs, you can add your own.

The package provides each input individually, so you can pick and choose which ones you want. Customization is possible via a configuration file.

Overview

This package has the modest aspiration of simplifying a certain class of monitoring (and eventually control) use cases for Raspberry Pi based projects, by providing pre-built recipes for acquiring data, and emitting the data in JSON format. It has no aspirations to replace more general RPi packages like GPIO and w1Therm (in fact it is built on top of them). Examples cases using JSON data:

  • Delivering a status website on your RPi, available on your local network
  • Modeling your RPi as an Amazon Web Services (AWS) IOT "thing", uploading your data as thing shadow "updates", and using AWS Lambdas to do stuff (store data, render static websites, etc.)
  • Inputting data to a Signal K server running on your RPi, and leveraging the Signal K ecosystem to process and display your data
  • Capturing a time series collection of the data in a database such as InfluxDB, and analyzing in a charting tool such as Grafana

Standard call interface, Diverse inputs, JSON output

The core concept of this package is simple - go get some data from somewhere related to your use case (a piece of HVAC equipment or a home automation product website) and return it as a JSON-formatted data collection. You can then reformat it and deliver it in a number of different ways. A standard call interface and configuration file format makes it easy to add new modules for new input types.

Why Python?

What can I say, I like Python. It's easy and fun to write some pretty fancy stuff. And supposedly it's where the Pi in Raspberry Pi comes from (Pi-thon, get it?).

Why RPi and not Arduino

I actually started this project on Arduino, using a packaged Arduino-based PLC from Digital Loggers. There are some advantages to using the Arduino in terms of analog inputs, PWM handling, performance, etc. and the DL PLC is pretty cool, with optical input isolators, open collector outputs, LCD screen, etc. I didn't really need these features, and the programming model was not fun (e.g. coding Lua on NodeMCU to do your Wi-Fi comms., serial-loading sketches constantly, etc.) Great if you need it, a pain if you don't. I abandoned this because the RPi is way simpler - I can SSH in headless, use a standard UNIX programming model and tools (which I know very well) code in fun languages like Python -- what's not to like? The code for the (archived) Arduino version is at HVAC-plc.

Ambitious contributors/forkers who like the RPi model and also have more need for Arduino features could follow the suggestion in this excellent Maker article to piggy-back an Arduino to your RPi's serial port, and add a module that reads the Arduino and supplies JSON as output.

Why polling of the GPIO pins, rather than events?

  • It's simpler
  • I want to store time series data without interpolating
  • It makes it easy to disconnect and reconnect to your aggregator and still see the complete picture of current state

I suppose you could do some work to optimize by only generating output when state changes, but that gets a lot more complicated and I don't really see the benefit for my use case, so I didn't do it.

Why not control too, not just monitoring?

Eventually. For the HVAC use case specifically, I have shied away from making the correct operation of the HVAC system dependent on the RPi:

  • It means you can't condition your space if you have a problem with the RPi
  • It's very hard to find HVAC technicians that can or will work on RPi-based controls

I'm pondering a design that allows some control or automation by the RPi, but fallback to full mechanical behavior if the RPi is down. When I have a good plan for that I'll reconsider having the RPi control the system.

Suggestions for Processing the Output

My original project delivered the JSON from my data sources into a python script that dynamically generated a simple web page on the RPi as an Apache-based Python CGI. I then internet-enabled my project (to avoid doing port forwarding to my RPi, and to learn AWS IOT...) by setting up the RPi as a thing on Amazon's IOT service, and using a Lambda to generate an S3-based static page whenever the thing shadow is updated by my RPi, once every few seconds. I know kinda sloppy but it works... You could optimize your updating to minimize charges for Lambda, etc., but at the volume I consume (low and cheap) it's not a priority. The github repository for the archived version is at HVAC-pi, including the python cgi in the /local subdirectory, and the AWS thing code in /hvac-pi-aws.

Signal K!

I serendipitously stumbled on a project targeted at the boating community, Signal K. Some features of that project are strikingly similar to mine (gather data from a variety of disparate sources and protocols and rationalize them into a JSON-based data stream). They have more and better developers, and a pretty highly evolved ecosystem. So I've converted to outputting my JSON to a Signal K server, and then using the tools in that ecosystem (such as the excellent iOS app WilhelmSK) to store and render my data. It's very cool. Check it out. Here are screenshots from the WilhelmSK iPhone and iPad app. The first screen is the real-time gauges to monitor all my vital statistics, and the second screen is a Grafana dashboard visualizing time series data for all the inputs, stored in an InfluxDB database.

Wilhelm SK dashboard Grafana dashboard Wilhelm SK dashboard Grafana dashboard

Architecture

The features of the architecture are:

  • Isolated Operation: Doesn't impact operation of the HVAC equipment (no direct control, to avoid HVAC downtime due to RPI and sensor issues)
  • Real Time and Historical Data: Displays status of all the equipment, stores and plots historical data automatically for all modules
  • Modular Sensor Inputs: Modular sensor feeds so it can be adapted to different systems with minimal coding
  • User-Configurable Display: Easily configurable user interface for personal preferences, mobile display works on-premise or remote

The data flow and component view of the architecture is as follows:

%%{init: {'flowchart': {'htmlLabels': true, 'curve': 'linear', 'subgraphTitleMargin': {'top': 30, 'bottom': 30}}, 'theme': 'base'}}%%
graph TD
    subgraph Devices["Physical Devices"]
        A1["Arduino<br/>10.0.0.114<br/>Hydronic PSI<br/>(Fusch 100 PSI)"]
        A2["Arduino<br/>10.0.0.219<br/>DHW PSI<br/>(Fusch 200 PSI)"]
        CAM["Tapo C120<br/>10.0.0.19<br/>RTSP 2560×1440"]
        OW["1-Wire Sensors<br/>GPIO (temp)"]
        GPIO["GPIO Pins<br/>Switches / Relays"]
        RL["Honeywell<br/>mytotalconnectcomfort.com<br/>(HTTPS)"]
        TED["TED5000<br/>10.0.0.49<br/>⚠ disabled"]
        FLIR["FLIR FX Camera<br/>⚠ disabled"]
    end

    subgraph Pivac["pivac"]
        NOTE["📝 Python | ~/github/pivac<br/>systemd services"]
        P1["pivac.ArduinoPSI<br/>pivac-arduino-psi.service"]
        P2["pivac.ArduinoThermPSI<br/>pivac-arduino-therm-psi.service"]
        P3["pivac.Sentry<br/>pivac-sentry.service<br/>(OpenCV perspective warp<br/>+ 7-seg digit recognition)"]
        P4["pivac.OneWireTherm<br/>pivac-1wire.service"]
        P5["pivac.GPIO<br/>pivac-gpio.service"]
        P6["pivac.RedLink<br/>pivac-redlink.service<br/>(screen scraping)"]
        PROV["pivac-provider.py<br/>(WebSocket delta push<br/>+ JWT auth)"]
    end

    subgraph RPi["Raspberry Pi 4 (4GB, Debian 13 aarch64)"]
        SK["SignalK v2.22.1<br/>:3000"]
        INFLUX["InfluxDB 2.7.10<br/>:8086"]
    end

    subgraph Visualization["Visualization"]
        GRAFANA["Grafana 11.2.2<br/>30s refresh<br/>:3001"]
        WILHELM["WilhelmSK<br/>(iPad / iPhone)"]
    end

    %% Device → Module connections
    A1 -->|TCP/serial| P1
    A2 -->|TCP/serial| P2
    CAM -->|RTSP stream| P3
    OW -->|GPIO| P4
    GPIO -->|GPIO| P5
    RL -->|HTTPS scrape| P6

    %% Modules → provider
    P1 & P2 & P3 & P4 & P5 & P6 --> PROV

    %% Provider → SignalK
    PROV -->|WebSocket delta<br/>hvac.boiler.sentry.*<br/>hvac.*.temp, psi, etc.| SK

    %% SignalK → storage
    SK -->|InfluxDB plugin| INFLUX

    %% Visualization
    INFLUX --> GRAFANA
    SK -->|WebSocket| WILHELM

    %% Styling
    classDef disabled fill:#eee,stroke:#999,color:#999
    classDef device fill:#dbeafe,stroke:#3b82f6
    classDef module fill:#dcfce7,stroke:#22c55e
    classDef infra fill:#fef9c3,stroke:#eab308
    classDef viz fill:#fce7f3,stroke:#ec4899

    class TED,FLIR disabled
    class A1,A2,CAM,OW,GPIO,RL device
    class P1,P2,P3,P4,P5,P6,PROV module
    class SK,INFLUX,RPi infra
    class GRAFANA,WILHELM viz
    
    style NOTE fill:#fffacd,stroke:#999,color:#333
Loading

Installing the Package

Clone the repository and install into a Python virtual environment:

git clone https://github.com/dglcinc/pivac
cd pivac
python3 -m venv ~/pivac-venv
source ~/pivac-venv/bin/activate
pip install -e .

If you need the RPi-specific hardware dependencies (GPIO, 1-wire), also run:

pip install RPi.GPIO w1thermsensor --break-system-packages

If you prefer not to install at all, the scripts and config will work properly using the local repository directory as the launch point — just run python scripts/pivac-provider.py directly from the clone.

Configuring the Package

Copy the sample config and edit it for your setup:

sudo mkdir -p /etc/pivac
sudo cp config/config.yml.sample /etc/pivac/config.yml
sudo nano /etc/pivac/config.yml

Each top-level key in the config file (starting with pivac.) is a provider instance name. By default, it is also the Python module that gets loaded. If you want multiple instances of the same module (e.g. two sensors of the same type with different IPs), add a module: key to point to the shared implementation:

pivac.Sensor1:
    module: pivac.ArduinoSensor
    ipaddr: 10.0.0.10
    ...

pivac.Sensor2:
    module: pivac.ArduinoSensor
    ipaddr: 10.0.0.11
    ...

Enable or disable modules by setting enabled: true/false. A complete, commented sample is provided as config/config.yml.sample.

Signal K integration requires a pivac_config: section in your config file containing the SignalK host, port, and credentials:

pivac_config:
    signalk:
        host: localhost
        port: 3000
        username: admin
        password: yourpassword

Note: pivac_config is a reserved key for framework settings — it is not treated as a provider module.

Using the Package

The Scripts

  • pivac-provider.py — the main script. It dynamically loads modules from your config file and either pushes data to Signal K via WebSocket (daemon mode) or outputs JSON to stdout (useful for testing). The --help option provides full documentation; arguments and choices are dynamically configured from the contents of the config file.
  • pivac-provider.sh — a wrapper for the .py script that restarts it if it fails (e.g. when running with --daemon). This script execs to provider.sh and inherits its behavior.
  • provider.sh — a generic wrapper script that restarts a script if it fails and catches the SIGHUP signal (kill -1) to restart the script. A standard kill (SIGINT) will stop both the wrapper and the script.
# Test a module - outputs JSON to stdout
python scripts/pivac-provider.py pivac.GPIO

# Pretty-print output for debugging
python scripts/pivac-provider.py pivac.OneWireTherm --format pretty

# Run continuously (daemon mode) - connects to Signal K via WebSocket
python scripts/pivac-provider.py pivac.RedLink --daemon

# Options
#   --loglevel DEBUG|INFO|WARNING|ERROR|CRITICAL
#   --format compact|pretty
#   --daemon [N]   (run forever, or N iterations)

Process Management

In production, each module runs as a dedicated systemd service. Service files are provided in scripts/systemd/. To install:

sudo cp scripts/systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable pivac-1wire pivac-redlink pivac-gpio pivac-arduino-psi pivac-arduino-therm-psi pivac-emporia pivac-sentry
sudo systemctl start pivac-1wire pivac-redlink pivac-gpio pivac-arduino-psi pivac-arduino-therm-psi pivac-emporia pivac-sentry

Each service connects independently to Signal K via WebSocket and restarts automatically on failure.

The Modules

The pivac package currently contains the following modules:

  • GPIO
  • OneWireTherm
  • TED5000
  • RedLink
  • ArduinoSensor
  • Emporia
  • Sentry
  • FlirFX (disabled by default)

Adding a New Provider Module

Pivac is designed so that new data providers can be added without modifying any framework code. The steps are:

1. Write the module

Create pivac/MyModule.py. The module must implement a status() function with this signature:

def status(config={}, output="default"):
    ...
  • When output="default": return a plain Python dict of sensor readings (used for command-line testing)
  • When output="signalk": return a Signal K delta dict built using the pivac helper functions:
from pivac import sk_init_deltas, sk_add_source, sk_add_value

deltas = sk_init_deltas()
source = sk_add_source(deltas)
sk_add_value(source, "environment.inside.myreading", value)
return deltas
  • On error: log a warning and return an empty delta — don't raise an exception, so the service keeps running
  • The module name must start with pivac. (e.g. pivac.MyModule)

You can use the propagate_defaults() helper to copy top-level config values down to each entry in inputs: — see GPIO.py or OneWireTherm.py for examples.

2. Add a config section

Add a section to /etc/pivac/config.yml using the module's full Python name as the key:

pivac.MyModule:
    description: My new sensor module
    enabled: true
    daemon_sleep: 5
    # ... module-specific config keys ...

If you need multiple independent instances of the same module (e.g. two sensors with different IPs), give each a unique key and use the module: key to point them both at the same implementation:

pivac.MySensor1:
    module: pivac.MyModule
    ipaddr: 10.0.0.10

pivac.MySensor2:
    module: pivac.MyModule
    ipaddr: 10.0.0.11

3. Test standalone

Verify the module works before connecting it to Signal K:

source ~/pivac-venv/bin/activate
python scripts/pivac-provider.py pivac.MyModule --format pretty

This outputs the raw JSON to stdout without needing Signal K running.

4. Create a systemd service file

Copy an existing service file and adapt it:

cp scripts/systemd/pivac-gpio.service scripts/systemd/pivac-mymodule.service

Edit the new file — change the Description and the module name in ExecStart:

[Unit]
Description=Pivac MyModule provider
After=network.target signalk.service
Requires=signalk.service

[Service]
Type=simple
User=pi
Environment=PIVAC_CFG=/etc/pivac/config.yml
ExecStart=/home/pi/pivac-venv/bin/python3 /home/pi/github/pivac/scripts/pivac-provider.py pivac.MyModule --loglevel ERROR --daemon
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

5. Install and start the service

sudo cp scripts/systemd/pivac-mymodule.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable pivac-mymodule
sudo systemctl start pivac-mymodule

6. Verify

journalctl -u pivac-mymodule -n 50 --no-pager

Initialization

  • pivac.set_config(configfile=""): This method must be called before using any of the modules. It locates and reads the config file, first in /etc/pivac/config.yml then in config/config.yml relative to the parent directory of the scripts, if you do not specify it. Returns the config dictionary. This currently can only be set once, because the data read from the config file is used to load modules by the pivac-provider.py script. If you attempt to set again it will return the current config dict.

  • pivac.get_config(): Returns the config dictionary read from the config file.

  • [module].status(config={}, output="default"): Each module implements (at least) this method, and can be used individually in your project. Each module supports one interface method, status(). Status takes two arguments - a dictionary containing the configuration for the module read from the configuration file, and a string indicating the output type ("default" for plain JSON dict, "signalk" for a Signal K delta structure).

Configuration of RPi 1-Wire (OneWireTherm)

In order to use the OneWireTherm module, you need to enable the OneWire RPi driver. You can do this in either the GUI system configuration (if you're using the desktop) or on the command line:

sudo raspi-config
[select "Interfacing Options"]
[select "1-Wire"]
[select "<Yes>"]
exit

GPIO

  • GPIO.status(config={}, output="default"): each pin included in the configuration file will be reported as "true" if on and "false" if off.
    • For pins configured with pull-down resistors, "on" or "true" means the pin is attached to voltage.
    • The module configuration file provides options for specifying pull-up or pull-down, as well as names to map pins to in the JSON output.
    • For pins configured with pull-up resistors, "on" or "true" means the pin is attached to ground.

Example output using the sample yml file with python pivac-provider.py pivac.GPIO --format pretty:

{
    "ZV": true,
    "DHW": false,
    "BLR": true,
    "BOS2": false,
    "BOS1": false,
    "DEHUM": false,
    "HPCALL": false
}

OneWireTherm

  • OneWireTherm.status(config={}, output="default"): the module will return the current temperature reported by every sensor on the 1-wire bus. Options in the config file allow you to assign names, and specify a temperature scale and rounding. Example output using the sample yml file with python pivac-provider.py pivac.OneWireTherm --format pretty:
{
    "UBT": 45,
    "OUT": 47,
    "LBT": 73,
    "IN": 40
}

TED5000

  • TED5000.status(): Returns JSON for the current PowerNow values of MTUs 1-4 from the URL <my_ted_gateway_ip_or_hostname>/LiveData.xml. All TED data is available; The configuration file allows you to specify any path from the LiveData.xml url using an XPath-style path, and the "friendly" name it should be mapped to. Example output using the sample yml file with python pivac-provider.py pivac.TED5000 --format pretty:
{
    "MainPanel": 1161,
    "SubPanel": 398,
    "HVAC": 30
}

ArduinoSensor

  • ArduinoSensor.status(config={}, output="default"): Polls an Arduino over HTTP and returns the pressure reading in PSI. The Arduino serves a simple JSON response containing a psi key. Designed for use with ratiometric pressure sensors (e.g. Fusch 100PSI, Fusch 200PSI, 0.5–4.5V output). Multiple config sections can share this implementation using the module: key — for example, pivac.ArduinoPSI and pivac.ArduinoThermPSI both point to pivac.ArduinoSensor with different ipaddr values. Example output:
{
    "psi": 18.4
}

RedLink

  • Redlink.status(verbose=False): Returns the current temperature, humidity, status, and name (as set on the thermostat) for every thermostat associated with the location defined on mytotalconnectcomfort.com. Temperature will be defined as on the thermostat. Humidity will be a whole number. Status will be one of "cool", "heat", "fanOn", or "off". Outdoor humidity is returned as "outhum". The key for each object is the thermostat identifier used in the RedLink registry. verbose = True causes the scraper to navigate to the sub-page for each thermostat, collect all available detailed data like setpoints, and return it as a nested JSON object for each thermostat; this option is slower, so don't use it unless you need it. Example output using the sample yml file with python pivac-provider.py pivac.RedLink --format pretty:
{
    "2834229": {
      "status": "fan",
      "hum": 51,
      "name": "KIDS ROOM",
      "temp": 74
    },
    "outhum": 100.0,
    "2528432": {
      "status": "off",
      "hum": 47,
      "name": "KITCHEN",
      "temp": 75
    }
}

Emporia

  • Emporia.status(config={}, output="default"): Polls an Emporia Vue Gen 2 energy monitor via the PyEmVue cloud API and returns per-circuit power readings in Watts. Each circuit in the Emporia app becomes a separate value in the output, keyed by <panel_name>.<circuit_name>. Circuit names are taken from the Emporia app labels and sanitized to lowercase with underscores.

The module authenticates with the Emporia cloud using your account credentials and caches the auth token locally. On first run it discovers all devices on the account; the device list is cached in memory for the lifetime of the process.

Config keys:

Key Required Default Description
username Yes — Emporia account email
password Yes — Emporia account password
token_file No /etc/pivac/emporia-tokens.json Path for cached auth token
sk_path No electrical.emporia Signal K base path for all readings
panels No all panels, auto-named Dict mapping device GID strings to friendly panel names
daemon_sleep No 0 Seconds between polls — recommended 60 to match the 1-minute API window

Run scripts/emporia-discover.py to find your device GIDs and generate a suggested config block.

Example output with two panels configured:

{
  "house.main": 3020.4,
  "house.utility_sub_panel": 331.4,
  "house.wall_oven": 0.0,
  "house.bosch_bova": 6.5,
  "apartment.main": 268.7,
  "apartment.furnace": 10.4,
  "apartment.clothes_washer": 0.0
}

Sentry

  • Sentry.status(config={}, output="default"): Reads the Sentry 2100 controller display on an NTI Trinity Ti-200 boiler using a Tapo C120 IP camera over RTSP. On each poll cycle the module captures frames for up to cycle_timeout seconds, reads each display mode as the indicator lights cycle, and emits the results as Signal K deltas.

The display cycles through three modes: water temp, outdoor air temp, and gas input value. Indicator lights below the display identify the active mode. The dhw_temp indicator is a boiler status light (DHW priority active), not a display mode — it is emitted as a boolean regardless of what the display is showing.

Digit recognition uses 7-segment pattern matching (not OCR). LED and indicator state is detected via brightness ratio in each region of interest. Coordinates for all regions must be calibrated once using scripts/sentry-calibrate.py.

Config keys:

Key Required Default Description
rtsp_url Yes — RTSP stream URL including camera credentials
display_warp Yes — Perspective-warp corners and output size (from calibration)
digit_positions Yes — List of 3 digit bounding boxes relative to warped display
leds Yes — Centre-pixel coords for burner/circ/circ_aux/thermostat_demand
indicators Yes — Centre-pixel coords for water_temp/air/gas_input/dhw_temp
cycle_timeout No 30 Seconds to wait for all three display modes
mode_stable_frames No 3 Consecutive stable frames before accepting a reading
led_ratio No 1.15 Spot/background brightness ratio for LED detection
digit_threshold_factor No 0.50 Threshold = mean + factor×(max−mean) per digit
daemon_sleep No 0 Seconds between polls — recommended 60

Signal K paths emitted:

Path Type Description
hvac.boiler.sentry.waterTemp number °F as shown on display — boiler supply water temperature
hvac.boiler.sentry.outdoorTemp number °F as shown on display — outdoor air temperature
hvac.boiler.sentry.gasInputValue number Raw 40–240 scale (see boiler manual for BTU/hr conversion)
hvac.boiler.sentry.dhwPriority boolean True when boiler is in DHW priority mode
hvac.boiler.sentry.errorCode string Error/status code e.g. ER3 (only when displayed)
hvac.boiler.sentry.burnerOn boolean Burner firing
hvac.boiler.sentry.circOn boolean Circulator pump on
hvac.boiler.sentry.circAuxOn boolean Auxiliary circulator on
hvac.boiler.sentry.thermostatDemand boolean Thermostat demand active

Run scripts/sentry-calibrate.py to capture a reference frame and identify pixel coordinates for the display ROI, digit positions, LEDs, and indicators.

FlirFX

The FlirFX module lets you collect temperature and humidity data from a FLIR camera. The config file format allows you to specify multiple cameras (inputs) and your login credentials. Example output using the sample yml file with python pivac-provider.py pivac.FlirFX --format pretty:

{
  "192.168.2.79": {
    "temperature": 67.98,
    "humidity": 49
  }
}

Example Project

The goal of my RPi project was to create a simple app (single responsive HTML dashboard) that allows me to monitor my HVAC system. It pulls data from four sources:

  • Several Dallas DS18B20 waterproof one-wire temperature sensors connected to the one-wire bus on the RPi, to monitor key control points in my hydronic heating and cooling system.
  • Several mechanical (4PDT) relays connected to GPIO pins, to monitor the operational status of the HVAC equipment (what is off or on, heating or cooling, making hot water, etc.)
  • A TED5000 power panel monitor, to show energy consumption related to the HVAC operation (mainly two five-ton chillers, that each consume about 8Kwh, and whose operation understandably I would like to optimize)
  • Several Honeywell RedLink IAQ 2.0 thermostats, so I can keep track of the conditions in each zone in my HVAC system by seeing current temperature and humidity, equipment status (heating, cooling, etc.), and eventually setpoints.

Here's a photo of the finished install. The pipes on the left are the hydronic supply and return for one of the zones, with the zone valve bottom middle. The right side is a vertically mounted DIN rail. At the top of the rail are feed-throughs for all of the zones and power (kind of like a customizable punch-down block), using Phoenix Contact DIN rail mounted modular terminal blocks. In the middle are the Magnecraft relays (with timer and alternating relays at bottom to trim chiller runs), and at the bottom is the RPi in a commercially available DIN box (Phoenix Contact). On the left of the RPI is the header where the relay and 1-wire inputs go in. The RPi is powered by a 24VAC to 5VDC transformer (just visible under the header) that allows me to run the RPi off the standard HVAC power supply.

HVAC-pi installation

Example 1: 1-Wire DS18B20 Temperature Sensors

These are pretty commonly used sensors, because they are cheap and easy to use. There are plenty of tutorials on how to wire these (e.g. this one), so I'm not going to cover here. The best Python library I found for these is w1thermsensor, so that's the one I use. Note that without some hacking the data line for the 1-wire bus must be GPIO pin 4. I prefer to power the sensors rather than use "parasitic" mode (which has some limitations), but that's just me.

Other RPi protocols

The RPi supports several protocols out of the box, such as I2C and SPI. 1-Wire suited my needs, so that's the only one pivac currently supports. The module framework can be easily extended for these by following the 1-Wire Therm example.

Example 2: GPIO pin inputs

The GPIO pins on the RPi are a good way to collect data. A good way to know which pins you can/should use for inputs is to use a pin mapping chart, like this one. Make sure to use the one appropriate for your Pi.

When reading from a digital pin, in order to get a consistent reading you need to pull it up to a known voltage (e.g. VCC, 3.3V on the board), or pull it down to ground. If you pull up, then you can signal a change by grounding the pin to board ground. If you pull it to ground, then you need to send it a voltage. The ground and the voltage need to be from the same source, so best to reference board ground and board 3.3v for these pins.

The RPi provides both pull-up and pull-down resistors for the pins (if you don't know what these are, see here). I use pull-ups, so my circuits are normally connected only to board ground unless my relays close. You may have your own purposes. My library lets you configure a pin as either pull-up or pull-down. Technically you can set them all individually but it's easier to remember if you do one or the other.

Example 3: TED 5000 (The Energy Detective)

The TED5000 is a power panel monitor that allows you to monitor energy consumption in your home by placing CTs (current transformers) on your main feed or subcircuits in your home's power panel. A small gateway attached to the CTs runs a web server that publishes a website accessible on your local network, that allows you to view a dashboard for current and historical data, charting, and also publishes XML-based data via HTTP GET.

The gateway can be a little flaky and also it's a closed ecosystem. You can't easily integrate it with anything else, except by using a couple of APIs it provides. One is a built in capability to publish historical data in one-minute intervals. Another is the "real time" HTTP URL that allows you to collect the current values of all the system parameters in a structured HTML document.

I opted for using the "real time" interface, since that would allow me to control how often and where the data are reported. So my python script calls the real time interface URL, parses the response using the Python lxml parser, and prints the "PowerNow" values for the four possible MTUs (I have 3) to stdout as a JSON-formatted object.

Others may be interested in more or different values from the real-time XML document. The script can easily be extended as desired. Note: TED5000 has been discontinued and is difficult to source; the module remains in pivac for existing users.

Example 4: Honeywell mytotalconnectcomfort.com

This is a website targeted specifically at owners of Honeywell WiFi thermostats, or RedLink thermostats connected to a RedLink gateway. Honeywell does not make the data APIs for this site publicly available. There are APIs called by the site, but more header spoofing would be required to get the server to generate a valid request. The same information can be retrieved by screen-scraping the site, so I didn't bother to decode the API headers.

This code makes a number of assumptions, so if your personal use is different, you'll have to tweak the scraping code, which uses Mechanize.

Some points to keep in mind:

  • You must have a login account on the site.
  • You must have at least one supported thermostat configuration.
  • Not all thermostats support all options; for example, older thermostats do not support query of their state (are they cooling, heating, etc.)
  • The code maintains an active login session to improve performance and minimize booting due to "too many logins"
  • The code supports only one location. If you have multiple locations you will need to modify the scraping code.
  • The thermostats will report additional properties such as setpoint. These are output in the JSON as a nested JSON object.
  • The website supports the ability to change thermostat settings (setpoint, mode, etc.) This library does not currently implement that feature. You will need to change the scraping code.
  • Mechanize has its History object set to NoHistory in the code. Keep in mind if you change this, the history will cause the process to grow and eventually crash your RPi. If you do this, make sure you periodically call Browser's clear_history() method.
  • Mechanize and the libraries it uses went through some changes from Python 2 to Python 3. Pivac has been fully converted. Make sure you get the right stuff. Also, the regex library it uses changed behavior with Python 3.9. My code requires regex=2022.3.2 to avoid the crazy escape sequence updates.

There is a site that provides an undocumented way to access Honeywell's REST API interface, here.

Example 5: Arduino Pressure Sensors

The ArduinoSensor module polls a small Arduino web server over HTTP. The Arduino reads a ratiometric pressure sensor (0.5–4.5V output), converts the voltage to PSI, and returns it as a simple JSON response. This approach is useful when you need analog inputs that the RPi doesn't natively support well.

A single module implementation can serve multiple sensor instances by using the module: key in each config section. For example, the hydronic loop and DHW pressure sensors each have their own config section (pivac.ArduinoPSI and pivac.ArduinoThermPSI) with different IP addresses, both pointing to module: pivac.ArduinoSensor.

The Arduinos are configured with static IP addresses in your config file. If an Arduino is unreachable (timeout), a warning is logged and the module returns an empty delta — the service continues running and retries on the next poll interval.

About

Python library to vacuum data from Raspberry-Pi-based monitoring and control solutions (like HVAC)

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages