From 129c825a77fb3cc039ab978785d72f92023ef1ab Mon Sep 17 00:00:00 2001 From: xengineering Date: Tue, 7 Apr 2026 14:53:37 +0200 Subject: Refactor message routing This allows to route incoming MQTT messages in the _onMessage() method and handle it in different handlers per topic. --- lib/data.dart | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/data.dart b/lib/data.dart index 4962a50..201550e 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -161,36 +161,38 @@ class AppState with ChangeNotifier { void _onMessage(List> messages) { for (final MqttReceivedMessage message in messages) { - final String topic = message.topic; + final List parts = message.topic.split('/'); + final String payload = MqttPublishPayload.bytesToStringAsString( + (message.payload as MqttPublishMessage).payload.message); - final MqttPublishMessage payloadMessage = - message.payload as MqttPublishMessage; - final String payload = - MqttPublishPayload.bytesToStringAsString(payloadMessage.payload.message); - - if (topic == '$topicPrefix/server/health') { - if (payload == 'good') { - process(MachineEvent.reachable); - } - - if (payload == 'bad') { - process(MachineEvent.unreachable); - } + if (message.topic == '$topicPrefix/server/health') { + _onHealthMessage(payload); + return; } - // format /contacts/
/state - final List parts = topic.split('/'); - if (parts.length != 4 || parts[1] != 'contact' || parts[3] != 'state') { - continue; + if (parts.length == 4 && parts[1] == 'contact' && parts[3] == 'state') { + String address = parts[2]; + _onContactMessage(payload, address); + return; } - final String address = parts[2]; + } + } - final bool? parsedState = _parseBool(payload); + void _onHealthMessage(String payload) { + switch (payload) { + case 'good': + process(MachineEvent.reachable); + case 'bad': + process(MachineEvent.unreachable); + } + } - if (parsedState != null) { - contacts[address] = parsedState; - notifyListeners(); - } + void _onContactMessage(String payload, String address) { + final bool? parsedState = _parseBool(payload); + + if (parsedState != null) { + contacts[address] = parsedState; + notifyListeners(); } } -- cgit v1.3 From 38705a8c1825638750cbee3e03bd182db1445020 Mon Sep 17 00:00:00 2001 From: xengineering Date: Tue, 7 Apr 2026 15:26:25 +0200 Subject: Add AppState.covers This set contains all the covers the server offers. This commit does not include displaying it. --- lib/data.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/data.dart b/lib/data.dart index 201550e..a2e26b3 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -51,6 +51,7 @@ class AppState with ChangeNotifier { MachineState state = MachineState.init; Map contacts = {}; + Set covers = {}; late MqttServerClient _client; late Directory supportDir; @@ -141,6 +142,7 @@ class AppState with ChangeNotifier { 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); @@ -148,6 +150,7 @@ class AppState with ChangeNotifier { void _onDisconnected() { contacts = {}; + covers = {}; process(MachineEvent.disconnected); } @@ -175,6 +178,12 @@ class AppState with ChangeNotifier { _onContactMessage(payload, address); return; } + + if (parts.length == 3 && parts[1] == 'cover') { + String id = parts[2]; + _onCoverMessage(payload, id); + return; + } } } @@ -196,6 +205,12 @@ class AppState with ChangeNotifier { } } + void _onCoverMessage(String payload, String id) { + if (payload != 'exists') return; + + covers.add(id); + } + bool? _parseBool(String payload) { switch (payload.toLowerCase()) { case 'open': -- cgit v1.3 From 5dc62a59bf12dcc0190c26eb3712c1b7eec3cfcd Mon Sep 17 00:00:00 2001 From: xengineering Date: Tue, 7 Apr 2026 17:09:45 +0200 Subject: Display available covers This does not yet allow control of the covers but existence is shown. --- lib/ui.dart | 54 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/lib/ui.dart b/lib/ui.dart index 3a98be9..94b6b8e 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -90,14 +90,25 @@ class DevicesPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text("Contacts")), - body: const Column( - children: [ - Expanded(child: ContactList()), - ], + return const Scaffold( + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(15.0), + child: Text("Contacts", style: TextStyle(fontSize: 22)), + ), + ContactList(), + Padding( + padding: EdgeInsets.all(15.0), + child: Text("Covers", style: TextStyle(fontSize: 22)), + ), + CoverList(), + ], + ), ), - bottomNavigationBar: const ConnectionStatus(), + bottomNavigationBar: ConnectionStatus(), ); } } @@ -110,6 +121,8 @@ class ContactList extends StatelessWidget { return Consumer( builder: (BuildContext context, AppState state, Widget? child) { return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), itemCount: state.contacts.length, itemBuilder: (BuildContext context, int index) { MapEntry data = state.contacts.entries.elementAt(index); @@ -129,6 +142,33 @@ class ContactList extends StatelessWidget { } } +class CoverList extends StatelessWidget { + const CoverList({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (BuildContext context, AppState state, Widget? child) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.covers.length, + itemBuilder: (BuildContext context, int index) { + return Column( + children: [ + ListTile( + leading: const Icon(Icons.roller_shades), + title: Text(state.covers.elementAt(index)), + ), + ], + ); + }, + ); + }, + ); + } +} + class ConnectionStatus extends StatelessWidget { const ConnectionStatus({super.key}); -- cgit v1.3 From e4c61d1b669a43d292be1f40d5b36dbcb93500a4 Mon Sep 17 00:00:00 2001 From: xengineering Date: Wed, 8 Apr 2026 10:58:29 +0200 Subject: Add cover control with up / stop / down This allows the user to move covers with the Sia app. --- lib/data.dart | 12 ++++++++++++ lib/ui.dart | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/data.dart b/lib/data.dart index a2e26b3..14e6811 100644 --- a/lib/data.dart +++ b/lib/data.dart @@ -221,4 +221,16 @@ class AppState with ChangeNotifier { return null; } } + + void publish(String topic, String payload, MqttQos qos) { + final MqttClientPayloadBuilder builder = MqttClientPayloadBuilder(); + builder.addString(payload); + + _client.publishMessage( + '$topicPrefix/$topic', + qos, + builder.payload!, + retain: false, + ); + } } diff --git a/lib/ui.dart b/lib/ui.dart index 94b6b8e..66078da 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:mqtt_client/mqtt_client.dart'; import 'data.dart'; @@ -154,11 +155,50 @@ class CoverList extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), itemCount: state.covers.length, itemBuilder: (BuildContext context, int index) { + String cover = state.covers.elementAt(index); return Column( children: [ ListTile( leading: const Icon(Icons.roller_shades), - title: Text(state.covers.elementAt(index)), + title: Text(cover), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.arrow_upward), + constraints: const BoxConstraints(), + onPressed: () { + state.publish( + 'cover/$cover/movement', + 'retract', + MqttQos.exactlyOnce + ); + }, + ), + IconButton( + icon: const Icon(Icons.stop), + constraints: const BoxConstraints(), + onPressed: () { + state.publish( + 'cover/$cover/movement', + 'stop', + MqttQos.exactlyOnce + ); + }, + ), + IconButton( + icon: const Icon(Icons.arrow_downward), + constraints: const BoxConstraints(), + onPressed: () { + state.publish( + 'cover/$cover/movement', + 'extend', + MqttQos.exactlyOnce + ); + }, + ), + ], + ), ), ], ); -- cgit v1.3