Oliver Kennedy a026f99b27 | ||
---|---|---|
shingle | ||
.gitignore | ||
README.md | ||
build.sc | ||
deploy.sh |
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 leastdelay
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
: Publishvalue
to the topic. The value may be any of the basic primitive types, or a Scala class with an implicitujson
Codec defined.Mqtt("topic/path/here").subscribe
: AStream[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 usex.as[]
or any of the standardujson
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 providedid
. If the intent has a payload, parameters will be passed as aMap[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 asString -> ujson.Value
pairs.Hass.service(domain, service, data1 -> value1[, data2 -> value2[, ...]]])
: Invoke the specified service. Data parameters may be provided asString -> ujson.Value
pairs.
Timer
Utilities for working with time. The Timer component provides support for triggering events periodically
Timer.every(duration)
: AStream[Unit]
that is triggered roughly with a period ofduration
.Timer.cron(start, end)
: AStream[Boolean]
that is true in the time interval between the cronstringstart
and the cronstringend
, 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 ScalaTry[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