Go to file
Oliver Kennedy a026f99b27
Some cleanup on the binding streams to topics.
2023-06-11 12:27:11 -04:00
shingle Some cleanup on the binding streams to topics. 2023-06-11 12:27:11 -04:00
.gitignore Initial Commit 2022-05-03 23:02:05 -04:00
README.md Justifications and removing debug code. 2023-06-05 11:58:39 -04:00
build.sc Some cleanup on the binding streams to topics. 2023-06-11 12:27:11 -04:00
deploy.sh Some cleanup on the binding streams to topics. 2023-06-11 12:27:11 -04:00

README.md

Shingle

Add reactive programming to your existing home automation toolkit.

What

Attach to Home Assistant, MQTT, or another event source and write automations in normal Scala 3 using a normal text editor.

Why?

Shingle was born out of frustration with the limited programming interfaces available on most popular frameworks.

  • Home Assistant programming is done through YAML files or a GUI that mimics the YAML files, and a custom templating language. This is perfectly fine for simple if-this-then-that type tasks, but makes specifying complex conditions (e.g., "when either of two sensors changes, but debounce the signal") incredibly painful. The custom templating language is also rather frustrating.

  • MQTT offers no programming framework. It's just a message bus, although admittedly an incredibly powerful one with extensive support across a multitude of languages.

  • Node.red is conceptually very nice, offering a stream-based solution not unlike Shingle. However, it's also painful to get running and is based on the "move fast and break things" javascript ecosystem.

Shingle is designed to add sensible programming support to existing home automation buses like Home Assistant or MQTT.

  • Shingle doesn't try to invent its own programming language. Write code in simple Scala 3.
  • Shingle doesn't try to be a message bus. Shingle plugs into MQTT or Home Assistant.
  • Shingle doesn't force you to use a crappy web-based editor. Use your existing code editor to write scripts, and Shingle will load them.

So what exactly does shingle provide:

  • Library code for reactive (stream-based) programming.
  • Library code for connecting to existing event buses (Home Assistant, MQTT) and entities (Hermes, MPD, REST endpoints).
  • A 'loader' that lets you dynamically load, enable, and disable individual pipelines.

Getting Started

Prerequisites

Although it is not required, Shingle works better if you have an MQTT server. We recommend Mosquitto

Setup

Create a folder ~/.shingle and a file ~/.shingle/config.json:

{
}

Example Script

Create a folder ~/.shingle/scripts/ and a file ~/.shingle/scripts/Test.scala

Timer.every(3.seconds)
     .trigger { _ => println("Hi!") }

Run Shingle

java Shingle.jar

or from the source directory

mill shingle.run

After shingle initializes itself, you should see it print Hi! every 3 seconds or so.

Concepts

The core concept of Shingle is a Stream, which delivers a series of events. Streams are typed. For example Stream[Boolean] delivers a series of Boolean-valued events.

Creating Streams

Streams are usually created through one of Shingle's components. In the example above, TimeEvents.every(3.seconds) creates a Unit-valued stream that delivers an event (roughly) every 3 seconds. See below for a full list of these.

Consuming Streams with Trigger

Stream's trigger method allows you to define a handler that is triggered whenever an event occurs.

Transforming Streams

Stream defines several methods that allow you to modify a stream:

  • map { event => ??? }: Generates a new stream by applying the provided logic to transform each event in the incoming stream.
  • filter { event => ??? }: Generates a new stream that includes only those events for which the provided logic evaluates to true.
  • join(other): Generates a new stream by merging events from two streams. Each event in the new stream will be a 2-tuple of the most recent events from the left and right streams.
  • debounce(delay): Generates a new stream where events are blocked until the stream has not seen an event for at least delay milliseconds.
  • onAnyChange: Generates a new stream where duplicate events are dropped. The first event is ignored.
  • onAnyChangeIncludingFirst: Generates a new stream where duplicate events are dropped. The first event is passed through.

Components

MQTT

MQTT is a simple, low-overhead message bus. To use MQTT, your config file must have an MQTT segment defined:

  "mqtt" : {
    "host" : "tcp://your_mqtt_host_here:port"
  }

The MQTT component provides hierarchical access to topics hosted by the MQTT server:

  • Mqtt("topic/path/here") << value: Publish value to the topic. The value may be any of the basic primitive types, or a Scala class with an implicit ujson Codec defined.
  • Mqtt("topic/path/here").subscribe: A Stream[Array[Byte]] pointing to the specified topic.
  • var x = Mqtt("topic/path/here").isState: x will refer to the most recent datum sent over the topic. You may use x.as[] or any of the standard ujson primitive value decoders (i.e., .str, .int, etc...).

Hermes

Connect to a Hermes-based voice assistant protocol, such as Rhasspy. To use Hermes, you need MQTT configured (as above) and your voice assistant must be configured to talk over MQTT. Your config file must also have a Hermes segment defined:

  "hermes" : {
    "default" : {
      "siteId" : "default_hermes_target_here"
    }
  }

The Hermes component provides support for intent handling and speech-to-text.

  • Hermes.say(text, siteId = default): Have your voice assistant speak the provided text aloud. If siteId is not provided, the default site from your configuration will be used.
  • Hermes.registerIntent(id) { parameters => ??? }: Register an intent handler for intents with the provided id. If the intent has a payload, parameters will be passed as a Map[String,ujson.Value].

Hass

Connect to a Home Assistant server. To use Hass, you need to provide access credentials in your config file:

  "hass" : {
    "token" : "_____",
    "host" : "https://hass_server:port"
  }

The Hass component provides support for publishing state to Home Assistant:

  • Hass.set(entity, state[, attribute1 -> value1[, attribute2 -> value2[, ...]]]): Set the state of the provided entity. Attributes may be provided as String -> ujson.Value pairs.
  • Hass.service(domain, service, data1 -> value1[, data2 -> value2[, ...]]]): Invoke the specified service. Data parameters may be provided as String -> ujson.Value pairs.

Timer

Utilities for working with time. The Timer component provides support for triggering events periodically

  • Timer.every(duration): A Stream[Unit] that is triggered roughly with a period of duration.
  • Timer.cron(start, end): A Stream[Boolean] that is true in the time interval between the cronstring start and the cronstring end, and false otherwise.

RestAPI

Utilities for working with REST APIs.

  • RestAPI(url).poll(topic, interval = 1.hour): Poll the provided URL every interval and post the result to the specified MQTT topic.
  • RestAPI(url).get: Retrieve the current contents of the GET url as a Scala Try[String]

MPD

The music player daemon. To use the MPD integration, you need to provide access information in your config file:

  "mpd" : {
    "host" : "localhost",
  }
  • MPD.status: A stream of the MPD status

  • MPD.playlist: A stream of MPD's current playlist

  • MPD.currentTrack: A stream of the currently playing song

  • MPD.position: A stream of the current timestamp in the song

  • MPD.play(): Start playing the current playlist

  • MPD.clear(): Clear the current playlist

  • MPD.list(path): List the MPD-local files at the specified path.

  • MPD.savedPlaylists: List all of the MPD saved playlists

  • MPD.savedPlaylist(name): List all songs in the specified saved playlist

  • MPD.append(path): Add the song at the specified path to the end of the playlist

The Admin interface

More coming here eventually, but Shingle exposes a simple web interface for runtime management.

  • http://localhost:4000/api/modules: List every available module (the contents of ~/.shingle/scripts without the .scala extension).
  • http://localhost:4000/api/modules/[script]: Show the contents of the specified script.
  • http://localhost:4000/api/modules/[script]/unload: Disable all triggers created by the specified script.
  • http://localhost:4000/api/modules/[script]/load: Disable all triggers created by the specified script and reload it.

Compiling Shingle

Setup

You need to install scala and mill. The easiest way to do this is with Coursier.

cs setup
cs install mill

Basic compilation

mill shingle.compile

Use the -w flag for iterative compilation

mill -w shingle.compile

Running Shingle

mill shingle.run

Deploying Shingle

mill shingle.assembly
cp out/shingle/assembly.dest/out.jar Shingle.jar