Introduction

equser is the open-source Python interface to EQ Wave power quality data and sensors. It provides tools for loading, analyzing, and visualizing high-resolution electrical measurements.

What equser does

equser can work with EQ Wave data in several ways:

  • Load and analyze Parquet files stored by an EQ gateway, including continuous waveform (CPOW) and power monitoring (PMon) data
  • Connect directly to an EQ Wave sensor to collect PMon data in real time
  • Capture waveform snapshots on demand via an EQ gateway
  • Query gateway APIs for historical data, events, and live streams
  • Visualize power quality data with publication-ready matplotlib plots

Data formats

FormatDescriptionSample RateFile Pattern
CPOWContinuous Point-on-Wave waveforms (7 channels: VA, VB, VC, IA, IB, IC, IN)32 kHzYYYYMMDD_HHMMSS.parquet
PMon10/12-cycle RMS power quality summaries (voltage, current, power, frequency)~5 HzYYYYMMDD_HHMM.parquet

Quick install

pip install equser              # Data loading and analysis
pip install equser[analysis]    # Add plotting and API client
pip install equser[jupyter]     # Full notebook environment

Get started

See the Installation guide, then follow the Quick Start tutorial.

The broader platform

equser is one component of the Energy Quotient platform for continuous waveform intelligence. For the full capabilities, including real-time CPOW visualization through EQ Sight and power system intelligence through EQ Syntropy, see the platform overview.



© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

Installation

Requirements

  • Python 3.10 or later
  • Linux (for hardware integration features)

Install

equser uses a tiered dependency model. Install only what you need.

Base (data loading and analysis)

pip install equser

Includes: numpy, pyarrow, pyyaml, argcomplete, colorlog. Enough to load CPOW/PMon files, run waveform analysis, and use the CLI with tab completion and colored output.

Plotting and API client

pip install equser[analysis]

Adds: matplotlib, requests, websocket-client. Enables equser.plotting, equser.api, and WebSocket streaming.

Jupyter notebook environment

pip install equser[jupyter]

Superset of [analysis]. Adds: jupyterlab, duckdb, ipywidgets, ipykernel, nbconvert. Full interactive notebook environment for data exploration.

Live sensor acquisition

pip install equser[daq]

Adds: avro, fastavro. Required for equser pmon acquire to connect to EQ Wave sensor hardware.

Full installation

pip install equser[full]

All optional extras.

Dependency tiers

ExtraDescriptionKey Packages
(base)Data loading, analysis, CLInumpy, pyarrow, pyyaml, argcomplete, colorlog
[daq]Live sensor acquisitionavro, fastavro
[analysis]Plotting + API clientmatplotlib, requests, websocket-client
[jupyter]Full notebook environment[analysis] + jupyterlab, duckdb, ipywidgets
[full]All of the above-


© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

Quick Start

Load CPOW waveform data

CPOW (Continuous Point-on-Wave) files contain high-resolution waveform captures at 32 kHz across 7 channels.

from equser.data import load_cpow_scaled

result = load_cpow_scaled('20250623_075056.parquet')

# Scaled voltage and current arrays (numpy float64)
print(f"Voltage A peak: {result['VA'].max():.1f} V")
print(f"Current A peak: {result['IA'].max():.3f} A")
print(f"Start time: {result['start_time']}")
print(f"Sample rate: {result['sample_rate']} Hz")

The function handles both data formats automatically:

  • int32 (current format): raw ADC counts scaled by vscale/iscale from Parquet metadata.
  • float (legacy format): values already in volts/amps.

Load PMon summary data

PMon (Power Monitor) files contain 10/12-cycle RMS measurements (~200 ms intervals).

from equser.data import load_pmon

table = load_pmon('20250623_0750.parquet')
print(table.column_names)
# ['time_us', 'FREQ', 'AVRMS', 'BVRMS', 'CVRMS', 'AIRMS', ...]

Analyze zero crossings

Zero-crossing detection is useful for frequency estimation, cycle extraction, and identifying waveform distortions.

import numpy as np
from equser.data import load_cpow_scaled, SAMPLE_RATE_HZ
from equser.analysis import find_zero_crossings

result = load_cpow_scaled('cpow_data.parquet')
time = np.arange(len(result['VA'])) / SAMPLE_RATE_HZ

crossings, indices = find_zero_crossings(result['VA'], time)
print(f"Found {len(crossings)} zero crossings")

# Estimate frequency from crossing intervals
periods = np.diff(crossings)
freq = 1.0 / np.mean(periods)
print(f"Estimated frequency: {freq:.2f} Hz")

Extract complete cycles

from equser.analysis import extract_complete_cycles

cycles = extract_complete_cycles(result['VA'], time, [0.0, 0.05], num_cycles=1)

for cycle_time, cycle_signal, start, end in cycles:
    duration_ms = (end - start) * 1000
    print(f"Cycle at {start:.6f}s, duration: {duration_ms:.2f} ms")

Plot data (requires [analysis])

from equser.plotting import PowerMonitorPlotter, WaveformPlotter

# PMon data
plotter = PowerMonitorPlotter()
plotter.plot_file('pmon_data.parquet')

# CPOW waveforms
wf_plotter = WaveformPlotter()
wf_plotter.plot_file('cpow_data.parquet')

Query a gateway (requires [analysis])

from equser.api import SynapseClient

client = SynapseClient('http://gateway:8080')
devices = client.list_devices()
table = client.get_pmon_data(devices[0]['id'])

# SQL queries
rows = client.query_sql("SELECT * FROM pmon ORDER BY time_us DESC", limit=10)

Stream real-time data (requires [analysis])

from equser.api import connect_cpow_stream

for batch_or_gap in connect_cpow_stream('http://gateway:8080'):
    if isinstance(batch_or_gap, dict):
        print(f"Gap: {batch_or_gap['skipped_samples']} samples")
    else:
        print(f"Batch: {batch_or_gap.num_rows} rows")

CLI tools

# Start power monitoring (requires [daq] + EQ Wave hardware)
equser pmon acquire -c config.yaml

# Convert Avro files to Parquet (requires [daq])
equser pmon convert data/*.avro --remove

# Plot data file (requires [analysis])
equser plot data.parquet

# Capture a waveform snapshot from a gateway (requires [analysis])
equser snapshot --host 192.168.1.10 --duration 10


© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

Configuration

equser uses YAML configuration files for sensor and acquisition settings. The configuration system follows XDG conventions and supports multiple lookup locations.

Configuration lookup order

equser searches for configuration files in this order:

  1. EQUSER_CONFIG environment variable (if set)
  2. ./equser.yaml (current directory)
  3. ~/.config/equser/config.yaml (XDG config)
  4. /etc/equser/config.yaml (system-wide)

The first file found is used.

Example configuration

sensor:
  address: "192.168.10.10"
  port: 1535

pmon:
  connection:
    retry_delay: 3
  parquet:
    interval: 86400
    compression:
      method: ZSTD
      level: 4

Configuration sections

sensor

Connection settings for EQ Wave sensor hardware.

KeyTypeDefaultDescription
addressstring"192.168.10.10"Sensor IP address
portint1535Sensor TCP port

pmon

Power monitor acquisition settings.

KeyTypeDefaultDescription
connection.portint1535Sensor TCP port
connection.retry_delayint3Seconds between reconnect attempts
connection.num_retriesint-1Retry count (-1 for infinite)
connection.connect_timeoutint5Connection timeout in seconds
connection.data_timeoutfloat0.6Data reception timeout in seconds
parquet.intervalint86400Seconds per Parquet file (86400 = 24 hours)
parquet.flush_everyint-1Rows between buffer flushes (-1 to disable)
parquet.compression.methodstring"ZSTD"Compression method (ZSTD, SNAPPY, GZIP, or NONE)
parquet.compression.levelint4Compression level

Data paths

Default paths follow XDG conventions:

PathContents
~/.local/share/equser/pmon/PMon Parquet files
~/.config/equser/config.yamlUser configuration

On EQ Synapse gateways, data is stored at:

PathContents
/var/lib/eq-synapse/data/pmon/PMon files (YYYYMMDD_HHMM.parquet)
/var/lib/eq-synapse/data/cpow/CPOW files (YYYYMMDD_HHMMSS.parquet)

Programmatic access

from equser.core.config import load_config, get_sensor_address

config = load_config()
address = get_sensor_address(config)
print(f"Sensor at {address}")


© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

JupyterLab

equser ships with bundled Jupyter notebooks and interactive widgets for exploring EQ Wave power quality data. Install the [jupyter] extra to get the full environment:

pip install equser[jupyter]

This installs JupyterLab, DuckDB, ipywidgets, and all analysis dependencies.

Bundled notebooks

equser includes six reference notebooks organized into two categories.

Tutorials

Step-by-step introductions to each data access method:

NotebookDescription
01-parquet-filesLoad and plot PMon and CPOW data directly from Parquet files
02-backend-apiQuery historical data through the EQ Synapse REST API (DuckDB-backed)
03-live-streamingReal-time waveform and spectral streaming via WebSocket

Analysis templates

Ready-to-use analysis workflows:

NotebookDescription
harmonic-analysisFFT-based harmonic spectrum, THD calculation, and spectrograms
power-trendsLong-term voltage, frequency, and power factor trends across multiple files
delta-analysisEquipment-specific configuration (wye vs. delta), interactive file selection

Listing and copying notebooks

Notebooks are bundled inside the installed package. Use the Python API or CLI to copy them to a working directory where you can edit and run them.

CLI

# List all bundled notebooks
equser notebooks list

# Copy all notebooks to a working directory
equser notebooks copy --dest ./my-notebooks

# Copy only tutorials
equser notebooks copy --dest ./my-notebooks --category tutorials

Python API

from equser.notebooks import list_notebooks, copy_notebooks

# See what is available
for nb in list_notebooks():
    print(nb)

# Copy to a working directory
copied = copy_notebooks('./my-notebooks')
print(f"Copied {len(copied)} notebooks")

# Copy only analysis templates
copy_notebooks('./my-notebooks', category='analysis')

File selector widget

The equser.widgets module provides an interactive file selector for browsing Parquet files inside notebooks. It displays a searchable list with file metadata (size, timestamps parsed from filenames).

from equser.widgets import create_file_selector

# Create a selector for a directory of CPOW files
selector = create_file_selector('/path/to/cpow/')
display(selector)

# After selecting a file interactively:
selected = selector.get_selected()

The widget includes:

  • Text search box for filtering by filename
  • Scrollable file list
  • Detail pane showing file size, path, and start time (parsed from the YYYYMMDD_HHMM filename convention)

Launching JupyterLab

# Copy notebooks and start JupyterLab
equser notebooks copy --dest ./eq-notebooks
cd eq-notebooks
jupyter lab

From there, open any notebook and follow the instructions in its markdown cells. The tutorials are numbered in recommended order.



© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

Data Loading

Module: equser.data Dependencies: base (numpy, pyarrow)

Load CPOW and PMon Parquet files with automatic scaling and timestamp parsing.

CPOW data

load_cpow_scaled(file_path) -> dict

Load a CPOW Parquet file and return scaled voltage/current arrays.

Handles both formats automatically:

  • int32 (current): raw ADC counts scaled by vscale/iscale from Parquet user metadata.
  • float (legacy): values already in V/A; scaling factors are 1.0.

Returns a dict with:

KeyTypeDescription
tablepa.TableRaw PyArrow Table
VA, VB, VCnp.ndarrayScaled voltage arrays (float64)
IA, IB, IC, INnp.ndarrayScaled current arrays (float64)
vscalefloatVoltage scaling factor applied
iscalefloatCurrent scaling factor applied
start_timedatetime or NoneParsed from metadata
sample_rateintAlways 32000
from equser.data import load_cpow_scaled

result = load_cpow_scaled('20250623_075056.parquet')
print(f"Peak voltage A: {result['VA'].max():.1f} V")
print(f"Samples: {len(result['VA'])}")

load_cpow(file_path) -> pa.Table

Load a CPOW Parquet file as a raw PyArrow Table with no scaling applied. Use this when you need the raw integer ADC values or want to handle scaling yourself.

Constants

ConstantValueDescription
SAMPLE_RATE_HZ32000CPOW sample rate
CHANNELS['VA', 'VB', 'VC', 'IA', 'IB', 'IC', 'IN']Channel names
NEUTRAL_CT_RATIO30Neutral CT sensitivity ratio vs. phase CTs

PMon data

load_pmon(file_path) -> pa.Table

Load a PMon Parquet file as a PyArrow Table.

PMon files contain 10/12-cycle RMS measurements (10 cycles for 50 Hz grids, 12 cycles for 60 Hz). Common columns include:

ColumnDescription
time_usTimestamp in microseconds
FREQLine frequency (Hz)
AVRMS, BVRMS, CVRMSPhase RMS voltage
AIRMS, BIRMS, CIRMSPhase RMS current
NIRMSNeutral RMS current
AWATT, BWATT, CWATTPhase active power
from equser.data import load_pmon

table = load_pmon('20250623_0750.parquet')
freq = table.column('FREQ').to_numpy()
print(f"Mean frequency: {freq.mean():.3f} Hz")

Timestamp parsing

parse_start_time(s) -> datetime

Parse an ISO 8601 timestamp string from CPOW metadata. Handles nanosecond precision by truncating to microseconds (Python datetime limit).

from equser.data import parse_start_time

dt = parse_start_time("2025-06-23T07:50:56.123456789Z")
print(dt)  # 2025-06-23 07:50:56.123456

Note: The returned datetime is naive (no timezone info). The Z suffix is stripped and UTC is assumed.

parse_filename_timestamp(filename) -> datetime | None

Extract a timestamp from an EQ data filename pattern.

Supports:

  • YYYYMMDD_HHMM (PMon files)
  • YYYYMMDD_HHMMSS (CPOW files)
from equser.data import parse_filename_timestamp

dt = parse_filename_timestamp("20250623_075056.parquet")
print(dt)  # 2025-06-23 07:50:56


© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

Waveform Analysis

Module: equser.analysis Dependencies: base (numpy only)

Functions for zero-crossing detection, cycle extraction, and waveform analysis.

find_zero_crossings(signal, time_array)

Find negative-to-positive zero crossings in a signal using linear interpolation between adjacent samples.

Args:

  • signal (ndarray): Voltage or current waveform array.
  • time_array (ndarray): Corresponding time array (same length).

Returns: (crossing_times, crossing_indices)

  • crossing_times: interpolated times of each zero crossing
  • crossing_indices: index of the sample just before each crossing
import numpy as np
from equser.data import load_cpow_scaled, SAMPLE_RATE_HZ
from equser.analysis import find_zero_crossings

result = load_cpow_scaled('cpow_data.parquet')
time = np.arange(len(result['VA'])) / SAMPLE_RATE_HZ

crossings, indices = find_zero_crossings(result['VA'], time)
print(f"Found {len(crossings)} zero crossings")

# Frequency estimation
periods = np.diff(crossings)
print(f"Mean frequency: {1.0 / np.mean(periods):.2f} Hz")
print(f"Frequency std:  {np.std(1.0 / periods):.4f} Hz")

extract_complete_cycles(signal, time_array, start_times, num_cycles=1)

Extract complete AC cycles (zero-crossing to zero-crossing) starting from specified times.

Args:

  • signal (ndarray): Waveform array.
  • time_array (ndarray): Time array.
  • start_times (list of float): Times to start extracting from.
  • num_cycles (int): Number of cycles per start time (default 1).

Returns: List of tuples (cycle_time, cycle_signal, start, end) where:

  • cycle_time: time array normalized to start at 0
  • cycle_signal: signal values for the window
  • start: actual start time (first zero crossing)
  • end: actual end time (last zero crossing)

Returns an empty list if there are not enough zero crossings for the requested number of cycles.

from equser.analysis import extract_complete_cycles

# Extract single cycles at 3 different times
cycles = extract_complete_cycles(result['VA'], time, [0.0, 0.05, 0.1])

for cycle_time, cycle_signal, start, end in cycles:
    duration_ms = (end - start) * 1000
    peak = np.max(np.abs(cycle_signal))
    print(f"Cycle at {start:.4f}s: {duration_ms:.2f} ms, peak {peak:.1f} V")

plot_extracted_cycles(signal_dict, time_array, start_times, ...)

Plot extracted cycles for one or more phases side-by-side. Requires matplotlib ([analysis] extra).

Args:

  • signal_dict: Dict mapping phase names to arrays (e.g. {'VA': va, 'VB': vb}), or a single array (treated as {'VA': array}).
  • time_array: Time array.
  • start_times: Times to extract cycles from.
  • num_cycles (int): Cycles per window (default 1).
  • epoch_start_time (datetime, optional): Absolute start time for labels.

Returns: (fig, axes, window_times)

from equser.analysis.waveform import plot_extracted_cycles

fig, axes, windows = plot_extracted_cycles(
    {'VA': result['VA'], 'VB': result['VB'], 'VC': result['VC']},
    time,
    start_times=[0.0, 0.1, 0.2],
    num_cycles=2,
    epoch_start_time=result['start_time'],
)
fig.savefig('cycles.png', dpi=150, bbox_inches='tight')


© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

Plotting

Module: equser.plotting Dependencies: [analysis] extra (matplotlib)

Static matplotlib plots for PMon and CPOW data. Install with:

pip install equser[analysis]

PowerMonitorPlotter

Plot power monitor (PMon) time-series data. Generates separate SVG files for frequency, voltage, current, and power.

from equser.plotting import PowerMonitorPlotter

plotter = PowerMonitorPlotter()
plotter.plot_file('pmon_data.parquet')
# Creates: pmon_data_frequency.svg, pmon_data_voltage.svg, etc.

Constructor

PowerMonitorPlotter(
    visible_channels=DEFAULT_VISIBLE_CHANNELS,  # set of channel names to plot
    output_dir=None,                             # output directory (default: same as input)
)

Methods

MethodReturnsDescription
plot_file(path)boolLoad and plot a PMon Parquet file. Returns True on success.

Output

Generates multi-panel SVG figures:

  • Frequency vs. time
  • Phase RMS voltages vs. time (with fundamental overlay)
  • Phase RMS currents vs. time (with fundamental and neutral)
  • Phase active power and reactive power vs. time

WaveformPlotter

Plot CPOW waveform captures. Generates a two-panel voltage/current plot with proper ADC scaling from Parquet metadata.

from equser.plotting import WaveformPlotter

plotter = WaveformPlotter()
plotter.plot_file('cpow_data.parquet')
# Creates: cpow_data_waveform.svg

Constructor

WaveformPlotter(
    output_dir=None,   # output directory (default: same as input)
)

Methods

MethodReturnsDescription
plot_file(path, start_sec=0.0, duration_ms=100.0)boolPlot a CPOW Parquet file. Returns True on success.

Output

Generates a two-panel SVG figure:

  • Three-phase voltage waveforms (VA, VB, VC)
  • Three-phase current waveforms (IA, IB, IC) with neutral current adjusted by the CT sensitivity ratio (NEUTRAL_CT_RATIO = 30)

Color scheme

Both plotters use the standard power systems convention:

PhaseColor
ABlack
BRed
CBlue
NeutralGray
Fundamental valuesLighter shades of the above

Saving in other formats

The plotters write SVG files by default. For other formats, use matplotlib directly after loading data:

from equser.data import load_pmon
import matplotlib.pyplot as plt

table = load_pmon('pmon_data.parquet')
fig, ax = plt.subplots()
ax.plot(table['FREQ'].to_numpy())
fig.savefig('frequency.png', dpi=150, bbox_inches='tight')
fig.savefig('frequency.pdf')

Or from the CLI:

equser plot data.parquet -o output.png


© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

API Client

Module: equser.api Dependencies: [analysis] extra (requests, websocket-client)

REST and WebSocket clients for EQ Synapse gateways. Install with:

pip install equser[analysis]

SynapseClient

REST client for the EQ Synapse API (Actix-web server at port 8080).

from equser.api import SynapseClient

client = SynapseClient('http://gateway:8080')

list_devices() -> list[dict]

List all registered devices.

devices = client.list_devices()
for d in devices:
    print(f"{d['id']}: {d.get('name', 'unnamed')}")

get_pmon_data(device_id, **params) -> pa.Table

Fetch PMon data as an Arrow Table.

table = client.get_pmon_data('wave-001')
print(f"Rows: {table.num_rows}, Columns: {table.column_names}")

Optional query parameters: start_time, end_time, metrics, limit.

get_cpow_data(device_id, **params) -> pa.Table

Fetch CPOW waveform data as an Arrow Table.

table = client.get_cpow_data('wave-001')

Optional query parameters: start_time, end_time, limit.

get_events(device_id=None, limit=100) -> list[dict]

Fetch recent power quality events.

events = client.get_events(limit=10)
for e in events:
    print(f"{e['timestamp']}: {e['type']}")

query_sql(query, device_id=None, limit=None) -> list[dict]

Execute a SELECT query via the SQL endpoint. Only SELECT statements are allowed by the server.

rows = client.query_sql(
    "SELECT time_us, FREQ, AVRMS FROM pmon ORDER BY time_us DESC",
    limit=10,
)

WebSocket streaming

connect_cpow_stream(gateway_url) -> Generator

Connect to the CPOW waveform WebSocket and yield data in real time.

Each binary message is an Arrow IPC RecordBatch (~512 rows, 16 ms at 32 kHz). Text messages are JSON gap markers indicating dropped samples.

from equser.api import connect_cpow_stream

for item in connect_cpow_stream('http://gateway:8080'):
    if isinstance(item, dict):
        print(f"Gap: {item['skipped_samples']} samples")
    else:
        # item is a pyarrow.RecordBatch
        va = item.column('VA').to_numpy()
        print(f"Batch: {item.num_rows} rows, VA peak: {va.max()}")

connect_spectral_stream(device_id, ...) -> Generator

Connect to the spectral analysis WebSocket and yield JSON frames.

Args:

ParameterDefaultDescription
device_id(required)Device identifier
phase'va'Channel: va, vb, vc, ia, ib, ic
fft_size4096FFT window size (power of 2)
update_rate10.0Frames per second
freq_min0Minimum frequency (Hz)
freq_max3000Maximum frequency (Hz)
from equser.api import connect_spectral_stream

for frame in connect_spectral_stream('wave-001', phase='va', fft_size=8192):
    print(f"Spectral frame: {len(frame.get('magnitudes', []))} bins")

API endpoints

The EQ Synapse server (Actix-web) exposes these endpoints:

EndpointMethodDescription
/api/v1/devicesGETList devices
/api/v1/devices/{id}/pmon/dataGETPMon data (Arrow IPC)
/api/v1/devices/{id}/cpow/dataGETCPOW data (Arrow IPC)
/api/v1/eventsGETPower quality events
/api/v1/events/streamGETEvent stream (SSE)
/api/v1/query/sqlPOSTSQL query (SELECT only)
/api/ws/cpow_streamWSReal-time waveform stream
/api/ws/spectralWSReal-time spectral stream

Data endpoints return Arrow IPC binary format for efficient transfer.



© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

Live Acquisition

Module: equser.pmon Dependencies: [daq] extra (avro, fastavro)

Real-time power monitor data acquisition from EQ Wave sensor hardware. Install with:

pip install equser[daq]

Note: Live acquisition requires physical EQ Wave sensor hardware connected via Ethernet.

PowerMonitor

The main acquisition class. Connects to an EQ Wave sensor over TCP, reads 10/12-cycle power quality measurements, and writes them to timestamped Parquet files with automatic file rotation.

from equser.pmon import PowerMonitor

with PowerMonitor('192.168.10.10') as monitor:
    monitor.run()  # Blocks until interrupted

Constructor

PowerMonitor(
    ip_address,
    connection=None,    # dict: port, retry_delay, num_retries, connect_timeout, data_timeout
    parquet=None,       # dict: interval, flush_every, compression
    data_dir=None,      # Override data directory (Path)
    convert_files_on_startup=True,
)

If connection or parquet are None, sensible defaults are used (see Configuration for details).

acquire(config_path=None)

Convenience function that loads configuration from a YAML file and starts monitoring with automatic reconnection. This is what equser pmon acquire calls internally.

from equser.pmon import acquire

acquire()                     # Use default config resolution
acquire('equser.yaml')        # Use specific config file

Avro-to-Parquet conversion

The sensor writes data in Avro format, which is automatically converted to Parquet. You can also convert files manually:

from equser.pmon import convert_avro_to_parquet

result = convert_avro_to_parquet('20250623_0750.avro')
print(result)  # Path to created .parquet file, or None on failure

The function signature:

convert_avro_to_parquet(
    avro_path,
    compression='ZSTD',        # ZSTD, SNAPPY, GZIP, or NONE
    compression_level=4,
    remove=False,               # Delete Avro file after conversion
)

For batch conversion from the CLI:

equser pmon convert data/*.avro --remove

Schema

The PMon schema defines 10/12-cycle power quality measurements. Use create_schema to generate an Avro schema for a given phase configuration:

from equser.pmon.schema import create_schema

schema, time_name, var_names = create_schema(num_phases=3)
print(time_name)    # 'time_us'
print(var_names[:5])
# ['FREQ', 'AVRMS', 'BVRMS', 'CVRMS', 'AIRMS']

Supports 1, 2, or 3-phase configurations. The full 3-phase field list:

FieldDescription
time_usMeasurement timestamp (microseconds since epoch)
FREQLine frequency (Hz)
{A,B,C}VRMSPhase RMS voltage
{A,B,C}IRMSPhase RMS current
NIRMSNeutral RMS current
{A,B,C}WATTPhase active power
{A,B,C}FVRMSPhase fundamental RMS voltage
{A,B,C}FIRMSPhase fundamental RMS current
{A,B,C}FWATTPhase fundamental active power
{A,B,C}FVARPhase fundamental reactive power

Field descriptions are also available programmatically:

from equser.pmon import FIELD_DESCRIPTIONS
print(FIELD_DESCRIPTIONS['AFVAR'])
# 'Line A fundamental reactive power [var]'

Error hierarchy

DataAcquisitionError (RuntimeError)
├── SensorConnectionError (+ OSError)  — sensor connection failures
└── ConfigurationError (+ ValueError)  — invalid configuration
from equser.pmon.errors import SensorConnectionError, ConfigurationError

CLI

# Start acquisition
equser pmon acquire -c config.yaml

# Convert Avro to Parquet
equser pmon convert data/*.avro --remove


© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

CLI Reference

equser provides a command-line interface for common operations.

Usage

equser <command> [options]

Commands

equser pmon acquire

Start power monitoring from an EQ Wave sensor. Requires [daq] extra and physical sensor hardware.

equser pmon acquire -c config.yaml
OptionDescription
-c, --configPath to YAML configuration file

Reads 10/12-cycle RMS measurements and writes Parquet files to the configured data directory. Runs continuously until interrupted (Ctrl+C).

equser pmon convert

Convert Avro data files to Parquet format. Requires [daq] extra.

equser pmon convert data/*.avro
equser pmon convert data/*.avro --remove
OptionDescription
--removeDelete source Avro files after successful conversion

equser plot

Plot a PMon or CPOW data file. Requires [analysis] extra.

equser plot data.parquet
equser plot data.parquet --cpow -o waveforms.png
OptionDescription
--pmonForce interpretation as PMon data
--cpowForce interpretation as CPOW waveform data
-o, --outputSave to file instead of displaying interactively

Automatically detects the file type from the Parquet schema if --pmon or --cpow is not specified.

equser notebooks list

List the reference notebooks bundled in the equser package.

equser notebooks list

equser notebooks copy

Copy reference notebooks to a directory.

equser notebooks copy --dest ./notebooks
equser notebooks copy --dest ./notebooks --category tutorials
equser notebooks copy --dest ./notebooks --overwrite
OptionDescription
--destDestination directory (default: current directory)
--categoryOnly copy notebooks from this category (tutorials or analysis)
--overwriteOverwrite existing files

equser snapshot

Capture live waveform data from a gateway and save to a Parquet file.

equser snapshot
equser snapshot --host 192.168.1.10 --duration 10 --output capture.parquet
OptionDescription
--hostGateway hostname or IP (default: localhost)
--portGateway port (default: 8080)
--durationCapture duration in seconds (default: 5.0)
--outputOutput Parquet file path (auto-generated if omitted)

Tab completion

Tab completion is available via argcomplete (included in the base install):

eval "$(register-python-argcomplete equser)"

Add the eval line to your shell profile (e.g. ~/.bashrc) for persistent completion.



© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

0.1.0 - 2026-02-06

Initial public release. User toolkit for EQ Wave power quality data.

Modules

  • equser.data - Load CPOW and PMon Parquet files with automatic scaling, timestamp parsing
  • equser.analysis - Waveform analysis: zero-crossing detection, AC cycle extraction
  • equser.api - REST and WebSocket clients for EQ Synapse gateways (requires [analysis])
  • equser.plotting - Static matplotlib plots for PMon and CPOW data (requires [analysis])
  • equser.pmon - Live sensor acquisition and Avro-to-Parquet conversion (requires [daq])
  • equser.core - YAML configuration loading, XDG-compliant path resolution
  • equser.utils - Logging with optional color, DateTime with floor-division
  • equser.notebooks - Bundled reference notebooks with list/copy API
  • equser.widgets - Interactive file selector for JupyterLab (requires [jupyter])
  • equser.snapshot - Waveform capture via gateway WebSocket

Dependency Tiers

  • Base: numpy, pyarrow, pyyaml, argcomplete, colorlog (data loading, analysis, CLI)
  • [daq]: avro, fastavro (live sensor acquisition)
  • [analysis]: matplotlib, requests, websocket-client (plotting + API)
  • [jupyter]: [analysis] + jupyterlab, duckdb, ipywidgets, ipykernel, nbconvert
  • [full]: all of the above

CLI

  • equser pmon acquire - Start power monitoring from EQ Wave sensor
  • equser pmon convert - Convert Avro files to Parquet
  • equser plot - Plot PMon or CPOW data files
  • equser notebooks list - List bundled reference notebooks
  • equser notebooks copy - Copy reference notebooks to a directory
  • equser snapshot - Capture live waveform data to a Parquet file


© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026

MIT License

Copyright (c) 2026 EQ Systems Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



© 2026 EQ Systems Inc.GitHubMIT License
Last updated: February 8, 2026