BMS-проєкти починаються однаково: замовник показує схему будівлі з контролерами Siemens Desigo CC, Schneider Electric EcoStruxure або Johnson Controls Metasys і каже «хочемо бачити все це в телефоні». За цим «все це» ховаються десятки протоколів, polling-цикли від 1 секунди до 15 хвилин, історична база на роки назад та вимога працювати навіть коли основний сервер BMS перезавантажується. Ми беремо такі проєкти під ключ: від аналізу протоколів до публікації в stores. Наш досвід — 10+ років у BMS-інтеграції та понад 50 реалізованих об'єктів. Оцінюємо проєкт за 2 дні та гарантуємо стабільну роботу при будь-якому навантаженні.
Протоколи та шлюзи
Промислові BMS говорять на BACnet/IP, Modbus TCP/RTU, KNX/IP та LonWorks. Напряму з мобільного застосунку до них не ходять — між контролерами та REST/WebSocket API стоїть шлюз або middleware.
Типовий стек інтеграції:
| Рівень | Технологія |
|---|---|
| Контролери | BACnet/IP, Modbus TCP, KNX |
| Шлюз | Node-RED, Niagara Framework 4, власний Python/Go сервіс |
| Transport | MQTT over TLS, REST, WebSocket |
| Мобільний клієнт | Flutter / Swift / Kotlin |
Niagara Framework 4 (Tridium) — де-факто стандарт для великих об'єктів. Він вміє нормалізувати BACnet-об'єкти в єдиний REST API (/haystack/api/read?filter=bacnet) та віддавати WebSocket-стрим змін. Робота з Haystack API через Dart:
class HaystackClient {
final Dio _dio;
final String _baseUrl;
HaystackClient(this._baseUrl, String username, String password) :
_dio = Dio(BaseOptions(
baseUrl: _baseUrl,
headers: {
'Authorization': 'Basic ${base64Encode(utf8.encode('$username:$password'))}',
'Accept': 'application/json',
},
));
Future<List<HaystackRow>> read(String filter) async {
final response = await _dio.get('/haystack/api/read',
queryParameters: {'filter': filter});
final grid = HaystackGrid.fromJson(response.data);
return grid.rows;
}
Future<Map<String, dynamic>> readPoint(String pointId) async {
final response = await _dio.get('/haystack/api/hisRead',
queryParameters: {
'id': '@$pointId',
'range': 'today',
});
return response.data;
}
}
Для об'єктів з MQTT-шлюзом (Node-RED конвертує BACnet → MQTT JSON) використовуємо mqtt_client у Flutter. Топіки організовуємо за ієрархією будівлі: building/{buildingId}/floor/{floor}/zone/{zone}/{parameter}.
Як ми інтегруємося з існуючою BMS?
Процес завжди починаємо з аудиту контролерів: з'ясовуємо, які протоколи використовуються, яка версія Niagara або іншого middleware, чи є вже REST/WebSocket API або потрібна установка шлюзу. Потім проєктуємо схему потоків даних: які точки читаємо, які пишемо, з якою періодичністю. Сертифіковані інженери налаштовують шлюз та проводять інтеграційне тестування. Результат — єдиний інтерфейс на телефоні замість кількох панелей керування.
Архітектура даних реального часу: чому DataHub критичний?
Найскладніший момент у BMS-застосунку — не підключення, а керування потоком даних. Температура в 200 зонах оновлюється кожні 30 секунд, освітлення — за подією, енергоспоживання — щохвилини. Все це не можна перепідписувати при кожному перемальовуванні UI.
Рішення — централізований DataHub на рівні застосунку:
class BmsDataHub {
final MqttClient _mqtt;
final _streams = <String, BehaviorSubject<BmsPoint>>{};
Stream<BmsPoint> watchPoint(String pointId) {
if (!_streams.containsKey(pointId)) {
_streams[pointId] = BehaviorSubject();
_mqtt.subscribe('building/+/+/+/$pointId', MqttQos.atLeastOnce);
}
return _streams[pointId]!.stream;
}
void _onMessage(List<MqttReceivedMessage<MqttMessage>> events) {
for (final event in events) {
final topic = event.topic;
final payload = MqttPublishPayload.bytesToStringAsString(
(event.payload as MqttPublishMessage).payload.message);
final point = BmsPoint.fromJson(jsonDecode(payload));
_streams[point.id]?.add(point);
}
}
}
BehaviorSubject з пакету rxdart зберігає останнє значення — віджет, який підписався після приходу даних, одразу отримує актуальний стан без очікування наступного циклу polling.
Інтерактивний план поверху
Замовники завжди хочуть план будівлі з живими даними. DXF або SVG-план конвертуємо в SVG (через ODA File Converter для DXF), рендеримо через flutter_svg + InteractiveViewer. Точки датчиків — overlay поверх SVG з позиціонуванням за нормалізованими координатами:
class FloorPlanWidget extends StatelessWidget {
final FloorPlan plan;
final Map<String, BmsPoint> liveData;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return Stack(children: [
SvgPicture.asset('assets/floors/${plan.id}.svg',
width: constraints.maxWidth),
...plan.sensors.map((sensor) => Positioned(
left: sensor.x * constraints.maxWidth,
top: sensor.y * constraints.maxHeight,
child: SensorMarker(
point: liveData[sensor.pointId],
type: sensor.type,
),
)),
]);
});
}
}
Маркери змінюють колір за порогами: зелений (норма), жовтий (попередження), червоний (аварія). Пороги беремо з BMS-конфігурації, не хардкодимо.
Керування: запис значень у BACnet-точки
Читати простіше, ніж писати. Для командування BACnet-точками (setpoint температури, увімкнення/вимкнення освітлення) через REST-шлюз:
Future<void> writePoint(String pointId, dynamic value) async {
// Оптимістичне оновлення UI
_hub.updateLocally(pointId, value);
try {
await _api.put('/haystack/api/pointWrite', data: {
'id': '@$pointId',
'level': 8, // пріоритет запису BACnet (1-16, нижче = вищий пріоритет)
'val': value,
'who': _authService.currentUser,
'duration': 'PT0S', // permanent
});
} on DioException catch (e) {
// Відкат при помилці
_hub.revertLocally(pointId);
rethrow;
}
}
BACnet Priority Array — деталь, яку ігнорують і потім не можуть зрозуміти, чому уставка температури не змінюється: контролер приймає команди, але вони перебиваються вищим пріоритетом з BMS-розкладу (рівень 2-4). Рівень 8 — стандартний для ручного оператора.
Алери та журнал подій
Аварійні події з BMS надходять через MQTT або WebSocket. Локальні push-сповіщення генеруємо через flutter_local_notifications, серверні push (коли застосунок закритий) — через FCM з високим пріоритетом (priority: high, content_available: true).
Журнал подій: SQLite через drift для офлайн-зберігання 30 днів історії, посторінкове завантаження з API для старіших записів.
Розмежування прав
У реальних об'єктах різні користувачі бачать різні поверхи та зони. Права зберігаються на бекенді, мобільний клієнт запитує список доступних об'єктів при логіні та не будує маршрути до недоступних ресурсів. Спроба записати в заборонену точку → HTTP 403 → локальний відкат + сповіщення користувачеві.
Що входить у розробку?
- Аналіз протоколів контролерів та архітектури BMS.
- Проєктування схеми інтеграції та потоків даних.
- Реалізація мобільного клієнта (iOS/Android на Flutter або нативному стеку).
- Налаштування шлюзу та інтеграційне тестування.
- Публікація в App Store та Google Play.
- 30 днів технічної підтримки після релізу.
- Документація по API та навчання персоналу (опціонально).
Терміни та вартість
| Етап | Термін |
|---|---|
| MVP (план поверху + real-time моніторинг) | 8–12 тижнів |
| Повноцінна система (кілька об'єктів, графіки, алери, права) | 4–6 місяців |
| Інтеграція з нестандартними протоколами | +2–4 тижні |
Вартість розраховується індивідуально після аналізу ваших контролерів та вимог. Зв'яжіться з нами для оцінки — ми підготуємо комерційну пропозицію за 2 робочих дні.







