Skip to content

Commit

Permalink
events: add events service
Browse files Browse the repository at this point in the history
  • Loading branch information
zegl committed Aug 10, 2023
1 parent 2c893c9 commit 7afe37b
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 0 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/build-events.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Build Events

on:
push:
branches: ["main", "ci"]

env:
REGISTRY: ghcr.io
IMAGE_NAME: volundsgatan/events

jobs:
build-events:
runs-on: ubuntu-22.04

permissions:
contents: read
packages: write

steps:
- uses: actions/checkout@v3

- name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: events
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: events/Containerfile
13 changes: 13 additions & 0 deletions events/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# syntax=docker/dockerfile:1
FROM docker.io/golang:1.21-bookworm as builder
WORKDIR /app
COPY go.mod .
COPY go.sum .
COPY main.go .
RUN go build -v -o /app/events .

FROM docker.io/debian:bookworm as runner
WORKDIR /app
COPY --from=builder /app/events /app/events
RUN apt-get update && apt-get install -y ca-certificates
CMD /app/events
8 changes: 8 additions & 0 deletions events/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/volundsgatan/VLG/events

go 1.20

require (
github.com/klauspost/compress v1.10.3 // indirect
nhooyr.io/websocket v1.8.7 // indirect
)
39 changes: 39 additions & 0 deletions events/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
204 changes: 204 additions & 0 deletions events/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package main

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"sync"
"time"

"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
)

/*
{
"payload":{
"battery":null,
"contact":true,
"device":{
"applicationVersion":3,
"dateCode":"20161128",
"friendlyName":"Bedroom Window Sensor",
"hardwareVersion":2,
"ieeeAddr":"0x00158d00090d3d69",
"manufacturerID":4151,
"manufacturerName":"LUMI",
"model":"MCCGQ11LM", // contact sensor!
"networkAddress":3406,
"powerSource":"Battery",
"softwareBuildID":"3000-0001",
"stackVersion":2,
"type":"EndDevice",
"zclVersion":1
},
"device_temperature":null,
"last_seen":1691679136308,
"linkquality":175,
"power_outage_count":null,
"voltage":null
},
"topic":"Bedroom Window Sensor"
}
*/
type Z2mEvent struct {
Payload json.RawMessage `json:"payload"`
Topic string `json:"topic"`
}

type ContactSensorPayload struct {
// Battery any `json:"battery"`
Contact *bool `json:"contact"`
Device struct {
// ApplicationVersion int `json:"applicationVersion"`
// DateCode string `json:"dateCode"`
FriendlyName string `json:"friendlyName"`
// HardwareVersion int `json:"hardwareVersion"`
IeeeAddr string `json:"ieeeAddr"`
// ManufacturerID int `json:"manufacturerID"`
// ManufacturerName string `json:"manufacturerName"`
Model string `json:"model"`
NetworkAddress int `json:"networkAddress"`
// PowerSource string `json:"powerSource"`
SoftwareBuildID string `json:"softwareBuildID"`
// StackVersion int `json:"stackVersion"`
Type string `json:"type"`
// ZclVersion int `json:"zclVersion"`
} `json:"device"`
// DeviceTemperature any `json:"device_temperature"`
// LastSeen int64 `json:"last_seen"`
// Linkquality int `json:"linkquality"`
// PowerOutageCount any `json:"power_outage_count"`
// Voltage any `json:"voltage"`
}

func main() {
events := NewEvents()

go z2msubscriber(events)
go api(events)
select {}
}

func api(events *Events) {
http.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) {
j, err := json.Marshal(events.Latest())
if err != nil {
log.Println(fmt.Errorf("failed to marshal events: %+v", err))
return
}
if _, err := w.Write(j); err != nil {
log.Println(fmt.Errorf("failed to write request response: %+v", err))
}
})

log.Fatal(http.ListenAndServe(":8080", nil))
}

func z2msubscriber(events *Events) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()

c, _, err := websocket.Dial(ctx, "wss://zigbee2mqtt.unicorn-alligator.ts.net/api", nil)
if err != nil {
log.Fatalf("failed to dial z2m: %+v", err)
}

c.SetReadLimit(32769 * 100)

for {
var event Z2mEvent
if err := wsjson.Read(ctx, c, &event); err != nil {
log.Println(fmt.Errorf("failed to read message: %w", err))
break
}

var sensor ContactSensorPayload
if err := json.Unmarshal(event.Payload, &sensor); err == nil {
if sensor.Contact == nil {
continue
}
if sensor.Device.Model != "MCCGQ11LM" {
continue
}

// is sensor!
fmt.Println("sensor!", sensor.Contact, sensor.Device.Model, sensor.Device.FriendlyName)
events.Add(Event{
Type: Contact,
Timestamp: time.Now(),
FriendlyName: sensor.Device.FriendlyName,
IeeeAddr: sensor.Device.IeeeAddr,
Data: ContactEvent{Contact: *sensor.Contact},
})
}
}

c.Close(websocket.StatusNormalClosure, "")
}

type Events struct {
events []Event
mx sync.RWMutex
fp *os.File
}

func NewEvents() *Events {
fp, err := os.OpenFile("events.jsonl", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Println(err)
}

return &Events{
events: []Event{},
mx: sync.RWMutex{},
fp: fp,
}
}

type EventType string

const (
Contact EventType = "contact"
)

type Event struct {
Type EventType
Timestamp time.Time
Data any
IeeeAddr string
FriendlyName string
}

type ContactEvent struct {
Contact bool
}

func (e *Events) Add(ev Event) {
e.mx.Lock()
e.events = append(e.events, ev)
e.mx.Unlock()

// persist
if js, err := json.Marshal(ev); err == nil {
_, err := e.fp.WriteString(string(js) + "\n")
if err != nil {
log.Println("failed to append to log:", err)
}
}
}

func (e *Events) Latest() []Event {
e.mx.RLock()
defer e.mx.RUnlock()

start := 0
if len(e.events) > 100 {
start = len(e.events) - 100
}

return e.events[start:]
}

0 comments on commit 7afe37b

Please sign in to comment.