Pulse Rate Calculator

Pulse Rate Calculator

Plan for Pulse Rate Calculator

Overview

This document outlines the implementation plan for the Pulse Rate Calculator feature. The goal is to automatically calculate pulse rates by capturing two meter readings at different times and correlating them with pulse counts from Timestream.


Part 1: Backend Implementation

1.1 New Lambda: calculate_pulse_rate_lambda.py

Location: /backend/installer/calculate_pulse_rate_lambda.py

Purpose: Calculate pulse rates by comparing meter readings with Timestream pulse counts.

Required Permissions

Resource

Actions

Purpose

Resource

Actions

Purpose

LorawanDevicesTableBmsProd

GetItem, UpdateItem, Query

Read device info, write calculated pulse rates

LorawanRawTableBmsProd (Timestream)

SELECT

Query pulse counts for _P1, _P2, _P3 virtual devices

InstallerAppDeviceDetails

GetItem, UpdateItem

Read/update meter readings and timestamps

API Endpoints

1. Get Pulse Counts - GET /installer/pulse-rate/{devEui}

// Request { "devEui": "ABC12" // 5-digit partial DevEui } // Response { "success": true, "fullDevEui": "0004A30B00ABC12", "pulseChannels": { "P1": { "pulseCount": 1250, "firstReading": "2025-12-29T10:00:00Z", "lastReading": "2025-12-29T14:30:00Z" }, "P2": { "pulseCount": 890, "firstReading": "2025-12-29T10:00:00Z", "lastReading": "2025-12-29T14:30:00Z" }, "P3": null // No data or not configured } }

2. Calculate Pulse Rate - POST /installer/pulse-rate/calculate

// Request { "devEui": "ABC12", "pulseChannel": 1, // 1, 2, or 3 "initialMeterReading": { "value": 12500.5, "unit": "gallons", "timestamp": "2025-12-29T10:00:00Z" }, "finalMeterReading": { "value": 12650.3, "unit": "gallons", "timestamp": "2025-12-29T14:30:00Z" } } // Response { "success": true, "calculation": { "meterDelta": 149.8, "pulseCount": 1498, "pulseRate": 0.1, // gallons per pulse "unit": "gallons", "confidence": "high" // based on time window and pulse count }, "saved": true }

3. Get Uncaptured Pulse Devices - GET /installer/pulse-rate/uncaptured/{siteId}

// Response { "success": true, "siteId": "SITE123", "uncapturedDevices": [ { "devEui": "ABC12", "fullDevEui": "0004A30B00ABC12", "deviceModel": "RAK2171", "pulseChannels": [ { "channel": 1, "deviceType": "PWM", "initialReading": 12500.5, "initialTimestamp": "2025-12-29T10:00:00Z", "unit": "gallons", "hasPulseData": true, "pulseCountAvailable": 1498 }, { "channel": 2, "deviceType": "PGM", "initialReading": 5000.0, "initialTimestamp": "2025-12-29T10:00:00Z", "unit": "ccf", "hasPulseData": false, "pulseCountAvailable": 0 } ] } ] }

Lambda Logic Flow

┌─────────────────────────────────────────────────────────────────────────────┐ │ Calculate Pulse Rate Flow │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. Receive request with devEui + finalMeterReading │ │ │ │ │ ▼ │ │ 2. Lookup full DevEui from partial (scan LorawanDevicesTable) │ │ │ │ │ ▼ │ │ 3. Get initial meter reading from InstallerAppDeviceDetails │ │ (stored when installer first submitted the device) │ │ │ │ │ ▼ │ │ 4. Query Timestream for pulse counts between timestamps: │ │ SELECT SUM(measure_value::bigint) as pulse_count │ │ FROM "LorawanTimestreamDBBmsProd"."LorawanRawTableBmsProd" │ │ WHERE DevEui = '{fullDevEui}_P{channel}' │ │ AND time BETWEEN '{initialTimestamp}' AND '{finalTimestamp}' │ │ AND measure_name = 'PulseCount' │ │ │ │ │ ▼ │ │ 5. Calculate pulse rate: │ │ pulseRate = (finalReading - initialReading) / totalPulseCount │ │ │ │ │ ▼ │ │ 6. Save to LorawanDevicesTable: │ │ - PulseScalarValue = pulseRate │ │ - PulseScalarUnit = unit │ │ - PulseRateCapturedAt = timestamp │ │ │ │ │ ▼ │ │ 7. Update InstallerAppDeviceDetails: │ │ - Mark pulse rate as captured │ │ - Store calculation metadata │ │ │ └─────────────────────────────────────────────────────────────────────────────┘

1.2 CDK Stack Updates

Location: /devops/cdk/lib/cdk-stack.ts

New Lambda Definition

const installerAppCalculatePulseRateLambda = new lambda.Function( this, `${atriaInstallerAppName}CalculatePulseRateLambda`, { functionName: `${atriaInstallerAppName}CalculatePulseRateLambda`, runtime: lambda.Runtime.PYTHON_3_11, handler: "calculate_pulse_rate_lambda.lambda_handler", code: lambda.Code.fromAsset("../../backend/installer"), timeout: cdk.Duration.seconds(30), memorySize: 256, role: installerAppCalculatePulseRateLambdaRole, environment: { INSTALLER_APP_DEVICE_DETAILS_TABLE: atriaInstallerDeviceDetailsTableName, INSTALLER_APP_LORAWAN_DEVICES_TABLE: installerAppLorawanDevicesTableName, TIMESTREAM_DATABASE: "LorawanTimestreamDBBmsProd", TIMESTREAM_TABLE: "LorawanRawTableBmsProd", }, } )

API Gateway Integration

Add new routes to the Installer API Gateway:

  • GET /pulse-rate/{devEui} → Get pulse counts

  • POST /pulse-rate/calculate → Calculate and save pulse rate

  • GET /pulse-rate/uncaptured/{siteId} → Get devices needing pulse capture


1.3 Migration Lambda Schedule Update

Current Schedule: Once daily at 7:00 AM UTC
New Schedule: Every 15 minutes

// Change from: schedule: events.Schedule.cron({ minute: "0", hour: "7" }) // Change to: schedule: events.Schedule.rate(cdk.Duration.minutes(15))

Rationale:

  • After installer clicks "Submit All", devices are saved to InstallerAppDeviceDetails

  • Migration lambda creates _P1, _P2, _P3 virtual devices in LorawanDevicesTable

  • LoRaWAN backend starts writing pulse data to Timestream

  • With 15-minute schedule, pulse data will be available within 15 minutes of submission


Part 2: Frontend Implementation

2.1 Device Form Changes (Pulse Counter Input)

File: /frontend/src/pages/InstallerDeviceFormPage.jsx

Current Behavior

  • Installer selects device type for each pulse channel

  • Installer enters "conversion factor" (pulse rate)

  • Installer enters units

New Behavior

  • Installer selects device type for each pulse channel

  • Installer enters initial meter reading (not the rate)

  • Installer enters units for the meter reading

  • System captures timestamp when reading is entered

// New pulse counter form fields { pulseChannel: 1, deviceType: "PWM", // Water meter initialMeterReading: 12500.5, // Current meter value unit: "gallons", // Unit of the meter reading timestamp: "2025-12-29T10:00:00Z" // Auto-captured }

UI Changes

┌────────────────────────────────────────────────────────────┐ │ Pulse Counter 1 │ ├────────────────────────────────────────────────────────────┤ │ Device Type: [Dropdown: Water Meter, Gas Meter, etc.] │ │ │ │ Current Meter Reading: [ 12500.5 ] │ │ │ │ Units: [Dropdown: gallons, liters, m³, ccf, therms] │ │ │ │ ⓘ The pulse rate will be calculated after installation │ │ when you provide a second meter reading. │ └────────────────────────────────────────────────────────────┘

2.2 Installation Submission Changes

File: /frontend/src/services/installationService.js

When submitting device to installation_submission_lambda:

// pulse_data structure changes const pulseData = { pulse1: { deviceType: "PWM", initialMeterReading: 12500.5, unit: "gallons", timestamp: new Date().toISOString(), // NO scalar/pulseRate - will be calculated later }, pulse2: {...}, pulse3: {...} }

2.3 Review Page: Uncaptured Pulse Counts Section

File: /frontend/src/pages/InstallerReviewPage.jsx

New Section: "Uncaptured Pulse Counts"

Location: Above "Devices Ready for Installation" section

┌─────────────────────────────────────────────────────────────────────────────┐ │ ⚠️ Uncaptured Pulse Counts (2 devices) [▼] │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ ABC12 - RAK2171 Pulse Counter │ │ │ │ ├─ P1: Water Meter - Initial: 12,500.5 gal @ 10:00 AM │ │ │ │ │ Status: ✅ Pulse data available (1,498 pulses) │ │ │ │ │ [Capture Pulse Rate] │ │ │ │ │ │ │ │ │ ├─ P2: Gas Meter - Initial: 5,000.0 ccf @ 10:00 AM │ │ │ │ │ Status: ⏳ Waiting for pulse data... │ │ │ │ │ [Capture Pulse Rate] (disabled) │ │ │ │ └─ P3: Not configured │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ DEF34 - RAK2171 Pulse Counter │ │ │ │ ├─ P1: Water Meter - Initial: 8,200.0 gal @ 10:15 AM │ │ │ │ │ Status: ⏳ Waiting for pulse data... │ │ │ │ │ [Capture Pulse Rate] (disabled) │ │ │ │ └─ P2, P3: Not configured │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘

Component Structure

// New component: UncapturedPulseCountsSection.jsx <UncapturedPulseCountsSection siteId={currentSiteId} onPulseRateCaptured={handlePulseRateCaptured} />

2.4 Capture Pulse Rate Modal

File: /frontend/src/components/installer/CapturePulseRateModal.jsx

Modal Flow

┌─────────────────────────────────────────────────────────────────────────────┐ │ Capture Pulse Rate [X] │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Device: ABC12 - RAK2171 │ │ Pulse Channel: P1 (Water Meter) │ │ │ │ ───────────────────────────────────────────────────────────────────────── │ │ │ │ Initial Reading (recorded during installation): │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ 12,500.5 gallons • Dec 29, 2025 at 10:00 AM │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ Pulses received since initial reading: │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ 1,498 pulses │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ ───────────────────────────────────────────────────────────────────────── │ │ │ │ Enter Current Meter Reading: │ │ ┌────────────────────────────────┐ │ │ │ 12650.3 │ gallons │ │ └────────────────────────────────┘ │ │ │ │ ───────────────────────────────────────────────────────────────────────── │ │ │ │ Calculated Pulse Rate: │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ (12,650.3 - 12,500.5) / 1,498 = 0.1 gallons per pulse │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ [Cancel] [Save Pulse Rate] │ │ │ └─────────────────────────────────────────────────────────────────────────────┘

Component Props

<CapturePulseRateModal isOpen={showCaptureModal} onClose={() => setShowCaptureModal(false)} device={{ devEui: "ABC12", fullDevEui: "0004A30B00ABC12", model: "RAK2171" }} pulseChannel={{ channel: 1, deviceType: "PWM", initialReading: 12500.5, initialTimestamp: "2025-12-29T10:00:00Z", unit: "gallons", pulseCount: 1498 }} onSuccess={handlePulseRateCaptured} />

2.5 New Service: Pulse Rate Service

File: /frontend/src/services/pulseRateService.js

// API calls to the new Calculate Pulse Rate Lambda export async function getUncapturedPulseDevices(siteId) { // GET /installer/pulse-rate/uncaptured/{siteId} } export async function getPulseCountsForDevice(devEui) { // GET /installer/pulse-rate/{devEui} } export async function calculatePulseRate(devEui, channel, finalReading) { // POST /installer/pulse-rate/calculate }

Part 3: Data Model Changes

3.1 InstallerAppDeviceDetails Table

New/Modified Fields:

Field

Type

Description

Field

Type

Description

PulseInitialReading1

Number

Initial meter reading for P1

PulseInitialReading2

Number

Initial meter reading for P2

PulseInitialReading3

Number

Initial meter reading for P3

PulseInitialTimestamp1

String (ISO)

When P1 reading was taken

PulseInitialTimestamp2

String (ISO)

When P2 reading was taken

PulseInitialTimestamp3

String (ISO)

When P3 reading was taken

PulseRateCaptured1

Boolean

Whether P1 pulse rate has been calculated

PulseRateCaptured2

Boolean

Whether P2 pulse rate has been calculated

PulseRateCaptured3

Boolean

Whether P3 pulse rate has been calculated

3.2 LorawanDevicesTable (for _P1, _P2, _P3 devices)

Fields set by Calculate Pulse Rate Lambda:

Field

Type

Description

Field

Type

Description

PulseScalarValue

Number

Calculated pulse rate (e.g., 0.1)

PulseScalarUnit

String

Unit per pulse (e.g., "gal")

PulseRateCapturedAt

String (ISO)

When pulse rate was calculated

PulseRateCalculation

Map

Metadata about the calculation


Part 4: Implementation Order

Phase 1: Backend (Priority: High)

  1. [ ] Create calculate_pulse_rate_lambda.py

  2. [ ] Update CDK stack with new Lambda and IAM permissions

  3. [ ] Add API Gateway routes for pulse rate endpoints

  4. [ ] Update migration lambda schedule to 15 minutes

  5. [ ] Deploy and test backend

Phase 2: Frontend - Form Changes (Priority: High)

  1. [ ] Update InstallerDeviceFormPage.jsx pulse counter inputs

  2. [ ] Update installationService.js submission payload

  3. [ ] Update installation_submission_lambda.py to handle new fields

Phase 3: Frontend - Review Page (Priority: High)

  1. [ ] Create UncapturedPulseCountsSection.jsx component

  2. [ ] Create CapturePulseRateModal.jsx component

  3. [ ] Create pulseRateService.js service

  4. [ ] Integrate into InstallerReviewPage.jsx

Phase 4: Testing & Refinement (Priority: Medium)

  1. [ ] End-to-end testing with real devices

  2. [ ] Edge case handling (no pulse data, timeout, etc.)

  3. [ ] UI/UX refinements based on testing


Part 5: Edge Cases & Error Handling

Scenario 1: No Pulse Data Available Yet

  • Cause: Migration hasn't run yet, or device hasn't sent uplinks

  • UI: Show "Waiting for pulse data..." with disabled button

  • Auto-refresh: Poll every 30 seconds for pulse data availability

Scenario 2: Zero Pulses Recorded

  • Cause: Device malfunction, or meter hasn't moved

  • UI: Show warning "No pulses recorded - check device connection"

  • Action: Allow manual pulse rate entry as fallback

Scenario 3: Pulse Rate Seems Unreasonable

  • Cause: Incorrect meter reading, or pulse count issue

  • UI: Show warning if calculated rate is outside expected range

  • Action: Allow user to confirm or re-enter readings

Scenario 4: Legacy Devices

  • Cause: Devices installed before this feature

  • UI: Show "Manual Entry" option

  • Action: Allow direct pulse rate input for legacy devices


Part 6: Success Criteria

  1. ✅ Installer can submit device with initial meter reading (no pulse rate)

  2. ✅ Migration runs every 15 minutes, creating _P1/_P2/_P3 devices

  3. ✅ Review page shows devices needing pulse rate capture

  4. ✅ Installer can capture final meter reading

  5. ✅ System calculates pulse rate: (final - initial) / pulseCount

  6. ✅ Calculated pulse rate is saved to LorawanDevicesTable

  7. ✅ Device shows as "complete" after pulse rate capture


Appendix: Calculation Example

Initial Reading: 12,500.5 gallons @ 10:00 AM Final Reading: 12,650.3 gallons @ 2:30 PM Pulses in Timestream: 1,498 pulses Meter Delta = 12,650.3 - 12,500.5 = 149.8 gallons Pulse Rate = 149.8 / 1,498 = 0.1 gallons per pulse Result: Every pulse from this device represents 0.1 gallons of water