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
alertType | Content class | Description |
|---|---|---|
unit_threshold | UnitThresholdAlertContent | Cumulative consumption exceeds the daily configured threshold |
energy_daily_threshold | UnitThresholdAlertContent | Daily energy total exceeds the configured limit |
hourly_threshold | HourlyThresholdAlertContent | Hourly consumption exceeds the configured limit |
monthly_threshold | MonthlyThresholdAlertContent | Monthly running total exceeds the configured limit |
energy_monthly_threshold | MonthlyThresholdAlertContent | Monthly energy total exceeds the configured limit |
device_status | DeviceStatusAlertContent | Device goes offline or comes back online |
level_exceed | LevelAlertContent | Tank level is too high or too low |
stable_flow_alert | StableFlowAlertContent | Non-zero flow shows no variation over time (stuck meter or undetected leak) |
flow_rate_above_the_range | FlowRateAlertContent | Flow rate exceeds the configured upper bound |
flow_rate_below_the_range | FlowRateAlertContent | Flow rate falls below the configured lower bound |
flow_rate_back_in_range | FlowRateAlertContent | Flow rate returns to normal after an out-of-range event |
quality_threshold | QualityThresholdAlertContent | A quality parameter (pH, TDS, COD, etc.) is outside configured bounds |
energy_threshold | EnergyAlertContent | Hourly energy consumption exceeds the threshold |
no_flow_n_days | NoFlowAlertContent | Zero flow readings for N consecutive days |
no_level_variation_n_days | NoLevelVariationAlertContent | Tank level remains constant for N consecutive days |
water_balance_leakage | WaterBalanceLeakageAlertContent | Inflow vs outflow discrepancy exceeds the configured leak tolerance |
An unrecognised alertType causes get_content() to return None, and the processor returns "Alert type not implemented" without dispatching anything.
Notification Channels
| Channel | Transport | Recipient source |
|---|---|---|
| SMTP (Gmail) | contact['emailIds'] — array, one message sent per address | |
| SMS | SMS gateway | contact['phoneNo'] — single value |
| WhatsApp gateway | Same phoneNo as SMS | |
| FCM | Firebase Cloud Messaging | Batch-fetched via DatabaseSupporter.get_fcm_ids(industryId, user_ids) |
| Web | Azure Web PubSub | Fixed channel name V2_{industryId} |
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.
| Field | Required | Type | Description |
|---|---|---|---|
type | Yes | string | Top-level alert category (e.g. "threshold") |
unitId | Yes | string | The unit the alert concerns |
meta.alertType | Yes | string | One of the 16 alertType values in the table above |
createdOn | No | string | ISO 8601 timestamp in YYYY-MM-DDTHH:MM:SSZ format |
recipients | No | array | List of user objects to notify. Empty array → nothing dispatched |
| (alert-specific fields) | Varies | — | Additional 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()
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 unitsMapping → 404 "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— allemailIdsacross all recipientssms— allphoneNovalues across all recipientswhatsapp— same phone numbers assmsfcm— batch-fetched viaDatabaseSupporter.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
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
| Scenario | Behaviour |
|---|---|
createdOn present but wrong format | 400 with exact format hint |
unitId absent or not in unitsMapping | 404 with the literal unitId value interpolated into the message |
recipients is an empty array | All five channel sets are empty; alert is processed but nothing is dispatched; returns 200 |
Unknown alertType | get_content() returns None; returns "Alert type not implemented"; no channel dispatch |
| A channel has no recipients | That 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
| Parameter | Required | Default | Description |
|---|---|---|---|
date | Yes | — | DD/MM/YYYY — the reference date |
type | No | monthly | monthly or daily — controls the date range |
alert_type | No | all | general, offline, insights, or general_offline |
unitId | No | all units | Scopes results to one unit |
energyEnabled | No | false | When false, energy-type alerts are excluded from results |
adminAlerts | No | false | When true, returns admin-relevant alert types only |
Execution flow
-
dateis parsed withdatetime.strptime(date_str, "%d/%m/%Y"). Parse failure →1403 "Invalid date format. Use DD/MM/YYYY". -
Date range is derived from
type:daily→start_date = date,end_date = datemonthly(default) →start_date = date.replace(day=1),end_date = date
-
NotificationService({startDate, endDate, unitId, energy_Enabled}).get_range_notifications()queries thenotificationcontainer for all records in the computed range. -
AlertsFormatter().format_alerts(raw, date_str, alert_type, fetch_type, admin_alerts)applies thealert_typefilter, 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
| Parameter | Required | Default | Description |
|---|---|---|---|
startDate | Yes | — | DD/MM/YYYY |
endDate | Yes | — | DD/MM/YYYY |
alert_type | No | all | general, offline, insights, general_offline |
unitId | No | all units | Scopes to one unit |
energyEnabled | No | false | Exclude energy-type alerts when false |
adminAlerts | No | false | Return admin-relevant types only when true |
Validation
| Check | Error |
|---|---|
startDate cannot be parsed | 1403 "Invalid startDate format. Use DD/MM/YYYY" |
endDate cannot be parsed | 1403 "Invalid endDate format. Use DD/MM/YYYY" |
startDate is after endDate | 1403 "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
| Parameter | Required | Default | Description |
|---|---|---|---|
startDate | Yes | — | DD/MM/YYYY |
endDate | No | startDate | DD/MM/YYYY — defaults to startDate if omitted |
categories | Yes | — | Comma-separated list of AlertCategoryType values. The parser uses action='split' so the values arrive as a Python list |
unit_ids | No | all units | Comma-separated unit IDs, split into a list |
Validation
| Check | Error |
|---|---|
startDate cannot be parsed | 1403 "Invalid startDate format. Use DD/MM/YYYY" |
endDate cannot be parsed | 1403 "Invalid endDate format. Use DD/MM/YYYY" |
startDate is after endDate | 1403 "startDate cannot be after endDate" |
Any value in categories is not in AlertCategoryType | 1403 "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
| Parameter | Required | Default | Description |
|---|---|---|---|
date1 | Yes | — | DD/MM/YYYY — the reference date |
date2 | Conditional | — | DD/MM/YYYY — required when type=CUSTOM |
unitId | No | all units | Comma-separated unit IDs, parsed with action='split' |
type | No | DAY | Aggregation window: DAY, MONTH, YEAR, or CUSTOM |
alertType | No | all | Comma-separated alert types, parsed with action='split' |
Date range derivation
type | start_date | end_date |
|---|---|---|
DAY | date1 | date1 |
MONTH | date1.replace(day=1) | date1 |
YEAR | date1.replace(month=1, day=1) | date1 |
CUSTOM | date1 | date2 |
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,
)
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
| Parameter | Required | Default | Description |
|---|---|---|---|
startDate | Yes | — | DD/MM/YYYY — the primary date |
endDate | Conditional | — | DD/MM/YYYY — required when type=dateRange |
type | No | monthly | daily, monthly, or dateRange |
adminAlerts | No | false | When true, returns admin-relevant alert types only |
Validation
For type=dateRange:
endDateis required →1403 "endDate is required for dateRange type"if absentstartDatemust not be afterendDate→1403 "startDate cannot be after endDate"
Execution flow
AllIndustriesAlertsService(startDate, endDate, fetchType, adminAlerts).get_alerts()queries thenotificationcontainer across all industry IDs.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 }
}
}
excludedIndustries always lists Constants.TEST_INDUSTRY_IDS — test industries are never included in the aggregated count regardless of the date range.
Status Code Reference
| Code | Meaning |
|---|---|
200 | Alert dispatched or results returned |
400 | Schema validation failure or createdOn format error |
404 | unitId not found in the user's unit mapping |
1403 | Missing or invalid query parameter (date, startDate, endDate, categories, date2) |
401 | Missing or invalid JWT |
500 | Internal server error during channel dispatch |