-
-
Notifications
You must be signed in to change notification settings - Fork 10
Email Notification Plan
Rin Nguyen edited this page Jun 18, 2026
·
1 revision
Send an email alert when a device connection drops — both local network (unintentional socket close/error) and MQTT disconnects. The feature is opt-in under Advanced Features, configured with SMTP credentials and a recipient address.
| Trigger | Fires email? |
|---|---|
Local socket close/error (intentionalDisconnect === false) |
✅ Yes |
| MQTT broker disconnect / offline | ✅ Yes |
| Intentional disconnect (reconnect cycle) | ❌ No |
| Initial connection failure (never connected) | ❌ No |
Instead of two separate listener classes, a single DisconnectNotificationListener is registered on both LocalNetworkClient.connectionBroadcaster and MQTTClient.connectionBroadcaster. The email subject distinguishes the connection type.
- Wraps
nodemailertransporter -
send(subject: string, body: string): Promise<void>— fire-and-forget with error logging - Instantiated once from
PlatformConfigManagersettings
- Implements
AbstractConnectionListener - Constructor takes
EmailNotificationService,AnsiLogger, andconnectionType: 'Local' | 'MQTT' -
onDisconnected(duid, message): sends email with connection type in subject -
onConnected,onError,onReconnect: no-ops
- Add
nodemailertodependencies - Add
@types/nodemailertodevDependencies
Add new emailNotification object under advancedFeature.settings:
"emailNotification": {
"title": "Email Notification on Disconnect",
"type": "object",
"properties": {
"enabled": {
"title": "Enable email notification when connection drops",
"type": "boolean",
"default": false
}
},
"allOf": [
{
"if": { "properties": { "enabled": { "const": true } }, "required": ["enabled"] },
"then": {
"properties": {
"smtpHost": { "title": "SMTP Host", "type": "string" },
"smtpPort": { "title": "SMTP Port", "type": "number", "default": 587 },
"smtpSecure": { "title": "Use TLS (port 465)", "type": "boolean", "default": false },
"smtpUser": { "title": "SMTP Username", "type": "string" },
"smtpPassword": { "title": "SMTP Password", "type": "string", "x-secret": true },
"recipient": { "title": "Recipient email", "type": "string" }
},
"required": ["smtpHost", "smtpUser", "smtpPassword", "recipient"]
}
}
]
}- Add
emailNotification: { enabled: false }underadvancedFeature.settings
- Add
EmailNotificationSettingsinterface - Add
emailNotification?: EmailNotificationSettingstoAdvancedFeatureSetting
- Add getter:
get emailNotificationSettings(): EmailNotificationSettings | undefined - Add getter:
get isEmailNotificationEnabled(): boolean
- Create
EmailNotificationServiceonce when config is enabled - Register
DisconnectNotificationListener('Local')on each local client after setup - Register
DisconnectNotificationListener('MQTT')on the MQTT client after setup
// src/types/roborockPluginPlatformConfig.ts
export interface EmailNotificationSettings {
enabled: boolean;
smtpHost?: string;
smtpPort?: number;
smtpSecure?: boolean;
smtpUser?: string;
smtpPassword?: string;
recipient?: string;
}import nodemailer from "nodemailer";
import { AnsiLogger } from "matterbridge/logger";
import { EmailNotificationSettings } from "../../types/roborockPluginPlatformConfig.js";
export class EmailNotificationService {
private readonly transporter: nodemailer.Transporter;
private readonly from: string;
private readonly recipient: string;
constructor(
settings: EmailNotificationSettings,
private readonly logger: AnsiLogger,
) {
this.from = settings.smtpUser ?? "";
this.recipient = settings.recipient ?? "";
this.transporter = nodemailer.createTransport({
host: settings.smtpHost,
port: settings.smtpPort ?? 587,
secure: settings.smtpSecure ?? false,
auth: { user: settings.smtpUser, pass: settings.smtpPassword },
});
}
public async send(subject: string, body: string): Promise<void> {
try {
await this.transporter.sendMail({
from: this.from,
to: this.recipient,
subject,
text: body,
});
} catch (error) {
this.logger.error(
`[EmailNotificationService] Failed to send email: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}import { AbstractConnectionListener } from "../abstractConnectionListener.js";
import { EmailNotificationService } from "../../../services/emailNotificationService.js";
import { AnsiLogger } from "matterbridge/logger";
export type ConnectionType = "Local" | "MQTT";
export class DisconnectNotificationListener implements AbstractConnectionListener {
constructor(
private readonly emailService: EmailNotificationService,
private readonly logger: AnsiLogger,
private readonly connectionType: ConnectionType,
) {}
public async onDisconnected(duid: string, message: string): Promise<void> {
this.logger.warn(
`[DisconnectNotificationListener] ${duid} ${this.connectionType} disconnected — sending email`,
);
await this.emailService.send(
`[Roborock] ${this.connectionType} connection dropped: ${duid}`,
`Device ${duid} lost its ${this.connectionType} connection.\n\nReason: ${message}\nTime: ${new Date().toISOString()}`,
);
}
public async onConnected(_duid: string): Promise<void> {}
public async onError(_duid: string, _message: string): Promise<void> {}
public async onReconnect(_duid: string, _message: string): Promise<void> {}
}// Shared instance (create once)
private emailService: EmailNotificationService | undefined;
private getEmailService(): EmailNotificationService | undefined {
if (!this.configManager.isEmailNotificationEnabled) return undefined;
this.emailService ??= new EmailNotificationService(this.configManager.emailNotificationSettings!, this.logger);
return this.emailService;
}
// In setupLocalClient() — after local client is created:
const emailService = this.getEmailService();
if (emailService) {
localClient.registerConnectionListener(
new DisconnectNotificationListener(emailService, this.logger, 'Local'),
);
}
// In initializeMessageClient() — after MQTT client is created:
const emailService = this.getEmailService();
if (emailService) {
mqttClient.registerConnectionListener(
new DisconnectNotificationListener(emailService, this.logger, 'MQTT'),
);
}| File | Tests |
|---|---|
src/tests/services/emailNotificationService.test.ts |
send success, send failure (logs error and does not throw) |
src/tests/roborockCommunication/.../disconnectNotificationListener.test.ts |
onDisconnected calls emailService.send with correct subject (Local/MQTT), onConnected/onError/onReconnect are no-ops |
Update src/tests/platform/platformConfigManager.test.ts
|
isEmailNotificationEnabled true/false, emailNotificationSettings getter |
- Install
nodemailerand@types/nodemailer - Add
EmailNotificationSettingsinterface to config types - Update schema JSON + config JSON
- Add
PlatformConfigManagergetters - Create
EmailNotificationService - Create
DisconnectNotificationListener - Register listener for both local and MQTT in
ConnectionService - Write unit tests
- Run
npm run build:localandnpm test