-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:] | ||
} |