Home Assistant integration is done via [MQTT Discovery][0]. The
application publishes configuration to a special topic, which Home
Assistant receives and uses to create entities and optionally assign
them to devices.
To start with, we're exposing two entites to Home Assistant for each
attached monitor: one for the current URL and one for the current title
of the window. The URL is exposed as a "text" sensor, which allows the
state to be changed directly; when the state changes, the new value is
puoblished to the "command" topic and thus triggering a navigation.
Since the client can only have a single "will" message, every entity
will be marked as available/unavailable together. This is probably not
an issue, but it does make it impossible to indicate a monitor is no
longer attached.
Note: for some reason, the will message doesn't seem to get sent when the
client disconnects. I am not sure why...
[0]: https://www.home-assistant.io/docs/mqtt/discovery/
For heads-up displays with multiple monitors, we're going to want one
Firefox window on each. To support this, we need to get a list of
connected monitors from the operating system and associate each with its
own window. Since Firefox may start with multiple taps open
automatically, we first close all but one and associate it with the
first monitor. Then, for each remaining monitor, we open a new window
to associate with it.
To maintain the monitor-window association, the `Session` structure has
a `HashMap`. When a naigation request arrives, the Firefox window to
control is found by looking up the specified screen name in the map.
Since the Marionette protocol is stateful, we have to "switch to" the
desired window and then send the navigation command.
I have tried to design the monitor information lookup API so that it can
be swapped out at compile time for different operating systems. For
now, only X11 is supported, but we could hypothetically support Wayland
or even Windows by implementing the appropriate `get_monitors` function
for those APIs.
The pieces are starting to come together. To control the browser via
MQTT messages, the `MqttClient` dispatches messages via a
`MessageHandler`, which parses them and makes the appropriate Marionette
requests. The `MessageHandler` trait defines callback methods for each
MQTT control operation, which currently is just `navigate`. The
operation type is determined by the MQTT topic on which the message was
received.
Several new types are necessary to make this work. The `MessageHandler`
trait and implementation are of course the core, reacting to incoming
MQTT messages. In order for the handler to be able to *send* MQTT
messages, though, it needs a reference to the Paho MQTT client. The
`MqttPublisher` provides a convenient wrapper around the client, with
specific methods for each type of message to send. Finally, there's the
`MessageType` enumeration, which works in conjunction with the
`TopicMatcher` to match topic names to message types using topic filter
patterns.
Naturally, we need a way to configure the MQTT connection parameters
(host, port, username, etc.). For that, we'll use a TOML configuration
file, which is read at startup and deserialized into a structure owned
by the Session.
The Session object now has a `run` method, which establishes the MQTT
connection and then repeatedly waits for messages from the broker. It
will continuously attempt to connect to the broker until it succeeds.
This way, if the broker is unavailable when the application starts, it
will eventually connect when it becomes available. Once the initial
connection is established, the client will automatically reconnect if it
gets disconnected later.
Since the `run` method loops forever and never returns, we need to use a
separate Tokio task to manage it. We keep the task handle so we can
cancel the task when the application shuts down.