import 'dart:io'; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_server_client.dart'; import 'db.dart'; const int brokerPort = 1883; const String topicPrefix = 'sia'; enum MachineState { init, // user does not want connection MQTT not connected disconnected, // user wants connection but MQTT not (yet) connected unreachable, // connected to MQTT broker but Sia server not reachable reachable, // connected to MQTT and Sia server reachable } enum MachineEvent { connect, // user wants to connect disconnect, // user does not want an connection anymore connected, // connection to MQTT broker established disconnected, // connection to MQTT broker lost reachable, // Sia server is reachable via MQTT unreachable, // Sia server not reachable via MQTT } class AppState with ChangeNotifier { final DB db = DB(); static const Map> machine = > { MachineState.init: { MachineEvent.connect: MachineState.disconnected, }, MachineState.disconnected: { MachineEvent.disconnect: MachineState.init, MachineEvent.connected: MachineState.unreachable, }, MachineState.unreachable: { MachineEvent.disconnect: MachineState.init, MachineEvent.disconnected: MachineState.disconnected, MachineEvent.reachable: MachineState.reachable, }, MachineState.reachable: { MachineEvent.disconnect: MachineState.init, MachineEvent.disconnected: MachineState.disconnected, MachineEvent.unreachable: MachineState.unreachable, }, }; MachineState state = MachineState.init; Map contacts = {}; Set covers = {}; late MqttServerClient _client; late Directory supportDir; String fqdn = ''; AppState() { unawaited(loadPersistence()); } Future loadPersistence() async { await db.connect(); String? dbFqdn = await db.getServerFqdn(); if (dbFqdn == null) return; fqdn = dbFqdn; notifyListeners(); } void setFqdn(String value) { fqdn = value; unawaited(db.setServerFqdn(value)); } void process(MachineEvent event) { MachineState lastState = state; Map? transitions = machine[lastState]; if (transitions == null) { return; } MachineState? nextState = transitions[event]; if (nextState == null) { return; } if (nextState == lastState) { return; } if (lastState == MachineState.init) { _initMqtt(); } if (nextState == MachineState.init) { _deinitMqtt(); } state = nextState; notifyListeners(); } @override void dispose() { _client.disconnect(); super.dispose(); } void _initMqtt() { _client = MqttServerClient( fqdn, 'sia_app_${DateTime.now().millisecondsSinceEpoch}', ); _client.port = brokerPort; _client.keepAlivePeriod = 2; _client.autoReconnect = true; _client.disconnectOnNoResponsePeriod = 1; _client.logging(on: false); _client.onConnected = _onConnected; _client.onDisconnected = _onDisconnected; _client.onAutoReconnect = _onAutoReconnect; _client.onAutoReconnected = _onAutoReconnected; try { unawaited(_client.connect()); } catch (e) { _client.disconnect(); return; } _client.updates?.listen(_onMessage); } void _deinitMqtt() { _client.disconnect(); } void _onConnected() { _client.subscribe('$topicPrefix/contact/+/state', MqttQos.exactlyOnce); _client.subscribe('$topicPrefix/cover/+', MqttQos.exactlyOnce); _client.subscribe('$topicPrefix/server/health', MqttQos.exactlyOnce); process(MachineEvent.connected); } void _onDisconnected() { contacts = {}; covers = {}; process(MachineEvent.disconnected); } void _onAutoReconnect() { process(MachineEvent.disconnected); } void _onAutoReconnected() { process(MachineEvent.connected); } void _onMessage(List> messages) { for (final MqttReceivedMessage message in messages) { final List parts = message.topic.split('/'); final String payload = MqttPublishPayload.bytesToStringAsString( (message.payload as MqttPublishMessage).payload.message); if (message.topic == '$topicPrefix/server/health') { _onHealthMessage(payload); return; } if (parts.length == 4 && parts[1] == 'contact' && parts[3] == 'state') { String address = parts[2]; _onContactMessage(payload, address); return; } if (parts.length == 3 && parts[1] == 'cover') { String id = parts[2]; _onCoverMessage(payload, id); return; } } } void _onHealthMessage(String payload) { switch (payload) { case 'good': process(MachineEvent.reachable); case 'bad': process(MachineEvent.unreachable); } } void _onContactMessage(String payload, String address) { final bool? parsedState = _parseBool(payload); if (parsedState != null) { contacts[address] = parsedState; notifyListeners(); } } void _onCoverMessage(String payload, String id) { if (payload != 'exists') return; covers.add(id); } bool? _parseBool(String payload) { switch (payload.toLowerCase()) { case 'open': return true; case 'closed': return false; default: return null; } } }