Skip to main content

Alerts

Alerts are event-driven notifications dispatched to external channels (email, SMS, WhatsApp, FCM, web push) when a configured condition is met on a device or industry. Alert records are stored in the notification Cosmos DB container. POST triggers dispatch; GET endpoints read them back with filtering and formatting applied.


Concepts

Alert Types

alertTypeContent classDescription
unit_thresholdUnitThresholdAlertContentCumulative consumption exceeds the daily configured threshold
energy_daily_thresholdUnitThresholdAlertContentDaily energy total exceeds the configured limit
hourly_thresholdHourlyThresholdAlertContentHourly consumption exceeds the configured limit
monthly_thresholdMonthlyThresholdAlertContentMonthly running total exceeds the configured limit
energy_monthly_thresholdMonthlyThresholdAlertContentMonthly energy total exceeds the configured limit
device_statusDeviceStatusAlertContentDevice goes offline or comes back online
level_exceedLevelAlertContentTank level is too high or too low
stable_flow_alertStableFlowAlertContentNon-zero flow shows no variation over time (stuck meter or undetected leak)
flow_rate_above_the_rangeFlowRateAlertContentFlow rate exceeds the configured upper bound
flow_rate_below_the_rangeFlowRateAlertContentFlow rate falls below the configured lower bound
flow_rate_back_in_rangeFlowRateAlertContentFlow rate returns to normal after an out-of-range event
quality_thresholdQualityThresholdAlertContentA quality parameter (pH, TDS, COD, etc.) is outside configured bounds
energy_thresholdEnergyAlertContentHourly energy consumption exceeds the threshold
no_flow_n_daysNoFlowAlertContentZero flow readings for N consecutive days
no_level_variation_n_daysNoLevelVariationAlertContentTank level remains constant for N consecutive days
water_balance_leakageWaterBalanceLeakageAlertContentInflow vs outflow discrepancy exceeds the configured leak tolerance
warning

An unrecognised alertType causes get_content() to return None, and the processor returns "Alert type not implemented" without dispatching anything.

Notification Channels

ChannelTransportRecipient source
EmailSMTP (Gmail)contact['emailIds'] — array, one message sent per address
SMSSMS gatewaycontact['phoneNo'] — single value
WhatsAppWhatsApp gatewaySame phoneNo as SMS
FCMFirebase Cloud MessagingBatch-fetched via DatabaseSupporter.get_fcm_ids(industryId, user_ids)
WebAzure Web PubSubFixed channel name V2_{industryId}
note

Contact data is always fetched fresh from Cosmos DB at dispatch time. It is never cached in the JWT.


POST /api/user/alerts — Dispatch an Alert

Validates the request, resolves recipients, and dispatches across all configured channels.

POST /api/user/alerts
Authorization: Bearer <access_token>
Content-Type: application/json
targetIndustryId: IND001 # optional — admin/internal users only

Request body

Fields are validated against AlertsInputModel before processing begins.

FieldRequiredTypeDescription
typeYesstringTop-level alert category (e.g. "threshold")
unitIdYesstringThe unit the alert concerns
meta.alertTypeYesstringOne of the 16 alertType values in the table above
createdOnNostringISO 8601 timestamp in YYYY-MM-DDTHH:MM:SSZ format
recipientsNoarrayList of user objects to notify. Empty array → nothing dispatched
(alert-specific fields)VariesAdditional payload consumed by the content class
{
"type": "threshold",
"unitId": "U001",
"createdOn": "2024-11-09T10:30:00Z",
"meta": {
"alertType": "unit_threshold",
"threshold": 5000,
"currentValue": 5342
},
"recipients": [
{ "userId": "user-abc" },
{ "userId": "user-xyz" }
]
}

Execution flow — AlertsPostHandler.handle()

info

Step 1 — Extract top-level fields

alert_type  = data.get("type")
alert = data.get("meta", {}).get("alertType")
created_on = data.get("createdOn")

Step 2 — createdOn validation

If createdOn is present it is parsed with:

datetime.strptime(created_on, "%Y-%m-%dT%H:%M:%SZ")

Parse failure → 400 "Timestamp for 'createdOn' is not in the correct format. Use YYYY-MM-DDTHH:MM:SSZ".

Step 3 — unitId permission check

unit_id = data.get("unitId")

If unit_id is falsy, or if unit_id is not a key in unitsMapping404 "Invalid unitId '{unit_id}' or you do not have permission to access this unit.". This is an in-memory check against the JWT-loaded unit map — no Cosmos DB query is made.

Step 4 — Timezone resolution

timezone = industryData.get("meta", {}).get("timezone", "Asia/Kolkata")

Defaults to "Asia/Kolkata" if the industry document has no meta.timezone field.

Step 5 — Build request objects

AlertInputData.from_dict(data) constructs the typed input object from the raw body. AlertsRequestData is then built with:

AlertsRequestData(
industry_id = industry_id,
industry_name = industry_name,
unit_id = unit_id,
created_on = created_on,
alert_type = alert_type,
alert = alert,
data = data,
timezone = timezone,
)

Step 6 — AlertsProcessor.process_alerts()

a) get_content() — maps alertType to the appropriate content class (see table above). Returns None for unknown types.

b) get_recipient_contacts() — reads recipients[] from the request body. For each recipient user ID, Cosmos DB is queried for emailIds[], phoneNo, and FCM token data. Five sets are built:

  • email — all emailIds across all recipients
  • sms — all phoneNo values across all recipients
  • whatsapp — same phone numbers as sms
  • fcm — batch-fetched via DatabaseSupporter.get_fcm_ids(industryId, user_ids)
  • web — always set to {"V2_{industryId}"} regardless of the recipient list

c) send_to_all_channel() — dispatches sequentially: email → SMS → WhatsApp → FCM → Web. Only channels with at least one recipient entry are executed. Each channel call returns {"success": [...], "failed": [...]} which are aggregated into the final response.

Response

{
"message": "Alert processing completed",
"status": "Success",
"success": ["email:user@example.com", "fcm:token-abc123"],
"failed": []
}

Admin and internal users

tip

Internal users can include the targetIndustryId header to dispatch an alert on behalf of another industry. When this header is present, industryId for all downstream lookups — unit map check, Cosmos queries, channel dispatch — is taken from that header value instead of the JWT.

Edge cases

warning
ScenarioBehaviour
createdOn present but wrong format400 with exact format hint
unitId absent or not in unitsMapping404 with the literal unitId value interpolated into the message
recipients is an empty arrayAll five channel sets are empty; alert is processed but nothing is dispatched; returns 200
Unknown alertTypeget_content() returns None; returns "Alert type not implemented"; no channel dispatch
A channel has no recipientsThat channel's dispatch call is skipped entirely

GET /api/user/alerts — List Alerts

Returns alerts for a date or date range, with optional type and unit filtering.

GET /api/user/alerts?date=09/11/2024&type=monthly&alert_type=general
Authorization: Bearer <access_token>

Parameters

ParameterRequiredDefaultDescription
dateYesDD/MM/YYYY — the reference date
typeNomonthlymonthly or daily — controls the date range
alert_typeNoallgeneral, offline, insights, or general_offline
unitIdNoall unitsScopes results to one unit
energyEnabledNofalseWhen false, energy-type alerts are excluded from results
adminAlertsNofalseWhen true, returns admin-relevant alert types only

Execution flow

info
  1. date is parsed with datetime.strptime(date_str, "%d/%m/%Y"). Parse failure → 1403 "Invalid date format. Use DD/MM/YYYY".

  2. Date range is derived from type:

    • dailystart_date = date, end_date = date
    • monthly (default) → start_date = date.replace(day=1), end_date = date
  3. NotificationService({startDate, endDate, unitId, energy_Enabled}).get_range_notifications() queries the notification container for all records in the computed range.

  4. AlertsFormatter().format_alerts(raw, date_str, alert_type, fetch_type, admin_alerts) applies the alert_type filter, groups by day, and shapes the response.


GET /api/user/alerts/dateRange — Custom Date Range

Returns alerts for an arbitrary date range. Follows the same filtering model as the standard GET endpoint.

GET /api/user/alerts/dateRange?startDate=01/11/2024&endDate=09/11/2024&alert_type=offline
Authorization: Bearer <access_token>

Parameters

ParameterRequiredDefaultDescription
startDateYesDD/MM/YYYY
endDateYesDD/MM/YYYY
alert_typeNoallgeneral, offline, insights, general_offline
unitIdNoall unitsScopes to one unit
energyEnabledNofalseExclude energy-type alerts when false
adminAlertsNofalseReturn admin-relevant types only when true

Validation

CheckError
startDate cannot be parsed1403 "Invalid startDate format. Use DD/MM/YYYY"
endDate cannot be parsed1403 "Invalid endDate format. Use DD/MM/YYYY"
startDate is after endDate1403 "startDate cannot be after endDate"

After validation, NotificationService.get_range_notifications() is called with the validated range, and AlertsFormatter.format_alerts_for_date_range() shapes the output.


GET /api/user/alerts/byType — Alerts by Category

Returns alerts filtered to one or more named categories, grouped by category in the response.

GET /api/user/alerts/byType?startDate=01/11/2024&endDate=09/11/2024&categories=device_status,level_exceed
Authorization: Bearer <access_token>

Parameters

ParameterRequiredDefaultDescription
startDateYesDD/MM/YYYY
endDateNostartDateDD/MM/YYYY — defaults to startDate if omitted
categoriesYesComma-separated list of AlertCategoryType values. The parser uses action='split' so the values arrive as a Python list
unit_idsNoall unitsComma-separated unit IDs, split into a list

Validation

CheckError
startDate cannot be parsed1403 "Invalid startDate format. Use DD/MM/YYYY"
endDate cannot be parsed1403 "Invalid endDate format. Use DD/MM/YYYY"
startDate is after endDate1403 "startDate cannot be after endDate"
Any value in categories is not in AlertCategoryType1403 "Invalid categories: {x}. Allowed values: {all sorted valid values}"

After validation, NotificationService.get_range_notifications() fetches the data and AlertsFormatter.format_alerts_by_category() returns a dict keyed by category name.

Response shape

{
"device_status": [ ... ],
"level_exceed": [ ... ]
}

GET /api/user/alerts/unitGraph — Alert Frequency for Chart

Returns alert counts grouped by unit and aggregated to a configurable time granularity, intended for chart rendering.

GET /api/user/alerts/unitGraph?date1=09/11/2024&type=MONTH&unitId=U001,U002
Authorization: Bearer <access_token>

Parameters

ParameterRequiredDefaultDescription
date1YesDD/MM/YYYY — the reference date
date2ConditionalDD/MM/YYYY — required when type=CUSTOM
unitIdNoall unitsComma-separated unit IDs, parsed with action='split'
typeNoDAYAggregation window: DAY, MONTH, YEAR, or CUSTOM
alertTypeNoallComma-separated alert types, parsed with action='split'

Date range derivation

typestart_dateend_date
DAYdate1date1
MONTHdate1.replace(day=1)date1
YEARdate1.replace(month=1, day=1)date1
CUSTOMdate1date2
warning

For CUSTOM, date2 is required. If absent → 1403 "date2 is required for CUSTOM type". If present but not parseable → 1403 "Invalid date2 format. Use DD/MM/YYYY".

UTC conversion

Alert timestamps stored in Cosmos DB are in UTC. The service uses a shift-aware conversion to align the local date window to UTC before querying:

start_date_local_str = f"{start_date.strftime('%Y-%m-%d')}T{shift_start_time}Z"
start_created_on = DateTimeUtil().convert_date_from_local_to_utc(start_date_local_str, t_zone=timezone)

end_created_on = DateTimeUtil().convert_date_from_local_to_utc(
f"{(end_date + timedelta(days=1)).strftime('%Y-%m-%d')}T{shift_start_time}Z",
t_zone=timezone,
)
note

end_date + timedelta(days=1) provides an exclusive upper bound so that the full final day is included.

NotificationService.get_alerts_by_date_range(industry_id, start_created_on, end_created_on, alert_types_list, unit_ids_list) executes the filtered Cosmos query, and UnitGraphAlertsFormatter(pattern_type, timezone, start_date, end_date).format_alerts_for_graph(raw) shapes the output for charting.


GET /api/user/alerts/allIndustries — Aggregate Across Industries

Returns alert counts aggregated across all industries. Requires admin or internal access.

GET /api/user/alerts/allIndustries?startDate=01/11/2024&type=monthly
Authorization: Bearer <access_token>

Parameters

ParameterRequiredDefaultDescription
startDateYesDD/MM/YYYY — the primary date
endDateConditionalDD/MM/YYYY — required when type=dateRange
typeNomonthlydaily, monthly, or dateRange
adminAlertsNofalseWhen true, returns admin-relevant alert types only

Validation

warning

For type=dateRange:

  • endDate is required → 1403 "endDate is required for dateRange type" if absent
  • startDate must not be after endDate1403 "startDate cannot be after endDate"

Execution flow

info
  1. AllIndustriesAlertsService(startDate, endDate, fetchType, adminAlerts).get_alerts() queries the notification container across all industry IDs.
  2. AllIndustriesAlertsFormatter().format_alerts_by_industry(notifications) groups and counts alerts per industry.

Response shape

{
"totalAlerts": 142,
"totalIndustries": 8,
"startDate": "01/11/2024",
"endDate": "09/11/2024",
"fetchType": "monthly",
"excludedIndustries": ["TEST_IND_001", "TEST_IND_002"],
"adminAlertsOnly": false,
"alertsByIndustry": {
"IND001": { "industryName": "Plant Alpha", "count": 34 },
"IND002": { "industryName": "Plant Beta", "count": 18 }
}
}
note

excludedIndustries always lists Constants.TEST_INDUSTRY_IDS — test industries are never included in the aggregated count regardless of the date range.


Status Code Reference

CodeMeaning
200Alert dispatched or results returned
400Schema validation failure or createdOn format error
404unitId not found in the user's unit mapping
1403Missing or invalid query parameter (date, startDate, endDate, categories, date2)
401Missing or invalid JWT
500Internal server error during channel dispatch