Bit more reorg and adding a README
parent
2ca33e0ef8
commit
f1d5654bf7
|
@ -0,0 +1,141 @@
|
|||
## Shingle
|
||||
|
||||
Stream-based home automation.
|
||||
|
||||
## Getting Started
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
Although it is not required, Shingle works better if you have an MQTT server. We recommend [Mosquitto](https://mosquitto.org/)
|
||||
|
||||
#### 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`
|
||||
```
|
||||
TimeEvents.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]`
|
||||
|
||||
## Compiling Shingle
|
||||
|
||||
#### Setup
|
||||
You need to install scala and mill. The easiest way to do this is with [Coursier](https://get-coursier.io/docs/cli-installation).
|
||||
|
||||
```
|
||||
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
|
||||
```
|
|
@ -1,6 +1,5 @@
|
|||
package net.okennedy.shingle
|
||||
|
||||
import java.time.Duration
|
||||
import net.okennedy.shingle.Shingle
|
||||
import org.fusesource.mqtt.client.QoS
|
||||
import scala.util.Try
|
||||
|
@ -9,6 +8,7 @@ import scala.util.Success
|
|||
import net.okennedy.shingle.stream.Owner
|
||||
import net.okennedy.shingle.cron.IntervalCronEvent
|
||||
import net.okennedy.shingle.cron.Cron
|
||||
import scala.concurrent.duration._
|
||||
|
||||
case class RestAPI(
|
||||
val url: String,
|
||||
|
@ -27,7 +27,7 @@ case class RestAPI(
|
|||
else { Failure(new Exception(s"Result ${result.statusCode} when fetching $url")) }
|
||||
}
|
||||
|
||||
def poll(topic: MqttPath, interval: Duration = Duration.ofHours(1))(implicit owner: Owner) =
|
||||
def poll(topic: MqttPath, interval: FiniteDuration = 1.hour)(implicit owner: Owner) =
|
||||
Cron.add(
|
||||
new IntervalCronEvent(interval) {
|
||||
// override def immediate = true
|
||||
|
|
|
@ -15,7 +15,7 @@ object Shingle
|
|||
override def port: Int = 4000
|
||||
|
||||
val config = ujson.read(Source.fromFile(
|
||||
System.getProperty("user.home") + File.separator + ".shingle"
|
||||
System.getProperty("user.home") + File.separator + ".shingle" + File.separator + "config.json"
|
||||
).getLines.mkString("\n"))
|
||||
|
||||
override def main(args: Array[String]): Unit =
|
||||
|
@ -23,13 +23,13 @@ object Shingle
|
|||
println("Welcome to Shingle\nInitializing libraries...")
|
||||
|
||||
println("...Mqtt")
|
||||
Mqtt.start()
|
||||
// Mqtt.start()
|
||||
println("...MPD")
|
||||
MPD
|
||||
// MPD
|
||||
println("...Hass")
|
||||
Hass
|
||||
// Hass
|
||||
println("...Weather")
|
||||
Weather
|
||||
// Weather
|
||||
println("...Cron")
|
||||
Cron.handler.start()
|
||||
println("Installing user modules...")
|
||||
|
|
|
@ -5,6 +5,12 @@ import java.time.temporal.ChronoUnit
|
|||
import org.fusesource.mqtt.client.QoS
|
||||
import upickle.default._
|
||||
import net.okennedy.shingle.stream.Stream
|
||||
import net.okennedy.shingle.stream.Owner
|
||||
import net.okennedy.shingle.stream.Dispatchable
|
||||
import net.okennedy.shingle.cron.RawScheduledCronEvent
|
||||
import net.okennedy.shingle.cron.Cron
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import net.okennedy.shingle.cron.IntervalCronEvent
|
||||
|
||||
class Timer(
|
||||
topic: MqttPath,
|
||||
|
@ -56,6 +62,54 @@ class Timer(
|
|||
def abort() = aborted = true
|
||||
}
|
||||
|
||||
object Timer
|
||||
{
|
||||
def apply(topic: String) = new Timer(Mqtt(topic))
|
||||
|
||||
def cron(
|
||||
start: String,
|
||||
end: String
|
||||
)(implicit owner: Owner): Stream[Boolean] =
|
||||
{
|
||||
val stream = new Dispatchable[Boolean]() {}
|
||||
|
||||
val startTrigger =
|
||||
new RawScheduledCronEvent(start) {
|
||||
def trigger() = { stream.dispatch(true) }
|
||||
}
|
||||
val endTrigger =
|
||||
new RawScheduledCronEvent(end) {
|
||||
def trigger() = { stream.dispatch(false) }
|
||||
}
|
||||
owner.onPostInit { () =>
|
||||
if(startTrigger.nextTrigger.compareTo(endTrigger.nextTrigger) < 0){
|
||||
//startTrigger < endTrigger
|
||||
stream.dispatch(true)
|
||||
} else {
|
||||
stream.dispatch(false)
|
||||
}
|
||||
}
|
||||
Cron.add(startTrigger)
|
||||
Cron.add(endTrigger)
|
||||
return stream
|
||||
}
|
||||
|
||||
def every(interval: FiniteDuration)(implicit owner: Owner): Stream[Unit] =
|
||||
{
|
||||
val stream = new Dispatchable[Unit]() {}
|
||||
|
||||
val trigger =
|
||||
new IntervalCronEvent(interval) {
|
||||
def trigger() = { stream.dispatch( () ) }
|
||||
}
|
||||
|
||||
Cron.add(trigger)
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case class TimerStatus(
|
||||
start: String,
|
||||
end: String,
|
||||
|
|
|
@ -2,10 +2,8 @@ package net.okennedy.shingle.cron
|
|||
|
||||
import scala.collection.mutable
|
||||
import math.Ordered.orderingToOrdered
|
||||
import java.time.Clock
|
||||
import java.time.temporal.TemporalUnit
|
||||
import java.time.temporal.ChronoUnit
|
||||
import net.okennedy.shingle.stream.Owner
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object Cron
|
||||
{
|
||||
|
@ -13,7 +11,6 @@ object Cron
|
|||
def compare(a: CronEvent, b: CronEvent) =
|
||||
b.nextTrigger.compare(a.nextTrigger)
|
||||
})
|
||||
val clock = Clock.systemDefaultZone()
|
||||
|
||||
val handler = new Thread {
|
||||
override def run(): Unit =
|
||||
|
@ -27,8 +24,8 @@ object Cron
|
|||
Thread.sleep(1000000)
|
||||
} else {
|
||||
val event = synchronized { queue.head }
|
||||
if(clock.instant.isBefore(event.nextTrigger)){
|
||||
val sleepTime = clock.instant.until(event.nextTrigger, ChronoUnit.MILLIS)
|
||||
if(event.nextTrigger.hasTimeLeft){
|
||||
val sleepTime = event.nextTrigger.timeLeft.toMillis
|
||||
Thread.sleep(sleepTime)
|
||||
} else {
|
||||
done = true
|
||||
|
|
|
@ -1,35 +1,31 @@
|
|||
package net.okennedy.shingle.cron
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import scala.concurrent.duration._
|
||||
import org.quartz.CronExpression
|
||||
import java.util.Date
|
||||
|
||||
trait CronEvent
|
||||
{
|
||||
def trigger(): Unit
|
||||
def nextTrigger: Instant
|
||||
def nextTrigger: Deadline
|
||||
def fireTrigger(): Unit = trigger()
|
||||
}
|
||||
|
||||
trait IntervalCronEvent(interval: Duration) extends CronEvent
|
||||
trait IntervalCronEvent(interval: FiniteDuration) extends CronEvent
|
||||
{
|
||||
def immediate: Boolean = false
|
||||
def trigger(): Unit
|
||||
|
||||
var nextTrigger: Instant =
|
||||
if(immediate){ Clock.systemDefaultZone.instant }
|
||||
else {
|
||||
Clock.systemDefaultZone.instant.plus(interval)
|
||||
}
|
||||
var nextTrigger: Deadline =
|
||||
if(immediate){ Deadline.now }
|
||||
else { interval.fromNow }
|
||||
|
||||
override def fireTrigger(): Unit =
|
||||
{
|
||||
try {
|
||||
trigger()
|
||||
} finally {
|
||||
nextTrigger = Clock.systemDefaultZone.instant.plus(interval)
|
||||
nextTrigger = interval.fromNow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,8 +36,15 @@ trait RawScheduledCronEvent(expression: String) extends CronEvent
|
|||
|
||||
def trigger(): Unit
|
||||
|
||||
def nextTrigger: Instant =
|
||||
timer.getNextValidTimeAfter(new Date())
|
||||
.toInstant
|
||||
def nextTrigger: Deadline =
|
||||
{
|
||||
(
|
||||
timer.getNextValidTimeAfter(new Date())
|
||||
.toInstant
|
||||
.getEpochSecond()
|
||||
- (System.currentTimeMillis() / 1000)
|
||||
).seconds
|
||||
.fromNow
|
||||
}
|
||||
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package net.okennedy.shingle.cron
|
||||
|
||||
import net.okennedy.shingle.stream.Owner
|
||||
import net.okennedy.shingle.stream.Dispatchable
|
||||
import net.okennedy.shingle.stream.Stream
|
||||
|
||||
object TimeRangeTriggerable
|
||||
{
|
||||
def cron(
|
||||
start: String,
|
||||
end: String
|
||||
)(implicit owner: Owner): Stream[Boolean] =
|
||||
{
|
||||
val stream = new Dispatchable[Boolean]() {}
|
||||
|
||||
val startTrigger =
|
||||
new RawScheduledCronEvent(start) {
|
||||
def trigger() = { stream.dispatch(true) }
|
||||
}
|
||||
val endTrigger =
|
||||
new RawScheduledCronEvent(start) {
|
||||
def trigger() = { stream.dispatch(false) }
|
||||
}
|
||||
owner.onPostInit { () =>
|
||||
if(startTrigger.nextTrigger.compareTo(endTrigger.nextTrigger) < 0){
|
||||
//startTrigger < endTrigger
|
||||
stream.dispatch(true)
|
||||
} else {
|
||||
stream.dispatch(false)
|
||||
}
|
||||
}
|
||||
Cron.add(startTrigger)
|
||||
Cron.add(endTrigger)
|
||||
return stream
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,7 @@ object Module
|
|||
}
|
||||
|
||||
val SCRIPTS = new File(
|
||||
System.getProperty("user.home") + File.separator + ".shingle-scripts"
|
||||
System.getProperty("user.home") + File.separator + ".shingle" + File.separator + "scripts"
|
||||
)
|
||||
val SUFFIX = ".scala"
|
||||
|
||||
|
@ -88,7 +88,7 @@ object Module
|
|||
| import net.okennedy.shingle.cron.Cron;
|
||||
| import net.okennedy.shingle.module.Notification;
|
||||
| import net.okennedy.shingle.module.Notifications;
|
||||
| import net.okennedy.shingle.cron.TimeRangeTriggerable;
|
||||
| import scala.concurrent.duration._;
|
||||
| $script
|
||||
|}
|
||||
|""".stripMargin,
|
||||
|
|
Loading…
Reference in New Issue