Compare commits

...

15 Commits

Author SHA1 Message Date
7205f9794a fix deploy
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline failed
2023-12-14 11:17:38 +01:00
985d05b0a0 fix deploy
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline failed
2023-12-14 11:14:50 +01:00
d8677d685b config
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-12-13 11:40:17 +01:00
2dd830907d emu stuff
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-12 16:39:22 +01:00
e99c9023a0 changes, still not working
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-12-11 22:35:36 +01:00
b3de6182b3 emu
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-12-11 16:50:28 +01:00
668fc20be9 that's better
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-08 17:17:05 +01:00
ea9db110a5 model parser
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-08 17:13:56 +01:00
4950b67afd basic ttn parsing
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-08 15:41:11 +01:00
77ac44742b ttn
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-07 19:52:00 +01:00
ad34f9b27b confgi
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-07 15:14:22 +01:00
65909becd6 Merge branch 'regex-jsonpath-separation'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-12-06 14:33:18 +01:00
d7c30ef0eb svej
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-06 14:32:50 +01:00
8085f8937e rename sve to sver, fixes in sver 2023-12-06 12:30:53 +01:00
22b1203ea8 not working jsonpath stuff
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-12-06 12:21:36 +01:00
23 changed files with 1278 additions and 72 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
config-*.json
src/udi/udi
src/udi/migrate_schema
tmp/

View File

@ -25,6 +25,8 @@ steps:
commands:
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- cat $CI_WORKSPACE/deployment/deploy-yml.tmpl | sed -e 's,%IMAGE%,'$IMAGE_NAME':'$CI_COMMIT_TAG',' | kubectl apply -f -
- for N in "udi udi-pg"; do
cat $CI_WORKSPACE/deployment/deploy-yml.tmpl | sed -e 's,%IMAGE%,'$IMAGE_NAME':'$CI_COMMIT_TAG',' | kubectl apply -n $N -f - ;
done
when:
- event: tag

View File

@ -2,7 +2,6 @@ apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: udi-archive
namespace: udi
spec:
accessModes:
- ReadWriteOnce
@ -15,11 +14,10 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: udi
namespace: udi
labels:
app: udi
annotations:
secret.reloader.stakater.com/reload: "udi-conf,udi-db-cred"
secret.reloader.stakater.com/reload: "udi-conf,udi-db-cred,mqtt-password"
spec:
replicas: 1
selector:
@ -33,15 +31,13 @@ spec:
containers:
- name: udi
image: %IMAGE%
env:
- name: UDI_CONF
valueFrom:
secretKeyRef:
name: udi-conf
key: UDI_CONF
envFrom:
- secretRef:
name: udi-db-cred
- secretRef:
name: mqtt-password
- secretRef:
name: udi-conf
volumeMounts:
- mountPath: /archive
name: udi-archive

View File

@ -5,9 +5,9 @@ if [ "$FILE" = "" ]; then
echo "give config file to load as first argument"
exit 1
fi
SECRET_NAME=$2
if [ "$SECRET_NAME" = "" ]; then
echo "give secret name to create/modify as second argument"
MQTT_PASSWORD=$2
if [ "$MQTT_PASSWORD" = "" ]; then
echo "give mqtt password as second argument"
exit 1
fi
NAMESPACE=$3
@ -16,7 +16,7 @@ if [ "$NAMESPACE" = "" ]; then
exit 1
fi
kubectl create secret generic $SECRET_NAME \
kubectl create secret generic udi-conf \
--from-literal=UDI_CONF="`cat $FILE`" \
-n $NAMESPACE \
--dry-run=client \
@ -24,13 +24,21 @@ kubectl create secret generic $SECRET_NAME \
--save-config | \
kubectl apply -f -
kubectl create secret generic mqtt-password \
--from-literal=MQTT_PASSWORD="$MQTT_PASSWORD" \
-n $NAMESPACE \
--dry-run=client \
-o yaml \
--save-config | \
kubectl apply -f -
. ~/Workspace/MyKubernetesEnv/ENVDB
DATABASE=udi
LOGIN=udi
DATABASE="udi-$NAMESPACE"
LOGIN="udi-$NAMESPACE"
PASSWORD=`openssl rand -base64 24`
psql <<EOF
ALTER USER $LOGIN WITH PASSWORD '$PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE $DATABASE TO $LOGIN;
ALTER USER "$LOGIN" WITH PASSWORD '$PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE "$DATABASE" TO "$LOGIN";
COMMIT;
EOF

View File

@ -37,3 +37,11 @@ create or replace view gas_v as
from measurements
where application = 'Gas' and
attributes->>'Status' = 'Ok';
create or replace view temperature_v as
select time,
cast(values->'Value'->>'value' as float) as temperature,
device
from measurements
where application in ('Temperature Multisensor', 'Temperature Shelly Plus HT');

6
src/udi/ENVDB.udi Normal file
View File

@ -0,0 +1,6 @@
PGUSER="udi"
PGHOST=`kubectl get services traefik -n system -o jsonpath="{.status.loadBalancer.ingress[0].ip}"`
PGPASSWORD=`kubectl get secrets udi-db-cred -n udi -o jsonpath="{.data.PGPASSWORD}" | base64 --decode`
PGSSLMODE=require
PGDATABASE="uditest"
export PGUSER PGHOST PGPASSWORD PGSSLMODE PGDATABASE

View File

@ -0,0 +1,105 @@
{
"mqtt": {
"broker": "mqtt://emqx01-anonymous-cluster-internal.broker.svc.cluster.local:1883",
"tlsEnable": "false"
},
"topicMappings": [
{
"topics": [ "IoT/PV/Values" ],
"handler": "PV",
"id": "PV",
"config": {
"databaseConnStr": "",
"attributes": {
}
}
},
{
"topics": [ "IoT/MBGW3/Measurement" ],
"handler": "MBGW3",
"id": "MBGW3",
"config": {
"databaseConnStr": "",
"attributes": {
}
}
},
{
"topics": [ "IoT/OneWireGW/Bus 1/#" ],
"handler": "SVER",
"id": "SVER0",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Temperature Heating",
"payloadRegex": "(\\d+(\\.\\d+)?)\\s*([^0-9\\s]\\S*)",
"deviceFrom": "topic",
"devicePart": "3",
"valueFrom": "payload",
"valuePart": "1",
"unitFrom": "payload",
"unitPart": "3"
}
}
},
{
"topics": [ "NR/Multisensor/+/Temperatur" ],
"handler": "SVEJ",
"id": "SVEJ0",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Temperature Multisensor",
"deviceSelector": "T:2",
"valueSelector": "J:$.CurrentTemperature",
"unitSelector": "C:°C"
}
}
},
{
"topics": [ "NR/Multisensor/+/Feuchte" ],
"handler": "SVEJ",
"id": "SVEJ1",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Humidity Multisensor",
"deviceSelector": "T:2",
"valueSelector": "J:$.CurrentRelativeHumidity",
"unitSelector": "C:%"
}
}
},
{
"topics": [ "shellyplusht/+/status/temperature:0" ],
"handler": "SVEJ",
"id": "SVEJ2",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Temperature Shelly Plus HT",
"deviceSelector": "T:1",
"valueSelector": "J:$.tC",
"unitSelector": "C:°C"
}
}
},
{
"topics": [ "shellyplusht/+/status/humidity:0" ],
"handler": "SVEJ",
"id": "SVE4",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Humidity Shelly Plus HT",
"deviceSelector": "T:1",
"valueSelector": "J:$.rh",
"unitSelector": "C:%"
}
}
}
],
"archiver": {
"dir": "/archive"
}
}

105
src/udi/config-iot.json Normal file
View File

@ -0,0 +1,105 @@
{
"mqtt": {
"broker": "mqtt://172.23.1.102:1883",
"tlsEnable": "false"
},
"topicMappings": [
{
"topics": [ "IoT/PV/Values" ],
"handler": "PV",
"id": "PV",
"config": {
"databaseConnStr": "",
"attributes": {
}
}
},
{
"topics": [ "IoT/MBGW3/Measurement" ],
"handler": "MBGW3",
"id": "MBGW3",
"config": {
"databaseConnStr": "",
"attributes": {
}
}
},
{
"topics": [ "IoT/OneWireGW/Bus 1/#" ],
"handler": "SVER",
"id": "SVER0",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Temperature Heating",
"payloadRegex": "(\\d+(\\.\\d+)?)\\s*([^0-9\\s]\\S*)",
"deviceFrom": "topic",
"devicePart": "3",
"valueFrom": "payload",
"valuePart": "1",
"unitFrom": "payload",
"unitPart": "3"
}
}
},
{
"topics": [ "NR/Multisensor/+/Temperatur" ],
"handler": "SVEJ",
"id": "SVEJ0",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Temperature Multisensor",
"deviceSelector": "T:2",
"valueSelector": "J:$.CurrentTemperature",
"unitSelector": "C:°C"
}
}
},
{
"topics": [ "NR/Multisensor/+/Feuchte" ],
"handler": "SVEJ",
"id": "SVEJ1",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Humidity Multisensor",
"deviceSelector": "T:2",
"valueSelector": "J:$.CurrentRelativeHumidity",
"unitSelector": "C:%"
}
}
},
{
"topics": [ "shellyplusht/+/status/temperature:0" ],
"handler": "SVEJ",
"id": "SVEJ2",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Temperature Shelly Plus HT",
"deviceSelector": "T:1",
"valueSelector": "J:$.tC",
"unitSelector": "C:°C"
}
}
},
{
"topics": [ "shellyplusht/+/status/humidity:0" ],
"handler": "SVEJ",
"id": "SVE4",
"config": {
"databaseConnStr": "",
"attributes": {
"application": "Humidity Shelly Plus HT",
"deviceSelector": "T:1",
"valueSelector": "J:$.rh",
"unitSelector": "C:%"
}
}
}
],
"archiver": {
"dir": "./tmp/udi"
}
}

21
src/udi/config-iot2.json Normal file
View File

@ -0,0 +1,21 @@
{
"mqtt": {
"broker": "mqtt://172.23.1.102:1883",
"tlsEnable": "false"
},
"topicMappings": [
{
"topics": [ "ttn/#" ],
"handler": "TTN",
"id": "TTN0",
"config": {
"databaseConnStr": "",
"attributes": {
}
}
}
],
"archiver": {
"dir": "./tmp/udi"
}
}

View File

@ -0,0 +1,23 @@
{
"mqtt": {
"broker": "ssl://eu1.cloud.thethings.network:8883",
"username": "com-passavant-geiger-poc@ttn",
"password": "ENV",
"tlsEnable": "true"
},
"topicMappings": [
{
"topics": [ "v3/com-passavant-geiger-poc@ttn/devices/#" ],
"handler": "TTN",
"id": "TTN0",
"config": {
"databaseConnStr": "",
"attributes": {
}
}
}
],
"archiver": {
"dir": "/archive"
}
}

23
src/udi/config-pg.json Normal file
View File

@ -0,0 +1,23 @@
{
"mqtt": {
"broker": "ssl://eu1.cloud.thethings.network:8883",
"username": "com-passavant-geiger-poc@ttn",
"password": "ENV",
"tlsEnable": "true"
},
"topicMappings": [
{
"topics": [ "v3/com-passavant-geiger-poc@ttn/devices/#" ],
"handler": "TTN",
"id": "TTN0",
"config": {
"databaseConnStr": "",
"attributes": {
}
}
}
],
"archiver": {
"dir": "./tmp/udi"
}
}

29
src/udi/config-ttn.json Normal file
View File

@ -0,0 +1,29 @@
{
"mqtt": {
"broker": "mqtt://172.23.1.102:1883",
"tlsEnable": "false"
},
"topicMappings": [
{
"topics": [ "v3/de-hottis-level-monitoring@ttn/devices/+/up" ],
"handler": "TTN"
}
],
"handlers": [
{
"name": "TTN",
"databaseConnStr": "",
"attributes": {
}
},
{
"name": "IoT",
"databaseConnStr": "",
"attributes": {
}
}
],
"archiver": {
"dir": "./tmp/udi"
}
}

View File

@ -35,5 +35,9 @@ func LoadConfiguration() {
if err != nil {
log.Fatalf("Unable to parse configuration: %s", err)
}
if Config.Mqtt.Password == "ENV" {
Config.Mqtt.Password = os.Getenv("MQTT_PASSWORD")
}
}

View File

@ -35,8 +35,8 @@ type DeviceType struct {
type Device struct {
gorm.Model
Label string `gorm:"not null"`
ApplicationID int `gorm:"not null"`
Label string `gorm:"not null;uniqueIndex:idx_label_application_id"`
ApplicationID int `gorm:"not null;uniqueIndex:idx_label_application_id"`
Application Application
DeviceTypeID int `gorm:"not null"`
DeviceType DeviceType

View File

@ -4,6 +4,7 @@ package database
import (
"log"
//"time"
"fmt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
@ -27,13 +28,13 @@ func NewDatabaseHandle(dsn string) *DatabaseHandle {
return &db
}
func (dbh *DatabaseHandle) StoreMeasurement(measurement *Measurement) {
if ! dbh.initialized {
func (self *DatabaseHandle) StoreMeasurement(measurement *Measurement) {
if ! self.initialized {
log.Printf("Database connection not initialized, can not store, measurement %s lost", measurement)
return
}
result := dbh.dbh.Create(measurement)
result := self.dbh.Create(measurement)
if result.Error != nil {
log.Printf("Unable to insert, measurement %s lost, error: %s", measurement, result.Error)
return
@ -42,4 +43,27 @@ func (dbh *DatabaseHandle) StoreMeasurement(measurement *Measurement) {
log.Println("Successfully stored measurement")
}
func (self *DatabaseHandle) GetDeviceByLabelAndApplication(applicationLabel string, deviceLabel string) (*Device, error) {
if ! self.initialized {
err := fmt.Errorf("Database connection not initialized")
return nil, err
}
var device Device
result := self.dbh.
Preload("Application").
Preload("DeviceType").
Joins("JOIN applications ON devices.application_id = applications.id").
Where("devices.label = ? AND applications.label = ?", deviceLabel, applicationLabel).
First(&device)
if result.Error != nil {
err := fmt.Errorf("Query failed: %s", result.Error)
return nil, err
}
return &device, nil
}

View File

@ -12,7 +12,8 @@ import "udi/handlers/ttn"
import "udi/handlers/iot"
import "udi/handlers/pv"
import "udi/handlers/mbgw3"
import "udi/handlers/sve"
import "udi/handlers/sver"
import "udi/handlers/svej"
var handlerMap map[string]handler.Handler = make(map[string]handler.Handler)
@ -35,8 +36,10 @@ func InitDispatcher() {
factory = pv.NewPvHandler
case "MBGW3":
factory = mbgw3.NewMbgw3Handler
case "SVE":
factory = sve.NewSveHandler
case "SVER":
factory = sver.NewSverHandler
case "SVEJ":
factory = svej.NewSvejHandler
default:
factory = nil
log.Printf("No handler %s found, ignore mapping", mapping.Handler)

View File

@ -0,0 +1,169 @@
package svej
import (
"log"
"time"
"strconv"
"strings"
"fmt"
"github.com/oliveagle/jsonpath"
"encoding/json"
"udi/config"
"udi/handlers/handler"
"udi/database"
)
var idSeq int = 0
type SingleValueExtractorJsonpathHandler struct {
id int
ready bool
application string
deviceSelector string
valueSelector string
unitSelector string
deviceJsonpath *jsonpath.Compiled
valueJsonpath *jsonpath.Compiled
unitJsonpath *jsonpath.Compiled
dbh *database.DatabaseHandle
}
/*
Valid values for selectors:
J:JsonpathExpression
T:TopicPartIndex
C:ConstantValue
*/
func NewSvejHandler(config config.HandlerConfigT) handler.Handler {
t := &SingleValueExtractorJsonpathHandler {
id: idSeq,
ready: false,
}
idSeq += 1
if config.Attributes["application"] == "" {
log.Println("Error: application not configured")
return t
}
t.application = config.Attributes["application"]
t.deviceSelector = config.Attributes["deviceSelector"]
if t.deviceSelector[:2] == "J:" {
jp, err := jsonpath.Compile(t.deviceSelector[2:])
if err != nil {
log.Printf("Unable to compile deviceJsonpath: %s, %s", t.deviceSelector[2:], err)
return t
}
t.deviceJsonpath = jp
}
t.valueSelector = config.Attributes["valueSelector"]
if t.valueSelector[:2] == "J:" {
jp, err := jsonpath.Compile(t.valueSelector[2:])
if err != nil {
log.Printf("Unable to compile valueJsonpath: %s, %s", t.valueSelector[2:], err)
return t
}
t.valueJsonpath = jp
}
t.unitSelector = config.Attributes["unitSelector"]
if t.unitSelector[:2] == "J:" {
jp, err := jsonpath.Compile(t.unitSelector[2:])
if err != nil {
log.Printf("Unable to compile unitJsonpath: %s, %s", t.unitSelector[2:], err)
return t
}
t.unitJsonpath = jp
}
t.ready = true
t.dbh = database.NewDatabaseHandle(config.DatabaseConnStr)
return t
}
func (self *SingleValueExtractorJsonpathHandler) GetId() string {
return fmt.Sprintf("SVE%d", self.id)
}
func lost(msg string, message handler.MessageT) {
log.Printf("Error: %s, message %s is lost", msg, message)
}
func extractionHelper(subTopics []string, jPayload interface{}, selector string, jp *jsonpath.Compiled) (string, error) {
var res string
switch selector[:2] {
case "J:":
r, e := jp.Lookup(jPayload)
if e != nil {
return "", fmt.Errorf("jp.Lookup failed with %s", e)
}
res = fmt.Sprint(r)
case "T:":
i, e := strconv.Atoi(selector[2:])
if e != nil {
return "", fmt.Errorf("Atoi failed with %s", e)
}
if i >= len(subTopics) {
return "", fmt.Errorf("not enough subtopics")
}
res = subTopics[i]
case "C:":
res = selector[2:]
default:
return "", fmt.Errorf("Invalid selector: %s", selector[:2])
}
return res, nil
}
func (self *SingleValueExtractorJsonpathHandler) Handle(message handler.MessageT) {
if ! self.ready {
log.Println("Handler is not marked as ready, message %s is lost", message)
return
}
log.Printf("Handler SingleValueExtractorJsonpath %d processing %s -> %s", self.id, message.Topic, message.Payload)
var measurement database.Measurement
measurement.Time = time.Now()
measurement.Application = self.application
subTopics := strings.Split(message.Topic, "/")
//log.Printf("Subtopics: %s", strings.Join(subTopics, ", "))
var jPayload interface{}
err := json.Unmarshal([]byte(message.Payload), &jPayload)
if err != nil {
lost(fmt.Sprintf("Unable to unmarshal payload: %s", err), message)
return
}
device, err1 := extractionHelper(subTopics, jPayload, self.deviceSelector, self.deviceJsonpath)
if err1 != nil {
lost(fmt.Sprintf("Device extraction failed with %s", err1), message)
return
}
value, err2 := extractionHelper(subTopics, jPayload, self.valueSelector, self.valueJsonpath)
if err2 != nil {
lost(fmt.Sprintf("Value extraction failed with %s", err2), message)
return
}
unit, err3 := extractionHelper(subTopics, jPayload, self.unitSelector, self.unitJsonpath)
if err3 != nil {
lost(fmt.Sprintf("Unit extraction failed with %s", err3), message)
return
}
measurement.Device = device
var variable database.VariableType
variable.Label = ""
variable.Variable = ""
variable.Unit = unit
variable.Value = value
measurement.Values = make(map[string]database.VariableType)
measurement.Values["Value"] = variable
log.Printf("Prepared measurement item: %s", measurement)
self.dbh.StoreMeasurement(&measurement)
}

View File

@ -1,4 +1,4 @@
package sve
package sver
import (
"log"
@ -7,6 +7,7 @@ import (
"strings"
"regexp"
"fmt"
"reflect"
"encoding/json"
"github.com/oliveagle/jsonpath"
"udi/config"
@ -16,7 +17,7 @@ import (
var idSeq int = 0
type SingleValueExtractorHandler struct {
type SingleValueExtractorRegexHandler struct {
id int
ready bool
config localConfig
@ -26,8 +27,7 @@ type SingleValueExtractorHandler struct {
}
const TOPIC_SEL = "topic"
const PAYLOAD_REGEX_SEL = "payload-regex"
const PAYLOAD_JSONPATH_SEL = "payload-jsonpath"
const PAYLOAD_SEL = "payload"
const PAYLOAD_FULL_SEL = "payload-full"
const CONSTANT_SEL = "constant"
@ -44,8 +44,8 @@ type localConfig struct {
}
func NewSveHandler(config config.HandlerConfigT) handler.Handler {
t := &SingleValueExtractorHandler {
func NewSverHandler(config config.HandlerConfigT) handler.Handler {
t := &SingleValueExtractorRegexHandler {
id: idSeq,
ready: false,
}
@ -77,7 +77,7 @@ func NewSveHandler(config config.HandlerConfigT) handler.Handler {
t.payloadJsonpath = nil
}
if config.Attributes["deviceFrom"] != TOPIC_SEL && config.Attributes["deviceFrom"] != PAYLOAD_REGEX_SEL && config.Attributes["deviceFrom"] != CONSTANT_SEL {
if config.Attributes["deviceFrom"] != TOPIC_SEL && config.Attributes["deviceFrom"] != PAYLOAD_SEL && config.Attributes["deviceFrom"] != CONSTANT_SEL {
log.Printf("Error: invalid value %s for deviceFrom", config.Attributes["deviceFrom"])
return t
}
@ -93,13 +93,13 @@ func NewSveHandler(config config.HandlerConfigT) handler.Handler {
// empty device is valid
localConfig.device = config.Attributes["device"]
if config.Attributes["valueFrom"] != PAYLOAD_REGEX_SEL && config.Attributes["valueFrom"] != PAYLOAD_JSONPATH_SEL && config.Attributes["valueFrom"] != PAYLOAD_FULL_SEL {
if config.Attributes["valueFrom"] != PAYLOAD_SEL && config.Attributes["valueFrom"] != PAYLOAD_FULL_SEL {
log.Printf("Error: invalid value %s for valueFrom", config.Attributes["valueFrom"])
return t
}
localConfig.valueFrom = config.Attributes["valueFrom"]
if config.Attributes["valueFrom"] == PAYLOAD_REGEX_SEL {
if config.Attributes["valueFrom"] == PAYLOAD_SEL {
valuePart, err2 := strconv.Atoi(config.Attributes["valuePart"])
if err2 != nil {
log.Printf("Error: unable to convert valuePart to number: %s", err2)
@ -108,13 +108,13 @@ func NewSveHandler(config config.HandlerConfigT) handler.Handler {
localConfig.valuePart = valuePart
}
if config.Attributes["unitFrom"] != TOPIC_SEL && config.Attributes["unitFrom"] != PAYLOAD_REGEX_SEL && config.Attributes["unitFrom"] != CONSTANT_SEL {
if config.Attributes["unitFrom"] != PAYLOAD_SEL && config.Attributes["unitFrom"] != CONSTANT_SEL {
log.Printf("Error: invalid value %s for unitFrom", config.Attributes["unitFrom"])
return t
}
localConfig.unitFrom = config.Attributes["unitFrom"]
if config.Attributes["unitFrom"] != CONSTANT_SEL {
if config.Attributes["unitFrom"] == PAYLOAD_SEL {
unitPart, err3 := strconv.Atoi(config.Attributes["unitPart"])
if err3 != nil {
log.Printf("Error: unable to convert unitPart to number: %s", err3)
@ -133,7 +133,7 @@ func NewSveHandler(config config.HandlerConfigT) handler.Handler {
return t
}
func (self *SingleValueExtractorHandler) GetId() string {
func (self *SingleValueExtractorRegexHandler) GetId() string {
return fmt.Sprintf("SVE%d", self.id)
}
@ -141,7 +141,7 @@ func lost(msg string, message handler.MessageT) {
log.Printf("Error: %s, message %s is lost", msg, message)
}
func (self *SingleValueExtractorHandler) Handle(message handler.MessageT) {
func (self *SingleValueExtractorRegexHandler) Handle(message handler.MessageT) {
if ! self.ready {
log.Println("Handler is not marked as ready, message %s is lost", message)
return
@ -160,6 +160,16 @@ func (self *SingleValueExtractorHandler) Handle(message handler.MessageT) {
payloadMatches = self.payloadRegex.FindStringSubmatch(message.Payload)
//log.Printf("Matches: %s", strings.Join(payloadMatches, ", "))
}
if self.payloadJsonpath != nil {
var jsonData interface{}
json.Unmarshal([]byte(message.Payload), &jsonData)
p, err := self.payloadJsonpath.Lookup(jsonData)
if err != nil {
lost(fmt.Sprintf("jsonpath error: %s", err), message)
return
}
log.Printf("XXXX: %s", reflect.TypeOf(p))
}
switch self.config.deviceFrom {
case TOPIC_SEL:
@ -168,12 +178,12 @@ func (self *SingleValueExtractorHandler) Handle(message handler.MessageT) {
return
}
measurement.Device = subTopics[self.config.devicePart]
case PAYLOAD_REGEX_SEL:
if self.payloadRegex == nil {
lost("no payloadRegex defined, devicePart can't be used", message)
case PAYLOAD_SEL:
if self.payloadRegex == nil && self.payloadJsonpath == nil {
lost("no payloadRegex or payloadJsonpath defined, devicePart can't be used", message)
return
}
if self.config.devicePart >= len(subTopics) {
if self.config.devicePart >= len(payloadMatches) {
lost("devicePart out of range", message)
return
}
@ -188,9 +198,9 @@ func (self *SingleValueExtractorHandler) Handle(message handler.MessageT) {
variable.Variable = ""
switch self.config.valueFrom {
case PAYLOAD_REGEX_SEL:
if self.payloadRegex == nil {
lost("no payloadRegex defined, valuePart can't be used", message)
case PAYLOAD_SEL:
if self.payloadRegex == nil && self.payloadJsonpath == nil {
lost("no payloadRegex or payloadJsonpath defined, valuePart can't be used", message)
return
}
if self.config.valuePart >= len(payloadMatches) {
@ -198,36 +208,17 @@ func (self *SingleValueExtractorHandler) Handle(message handler.MessageT) {
return
}
variable.Value = payloadMatches[self.config.valuePart]
case PAYLOAD_JSONPATH_SEL:
if self.payloadJsonpath == nil {
lost("no payloadJsonpath defined, valuePart can't be used", message)
return
}
var jsonData interface{}
json.Unmarshal([]byte(message.Payload), &jsonData)
result, err := self.payloadJsonpath.Lookup(jsonData)
if err != nil {
lost(fmt.Sprintf("jsonpath error: %s", err), message)
return
}
variable.Value = result
case PAYLOAD_FULL_SEL:
variable.Value = message.Payload
}
switch self.config.unitFrom {
case TOPIC_SEL:
if self.config.unitPart >= len(subTopics) {
lost("unitPart out of range", message)
case PAYLOAD_SEL:
if self.payloadRegex == nil && self.payloadJsonpath == nil {
lost("no payloadRegex or payloadJsonpath defined, unitPart can't be used", message)
return
}
variable.Unit = subTopics[self.config.unitPart]
case PAYLOAD_REGEX_SEL:
if self.payloadRegex == nil {
lost("no payloadRegex defined, unitPart can't be used", message)
return
}
if self.config.unitPart >= len(subTopics) {
if self.config.unitPart >= len(payloadMatches) {
lost("unitPart out of range", message)
return
}

View File

@ -0,0 +1,84 @@
{
"end_device_ids": {
"device_id": "eui-a840419641867eb5",
"application_ids": {
"application_id": "de-hottis-level-monitoring"
},
"dev_eui": "A840419641867EB5",
"join_eui": "A840410000000101",
"dev_addr": "260B91F9"
},
"correlation_ids": [
"gs:uplink:01HH1R112BNDQQ52N9FVV0TKPW"
],
"received_at": "2023-12-07T08:59:05.369293395Z",
"uplink_message": {
"session_key_id": "AYa9JUhNJp00t+hKqkQUog==",
"f_port": 2,
"f_cnt": 25665,
"frm_payload": "DAoAaQDJAA==",
"decoded_payload": {
"Bat": 3.082,
"DALARM_count": 0,
"Distance_alarm": 0,
"Interrupt_alarm": 0,
"dis1": 105,
"dis2": 201
},
"rx_metadata": [
{
"gateway_ids": {
"gateway_id": "eui-00005813d35e3021",
"eui": "00005813D35E3021"
},
"timestamp": 1141271036,
"rssi": -100,
"channel_rssi": -100,
"snr": 7.3,
"location": {
"latitude": 52.17065267448476,
"longitude": 7.629437184774199,
"source": "SOURCE_REGISTRY"
},
"uplink_token": "CiIKIAoUZXVpLTAwMDA1ODEzZDM1ZTMwMjESCAAAWBPTXjAhEPzTmaAEGgsI2ZLGqwYQnfLnTSDggLjIm5IF",
"channel_index": 6,
"received_at": "2023-12-07T08:59:05.163182877Z"
}
],
"settings": {
"data_rate": {
"lora": {
"bandwidth": 125000,
"spreading_factor": 7,
"coding_rate": "4/5"
}
},
"frequency": "868300000",
"timestamp": 1141271036
},
"received_at": "2023-12-07T08:59:05.163964824Z",
"consumed_airtime": "0.056576s",
"locations": {
"user": {
"latitude": 52.1707216912195,
"longitude": 7.63066603211241,
"altitude": 39,
"source": "SOURCE_REGISTRY"
}
},
"version_ids": {
"brand_id": "dragino",
"model_id": "lmds200",
"hardware_version": "_unknown_hw_version_",
"firmware_version": "1.0",
"band_id": "EU_863_870"
},
"network_ids": {
"net_id": "000013",
"ns_id": "EC656E0000000181",
"tenant_id": "ttn",
"cluster_id": "eu1",
"cluster_address": "eu1.cloud.thethings.network"
}
}
}

View File

@ -0,0 +1,124 @@
{
"end_device_ids": {
"device_id": "eui-102ceffffe01089c",
"application_ids": {
"application_id": "com-passavant-geiger-poc"
},
"dev_eui": "102CEFFFFE01089C",
"join_eui": "102CEF0000000000",
"dev_addr": "260B0E1A"
},
"correlation_ids": [
"gs:uplink:01HHF7YF14Y7HQBF9D8N8D20ZM"
],
"received_at": "2023-12-12T14:47:26.197129491Z",
"uplink_message": {
"session_key_id": "AYxJcJyrJgr7XiIUdO3EBA==",
"f_port": 1,
"f_cnt": 11738,
"frm_payload": "7HF4ZQsAAAAADAAAAAANAAAAAA4AAAAAFwAYABkA8ADc",
"decoded_payload": {
"Active Power L1": {
"cfgphase": 1,
"unit": "W",
"value": 0
},
"Active Power L123": {
"unit": "W",
"value": 0
},
"Active Power L2": {
"cfgphase": 2,
"unit": "W",
"value": 0
},
"Active Power L3": {
"cfgphase": 3,
"unit": "W",
"value": 0
},
"Powerfactor L1": {
"cfgphase": 1,
"unit": "Cos",
"value": 0,
"value_raw": 0
},
"Powerfactor L2": {
"cfgphase": 2,
"unit": "Cos",
"value": 0,
"value_raw": 0
},
"Powerfactor L3": {
"cfgphase": 3,
"unit": "Cos",
"value": 0,
"value_raw": 0
},
"errorcode": {
"CTRatioChange": false,
"ImpulseRatioChange": false,
"ImpulseWidthChange": false,
"LogbookFull": false,
"PowerFail": false,
"TimeChanged": false,
"VTRatioChange": false,
"value": 0
},
"medium": {
"desc": "Electricity",
"type": 1
},
"timeStamp": 1702392300
},
"rx_metadata": [
{
"gateway_ids": {
"gateway_id": "eui-b827ebfffe8b01dd",
"eui": "B827EBFFFE8B01DD"
},
"time": "2023-12-12T14:47:25.951668977Z",
"timestamp": 3710351237,
"rssi": -89,
"channel_rssi": -89,
"snr": 14,
"location": {
"latitude": 51.404164272478724,
"longitude": 7.060088589208832,
"source": "SOURCE_REGISTRY"
},
"uplink_token": "CiIKIAoUZXVpLWI4MjdlYmZmZmU4YjAxZGQSCLgn6//+iwHdEIXvnekNGgwI/eThqwYQpoGI1wMgiL+rkf5r",
"received_at": "2023-12-12T14:47:25.969479115Z"
}
],
"settings": {
"data_rate": {
"lora": {
"bandwidth": 125000,
"spreading_factor": 7,
"coding_rate": "4/5"
}
},
"frequency": "867100000",
"timestamp": 3710351237,
"time": "2023-12-12T14:47:25.951668977Z"
},
"received_at": "2023-12-12T14:47:25.988957776Z",
"confirmed": true,
"consumed_airtime": "0.092416s",
"version_ids": {
"brand_id": "emu",
"model_id": "emu-prof-ii",
"hardware_version": "1.0",
"firmware_version": "1.0",
"band_id": "EU_863_870"
},
"network_ids": {
"net_id": "000013",
"ns_id": "EC656E0000000181",
"tenant_id": "ttn",
"cluster_id": "eu1",
"cluster_address": "eu1.cloud.thethings.network"
}
}
}

View File

@ -0,0 +1,93 @@
{
"end_device_ids": {
"device_id": "eui-102ceffffe01089c",
"application_ids": {
"application_id": "com-passavant-geiger-poc"
},
"dev_eui": "102CEFFFFE01089C",
"join_eui": "102CEF0000000000",
"dev_addr": "260B0E1A"
},
"correlation_ids": [
"gs:uplink:01HH53T0RPG7QFT6N267J3Q2PA"
],
"received_at": "2023-12-08T16:22:41.900339885Z",
"uplink_message": {
"session_key_id": "AYxJcJyrJgr7XiIUdO3EBA==",
"f_port": 2,
"f_cnt": 374,
"frm_payload": "BEFzZSQAAAAAAAAAACYAAAAAAAAAACgAAAAAAAAAACoAAAAAAAAAAMk=",
"decoded_payload": {
"Active Energy Export T1 64bit": {
"unit": "Wh",
"value": 0
},
"Active Energy Import T1 64bit": {
"unit": "Wh",
"value": 0
},
"Reactive Energy Export T1 64bit": {
"unit": "varh",
"value": 0
},
"Reactive Energy Import T1 64bit": {
"unit": "varh",
"value": 0
},
"medium": {
"desc": "Electricity",
"type": 1
},
"timeStamp": 1702052100
},
"rx_metadata": [
{
"gateway_ids": {
"gateway_id": "eui-b827ebfffe8b01dd",
"eui": "B827EBFFFE8B01DD"
},
"time": "2023-12-08T16:22:40.969499111Z",
"timestamp": 383972090,
"rssi": -93,
"channel_rssi": -93,
"snr": 12.75,
"location": {
"latitude": 51.404164272478724,
"longitude": 7.060088589208832,
"source": "SOURCE_REGISTRY"
},
"uplink_token": "CiIKIAoUZXVpLWI4MjdlYmZmZmU4YjAxZGQSCLgn6//+iwHdEPrli7cBGgwI0YXNqwYQoteqxwIgkMGUtJb+pwI=",
"received_at": "2023-12-08T16:22:41.668146811Z"
}
],
"settings": {
"data_rate": {
"lora": {
"bandwidth": 125000,
"spreading_factor": 7,
"coding_rate": "4/5"
}
},
"frequency": "867700000",
"timestamp": 383972090,
"time": "2023-12-08T16:22:40.969499111Z"
},
"received_at": "2023-12-08T16:22:41.687510109Z",
"confirmed": true,
"consumed_airtime": "0.102656s",
"version_ids": {
"brand_id": "emu",
"model_id": "emu-prof-ii",
"hardware_version": "1.0",
"firmware_version": "1.0",
"band_id": "EU_863_870"
},
"network_ids": {
"net_id": "000013",
"ns_id": "EC656E0000000181",
"tenant_id": "ttn",
"cluster_id": "eu1",
"cluster_address": "eu1.cloud.thethings.network"
}
}
}

View File

@ -0,0 +1,259 @@
package emuProfIILoRaCfg1
// provisioning device with
// f_port=1: 01 00 0A 0B 0C 0D 0E 17 18 19 F0 4C
// 01 00 08 0B 0C 0D 0E 17 18 19 F0 BE
// f_port=2: 01 00 0A 24 26 28 2A 7A
// 01 00 08 24 26 28 2A BE
import (
"fmt"
//"log"
"encoding/json"
"udi/database"
)
/*
{
"Active Energy Export T1 64bit": {
"unit": "Wh",
"value": 0
},
"Active Energy Import T1 64bit": {
"unit": "Wh",
"value": 0
},
"Reactive Energy Export T1 64bit": {
"unit": "varh",
"value": 0
},
"Reactive Energy Import T1 64bit": {
"unit": "varh",
"value": 0
},
"medium": {
"desc": "Electricity",
"type": 1
},
"timeStamp": 1702052100
}
*/
type emuMessage2 struct {
ActiveEnergyExport struct {
Value int `json:"value"`
Unit string `json:"unit"`
} `json:"Active Energy Export T1 64bit"`
ReactiveEnergyExport struct {
Value int `json:"value"`
Unit string `json:"unit"`
} `json:"Reactive Energy Export T1 64bit"`
ActiveEnergyImport struct {
Value int `json:"value"`
Unit string `json:"unit"`
} `json:"Active Energy Import T1 64bit"`
ReactiveEnergyImport struct {
Value int `json:"value"`
Unit string `json:"unit"`
} `json:"Reactive Energy Import T1 64bit"`
Medium struct {
Desc string `json:"desc"`
Type int `json:"type"`
} `json:"medium"`
Timestamp int `json:"timestamp"`
}
/*
{
"Active Power L1": {
"cfgphase": 1,
"unit": "W",
"value": 0
},
"Active Power L123": {
"unit": "W",
"value": 0
},
"Active Power L2": {
"cfgphase": 2,
"unit": "W",
"value": 0
},
"Active Power L3": {
"cfgphase": 3,
"unit": "W",
"value": 0
},
"Powerfactor L1": {
"cfgphase": 1,
"unit": "Cos",
"value": 0,
"value_raw": 0
},
"Powerfactor L2": {
"cfgphase": 2,
"unit": "Cos",
"value": 0,
"value_raw": 0
},
"Powerfactor L3": {
"cfgphase": 3,
"unit": "Cos",
"value": 0,
"value_raw": 0
},
"errorcode": {
"CTRatioChange": false,
"ImpulseRatioChange": false,
"ImpulseWidthChange": false,
"LogbookFull": false,
"PowerFail": false,
"TimeChanged": false,
"VTRatioChange": false,
"value": 0
},
"medium": {
"desc": "Electricity",
"type": 1
},
"timeStamp": 1702392300
}
*/
type emuMessage1 struct {
ActivePowerL1 struct {
CfgPhase int `json:"cfgphase"`
Unit string `json:"unit"`
Value int `json:"value"`
} `json:"Active Power L1"`
ActivePowerL2 struct {
CfgPhase int `json:"cfgphase"`
Unit string `json:"unit"`
Value int `json:"value"`
} `json:"Active Power L2"`
ActivePowerL3 struct {
CfgPhase int `json:"cfgphase"`
Unit string `json:"unit"`
Value int `json:"value"`
} `json:"Active Power L3"`
ActivePowerL123 struct {
Unit string `json:"unit"`
Value int `json:"value"`
} `json:"Active Power L123"`
PowerfactorL1 struct {
CfgPhase int `json:"cfgphase"`
Unit string `json:"unit"`
Value float32 `json:"value"`
ValueRaw float32 `json:"value_raw"`
} `json:"Powerfactor L1"`
PowerfactorL2 struct {
CfgPhase int `json:"cfgphase"`
Unit string `json:"unit"`
Value float32 `json:"value"`
ValueRaw float32 `json:"value_raw"`
} `json:"Powerfactor L2"`
PowerfactorL3 struct {
CfgPhase int `json:"cfgphase"`
Unit string `json:"unit"`
Value float32 `json:"value"`
ValueRaw float32 `json:"value_raw"`
} `json:"Powerfactor L3"`
ErrorCode struct {
CTRatioChange bool `json:"CTRatioChange"`
ImpulseRatioChange bool `json:"ImpulseRatioChange"`
ImpulseWidthChange bool `json:"ImpulseWidthChange"`
LogbookFull bool `json:"LogbookFull"`
PowerFail bool `json:"PowerFail"`
TimeChanged bool `json:"TimeChanged"`
VTRatioChange bool `json:"VTRatioChange"`
Value int `json:"value"`
} `json:"errorcode"`
Medium struct {
Desc string `json:"desc"`
Type int `json:"type"`
} `json:"medium"`
Timestamp int `json:"timestamp"`
}
func Parse(fPort int, decodedPayload []byte) (map[string]database.VariableType, error) {
//log.Printf("Parse input: %d, %s", fPort, decodedPayload)
switch fPort {
case 1:
var emuMessage1 emuMessage1
err := json.Unmarshal(decodedPayload, &emuMessage1)
if err != nil {
return nil, fmt.Errorf("Unable to parse payload, fPort %d, error %s", fPort, err)
}
variables := make(map[string]database.VariableType)
variables["ActivePowerL1"] = database.VariableType {
Variable: "ActivePowerL1",
Unit: emuMessage1.ActivePowerL1.Unit,
Value: emuMessage1.ActivePowerL1.Value,
}
variables["ActivePowerL2"] = database.VariableType {
Variable: "ActivePowerL2",
Unit: emuMessage1.ActivePowerL2.Unit,
Value: emuMessage1.ActivePowerL2.Value,
}
variables["ActivePowerL3"] = database.VariableType {
Variable: "ActivePowerL3",
Unit: emuMessage1.ActivePowerL3.Unit,
Value: emuMessage1.ActivePowerL3.Value,
}
variables["ActivePowerL123"] = database.VariableType {
Variable: "ActivePowerL123",
Unit: emuMessage1.ActivePowerL123.Unit,
Value: emuMessage1.ActivePowerL123.Value,
}
variables["PowerfactorL1"] = database.VariableType {
Variable: "PowerfactorL1",
Unit: emuMessage1.PowerfactorL1.Unit,
Value: emuMessage1.PowerfactorL1.Value,
}
variables["PowerfactorL2"] = database.VariableType {
Variable: "PowerfactorL2",
Unit: emuMessage1.PowerfactorL2.Unit,
Value: emuMessage1.PowerfactorL2.Value,
}
variables["PowerfactorL3"] = database.VariableType {
Variable: "PowerfactorL3",
Unit: emuMessage1.PowerfactorL3.Unit,
Value: emuMessage1.PowerfactorL3.Value,
}
return variables, nil
case 2:
var emuMessage2 emuMessage2
err := json.Unmarshal(decodedPayload, &emuMessage2)
if err != nil {
return nil, fmt.Errorf("Unable to parse payload, fPort %d, error %s", fPort, err)
}
variables := make(map[string]database.VariableType)
variables["ActiveEnergyExport"] = database.VariableType {
Variable: "ActiveEnergyExport",
Unit: emuMessage2.ActiveEnergyExport.Unit,
Value: emuMessage2.ActiveEnergyExport.Value,
}
variables["ActiveEnergyImport"] = database.VariableType {
Variable: "ActiveEnergyImport",
Unit: emuMessage2.ActiveEnergyImport.Unit,
Value: emuMessage2.ActiveEnergyImport.Value,
}
variables["ReactiveEnergyExport"] = database.VariableType {
Variable: "ReactiveEnergyExport",
Unit: emuMessage2.ReactiveEnergyExport.Unit,
Value: emuMessage2.ReactiveEnergyExport.Value,
}
variables["ReactiveEnergyImport"] = database.VariableType {
Variable: "ReactiveEnergyImport",
Unit: emuMessage2.ReactiveEnergyImport.Unit,
Value: emuMessage2.ReactiveEnergyImport.Value,
}
return variables, nil
default:
return nil, fmt.Errorf("Unexpected fPort %d", fPort)
}
}

View File

@ -2,19 +2,82 @@ package ttn
import "log"
import "fmt"
import "time"
import "encoding/json"
import "udi/config"
import "udi/handlers/handler"
import "udi/handlers/ttn/models/emuProfIILoRaCfg1"
import "udi/database"
var idSeq int = 0
type TTNHandler struct {
id int
dbh *database.DatabaseHandle
}
func NewTTNHandler() handler.Handler {
type DecodedPayloaderHolder struct {
Payload []byte
}
type uplinkMessage struct {
EndDeviceIds struct {
DeviceId string `json:"device_id"`
ApplicationIds struct {
ApplicationId string `json:"application_id"`
} `json:"application_ids"`
DevEui string `json:"dev_eui"`
JoinEui string `json:"join_eui"`
DevAddr string `json:"dev_addr"`
} `json:"end_device_ids"`
ReceivedAt string `json:"received_at"`
UplinkMessage struct {
FCnt int `json:"f_cnt"`
FPort int `json:"f_port"`
FrmPayload string `json:"frm_payload"`
DecodedPayload DecodedPayloaderHolder `json:"decoded_payload"`
RxMetadata []struct {
GatewayIds struct {
GatewayId string `json:"gateway_id"`
Eui string `json:"eui"`
} `json:"gateway_ids"`
Time string `json:"time"`
Rssi int `json:"rssi"`
ChannelRssi int `json:"channel_rssi"`
Snr float32 `json:"snr"`
ChannelIndex int `json:"channel_index"`
} `json:"rx_metadata"`
ConsumedAirtime string `json:"consumed_airtime"`
} `json:"uplink_message"`
}
type gatewayAttributes struct {
GatewayId string `json:"gateway_id"`
Rssi int `json:"rssi"`
Snr float32 `json:"snr"`
}
type attributes struct {
DeviceId string `json:"device_id"`
ApplicationId string `json:"application_id"`
FCnt int `json:"f_cnt"`
FPort int `json:"f_port"`
FrmPayload string `json:"frm_payload"`
Gateways []gatewayAttributes `json:"gateways"`
ConsumedAirtime string `json:"consumed_airtime"`
}
func (self *DecodedPayloaderHolder) UnmarshalJSON(data []byte) error {
self.Payload = data
return nil
}
func NewTTNHandler(config config.HandlerConfigT) handler.Handler {
t := &TTNHandler {
id: idSeq,
}
idSeq += 1
t.dbh = database.NewDatabaseHandle(config.DatabaseConnStr)
return t
}
@ -22,8 +85,75 @@ func (self *TTNHandler) GetId() string {
return fmt.Sprintf("TTN%d", self.id)
}
func lost(msg string, message handler.MessageT) {
log.Printf("Error: %s, message %s is lost", msg, message)
}
func (self *TTNHandler) Handle(message handler.MessageT) {
log.Printf("Handler TTN %d processing %s -> %s", self.id, message.Topic, message.Payload)
// log.Printf("Handler TTN %d processing %s -> %s", self.id, message.Topic, message.Payload)
var measurement database.Measurement
measurement.Time = time.Now()
var uplinkMessage uplinkMessage
err := json.Unmarshal([]byte(message.Payload), &uplinkMessage)
if err != nil {
lost(fmt.Sprintf("Error when unmarshaling message: %s, ", err), message)
return
}
//log.Printf("Parsed message: %s", uplinkMessage)
var attributes attributes
attributes.DeviceId = uplinkMessage.EndDeviceIds.DeviceId
attributes.ApplicationId = uplinkMessage.EndDeviceIds.ApplicationIds.ApplicationId
attributes.FCnt = uplinkMessage.UplinkMessage.FCnt
attributes.FPort = uplinkMessage.UplinkMessage.FPort
attributes.FrmPayload = uplinkMessage.UplinkMessage.FrmPayload
attributes.ConsumedAirtime = uplinkMessage.UplinkMessage.ConsumedAirtime
for _, rxm := range uplinkMessage.UplinkMessage.RxMetadata {
log.Printf("RXM: %s", rxm)
g := gatewayAttributes { GatewayId: rxm.GatewayIds.GatewayId, Rssi: rxm.Rssi, Snr: rxm.Snr }
attributes.Gateways = append(attributes.Gateways, g)
}
//log.Printf("Attributes: %s", attributes)
measurement.Attributes = map[string]interface{} {
"DeviceId": attributes.DeviceId,
"ApplicationId": attributes.ApplicationId,
"FCnt": attributes.FCnt,
"FPort": attributes.FPort,
"FrmPayload": attributes.FrmPayload,
"Gateways": attributes.Gateways,
"ConsumedAirtime": attributes.ConsumedAirtime,
}
//log.Printf("ApplicationId: %s, DeviceId: %s", attributes.ApplicationId, attributes.DeviceId)
device, err2 := self.dbh.GetDeviceByLabelAndApplication(attributes.ApplicationId, attributes.DeviceId)
if err2 != nil {
lost(fmt.Sprintf("Error when loading device: %s, ", err2), message)
return
}
measurement.Application = attributes.ApplicationId
measurement.Device = attributes.DeviceId
//log.Printf("DeviceLabel: %s, DeviceType: %s", device.Label, device.DeviceType.ModelIdentifier)
var parser func(int, []byte) (map[string]database.VariableType, error)
switch device.DeviceType.ModelIdentifier {
case "emu-prof-ii-lora-cfg1":
parser = emuProfIILoRaCfg1.Parse
default:
lost(fmt.Sprintf("No parser found for %s", device.DeviceType.ModelIdentifier), message)
return
}
variables, err3 := parser(uplinkMessage.UplinkMessage.FPort, uplinkMessage.UplinkMessage.DecodedPayload.Payload)
if err3 != nil {
lost(fmt.Sprintf("Model parser failed: %s", err3), message)
return
}
measurement.Values = variables
log.Printf("Prepared measurement item: %s", measurement)
self.dbh.StoreMeasurement(&measurement)
}