feat(mqtt): add topic discovery with 30s wildcard subscribe (Task 10)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
db3ece1862
commit
2c9d218211
169
display/middleware/src/modules/mqtt/topic-discovery.ts
Normal file
169
display/middleware/src/modules/mqtt/topic-discovery.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import * as mqtt from 'mqtt';
|
||||||
|
import { getDb } from '../../db/index';
|
||||||
|
import { getConnection } from './broker-manager';
|
||||||
|
|
||||||
|
export interface MqttTopic {
|
||||||
|
id: number;
|
||||||
|
broker_id: number;
|
||||||
|
topic: string;
|
||||||
|
json_path: string | null;
|
||||||
|
selected: number;
|
||||||
|
ignored: number;
|
||||||
|
last_payload: string | null;
|
||||||
|
discovered_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateTopicData {
|
||||||
|
json_path?: string | null;
|
||||||
|
selected?: number;
|
||||||
|
ignored?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTopicsForBroker(brokerId: number): MqttTopic[] {
|
||||||
|
const db = getDb();
|
||||||
|
return db
|
||||||
|
.prepare('SELECT * FROM mqtt_topics WHERE broker_id = ? ORDER BY id ASC')
|
||||||
|
.all(brokerId) as MqttTopic[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTopic(id: number, data: UpdateTopicData): MqttTopic | undefined {
|
||||||
|
const db = getDb();
|
||||||
|
const fields: string[] = [];
|
||||||
|
const values: unknown[] = [];
|
||||||
|
|
||||||
|
if (data.json_path !== undefined) { fields.push('json_path = ?'); values.push(data.json_path); }
|
||||||
|
if (data.selected !== undefined) { fields.push('selected = ?'); values.push(data.selected); }
|
||||||
|
if (data.ignored !== undefined) { fields.push('ignored = ?'); values.push(data.ignored); }
|
||||||
|
|
||||||
|
if (fields.length === 0) {
|
||||||
|
return db.prepare('SELECT * FROM mqtt_topics WHERE id = ?').get(id) as MqttTopic | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(id);
|
||||||
|
db.run(`UPDATE mqtt_topics SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||||
|
return db.prepare('SELECT * FROM mqtt_topics WHERE id = ?').get(id) as MqttTopic | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureTopicExists(brokerId: number, topic: string): void {
|
||||||
|
const db = getDb();
|
||||||
|
const existing = db
|
||||||
|
.prepare('SELECT id FROM mqtt_topics WHERE broker_id = ? AND topic = ?')
|
||||||
|
.get(brokerId, topic);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO mqtt_topics (broker_id, topic, selected, ignored)
|
||||||
|
VALUES (?, ?, 0, 0)`,
|
||||||
|
[brokerId, topic]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startDiscovery(brokerId: number, prefix?: string): Promise<MqttTopic[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const db = getDb();
|
||||||
|
const broker = db
|
||||||
|
.prepare('SELECT * FROM mqtt_brokers WHERE id = ?')
|
||||||
|
.get(brokerId) as {
|
||||||
|
id: number;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string | null;
|
||||||
|
password: string | null;
|
||||||
|
use_tls: number;
|
||||||
|
} | undefined;
|
||||||
|
|
||||||
|
if (!broker) {
|
||||||
|
resolve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to use existing connection first, otherwise create a temporary one
|
||||||
|
const existingClient = getConnection(brokerId);
|
||||||
|
let tempClient: mqtt.MqttClient | null = null;
|
||||||
|
let client: mqtt.MqttClient;
|
||||||
|
|
||||||
|
const subscribePattern = prefix ? `${prefix}#` : '#';
|
||||||
|
const discoveredTopics: Set<string> = new Set();
|
||||||
|
|
||||||
|
const finalize = (): void => {
|
||||||
|
if (tempClient) {
|
||||||
|
tempClient.end(true);
|
||||||
|
}
|
||||||
|
resolve(getTopicsForBroker(brokerId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessage = (topic: string): void => {
|
||||||
|
if (!discoveredTopics.has(topic)) {
|
||||||
|
discoveredTopics.add(topic);
|
||||||
|
ensureTopicExists(brokerId, topic);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingClient && existingClient.connected) {
|
||||||
|
client = existingClient;
|
||||||
|
client.subscribe(subscribePattern, (err) => {
|
||||||
|
if (err) {
|
||||||
|
resolve(getTopicsForBroker(brokerId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.on('message', onMessage);
|
||||||
|
setTimeout(() => {
|
||||||
|
client.unsubscribe(subscribePattern);
|
||||||
|
client.removeListener('message', onMessage);
|
||||||
|
finalize();
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const protocol = broker.use_tls ? 'mqtts' : 'mqtt';
|
||||||
|
const url = `${protocol}://${broker.host}:${broker.port}`;
|
||||||
|
|
||||||
|
const options: mqtt.IClientOptions = {
|
||||||
|
clientId: `display-discovery-${brokerId}-${Date.now()}`,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectPeriod: 0,
|
||||||
|
};
|
||||||
|
if (broker.username) options.username = broker.username;
|
||||||
|
if (broker.password) options.password = broker.password;
|
||||||
|
|
||||||
|
tempClient = mqtt.connect(url, options);
|
||||||
|
client = tempClient;
|
||||||
|
|
||||||
|
tempClient.on('connect', () => {
|
||||||
|
client.subscribe(subscribePattern, (err) => {
|
||||||
|
if (err) {
|
||||||
|
finalize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.on('message', onMessage);
|
||||||
|
setTimeout(() => {
|
||||||
|
finalize();
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tempClient.on('error', () => {
|
||||||
|
finalize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addManualTopic(brokerId: number, topic: string): MqttTopic {
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
// Insert or ignore duplicate
|
||||||
|
const existing = db
|
||||||
|
.prepare('SELECT * FROM mqtt_topics WHERE broker_id = ? AND topic = ?')
|
||||||
|
.get(brokerId, topic) as MqttTopic | undefined;
|
||||||
|
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const result = db.run(
|
||||||
|
`INSERT INTO mqtt_topics (broker_id, topic, selected, ignored)
|
||||||
|
VALUES (?, ?, 0, 0)`,
|
||||||
|
[brokerId, topic]
|
||||||
|
);
|
||||||
|
const id = result.lastInsertRowid as number;
|
||||||
|
return db.prepare('SELECT * FROM mqtt_topics WHERE id = ?').get(id) as MqttTopic;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user