Skip to content

Commit

Permalink
examples/automation_cli: new automation CLI example
Browse files Browse the repository at this point in the history
Add a new automation CLI example with a README introduction.

Uptadate top-level README to refer to the example.
  • Loading branch information
smyrman committed Oct 26, 2023
1 parent 139c8f0 commit 39051b5
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ When access to the Clarify namespace is granted in Clarify (scoped to entire org

## Setting up automation routines

To quickly set-up your own automation routines, you can get started with our [automation template repository](https://github.com/clarify/template-clarify-automation). This template let's you customize and build your own automation binary and easily run it inside GitHub Actions; no external hosting environment is required (unless you want to).
By using our [automation](automation) and [automationcli](automation/automationcli) packages, you can quickly define a tree-structure of _Routines_ that can be recognized and run by path-name. See the [automation_cli](examples/automation_cli/) example, or fork our [automation template repository](https://github.com/clarify/template-clarify-automation) to get started. This template let's you customize and build your own automation CLI and easily run it inside GitHub Actions; no external hosting environment is required (unless you want to).

## Copyright

Expand Down
59 changes: 59 additions & 0 deletions examples/automation_cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!--
Copyright 2023 Searis AS
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# Automation CLI

This example shows how to use the [automationcli][cli] package to build your own CLI tool for running automation routines from the command-line. This is a basic example. For setting up your production routines, and running them on a schedule using GitHub actions or other means, you can fork the [template-clarify-automation][template] repo to get started quickly.

[cli]: https://pkg.go.dev/github.com/clarify/clarify-go/automation/automationcli
[template]: https://github.com/clarify/template-clarify-automation

The example contains the following automation routines:

- devdata/save-signals: Update meta-data for a status signal in Clarify. No requirements.
- devdata/insert-random: Insert a state for the status signal. No requirements.
- publish: Insert a state for the status signal. Require setting `CLARIFY_EXAMPLE_PUBLISH_INTEGRATION_ID`.
- evaluate/detect-fire: Log a message to the console if a fire is detected.

## Running the example

First, you myst download a credentials file for your integration, and a configure the integration to have access to all namespaces. This require that you are an admin in your clarify organization. To avoid passing in options each time, we will export it as a environment variable:

```sh
export CLARIFY_CREDENTIALS=path/to/clarify-credentials.json
```

Secondly, load the database using the devdata routines. Note that the routines are run in an alphanumerical order.

```sh
go run . -v devdata
```

Now, let's publish the signals. For that we need to know the integration ID we should publish from. You can find the integration ID in the Admin panel. Ideally, we would edit the routine code to hard-code the integration IDs to publish from. However, for this example, we allow parsing the information from the environment:

```sh
export CLARIFY_EXAMPLE_PUBLISH_INTEGRATION_ID=...
go run . -v publish
```

Now, let's run our evaluation routine. For that, we need to know the ID from the item that was just published. You can get this ID from the console output, or from Clarify.

```sh
export CLARIFY_EXAMPLE_STATUS_ITEM_ID=...
go run . evaluate/detect-fire
```

You should now have about 45 % chance to see the text "FIRE! FIRE! FIRE!".
25 changes: 25 additions & 0 deletions examples/automation_cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2023 Searis AS
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"os"

"github.com/clarify/clarify-go/automation/automationcli"
)

func main() {
os.Exit(automationcli.ParseAndRun((routines)))
}
196 changes: 196 additions & 0 deletions examples/automation_cli/routines.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright 2023 Searis AS
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"context"
"fmt"
"log/slog"
"math/rand"
"os"
"strings"
"time"

"github.com/clarify/clarify-go/automation"
"github.com/clarify/clarify-go/fields"
"github.com/clarify/clarify-go/views"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

const (
exampleName = "automation_cli"

// For more advanced applications, defining your annotations as constants,
// is less error prone. Annotation keys should be prefixed to avoid
// collision.
keyTransformVersion = "clarify/clarify-go/example/transform"
keySignalAttributesHash = "clarify/clarify-go/example/source-signal/attributes-hash"
keySignalID = "clarify/clarify-go/example/source-signal/id"

// In this example we filter which signals to expose using the following
// annotation keys and values.
keyExampleName = "clarify/clarify-go/example/name"
keyExamplePublish = "clarify/clarify-go/example/publish"
annotationTrue = "true"

// transformVersion must be incremented when updating transform, in order to
// force updates of already exposed items when there are no changes to the
// underlying signal attributes.
transformVersion = "v2"
)

var routines = automation.Routines{
"devdata": automation.Routines{
"save-signals": automation.RoutineFunc(saveSignals),
"insert-random": automation.RoutineFunc(insertRandom),
},
"publish": publishSignals,
"evaluate": automation.Routines{
"detect-fire": detectFire,
},
}

// insertRandom is an example of a custom routine that inserts a random value
// for the "banana-stand/status" signal.
func insertRandom(ctx context.Context, cfg *automation.Config) error {
logger := cfg.Logger()
client := cfg.Client()

series := make(views.DataSeries)
until := fields.AsTimestamp(time.Now()).Truncate(time.Minute)
start := until.Add(-time.Hour)
for t := start; t < until; t = t.Add(time.Minute) {
var value float64
if rand.Intn(100) > 1 {
// 1% chance of state fire.
value = 1
}
series[t] = value
}
df := views.DataFrame{
"banana-stand/status": series,
}

logger.Debug("Insert status signal", automation.AttrDataFrame(df))
if !cfg.DryRun() {
result, err := client.Insert(df).Do(ctx)
if err != nil {
return fmt.Errorf("insert: %w", err)
}
logger.Debug("Insert signal result", slog.Any("result", result))
}
return nil
}

// saveSignals is an example of a custom automation routine that updates signal
// meta-data for the "banana-stand/status" signal.
func saveSignals(ctx context.Context, cfg *automation.Config) error {
logger := cfg.Logger()
client := cfg.Client()

signalsByInput := map[string]views.SignalSave{
"banana-stand/status": {
MetaSave: views.MetaSave{
Annotations: fields.Annotations{
keyExampleName: "save_signals",
"clarify/clarify-go/example/publish": "true",
},
},
SignalSaveAttributes: views.SignalSaveAttributes{
Name: "Building status",
Description: "Overall building status, aggregated from environmental sensors.",
Labels: fields.Labels{
"data-source": {"clarify-go/examples"},
"location": {"banana stand", "pier"},
},
SourceType: views.Aggregation,
ValueType: views.Enum,
EnumValues: fields.EnumValues{
0: "not on fire",
1: "on fire",
},
SampleInterval: fields.AsFixedDurationNullZero(15 * time.Minute),
GapDetection: fields.AsFixedDurationNullZero(2 * time.Hour),
},
},
}
logger.Debug("Save status signal", slog.Any("signalsByInput", signalsByInput))
if !cfg.DryRun() {
result, err := client.SaveSignals(signalsByInput).Do(ctx)
if err != nil {
return fmt.Errorf("save signals: %w", err)
}
logger.Debug("Save signals result", slog.Any("result", result))
}

return nil
}

// publishSignals contain an automation that publish items from signals.
var publishSignals = automation.PublishSignals{
// NOTE: CLARIFY_EXAMPLE_PUBLISH_INTEGRATION_ID is read from env to allow
// the example to be runnable without code change. For production, you are
// recommended to hard-code the integration IDs to publish from.
Integrations: []string{os.Getenv("CLARIFY_EXAMPLE_PUBLISH_INTEGRATION_ID")},
SignalsFilter: fields.Comparisons{
"annotations." + keyExampleName: fields.Equal(exampleName),
"annotations." + keyExamplePublish: fields.Equal(annotationTrue),
},
TransformVersion: transformVersion,
Transforms: []func(item *views.ItemSave){
transformEnumValuesToFireEmoji,
transformLabelValuesToTitle,
},
}

// transformEnumValuesToFireEmoji is an example transform that replaces the enum
// values "on fire" and "not on fire" with appropriate emoji.
func transformEnumValuesToFireEmoji(item *views.ItemSave) {
for i, v := range item.EnumValues {
switch {
case strings.EqualFold(v, "on fire"):
item.EnumValues[i] = "🔥"
case strings.EqualFold(v, "not on fire"):
item.EnumValues[i] = "✅"
}
}
}

// transformLabelValuesToTitle transforms label values from format "multiple
// words" to "Multiple Words".
func transformLabelValuesToTitle(item *views.ItemSave) {
for k, labels := range item.Labels {
for i, label := range labels {
item.Labels[k][i] = cases.Title(language.AmericanEnglish).String(label)
}
}
}

var detectFire = automation.EvaluateActions{
Evaluation: automation.Evaluation{
Items: []fields.ItemAggregation{
{Alias: "fire_rate", ID: os.Getenv("CLARIFY_EXAMPLE_STATUS_ITEM_ID"), Aggregation: fields.AggregateStateHistRate, State: 1},
},
Calculations: []fields.Calculation{
{Alias: "has_fire", Formula: "fire_rate > 0"}, // return 1.0 when true.
},
SeriesIn: []string{"has_fire"},
},
Actions: []automation.ActionFunc{
automation.ActionSeriesContains("has_fire", 1),
automation.ActionRoutine(automation.LogInfo("FIRE! FIRE! FIRE!")),
},
}

0 comments on commit 39051b5

Please sign in to comment.